Build the before-hydration script (#4042)
* Build the before-hydration script * Adding a changeset
This commit is contained in:
parent
149780493e
commit
7e5ac1f45c
10 changed files with 166 additions and 107 deletions
5
.changeset/chilled-pandas-confess.md
Normal file
5
.changeset/chilled-pandas-confess.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Ensure the before-hydration scripts are built
|
|
@ -1,8 +1,6 @@
|
||||||
import { LitElement, html } from 'lit';
|
import { LitElement, html } from 'lit';
|
||||||
|
|
||||||
export const tagName = 'my-counter';
|
export default class Counter extends LitElement {
|
||||||
|
|
||||||
class Counter extends LitElement {
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
count: {
|
count: {
|
||||||
|
@ -33,4 +31,4 @@ class Counter extends LitElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define(tagName, Counter);
|
customElements.define('my-counter', Counter);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
import '../components/Counter.js';
|
import MyCounter from '../components/Counter.js';
|
||||||
|
|
||||||
const someProps = {
|
const someProps = {
|
||||||
count: 0,
|
count: 0,
|
||||||
|
@ -11,16 +11,16 @@ const someProps = {
|
||||||
<!-- Head Stuff -->
|
<!-- Head Stuff -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<my-counter id="client-idle" {...someProps} client:idle>
|
<MyCounter id="client-idle" {...someProps} client:idle>
|
||||||
<h1>Hello, client:idle!</h1>
|
<h1>Hello, client:idle!</h1>
|
||||||
</my-counter>
|
</MyCounter>
|
||||||
|
|
||||||
<my-counter id="client-load" {...someProps} client:load>
|
<MyCounter id="client-load" {...someProps} client:load>
|
||||||
<h1>Hello, client:load!</h1>
|
<h1>Hello, client:load!</h1>
|
||||||
</my-counter>
|
</MyCounter>
|
||||||
|
|
||||||
<my-counter id="client-visible" {...someProps} client:visible>
|
<MyCounter id="client-visible" {...someProps} client:visible>
|
||||||
<h1>Hello, client:visible!</h1>
|
<h1>Hello, client:visible!</h1>
|
||||||
</my-counter>
|
</MyCounter>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
import '../components/Counter.js';
|
import MyCounter from '../components/Counter.js';
|
||||||
|
|
||||||
const someProps = {
|
const someProps = {
|
||||||
count: 0,
|
count: 0,
|
||||||
|
@ -11,8 +11,8 @@ const someProps = {
|
||||||
<!-- Head Stuff -->
|
<!-- Head Stuff -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<my-counter id="client-media" {...someProps} client:media="(max-width: 50em)">
|
<MyCounter id="client-media" {...someProps} client:media="(max-width: 50em)">
|
||||||
<h1>Hello, client:media!</h1>
|
<h1>Hello, client:media!</h1>
|
||||||
</my-counter>
|
</MyCounter>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
import MyCounter from '../components/Counter.js';
|
||||||
|
|
||||||
|
const someProps = {
|
||||||
|
count: 0,
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!-- Head Stuff -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<MyCounter {...someProps} client:idle>
|
||||||
|
<h1>Hello, client:idle!</h1>
|
||||||
|
</MyCounter>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,100 +1,138 @@
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
import { testFactory } from './test-utils.js';
|
import { testFactory } from './test-utils.js';
|
||||||
|
|
||||||
const test = testFactory({ root: './fixtures/lit-component/' });
|
const test = testFactory({
|
||||||
|
root: './fixtures/lit-component/',
|
||||||
let devServer;
|
|
||||||
|
|
||||||
test.beforeEach(async ({ astro }) => {
|
|
||||||
devServer = await astro.startDevServer();
|
|
||||||
});
|
|
||||||
|
|
||||||
test.afterEach(async () => {
|
|
||||||
await devServer.stop();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: configure playwright to handle web component APIs
|
// TODO: configure playwright to handle web component APIs
|
||||||
// https://github.com/microsoft/playwright/issues/14241
|
// https://github.com/microsoft/playwright/issues/14241
|
||||||
test.describe.skip('Lit components', () => {
|
test.describe('Lit components', () => {
|
||||||
test('client:idle', async ({ page, astro }) => {
|
test.beforeEach(() => {
|
||||||
await page.goto(astro.resolveUrl('/'));
|
delete globalThis.window;
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Development', () => {
|
||||||
|
let devServer;
|
||||||
|
const t = test.extend({});
|
||||||
|
|
||||||
const counter = page.locator('#client-idle');
|
t.beforeEach(async ({ astro }) => {
|
||||||
await expect(counter, 'component is visible').toBeVisible();
|
devServer = await astro.startDevServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
t.afterEach(async () => {
|
||||||
|
await devServer.stop();
|
||||||
|
});
|
||||||
|
|
||||||
const count = counter.locator('p');
|
t('client:idle', async ({ page, astro }) => {
|
||||||
await expect(count, 'initial count is 0').toHaveText('Count: 0');
|
await page.goto(astro.resolveUrl('/'));
|
||||||
|
|
||||||
const inc = counter.locator('button');
|
const counter = page.locator('#client-idle');
|
||||||
await inc.click();
|
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 }) => {
|
test.describe('Production', () => {
|
||||||
await page.goto(astro.resolveUrl('/'));
|
let previewServer;
|
||||||
|
const t = test.extend({});
|
||||||
|
|
||||||
const counter = page.locator('#client-load');
|
t.beforeAll(async ({ astro }) => {
|
||||||
await expect(counter, 'component is visible').toBeVisible();
|
// 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');
|
t.beforeEach(async ({ astro }) => {
|
||||||
await expect(count, 'initial count is 0').toHaveText('Count: 0');
|
previewServer = await astro.preview();
|
||||||
|
});
|
||||||
|
|
||||||
const inc = counter.locator('button');
|
t.afterEach(async () => {
|
||||||
await inc.click();
|
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 }) => {
|
const counter = page.locator('my-counter');
|
||||||
await page.goto(astro.resolveUrl('/'));
|
await expect(counter, 'component is visible').toBeVisible();
|
||||||
|
await expect(counter, 'there is only one counter').toHaveCount(1);
|
||||||
// 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');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -154,7 +154,7 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
|
||||||
// SSR needs to be last
|
// SSR needs to be last
|
||||||
opts.astroConfig.output === 'server' &&
|
opts.astroConfig.output === 'server' &&
|
||||||
vitePluginSSR(internals, opts.astroConfig._ctx.adapter!),
|
vitePluginSSR(internals, opts.astroConfig._ctx.adapter!),
|
||||||
vitePluginAnalyzer(opts.astroConfig, internals),
|
vitePluginAnalyzer(internals),
|
||||||
],
|
],
|
||||||
publicDir: ssr ? false : viteConfig.publicDir,
|
publicDir: ssr ? false : viteConfig.publicDir,
|
||||||
root: viteConfig.root,
|
root: viteConfig.root,
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { getTopLevelPages } from './graph.js';
|
||||||
import { getPageDataByViteID, trackClientOnlyPageDatas } from './internal.js';
|
import { getPageDataByViteID, trackClientOnlyPageDatas } from './internal.js';
|
||||||
|
|
||||||
export function vitePluginAnalyzer(
|
export function vitePluginAnalyzer(
|
||||||
astroConfig: AstroConfig,
|
|
||||||
internals: BuildInternals
|
internals: BuildInternals
|
||||||
): VitePlugin {
|
): VitePlugin {
|
||||||
function hoistedScriptScanner() {
|
function hoistedScriptScanner() {
|
||||||
|
|
|
@ -145,8 +145,10 @@ function buildManifest(
|
||||||
|
|
||||||
// HACK! Patch this special one.
|
// HACK! Patch this special one.
|
||||||
const entryModules = Object.fromEntries(internals.entrySpecifierToBundleMap.entries());
|
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]';
|
'data:text/javascript;charset=utf-8,//[no before-hydration script]';
|
||||||
|
}
|
||||||
|
|
||||||
const ssrManifest: SerializedSSRManifest = {
|
const ssrManifest: SerializedSSRManifest = {
|
||||||
adapterName: opts.astroConfig._ctx.adapter!.name,
|
adapterName: opts.astroConfig._ctx.adapter!.name,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Plugin as VitePlugin } from 'vite';
|
import { Plugin as VitePlugin, ConfigEnv } from 'vite';
|
||||||
import { AstroConfig, InjectedScriptStage } from '../@types/astro.js';
|
import { AstroConfig, InjectedScriptStage } from '../@types/astro.js';
|
||||||
|
|
||||||
// NOTE: We can't use the virtual "\0" ID convention because we need to
|
// 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 const PAGE_SSR_SCRIPT_ID = `${SCRIPT_ID_PREFIX}${'page-ssr' as InjectedScriptStage}.js`;
|
||||||
|
|
||||||
export default function astroScriptsPlugin({ config }: { config: AstroConfig }): VitePlugin {
|
export default function astroScriptsPlugin({ config }: { config: AstroConfig }): VitePlugin {
|
||||||
|
let env: ConfigEnv | undefined = undefined;
|
||||||
return {
|
return {
|
||||||
name: 'astro:scripts',
|
name: 'astro:scripts',
|
||||||
|
|
||||||
|
config(_config, _env) {
|
||||||
|
env = _env;
|
||||||
|
},
|
||||||
|
|
||||||
async resolveId(id) {
|
async resolveId(id) {
|
||||||
if (id.startsWith(SCRIPT_ID_PREFIX)) {
|
if (id.startsWith(SCRIPT_ID_PREFIX)) {
|
||||||
return id;
|
return id;
|
||||||
|
@ -43,21 +49,14 @@ export default function astroScriptsPlugin({ config }: { config: AstroConfig }):
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
buildStart(options) {
|
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');
|
const hasHydrationScripts = config._ctx.scripts.some((s) => s.stage === 'before-hydration');
|
||||||
if (hasHydratedComponents && hasHydrationScripts) {
|
if (hasHydrationScripts && env?.command === 'build' && !env?.ssrBuild) {
|
||||||
this.emitFile({
|
this.emitFile({
|
||||||
type: 'chunk',
|
type: 'chunk',
|
||||||
id: BEFORE_HYDRATION_SCRIPT_ID,
|
id: BEFORE_HYDRATION_SCRIPT_ID,
|
||||||
name: BEFORE_HYDRATION_SCRIPT_ID,
|
name: BEFORE_HYDRATION_SCRIPT_ID,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue