From 7e5ac1f45c4865cfc04c658d18e70aedbae38772 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Mon, 25 Jul 2022 12:14:41 -0400 Subject: [PATCH] Build the before-hydration script (#4042) * Build the before-hydration script * Adding a changeset --- .changeset/chilled-pandas-confess.md | 5 + .../lit-component/src/components/Counter.js | 6 +- .../lit-component/src/pages/index.astro | 14 +- .../lit-component/src/pages/media.astro | 6 +- .../lit-component/src/pages/solo.astro | 18 ++ packages/astro/e2e/lit-component.test.js | 198 +++++++++++------- packages/astro/src/core/build/static-build.ts | 2 +- .../src/core/build/vite-plugin-analyzer.ts | 1 - .../astro/src/core/build/vite-plugin-ssr.ts | 4 +- .../astro/src/vite-plugin-scripts/index.ts | 19 +- 10 files changed, 166 insertions(+), 107 deletions(-) create mode 100644 .changeset/chilled-pandas-confess.md create mode 100644 packages/astro/e2e/fixtures/lit-component/src/pages/solo.astro diff --git a/.changeset/chilled-pandas-confess.md b/.changeset/chilled-pandas-confess.md new file mode 100644 index 000000000..2f77aac40 --- /dev/null +++ b/.changeset/chilled-pandas-confess.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Ensure the before-hydration scripts are built diff --git a/packages/astro/e2e/fixtures/lit-component/src/components/Counter.js b/packages/astro/e2e/fixtures/lit-component/src/components/Counter.js index 3316a7342..72843f8ef 100644 --- a/packages/astro/e2e/fixtures/lit-component/src/components/Counter.js +++ b/packages/astro/e2e/fixtures/lit-component/src/components/Counter.js @@ -1,8 +1,6 @@ import { LitElement, html } from 'lit'; -export const tagName = 'my-counter'; - -class Counter extends LitElement { +export default class Counter extends LitElement { static get properties() { return { count: { @@ -33,4 +31,4 @@ class Counter extends LitElement { } } -customElements.define(tagName, Counter); +customElements.define('my-counter', Counter); diff --git a/packages/astro/e2e/fixtures/lit-component/src/pages/index.astro b/packages/astro/e2e/fixtures/lit-component/src/pages/index.astro index 48eb7d2f9..ef86839d6 100644 --- a/packages/astro/e2e/fixtures/lit-component/src/pages/index.astro +++ b/packages/astro/e2e/fixtures/lit-component/src/pages/index.astro @@ -1,5 +1,5 @@ --- -import '../components/Counter.js'; +import MyCounter from '../components/Counter.js'; const someProps = { count: 0, @@ -11,16 +11,16 @@ const someProps = { - +

Hello, client:idle!

-
+ - +

Hello, client:load!

-
+ - +

Hello, client:visible!

-
+ diff --git a/packages/astro/e2e/fixtures/lit-component/src/pages/media.astro b/packages/astro/e2e/fixtures/lit-component/src/pages/media.astro index e54cec071..a05d52863 100644 --- a/packages/astro/e2e/fixtures/lit-component/src/pages/media.astro +++ b/packages/astro/e2e/fixtures/lit-component/src/pages/media.astro @@ -1,5 +1,5 @@ --- -import '../components/Counter.js'; +import MyCounter from '../components/Counter.js'; const someProps = { count: 0, @@ -11,8 +11,8 @@ const someProps = { - +

Hello, client:media!

-
+ diff --git a/packages/astro/e2e/fixtures/lit-component/src/pages/solo.astro b/packages/astro/e2e/fixtures/lit-component/src/pages/solo.astro new file mode 100644 index 000000000..1d2745e47 --- /dev/null +++ b/packages/astro/e2e/fixtures/lit-component/src/pages/solo.astro @@ -0,0 +1,18 @@ +--- +import MyCounter from '../components/Counter.js'; + +const someProps = { + count: 0, +}; +--- + + + + + + + +

Hello, client:idle!

+
+ + diff --git a/packages/astro/e2e/lit-component.test.js b/packages/astro/e2e/lit-component.test.js index acf07f9d9..66355af17 100644 --- a/packages/astro/e2e/lit-component.test.js +++ b/packages/astro/e2e/lit-component.test.js @@ -1,100 +1,138 @@ import { expect } from '@playwright/test'; import { testFactory } from './test-utils.js'; -const test = testFactory({ root: './fixtures/lit-component/' }); - -let devServer; - -test.beforeEach(async ({ astro }) => { - devServer = await astro.startDevServer(); -}); - -test.afterEach(async () => { - await devServer.stop(); +const test = testFactory({ + root: './fixtures/lit-component/', }); // TODO: configure playwright to handle web component APIs // https://github.com/microsoft/playwright/issues/14241 -test.describe.skip('Lit components', () => { - test('client:idle', async ({ page, astro }) => { - await page.goto(astro.resolveUrl('/')); +test.describe('Lit components', () => { + test.beforeEach(() => { + delete globalThis.window; + }); + + test.describe('Development', () => { + let devServer; + const t = test.extend({}); - const counter = page.locator('#client-idle'); - await expect(counter, 'component is visible').toBeVisible(); + t.beforeEach(async ({ astro }) => { + devServer = await astro.startDevServer(); + }); + + t.afterEach(async () => { + await devServer.stop(); + }); - const count = counter.locator('p'); - await expect(count, 'initial count is 0').toHaveText('Count: 0'); + t('client:idle', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/')); - const inc = counter.locator('button'); - await inc.click(); + const counter = page.locator('#client-idle'); + await expect(counter, 'component is visible').toBeVisible(); + await expect(counter).toHaveCount(1); - await expect(count, 'count incremented by 1').toHaveText('Count: 1'); + const count = counter.locator('p'); + await expect(count, 'initial count is 0').toHaveText('Count: 0'); + + const inc = counter.locator('button'); + await inc.click(); + + await expect(count, 'count incremented by 1').toHaveText('Count: 1'); + }); + + t('client:load', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/')); + + const counter = page.locator('#client-load'); + await expect(counter, 'component is visible').toBeVisible(); + + const count = counter.locator('p'); + await expect(count, 'initial count is 0').toHaveText('Count: 0'); + + const inc = counter.locator('button'); + await inc.click(); + + await expect(count, 'count incremented by 1').toHaveText('Count: 1'); + }); + + t('client:visible', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/')); + + // Make sure the component is on screen to trigger hydration + const counter = page.locator('#client-visible'); + await counter.scrollIntoViewIfNeeded(); + await expect(counter, 'component is visible').toBeVisible(); + + const count = counter.locator('p'); + await expect(count, 'initial count is 0').toHaveText('Count: 0'); + + const inc = counter.locator('button'); + await inc.click(); + + await expect(count, 'count incremented by 1').toHaveText('Count: 1'); + }); + + t('client:media', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/media')); + + const counter = page.locator('#client-media'); + await expect(counter, 'component is visible').toBeVisible(); + + const count = counter.locator('p'); + await expect(count, 'initial count is 0').toHaveText('Count: 0'); + + const inc = counter.locator('button'); + await inc.click(); + + await expect(count, 'component not hydrated yet').toHaveText('Count: 0'); + + // Reset the viewport to hydrate the component (max-width: 50rem) + await page.setViewportSize({ width: 414, height: 1124 }); + + await inc.click(); + await expect(count, 'count incremented by 1').toHaveText('Count: 1'); + }); + + t.skip('HMR', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/')); + + const counter = page.locator('#client-idle'); + const label = counter.locator('h1'); + + await astro.editFile('./src/pages/index.astro', (original) => + original.replace('Hello, client:idle!', 'Hello, updated client:idle!') + ); + + await expect(label, 'slot text updated').toHaveText('Hello, updated client:idle!'); + await expect(counter, 'component styles persisted').toHaveCSS('display', 'grid'); + }); }); - test('client:load', async ({ page, astro }) => { - await page.goto(astro.resolveUrl('/')); + test.describe('Production', () => { + let previewServer; + const t = test.extend({}); - const counter = page.locator('#client-load'); - await expect(counter, 'component is visible').toBeVisible(); + t.beforeAll(async ({ astro }) => { + // Playwright's Node version doesn't have these functions, so stub them. + process.stdout.clearLine = () => {}; + process.stdout.cursorTo = () => {}; + await astro.build(); + }); - const count = counter.locator('p'); - await expect(count, 'initial count is 0').toHaveText('Count: 0'); + t.beforeEach(async ({ astro }) => { + previewServer = await astro.preview(); + }); - const inc = counter.locator('button'); - await inc.click(); + t.afterEach(async () => { + await previewServer.stop(); + }); - await expect(count, 'count incremented by 1').toHaveText('Count: 1'); - }); + t('Only one component in prod', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/solo')); - test('client:visible', async ({ page, astro }) => { - await page.goto(astro.resolveUrl('/')); - - // Make sure the component is on screen to trigger hydration - const counter = page.locator('#client-visible'); - await counter.scrollIntoViewIfNeeded(); - await expect(counter, 'component is visible').toBeVisible(); - - const count = counter.locator('p'); - await expect(count, 'initial count is 0').toHaveText('Count: 0'); - - const inc = counter.locator('button'); - await inc.click(); - - await expect(count, 'count incremented by 1').toHaveText('Count: 1'); - }); - - test('client:media', async ({ page, astro }) => { - await page.goto(astro.resolveUrl('/media')); - - const counter = page.locator('#client-media'); - await expect(counter, 'component is visible').toBeVisible(); - - const count = counter.locator('p'); - await expect(count, 'initial count is 0').toHaveText('Count: 0'); - - const inc = counter.locator('button'); - await inc.click(); - - await expect(count, 'component not hydrated yet').toHaveText('Count: 0'); - - // Reset the viewport to hydrate the component (max-width: 50rem) - await page.setViewportSize({ width: 414, height: 1124 }); - - await inc.click(); - await expect(count, 'count incremented by 1').toHaveText('Count: 1'); - }); - - test('HMR', async ({ page, astro }) => { - await page.goto(astro.resolveUrl('/')); - - const counter = page.locator('#client-idle'); - const label = counter.locator('h1'); - - await astro.editFile('./src/pages/index.astro', (original) => - original.replace('Hello, client:idle!', 'Hello, updated client:idle!') - ); - - await expect(label, 'slot text updated').toHaveText('Hello, updated client:idle!'); - await expect(counter, 'component styles persisted').toHaveCSS('display', 'grid'); + const counter = page.locator('my-counter'); + await expect(counter, 'component is visible').toBeVisible(); + await expect(counter, 'there is only one counter').toHaveCount(1); + }); }); }); diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 64430da6e..110a85d67 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -154,7 +154,7 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp // SSR needs to be last opts.astroConfig.output === 'server' && vitePluginSSR(internals, opts.astroConfig._ctx.adapter!), - vitePluginAnalyzer(opts.astroConfig, internals), + vitePluginAnalyzer(internals), ], publicDir: ssr ? false : viteConfig.publicDir, root: viteConfig.root, diff --git a/packages/astro/src/core/build/vite-plugin-analyzer.ts b/packages/astro/src/core/build/vite-plugin-analyzer.ts index 8b9950ff1..1f84dffe3 100644 --- a/packages/astro/src/core/build/vite-plugin-analyzer.ts +++ b/packages/astro/src/core/build/vite-plugin-analyzer.ts @@ -10,7 +10,6 @@ import { getTopLevelPages } from './graph.js'; import { getPageDataByViteID, trackClientOnlyPageDatas } from './internal.js'; export function vitePluginAnalyzer( - astroConfig: AstroConfig, internals: BuildInternals ): VitePlugin { function hoistedScriptScanner() { diff --git a/packages/astro/src/core/build/vite-plugin-ssr.ts b/packages/astro/src/core/build/vite-plugin-ssr.ts index d8e6ff728..bf46fc5d6 100644 --- a/packages/astro/src/core/build/vite-plugin-ssr.ts +++ b/packages/astro/src/core/build/vite-plugin-ssr.ts @@ -145,8 +145,10 @@ function buildManifest( // HACK! Patch this special one. const entryModules = Object.fromEntries(internals.entrySpecifierToBundleMap.entries()); - entryModules[BEFORE_HYDRATION_SCRIPT_ID] = + if(!(BEFORE_HYDRATION_SCRIPT_ID in entryModules)) { + entryModules[BEFORE_HYDRATION_SCRIPT_ID] = 'data:text/javascript;charset=utf-8,//[no before-hydration script]'; + } const ssrManifest: SerializedSSRManifest = { adapterName: opts.astroConfig._ctx.adapter!.name, diff --git a/packages/astro/src/vite-plugin-scripts/index.ts b/packages/astro/src/vite-plugin-scripts/index.ts index 20f4fdafe..a722d3534 100644 --- a/packages/astro/src/vite-plugin-scripts/index.ts +++ b/packages/astro/src/vite-plugin-scripts/index.ts @@ -1,4 +1,4 @@ -import { Plugin as VitePlugin } from 'vite'; +import { Plugin as VitePlugin, ConfigEnv } from 'vite'; import { AstroConfig, InjectedScriptStage } from '../@types/astro.js'; // NOTE: We can't use the virtual "\0" ID convention because we need to @@ -12,8 +12,14 @@ export const PAGE_SCRIPT_ID = `${SCRIPT_ID_PREFIX}${'page' as InjectedScriptStag export const PAGE_SSR_SCRIPT_ID = `${SCRIPT_ID_PREFIX}${'page-ssr' as InjectedScriptStage}.js`; export default function astroScriptsPlugin({ config }: { config: AstroConfig }): VitePlugin { + let env: ConfigEnv | undefined = undefined; return { name: 'astro:scripts', + + config(_config, _env) { + env = _env; + }, + async resolveId(id) { if (id.startsWith(SCRIPT_ID_PREFIX)) { return id; @@ -43,21 +49,14 @@ export default function astroScriptsPlugin({ config }: { config: AstroConfig }): return null; }, buildStart(options) { - // We only want to inject this script if we are building - // for the frontend AND some hydrated components exist in - // the final build. We can detect this by looking for a - // `astro/client/*` input, which signifies both conditions are met. - const hasHydratedComponents = - Array.isArray(options.input) && - options.input.some((input) => input.startsWith('astro/client')); const hasHydrationScripts = config._ctx.scripts.some((s) => s.stage === 'before-hydration'); - if (hasHydratedComponents && hasHydrationScripts) { + if (hasHydrationScripts && env?.command === 'build' && !env?.ssrBuild) { this.emitFile({ type: 'chunk', id: BEFORE_HYDRATION_SCRIPT_ID, name: BEFORE_HYDRATION_SCRIPT_ID, }); } - }, + } }; }