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
32 changes: 28 additions & 4 deletions src/load-config-meta.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @ts-ignore - position of type import changed in Stencil 5
import { loadConfig, OutputTargetWww } from '@stencil/core/compiler';
import { loadConfig, OutputTargetLoaderBundle, OutputTargetStandalone, OutputTargetWww } from '@stencil/core/compiler';
import { findUp } from 'find-up';
import { isAbsolute, join, relative } from 'path';

Expand All @@ -17,6 +17,19 @@ const DEFAULT_NAMESPACE = 'app';
const DEFAULT_BASE_URL = 'http://localhost:3333';
const DEFAULT_WEB_SERVER_URL = `${DEFAULT_BASE_URL}/ping`;

/**
* The file name of a `standalone` output target's auto-loader, if it has one that starts on import.
* @param target The `standalone` output target.
* @returns The loader's file name, or `undefined` when there's no auto-starting loader.
*/
const getStandaloneLoaderFile = (target: OutputTargetStandalone): string | undefined => {
const { autoLoader } = target;
if (autoLoader === false) return undefined;
if (typeof autoLoader === 'object' && autoLoader.autoStart === false) return undefined;
const fileName = (typeof autoLoader === 'object' && autoLoader.fileName) || 'loader';
return fileName.endsWith('.js') ? fileName : `${fileName}.js`;
};

const DEFAULT_STENCIL_ENTRY_PATH_PREFIX = './build';
const DEFAULT_STENCIL_ENTRY_PATH = `${DEFAULT_STENCIL_ENTRY_PATH_PREFIX}/${DEFAULT_NAMESPACE}`;

