Skip to content

Commit 18c55e1

Browse files
authored
Stub out .astro imports in client modules (#14751)
1 parent 5bc37fd commit 18c55e1

File tree

5 files changed

+94
-10
lines changed

5 files changed

+94
-10
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"astro": patch
3+
---
4+
5+
Fixes hydration of client components when running the dev server and using a barrel file that re-exports both Astro and UI framework components.

packages/astro/src/vite-plugin-astro/index.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl
205205
return null;
206206
}
207207
},
208-
async transform(source, id) {
208+
async transform(source, id, options) {
209209
if (hasSpecialQueries(id)) return;
210210

211211
const parsedId = parseAstroRequest(id);
@@ -228,6 +228,22 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl
228228
}
229229

230230
const filename = normalizePath(parsedId.filename);
231+
232+
// If an Astro component is imported in code used on the client, we return an empty
233+
// module so that Vite doesn’t bundle the server-side Astro code for the client.
234+
if (!options?.ssr) {
235+
return {
236+
code: `export default import.meta.env.DEV
237+
? () => {
238+
throw new Error(
239+
'Astro components cannot be used in the browser.\\nTried to render "${filename}".'
240+
);
241+
}
242+
: {};`,
243+
meta: { vite: { lang: 'ts' } },
244+
};
245+
}
246+
231247
const transformResult = await compile(source, filename);
232248

233249
const astroMetadata: AstroPluginMetadata['astro'] = {
Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,70 @@
11
import assert from 'node:assert/strict';
2-
import { before, describe, it } from 'node:test';
2+
import { after, before, describe, it } from 'node:test';
33
import { loadFixture } from './test-utils.js';
44

55
describe('Component bundling', () => {
6+
/** @type {import('./test-utils.js').Fixture} */
67
let fixture;
78

89
before(async () => {
910
fixture = await loadFixture({ root: './fixtures/astro-component-bundling/' });
10-
await fixture.build();
1111
});
1212

13-
it('should treeshake FooComponent', async () => {
14-
const astroChunkDir = await fixture.readdir('/_astro');
15-
const manyComponentsChunkName = astroChunkDir.find((chunk) =>
16-
chunk.startsWith('ManyComponents'),
17-
);
18-
const manyComponentsChunkContent = await fixture.readFile(`/_astro/${manyComponentsChunkName}`);
19-
assert.equal(manyComponentsChunkContent.includes('FooComponent'), false);
13+
describe('dev', () => {
14+
/** @type {import('./test-utils.js').DevServer} */
15+
let devServer;
16+
17+
before(async () => {
18+
devServer = await fixture.startDevServer();
19+
});
20+
21+
after(async () => {
22+
await devServer.stop();
23+
});
24+
25+
it('should not include Astro components in client bundles', async () => {
26+
const importedComponent = await fixture.fetch('/src/components/AstroComponent.astro');
27+
const moduleContent = await importedComponent.text();
28+
assert(
29+
moduleContent.includes('Astro components cannot be used in the browser.'),
30+
'Astro component imported from client should include error text in dev.',
31+
);
32+
assert(
33+
moduleContent.length < 3500,
34+
'Module content should be small and not include full server-side code.',
35+
);
36+
assert(
37+
!moduleContent.includes('import '),
38+
'Astro component imported from client should not include import statements.',
39+
);
40+
});
41+
});
42+
43+
describe('build', () => {
44+
before(async () => {
45+
await fixture.build();
46+
});
47+
48+
it('should treeshake FooComponent', async () => {
49+
const astroChunkDir = await fixture.readdir('/_astro');
50+
const manyComponentsChunkName = astroChunkDir.find((chunk) =>
51+
chunk.startsWith('ManyComponents'),
52+
);
53+
const manyComponentsChunkContent = await fixture.readFile(
54+
`/_astro/${manyComponentsChunkName}`,
55+
);
56+
assert.equal(manyComponentsChunkContent.includes('FooComponent'), false);
57+
});
58+
59+
it('should not include Astro components in client bundles', async () => {
60+
const html = await fixture.readFile('/astro-in-client/index.html');
61+
const match = /<script.+<\/script>/.exec(html);
62+
assert(match, 'Expected a <script> tag to be present');
63+
assert.match(
64+
match[0],
65+
/^<script type="module">const \w=\{\};console.log\(\w\);<\/script>$/,
66+
'Astro component on the client should be an empty object in prod',
67+
);
68+
});
2069
});
2170
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<p>Example component</p>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
import AstroComponent from '../components/AstroComponent.astro';
3+
---
4+
<html>
5+
<head><title>Component bundling</title></head>
6+
<body>
7+
<AstroComponent />
8+
<script>
9+
import AstroComponent from '../components/AstroComponent.astro';
10+
console.log(AstroComponent);
11+
</script>
12+
</body>
13+
</html>

0 commit comments

Comments
 (0)