Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions packages/cli/src/_test_/task-build.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';

const stdEnv = vi.hoisted(() => ({ isCI: false }));

vi.mock('std-env', () => ({
get isCI() {
return stdEnv.isCI;
},
}));
vi.mock('@clack/prompts', () => ({
select: vi.fn(),
isCancel: vi.fn(() => false),
}));

import * as clack from '@clack/prompts';

import { promptForMigration } from '../task-build';

describe('promptForMigration', () => {
const identity = (s: string) => s;
const logger = { info: vi.fn(), bold: identity, yellow: identity, cyan: identity };
const config = { logger, rootDir: '/project' } as any;
const migrationResult = {
hasMigrations: true,
totalMatches: 1,
filesAffected: 1,
migrations: [{ filePath: '/project/stencil.config.ts', matches: [{}] }],
} as any;

const originalIsTTY = process.stdin.isTTY;
const setStdinTTY = (value: boolean | undefined) =>
Object.defineProperty(process.stdin, 'isTTY', { value, configurable: true });

beforeEach(() => {
vi.clearAllMocks();
stdEnv.isCI = false;
});

afterEach(() => {
setStdinTTY(originalIsTTY);
});

it('prompts when stdin is a terminal', async () => {
setStdinTTY(true);
vi.mocked(clack.select).mockResolvedValue('run');

await expect(promptForMigration(config, migrationResult, 'pre-build')).resolves.toBe('run');
expect(clack.select).toHaveBeenCalledTimes(1);
});

it("doesn't prompt when stdin isn't a terminal - it exits with instructions", async () => {
setStdinTTY(undefined);

await expect(promptForMigration(config, migrationResult, 'pre-build')).resolves.toBe('exit');
expect(clack.select).not.toHaveBeenCalled();
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('stencil migrate'));
});

it("doesn't prompt in CI, even with a terminal", async () => {
setStdinTTY(true);
stdEnv.isCI = true;

await expect(promptForMigration(config, migrationResult, 'post-error')).resolves.toBe('exit');
expect(clack.select).not.toHaveBeenCalled();
});
});
14 changes: 13 additions & 1 deletion packages/cli/src/task-build.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { relative } from 'path';
import { isCancel, select } from '@clack/prompts';
import { isCI } from 'std-env';
import type * as d from '@stencil/core/compiler';

import { printCheckVersionResults, startCheckVersion } from './check-version';
Expand Down Expand Up @@ -138,12 +139,16 @@ type MigrationAction = 'run' | 'dry-run' | 'exit';
/**
* Prompt the user about available migrations.
* Shows what migrations are available and lets them choose to run them.
*
* Without someone to answer (in CI, or when stdin isn't a terminal) it doesn't prompt: it says
* how to apply the migrations and returns `'exit'`, so the build fails instead of waiting
* forever on a prompt, or ending with exit code 0 when stdin closes.
* @param config the Stencil config
* @param migrationResult the result of migration detection with available migrations
* @param context whether this is a pre-build check or post-error check
* @returns the user's chosen action for handling migrations
*/
async function promptForMigration(
export async function promptForMigration(
config: d.ValidatedConfig,
migrationResult: MigrationDetectionResult,
context: 'pre-build' | 'post-error',
Expand Down Expand Up @@ -171,6 +176,13 @@ async function promptForMigration(
logger.info('These migrations may help resolve the build errors above.');
}

if (isCI || !process.stdin.isTTY) {
logger.info(
`Run ${logger.cyan('stencil migrate')} to apply them (or ${logger.cyan('stencil migrate --dry-run')} to preview the changes), then build again.`,
);
return 'exit';
}

const action = await select<string>({
message: 'What would you like to do?',
options: [
Expand Down
25 changes: 13 additions & 12 deletions packages/core/src/runtime/client-hydrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ export const initializeClientHydrate = (
const node = childRenderNode.$elm$ as d.RenderNode;

if (!shadowRoot) {
node['s-hn'] = transformTag(tagName).toUpperCase();
if (childRenderNode.$hostId$ === hostId) {
// Only nodes this component rendered belong to it.
node['s-hn'] = transformTag(tagName).toUpperCase();
}

if (childRenderNode.$tag$ === 'slot') {
// If this is a virtual 'slot', add it's Content-position Reference now.
Expand Down Expand Up @@ -218,17 +221,15 @@ export const initializeClientHydrate = (
// Try to set an appropriate Content-position Reference (CR) node for this host element

if (!siSlot['s-cr']) {
// Is a CR already set on the host?
siSlot['s-cr'] = hostEle['s-cr'];

if (!siSlot['s-cr'] && hostEle.shadowRoot) {
// Host has shadowDOM - just use the host itself as the CR for native slotting
siSlot['s-cr'] = hostEle;
} else {
// If all else fails - just set the CR as the first child
// (9/10 if node['s-cr'] hasn't been set, the node will be at the element root)
siSlot['s-cr'] = ((hostEle as any).__childNodes || hostEle.childNodes)[0];
}
siSlot['s-cr'] =
// the CR already set on the host, if there is one
hostEle['s-cr'] ||
// a host with shadowDOM - just use the host itself as the CR for native slotting
(hostEle.shadowRoot
? hostEle
: // if all else fails - the first child
// (9/10 if node['s-cr'] hasn't been set, the node will be at the element root)
((hostEle as any).__childNodes || hostEle.childNodes)[0]);
}
// Create our 'Original Location' node
addSlotRelocateNode(siNode, siSlot, false, siNode['s-oo'] || currentPos);
Expand Down
13 changes: 12 additions & 1 deletion packages/core/src/runtime/vdom/vdom-render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ let hostTagName: string;
let hostRegistry: CustomElementRegistry;
let useNativeShadowDom = false;
let checkSlotFallbackVisibility = false;
/** Whether the current render has slotted content to relocate. Set at the start of each render. */
let checkSlotRelocate = false;
let isSvgMode = false;

Expand Down Expand Up @@ -1076,6 +1077,11 @@ export const insertBefore = (
isInitialLoad?: boolean,
): Node => {
if (BUILD.slotRelocation) {
if ((parent as d.RenderNode | null)?.['s-cr']) {
// `parent` is the host of a component that relocates its slotted content (it has a
// content ref). The node just added to it has to be moved into its slot in this render
checkSlotRelocate = true;
}
if (
BUILD.scoped &&
typeof newNode['s-sn'] === 'string' &&
Expand Down Expand Up @@ -1293,6 +1299,12 @@ render() {

// always reset
checkSlotFallbackVisibility = false;

// Decided per render. A component with `<slot>`s that isn't rendering into a native shadow
// root has to move its slotted content into place on every render - including content a
// parent added since the last one. The patch below also sets it when it adds nodes to
// another component's host (see `insertBefore`) or moves slot content around.
checkSlotRelocate = !useNativeShadowDom && !!(cmpMeta.$flags$ & CMP_FLAGS.hasSlot);
}

// synchronous patch
Expand Down Expand Up @@ -1469,7 +1481,6 @@ render() {
// Only an issue if there were no "slots" rendered. Otherwise, nodes are hidden correctly.
if (
BUILD.slotRelocation &&
!useNativeShadowDom &&
!(cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) &&
hostElm['s-cr']
) {
Expand Down
Loading
Loading