Expand Down Expand Up @@ -47,13 +60,17 @@ export const loadConfigMeta = async (cwd?: string) => {
const { devServer, fsNamespace, outputTargets, rootDir: resolvedRootDir } = results.config;

// Grab a suitable output target for script injection.
// Priority: www > dist (v4) > loader-bundle (v5)
// Priority: www > dist (v4) > loader-bundle (v5) > standalone (v5)
const wwwTarget = outputTargets.find((o): o is OutputTargetWww => o.type === 'www');
const distTarget = outputTargets.find((o) => (o as OutputTargetWithDir).type === 'dist') as
OutputTargetWithDir | undefined;
// loader-bundle is a v5 output target type, so we need to cast to avoid type errors on v4
const loaderBundleTarget = outputTargets.find((o) => (o as OutputTargetWithDir).type === 'loader-bundle') as
OutputTargetWithDir | undefined;
OutputTargetLoaderBundle | undefined;

const standaloneTarget = outputTargets.find((o) => (o as OutputTargetWithDir).type === 'standalone') as
OutputTargetStandalone | undefined;
const standaloneLoaderFile = standaloneTarget ? getStandaloneLoaderFile(standaloneTarget) : undefined;

// Use the resolved project rootDir as fallback if devServer.root is not usable
const rootDir = devServer.root && devServer.root !== '/' ? devServer.root : resolvedRootDir;
Expand Down Expand Up @@ -92,12 +109,19 @@ export const loadConfigMeta = async (cwd?: string) => {
// Path structure: dir/buildDir/namespace/namespace (extra namespace folder)
const entryPath = join(relativePath, buildDir, fsNamespace, fsNamespace);
stencilEntryPath = entryPath === '' ? '.' : entryPath.startsWith('.') ? entryPath : `./${entryPath}`;
} else if (standaloneTarget?.dir && standaloneLoaderFile) {
// A standalone-only project: its auto-loader defines components as they appear in the page.
// The entry path is the full file name, which tells `setContent` to load it as one module.
const targetDir = standaloneTarget.dir;
const relativePath = isAbsolute(targetDir) ? relative(rootDir, targetDir) : targetDir;
const entryPath = join(relativePath, standaloneLoaderFile);
stencilEntryPath = entryPath.startsWith('.') ? entryPath : `./${entryPath}`;
} else {
// Make a best guess at the entry path
stencilEntryPath = `${DEFAULT_STENCIL_ENTRY_PATH_PREFIX}/${fsNamespace}`;

console.warn(
`No "www", "dist", or "loader-bundle" output target found in the Stencil config. Using default entry path: "${stencilEntryPath}". Tests using 'setContent' may fail to execute.`,
`No "www", "dist", "loader-bundle", or auto-loading "standalone" output target found in the Stencil config. Using default entry path: "${stencilEntryPath}". Tests using 'setContent' may fail to execute.`,
);
}

Expand Down
9 changes: 7 additions & 2 deletions src/page/utils/set-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export const setContent = async (page: Page, html: string, testInfo: TestInfo, o
const headContent = headMatch ? headMatch[1] : '';
const bodyContent = bodyMatch ? bodyMatch[1] : html;

// A `standalone` auto-loader is one module (its entry path is the full file name);
// the lazy loader has a module build and a legacy `nomodule` one.
const entryScripts = baseEntryPath?.endsWith('.js')
? `<script type="module" src="${baseEntryPath}"></script>`
: `<script type="module" src="${baseEntryPath}.esm.js"></script>`;

const output = `
<!DOCTYPE html>
<html lang="en">
Expand All @@ -36,8 +42,7 @@ export const setContent = async (page: Page, html: string, testInfo: TestInfo, o
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
${headContent}
<script src="${baseEntryPath}.js" nomodule></script>
<script type="module" src="${baseEntryPath}.esm.js"></script>
${entryScripts}
</head>
<body>
${bodyContent}
Expand Down
49 changes: 47 additions & 2 deletions src/test/load-config-meta.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const stencilConfig: {
pingRoute: string;
root: string;
};
outputTargets: Array<{ type: string; dir: string; buildDir?: string }>;
outputTargets: Array<{ type: string; dir: string; buildDir?: string; autoLoader?: unknown }>;
} = {
rootDir: '/mock-path',
fsNamespace: 'mock-namespace',
Expand Down Expand Up @@ -123,7 +123,7 @@ describe('loadConfigMeta', () => {
const configMeta = await loadConfigMeta();

expect(consoleWarnSpy).toHaveBeenCalledWith(
`No "www", "dist", or "loader-bundle" output target found in the Stencil config. Using default entry path: "./build/mock-namespace". Tests using 'setContent' may fail to execute.`,
`No "www", "dist", "loader-bundle", or auto-loading "standalone" output target found in the Stencil config. Using default entry path: "./build/mock-namespace". Tests using 'setContent' may fail to execute.`,
);
expect(configMeta).toEqual({
baseURL: 'http://localhost:4444',
Expand Down Expand Up @@ -191,6 +191,51 @@ describe('loadConfigMeta', () => {
});
});

it('should fall back to the standalone auto-loader when it is the only usable target', async () => {
stencilConfig.outputTargets = [
{
type: 'standalone',
dir: '/mock-path/dist/standalone',
autoLoader: { fileName: 'loader', autoStart: true },
},
];
findUpMock.mockResolvedValueOnce('/mock-path/stencil.config.ts');

const configMeta = await loadConfigMeta();

expect(configMeta).toEqual({
baseURL: 'http://localhost:4444',
stencilEntryPath: './dist/standalone/loader.js',
stencilNamespace: 'mock-namespace',
webServerUrl: 'http://localhost:4444/status',
});
});

it('should prefer loader-bundle over standalone', async () => {
stencilConfig.outputTargets = [
{ type: 'standalone', dir: '/mock-path/dist/standalone', autoLoader: { fileName: 'loader', autoStart: true } },
{ type: 'loader-bundle', dir: '/mock-path/dist/loader-bundle' },
];
findUpMock.mockResolvedValueOnce('/mock-path/stencil.config.ts');

const configMeta = await loadConfigMeta();

expect(configMeta.stencilEntryPath).toBe('./dist/loader-bundle/mock-namespace/mock-namespace');
});

it('should not use a standalone target whose auto-loader is off or does not start on import', async () => {
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
for (const autoLoader of [false, { fileName: 'loader', autoStart: false }]) {
stencilConfig.outputTargets = [{ type: 'standalone', dir: '/mock-path/dist/standalone', autoLoader }];
findUpMock.mockResolvedValueOnce('/mock-path/stencil.config.ts');

const configMeta = await loadConfigMeta();

expect(configMeta.stencilEntryPath).toBe('./build/mock-namespace');
}
expect(consoleWarnSpy).toHaveBeenCalledTimes(2);
});

it('should prefer www target over dist and loader-bundle', async () => {
stencilConfig.outputTargets = [
{
Expand Down
17 changes: 14 additions & 3 deletions src/test/wizard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ describe('wizard.init.run', () => {
expect(ctx.openStencilConfig).not.toHaveBeenCalled();
});

it('offers to add a www output target when stencil.config.ts exists and none is configured', async () => {
it('offers to add a loader-bundle output target when stencil.config.ts exists and none is configured', async () => {
writeFileSync(join(tmpDir, 'stencil.config.ts'), 'export const config = {};\n');
const ctx = makeInitCtx(tmpDir, []);
const editor = { addOutputTarget: vi.fn(), save: vi.fn().mockResolvedValue(undefined) };
Expand All @@ -164,7 +164,7 @@ describe('wizard.init.run', () => {

await wizard.init!.run(ctx as any);

expect(editor.addOutputTarget).toHaveBeenCalledWith(expect.stringContaining("type: 'www'"));
expect(editor.addOutputTarget).toHaveBeenCalledWith("{ type: 'loader-bundle' }");
expect(editor.save).toHaveBeenCalled();
});

Expand All @@ -177,10 +177,21 @@ describe('wizard.init.run', () => {

expect(ctx.openStencilConfig).not.toHaveBeenCalled();
expect(ctx.prompts.log.warn).toHaveBeenCalledWith(
expect.stringContaining('No "www" or "loader-bundle" output target configured'),
expect.stringContaining('No "www", "loader-bundle", or "standalone" output target configured'),
);
});

it('does not prompt or warn when only a "standalone" output target is configured', async () => {
writeFileSync(join(tmpDir, 'stencil.config.ts'), 'export const config = {};\n');
const ctx = makeInitCtx(tmpDir, [{ type: 'standalone' }]);

await wizard.init!.run(ctx as any);

expect(ctx.prompts.confirm).not.toHaveBeenCalled();
expect(ctx.openStencilConfig).not.toHaveBeenCalled();
expect(ctx.prompts.log.warn).not.toHaveBeenCalled();
});

it('does not warn when a "www" output target is already configured', async () => {
const ctx = makeInitCtx(tmpDir, [{ type: 'www' }]);

Expand Down
22 changes: 12 additions & 10 deletions src/wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ async function fileExists(path: string): Promise<boolean> {
}

/**
* Checks for a "www" or "loader-bundle" output target.
* Checks for an output target the tests can load components from.
* @param config The resolved Stencil project config.
* @returns Whether a "www" and/or "loader-bundle" output target is present.
* @returns Whether a "www", "loader-bundle", and/or "standalone" output target is present.
*/
function detectOutputTargets(config: ProjectConfig) {
const outputs = config.outputTargets as ReadonlyArray<{ type: string }>;
const www = outputs.some((o) => o.type === 'www');
const loaderBundle = outputs.some((o) => o.type === 'loader-bundle');
return { www, loaderBundle };
const standalone = outputs.some((o) => o.type === 'standalone');
return { www, loaderBundle, standalone };
}

const PLAYWRIGHT_CONFIG_TEMPLATE = `import { expect } from '@playwright/test';
Expand Down Expand Up @@ -79,9 +80,10 @@ async function ensureDisposableLib(rootDir: string): Promise<void> {
*/
async function ensureOutputTarget(context: WizardContext): Promise<void> {
const { config, prompts } = context;
const { www, loaderBundle } = detectOutputTargets(config);
const { www, loaderBundle, standalone } = detectOutputTargets(config);

if (www || loaderBundle) return;
// A standalone-only project builds `standalone` in dev too, and its auto-loader is enough to test against.
if (www || loaderBundle || standalone) return;

const stencilConfigPath = join(config.rootDir, 'stencil.config.ts');
if (!(await fileExists(stencilConfigPath))) {
Expand All @@ -90,19 +92,19 @@ async function ensureOutputTarget(context: WizardContext): Promise<void> {
return;
}

const addWww = await prompts.confirm({
message: 'No "www" or "loader-bundle" output target found. Add a "www" output target now?',
const addLoaderBundle = await prompts.confirm({
message: 'No "www", "loader-bundle", or "standalone" output target found. Add a "loader-bundle" output target now?',
initialValue: true,
});
if (!prompts.isCancel(addWww) && addWww) {
if (!prompts.isCancel(addLoaderBundle) && addLoaderBundle) {
const editor = await context.openStencilConfig();
editor.addOutputTarget("{ type: 'www', serviceWorker: null }");
editor.addOutputTarget("{ type: 'loader-bundle' }");
await editor.save();
return;
}

prompts.log.warn(
'No "www" or "loader-bundle" output target configured - Playwright tests may not have anything to run against.',
'No "www", "loader-bundle", or "standalone" output target configured - Playwright tests may not have anything to run against.',
);
}

Expand Down
Loading