Supports hoisted scripts in the static build (#2414)
* Supports hoisted scripts in the static build * Adds a changeset * Update packages/astro/src/core/build/internal.ts Co-authored-by: Evan Boehs <evan@boehs.org> * Update based on feedback * Fix lint * Fix getting hoist script for Windows * Try with the pre compiler * use compiler 0.8.2 * update compiler version * update yarn.lock Co-authored-by: Evan Boehs <evan@boehs.org>
This commit is contained in:
parent
fda857eb22
commit
f2b8372c0c
18 changed files with 227 additions and 26 deletions
5
.changeset/sour-games-boil.md
Normal file
5
.changeset/sour-games-boil.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Adds support for hoisted scripts to the static build
|
|
@ -11,6 +11,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"astro": "^0.22.16",
|
"astro": "^0.22.16",
|
||||||
|
"preact": "~10.5.15",
|
||||||
"unocss": "^0.15.5",
|
"unocss": "^0.15.5",
|
||||||
"vite-imagetools": "^4.0.1"
|
"vite-imagetools": "^4.0.1"
|
||||||
}
|
}
|
||||||
|
|
2
examples/fast-build/src/components/ExternalHoisted.astro
Normal file
2
examples/fast-build/src/components/ExternalHoisted.astro
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<div id="external-hoist"></div>
|
||||||
|
<script type="module" hoist src="/src/scripts/external-hoist"></script>
|
13
examples/fast-build/src/components/InlineHoisted.astro
Normal file
13
examples/fast-build/src/components/InlineHoisted.astro
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<script type="module" hoist>
|
||||||
|
import { h, render } from 'preact';
|
||||||
|
|
||||||
|
|
||||||
|
const mount = document.querySelector('#inline-hoist');
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return h('strong', null, 'Hello again');
|
||||||
|
}
|
||||||
|
|
||||||
|
render(h(App), mount);
|
||||||
|
</script>
|
||||||
|
<div id="inline-hoist"></div>
|
|
@ -4,6 +4,8 @@ import grayscaleUrl from '../images/random.jpg?grayscale=true';
|
||||||
import Greeting from '../components/Greeting.vue';
|
import Greeting from '../components/Greeting.vue';
|
||||||
import Counter from '../components/Counter.vue';
|
import Counter from '../components/Counter.vue';
|
||||||
import { Code } from 'astro/components';
|
import { Code } from 'astro/components';
|
||||||
|
import InlineHoisted from '../components/InlineHoisted.astro';
|
||||||
|
import ExternalHoisted from '../components/ExternalHoisted.astro';
|
||||||
---
|
---
|
||||||
|
|
||||||
<html>
|
<html>
|
||||||
|
@ -44,5 +46,11 @@ import { Code } from 'astro/components';
|
||||||
<h1>Hydrated component</h1>
|
<h1>Hydrated component</h1>
|
||||||
<Counter client:idle />
|
<Counter client:idle />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h1>Hoisted scripts</h1>
|
||||||
|
<InlineHoisted />
|
||||||
|
<ExternalHoisted />
|
||||||
|
</section>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
2
examples/fast-build/src/scripts/external-hoist.ts
Normal file
2
examples/fast-build/src/scripts/external-hoist.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
const el = document.querySelector('#external-hoist');
|
||||||
|
el.textContent = `This was loaded externally`;
|
|
@ -15,6 +15,9 @@ export interface BuildInternals {
|
||||||
// A mapping to entrypoints (facadeId) to assets (styles) that are added.
|
// A mapping to entrypoints (facadeId) to assets (styles) that are added.
|
||||||
facadeIdToAssetsMap: Map<string, string[]>;
|
facadeIdToAssetsMap: Map<string, string[]>;
|
||||||
|
|
||||||
|
hoistedScriptIdToHoistedMap: Map<string, Set<string>>;
|
||||||
|
facadeIdToHoistedEntryMap: Map<string, string>;
|
||||||
|
|
||||||
// A mapping of specifiers like astro/client/idle.js to the hashed bundled name.
|
// A mapping of specifiers like astro/client/idle.js to the hashed bundled name.
|
||||||
// Used to render pages with the correct specifiers.
|
// Used to render pages with the correct specifiers.
|
||||||
entrySpecifierToBundleMap: Map<string, string>;
|
entrySpecifierToBundleMap: Map<string, string>;
|
||||||
|
@ -39,12 +42,18 @@ export function createBuildInternals(): BuildInternals {
|
||||||
// A mapping to entrypoints (facadeId) to assets (styles) that are added.
|
// A mapping to entrypoints (facadeId) to assets (styles) that are added.
|
||||||
const facadeIdToAssetsMap = new Map<string, string[]>();
|
const facadeIdToAssetsMap = new Map<string, string[]>();
|
||||||
|
|
||||||
|
// These are for tracking hoisted script bundling
|
||||||
|
const hoistedScriptIdToHoistedMap = new Map<string, Set<string>>();
|
||||||
|
const facadeIdToHoistedEntryMap = new Map<string, string>();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pureCSSChunks,
|
pureCSSChunks,
|
||||||
chunkToReferenceIdMap,
|
chunkToReferenceIdMap,
|
||||||
astroStyleMap,
|
astroStyleMap,
|
||||||
astroPageStyleMap,
|
astroPageStyleMap,
|
||||||
facadeIdToAssetsMap,
|
facadeIdToAssetsMap,
|
||||||
|
hoistedScriptIdToHoistedMap,
|
||||||
|
facadeIdToHoistedEntryMap,
|
||||||
entrySpecifierToBundleMap: new Map<string, string>(),
|
entrySpecifierToBundleMap: new Map<string, string>(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { getParamsAndProps } from '../ssr/index.js';
|
||||||
import { createResult } from '../ssr/result.js';
|
import { createResult } from '../ssr/result.js';
|
||||||
import { renderPage } from '../../runtime/server/index.js';
|
import { renderPage } from '../../runtime/server/index.js';
|
||||||
import { prepareOutDir } from './fs.js';
|
import { prepareOutDir } from './fs.js';
|
||||||
|
import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js';
|
||||||
|
|
||||||
export interface StaticBuildOptions {
|
export interface StaticBuildOptions {
|
||||||
allPages: AllPagesData;
|
allPages: AllPagesData;
|
||||||
|
@ -70,6 +71,12 @@ function* throttle(max: number, inPaths: string[]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getByFacadeId<T>(facadeId: string, map: Map<string, T>): T | undefined {
|
||||||
|
return map.get(facadeId) ||
|
||||||
|
// Check with a leading `/` because on Windows it doesn't have one.
|
||||||
|
map.get('/' + facadeId);
|
||||||
|
}
|
||||||
|
|
||||||
export async function staticBuild(opts: StaticBuildOptions) {
|
export async function staticBuild(opts: StaticBuildOptions) {
|
||||||
const { allPages, astroConfig } = opts;
|
const { allPages, astroConfig } = opts;
|
||||||
|
|
||||||
|
@ -91,7 +98,12 @@ export async function staticBuild(opts: StaticBuildOptions) {
|
||||||
jsInput.add(polyfill);
|
jsInput.add(polyfill);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build internals needed by the CSS plugin
|
||||||
|
const internals = createBuildInternals();
|
||||||
|
|
||||||
for (const [component, pageData] of Object.entries(allPages)) {
|
for (const [component, pageData] of Object.entries(allPages)) {
|
||||||
|
const astroModuleURL = new URL('./' + component, astroConfig.projectRoot);
|
||||||
|
const astroModuleId = astroModuleURL.pathname;
|
||||||
const [renderers, mod] = pageData.preload;
|
const [renderers, mod] = pageData.preload;
|
||||||
const metadata = mod.$$metadata;
|
const metadata = mod.$$metadata;
|
||||||
|
|
||||||
|
@ -104,18 +116,23 @@ export async function staticBuild(opts: StaticBuildOptions) {
|
||||||
...renderers.filter((renderer) => !!renderer.source).map((renderer) => renderer.source!),
|
...renderers.filter((renderer) => !!renderer.source).map((renderer) => renderer.source!),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Add hoisted scripts
|
||||||
|
const hoistedScripts = new Set(metadata.hoistedScriptPaths());
|
||||||
|
if(hoistedScripts.size) {
|
||||||
|
const moduleId = new URL('./hoisted.js', astroModuleURL + '/').pathname;
|
||||||
|
internals.hoistedScriptIdToHoistedMap.set(moduleId, hoistedScripts);
|
||||||
|
topLevelImports.add(moduleId);
|
||||||
|
}
|
||||||
|
|
||||||
for (const specifier of topLevelImports) {
|
for (const specifier of topLevelImports) {
|
||||||
jsInput.add(specifier);
|
jsInput.add(specifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
let astroModuleId = new URL('./' + component, astroConfig.projectRoot).pathname;
|
|
||||||
pageInput.add(astroModuleId);
|
pageInput.add(astroModuleId);
|
||||||
facadeIdToPageDataMap.set(astroModuleId, pageData);
|
facadeIdToPageDataMap.set(astroModuleId, pageData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build internals needed by the CSS plugin
|
|
||||||
const internals = createBuildInternals();
|
|
||||||
|
|
||||||
// Empty out the dist folder, if needed. Vite has a config for doing this
|
// Empty out the dist folder, if needed. Vite has a config for doing this
|
||||||
// but because we are running 2 vite builds in parallel, that would cause a race
|
// but because we are running 2 vite builds in parallel, that would cause a race
|
||||||
// condition, so we are doing it ourselves
|
// condition, so we are doing it ourselves
|
||||||
|
@ -189,6 +206,7 @@ async function clientBuild(opts: StaticBuildOptions, internals: BuildInternals,
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
vitePluginNewBuild(input, internals, 'js'),
|
vitePluginNewBuild(input, internals, 'js'),
|
||||||
|
vitePluginHoistedScripts(internals),
|
||||||
rollupPluginAstroBuildCSS({
|
rollupPluginAstroBuildCSS({
|
||||||
internals,
|
internals,
|
||||||
}),
|
}),
|
||||||
|
@ -249,16 +267,14 @@ async function generatePage(output: OutputChunk, opts: StaticBuildOptions, inter
|
||||||
|
|
||||||
let url = new URL('./' + output.fileName, astroConfig.dist);
|
let url = new URL('./' + output.fileName, astroConfig.dist);
|
||||||
const facadeId: string = output.facadeModuleId as string;
|
const facadeId: string = output.facadeModuleId as string;
|
||||||
let pageData =
|
let pageData = getByFacadeId<PageBuildData>(facadeId, facadeIdToPageDataMap);
|
||||||
facadeIdToPageDataMap.get(facadeId) ||
|
|
||||||
// Check with a leading `/` because on Windows it doesn't have one.
|
|
||||||
facadeIdToPageDataMap.get('/' + facadeId);
|
|
||||||
|
|
||||||
if (!pageData) {
|
if (!pageData) {
|
||||||
throw new Error(`Unable to find a PageBuildData for the Astro page: ${facadeId}. There are the PageBuilDatas we have ${Array.from(facadeIdToPageDataMap.keys()).join(', ')}`);
|
throw new Error(`Unable to find a PageBuildData for the Astro page: ${facadeId}. There are the PageBuilDatas we have ${Array.from(facadeIdToPageDataMap.keys()).join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let linkIds = internals.facadeIdToAssetsMap.get(facadeId) || [];
|
const linkIds = getByFacadeId<string[]>(facadeId, internals.facadeIdToAssetsMap) || [];
|
||||||
|
const hoistedId = getByFacadeId<string>(facadeId, internals.facadeIdToHoistedEntryMap) || null;
|
||||||
|
|
||||||
let compiledModule = await import(url.toString());
|
let compiledModule = await import(url.toString());
|
||||||
let Component = compiledModule.default;
|
let Component = compiledModule.default;
|
||||||
|
@ -267,6 +283,7 @@ async function generatePage(output: OutputChunk, opts: StaticBuildOptions, inter
|
||||||
pageData,
|
pageData,
|
||||||
internals,
|
internals,
|
||||||
linkIds,
|
linkIds,
|
||||||
|
hoistedId,
|
||||||
Component,
|
Component,
|
||||||
renderers,
|
renderers,
|
||||||
};
|
};
|
||||||
|
@ -288,13 +305,14 @@ interface GeneratePathOptions {
|
||||||
pageData: PageBuildData;
|
pageData: PageBuildData;
|
||||||
internals: BuildInternals;
|
internals: BuildInternals;
|
||||||
linkIds: string[];
|
linkIds: string[];
|
||||||
|
hoistedId: string | null;
|
||||||
Component: AstroComponentFactory;
|
Component: AstroComponentFactory;
|
||||||
renderers: Renderer[];
|
renderers: Renderer[];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) {
|
async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) {
|
||||||
const { astroConfig, logging, origin, pageNames, routeCache } = opts;
|
const { astroConfig, logging, origin, routeCache } = opts;
|
||||||
const { Component, internals, linkIds, pageData, renderers } = gopts;
|
const { Component, internals, linkIds, hoistedId, pageData, renderers } = gopts;
|
||||||
|
|
||||||
// This adds the page name to the array so it can be shown as part of stats.
|
// This adds the page name to the array so it can be shown as part of stats.
|
||||||
addPageName(pathname, opts);
|
addPageName(pathname, opts);
|
||||||
|
@ -316,8 +334,7 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G
|
||||||
debug(logging, 'generate', `Generating: ${pathname}`);
|
debug(logging, 'generate', `Generating: ${pathname}`);
|
||||||
|
|
||||||
const rootpath = new URL(astroConfig.buildOptions.site || 'http://localhost/').pathname;
|
const rootpath = new URL(astroConfig.buildOptions.site || 'http://localhost/').pathname;
|
||||||
const result = createResult({ astroConfig, logging, origin, params, pathname, renderers });
|
const links = new Set<SSRElement>(
|
||||||
result.links = new Set<SSRElement>(
|
|
||||||
linkIds.map((href) => ({
|
linkIds.map((href) => ({
|
||||||
props: {
|
props: {
|
||||||
rel: 'stylesheet',
|
rel: 'stylesheet',
|
||||||
|
@ -326,6 +343,14 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G
|
||||||
children: '',
|
children: '',
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
const scripts = hoistedId ? new Set<SSRElement>([{
|
||||||
|
props: {
|
||||||
|
type: 'module',
|
||||||
|
src: npath.posix.join(rootpath, hoistedId),
|
||||||
|
},
|
||||||
|
children: ''
|
||||||
|
}]) : new Set<SSRElement>();
|
||||||
|
const result = createResult({ astroConfig, logging, origin, params, pathname, renderers, links, scripts });
|
||||||
|
|
||||||
// Override the `resolve` method so that hydrated components are given the
|
// Override the `resolve` method so that hydrated components are given the
|
||||||
// hashed filepath to the component.
|
// hashed filepath to the component.
|
||||||
|
|
43
packages/astro/src/core/build/vite-plugin-hoisted-scripts.ts
Normal file
43
packages/astro/src/core/build/vite-plugin-hoisted-scripts.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import type { Plugin as VitePlugin } from '../vite';
|
||||||
|
import type { BuildInternals } from '../../core/build/internal.js';
|
||||||
|
|
||||||
|
function virtualHoistedEntry(id: string) {
|
||||||
|
return id.endsWith('.astro/hoisted.js') || id.endsWith('.md/hoisted.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function vitePluginHoistedScripts(internals: BuildInternals): VitePlugin {
|
||||||
|
return {
|
||||||
|
name: '@astro/rollup-plugin-astro-hoisted-scripts',
|
||||||
|
|
||||||
|
resolveId(id) {
|
||||||
|
if(virtualHoistedEntry(id)) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
load(id) {
|
||||||
|
if(virtualHoistedEntry(id)) {
|
||||||
|
let code = '';
|
||||||
|
for(let path of internals.hoistedScriptIdToHoistedMap.get(id)!) {
|
||||||
|
code += `import "${path}";`
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return void 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
async generateBundle(_options, bundle) {
|
||||||
|
// Find all page entry points and create a map of the entry point to the hashed hoisted script.
|
||||||
|
// This is used when we render so that we can add the script to the head.
|
||||||
|
for(const [id, output] of Object.entries(bundle)) {
|
||||||
|
if(output.type === 'chunk' && output.facadeModuleId && virtualHoistedEntry(output.facadeModuleId)) {
|
||||||
|
const facadeId = output.facadeModuleId!;
|
||||||
|
const filename = facadeId.slice(0, facadeId.length - "/hoisted.js".length);
|
||||||
|
internals.facadeIdToHoistedEntryMap.set(filename, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -219,7 +219,14 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO
|
||||||
if (!Component) throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
|
if (!Component) throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
|
||||||
if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
|
if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
|
||||||
|
|
||||||
const result = createResult({ astroConfig, logging, origin, params, pathname, renderers });
|
// Add hoisted script tags
|
||||||
|
const scripts = astroConfig.buildOptions.experimentalStaticBuild ?
|
||||||
|
new Set<SSRElement>(Array.from(mod.$$metadata.hoistedScriptPaths()).map(src => ({
|
||||||
|
props: { type: 'module', src },
|
||||||
|
children: ''
|
||||||
|
}))) : new Set<SSRElement>();
|
||||||
|
|
||||||
|
const result = createResult({ astroConfig, logging, origin, params, pathname, renderers, scripts });
|
||||||
// Resolves specifiers in the inline hydrated scripts, such as "@astrojs/renderer-preact/client.js"
|
// Resolves specifiers in the inline hydrated scripts, such as "@astrojs/renderer-preact/client.js"
|
||||||
result.resolve = async (s: string) => {
|
result.resolve = async (s: string) => {
|
||||||
// The legacy build needs these to remain unresolved so that vite HTML
|
// The legacy build needs these to remain unresolved so that vite HTML
|
||||||
|
|
|
@ -13,6 +13,8 @@ export interface CreateResultArgs {
|
||||||
params: Params;
|
params: Params;
|
||||||
pathname: string;
|
pathname: string;
|
||||||
renderers: Renderer[];
|
renderers: Renderer[];
|
||||||
|
links?: Set<SSRElement>;
|
||||||
|
scripts?: Set<SSRElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createResult(args: CreateResultArgs): SSRResult {
|
export function createResult(args: CreateResultArgs): SSRResult {
|
||||||
|
@ -23,8 +25,8 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
||||||
// calling the render() function will populate the object with scripts, styles, etc.
|
// calling the render() function will populate the object with scripts, styles, etc.
|
||||||
const result: SSRResult = {
|
const result: SSRResult = {
|
||||||
styles: new Set<SSRElement>(),
|
styles: new Set<SSRElement>(),
|
||||||
scripts: new Set<SSRElement>(),
|
scripts: args.scripts ?? new Set<SSRElement>(),
|
||||||
links: new Set<SSRElement>(),
|
links: args.links ?? new Set<SSRElement>(),
|
||||||
/** This function returns the `Astro` faux-global */
|
/** This function returns the `Astro` faux-global */
|
||||||
createAstro(astroGlobal: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null) {
|
createAstro(astroGlobal: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null) {
|
||||||
const site = new URL(origin);
|
const site = new URL(origin);
|
||||||
|
|
|
@ -18,7 +18,7 @@ interface CreateMetadataOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Metadata {
|
export class Metadata {
|
||||||
public fileURL: URL;
|
public mockURL: URL;
|
||||||
public modules: ModuleInfo[];
|
public modules: ModuleInfo[];
|
||||||
public hoisted: any[];
|
public hoisted: any[];
|
||||||
public hydratedComponents: any[];
|
public hydratedComponents: any[];
|
||||||
|
@ -31,12 +31,12 @@ export class Metadata {
|
||||||
this.hoisted = opts.hoisted;
|
this.hoisted = opts.hoisted;
|
||||||
this.hydratedComponents = opts.hydratedComponents;
|
this.hydratedComponents = opts.hydratedComponents;
|
||||||
this.hydrationDirectives = opts.hydrationDirectives;
|
this.hydrationDirectives = opts.hydrationDirectives;
|
||||||
this.fileURL = new URL(filePathname, 'http://example.com');
|
this.mockURL = new URL(filePathname, 'http://example.com');
|
||||||
this.metadataCache = new Map<any, ComponentMetadata | null>();
|
this.metadataCache = new Map<any, ComponentMetadata | null>();
|
||||||
}
|
}
|
||||||
|
|
||||||
resolvePath(specifier: string): string {
|
resolvePath(specifier: string): string {
|
||||||
return specifier.startsWith('.') ? new URL(specifier, this.fileURL).pathname : specifier;
|
return specifier.startsWith('.') ? new URL(specifier, this.mockURL).pathname : specifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPath(Component: any): string | null {
|
getPath(Component: any): string | null {
|
||||||
|
@ -81,6 +81,16 @@ export class Metadata {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
* hoistedScriptPaths() {
|
||||||
|
for(const metadata of this.deepMetadata()) {
|
||||||
|
let i = 0, pathname = metadata.mockURL.pathname;
|
||||||
|
while(i < metadata.hoisted.length) {
|
||||||
|
yield `${pathname}?astro&type=script&index=${i}`;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private *deepMetadata(): Generator<Metadata, void, unknown> {
|
private *deepMetadata(): Generator<Metadata, void, unknown> {
|
||||||
// Yield self
|
// Yield self
|
||||||
yield this;
|
yield this;
|
||||||
|
|
|
@ -19,6 +19,15 @@ interface AstroPluginOptions {
|
||||||
|
|
||||||
/** Transform .astro files for Vite */
|
/** Transform .astro files for Vite */
|
||||||
export default function astro({ config, logging }: AstroPluginOptions): vite.Plugin {
|
export default function astro({ config, logging }: AstroPluginOptions): vite.Plugin {
|
||||||
|
function normalizeFilename(filename: string) {
|
||||||
|
if (filename.startsWith('/@fs')) {
|
||||||
|
filename = filename.slice('/@fs'.length);
|
||||||
|
} else if (filename.startsWith('/') && !ancestor(filename, config.projectRoot.pathname)) {
|
||||||
|
filename = new URL('.' + filename, config.projectRoot).pathname;
|
||||||
|
}
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
let viteTransform: TransformHook;
|
let viteTransform: TransformHook;
|
||||||
return {
|
return {
|
||||||
name: '@astrojs/vite-plugin-astro',
|
name: '@astrojs/vite-plugin-astro',
|
||||||
|
@ -37,23 +46,37 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
|
||||||
let { filename, query } = parseAstroRequest(id);
|
let { filename, query } = parseAstroRequest(id);
|
||||||
if (query.astro) {
|
if (query.astro) {
|
||||||
if (query.type === 'style') {
|
if (query.type === 'style') {
|
||||||
if (filename.startsWith('/@fs')) {
|
|
||||||
filename = filename.slice('/@fs'.length);
|
|
||||||
} else if (filename.startsWith('/') && !ancestor(filename, config.projectRoot.pathname)) {
|
|
||||||
filename = new URL('.' + filename, config.projectRoot).pathname;
|
|
||||||
}
|
|
||||||
const transformResult = await cachedCompilation(config, filename, null, viteTransform, opts);
|
|
||||||
|
|
||||||
if (typeof query.index === 'undefined') {
|
if (typeof query.index === 'undefined') {
|
||||||
throw new Error(`Requests for Astro CSS must include an index.`);
|
throw new Error(`Requests for Astro CSS must include an index.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const transformResult = await cachedCompilation(config,
|
||||||
|
normalizeFilename(filename), null, viteTransform, opts);
|
||||||
const csses = transformResult.css;
|
const csses = transformResult.css;
|
||||||
const code = csses[query.index];
|
const code = csses[query.index];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code,
|
code,
|
||||||
};
|
};
|
||||||
|
} else if(query.type === 'script') {
|
||||||
|
if(typeof query.index === 'undefined') {
|
||||||
|
throw new Error(`Requests for hoisted scripts must include an index`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformResult = await cachedCompilation(config,
|
||||||
|
normalizeFilename(filename), null, viteTransform, opts);
|
||||||
|
const scripts = transformResult.scripts;
|
||||||
|
const hoistedScript = scripts[query.index];
|
||||||
|
|
||||||
|
if(!hoistedScript) {
|
||||||
|
throw new Error(`No hoisted script at index ${query.index}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: hoistedScript.type === 'inline' ?
|
||||||
|
hoistedScript.code! :
|
||||||
|
`import "${hoistedScript.src!}";`
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
packages/astro/test/fixtures/static-build/src/components/ExternalHoisted.astro
vendored
Normal file
2
packages/astro/test/fixtures/static-build/src/components/ExternalHoisted.astro
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<div id="external-hoist"></div>
|
||||||
|
<script type="module" hoist src="/src/scripts/external-hoist"></script>
|
13
packages/astro/test/fixtures/static-build/src/components/InlineHoisted.astro
vendored
Normal file
13
packages/astro/test/fixtures/static-build/src/components/InlineHoisted.astro
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<script type="module" hoist>
|
||||||
|
import { h, render } from 'preact';
|
||||||
|
|
||||||
|
|
||||||
|
const mount = document.querySelector('#inline-hoist');
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return h('strong', null, 'Hello again');
|
||||||
|
}
|
||||||
|
|
||||||
|
render(h(App), mount);
|
||||||
|
</script>
|
||||||
|
<div id="inline-hoist"></div>
|
17
packages/astro/test/fixtures/static-build/src/pages/hoisted.astro
vendored
Normal file
17
packages/astro/test/fixtures/static-build/src/pages/hoisted.astro
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
import InlineHoisted from '../components/InlineHoisted.astro';
|
||||||
|
import ExternalHoisted from '../components/ExternalHoisted.astro';
|
||||||
|
---
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Demo app</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section>
|
||||||
|
<h1>Hoisted scripts</h1>
|
||||||
|
<InlineHoisted />
|
||||||
|
<ExternalHoisted />
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
</html>
|
2
packages/astro/test/fixtures/static-build/src/scripts/external-hoist.ts
vendored
Normal file
2
packages/astro/test/fixtures/static-build/src/scripts/external-hoist.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
const element: HTMLElement = document.querySelector('#external-hoist');
|
||||||
|
element.textContent = `This was loaded externally`;
|
|
@ -77,4 +77,21 @@ describe('Static build', () => {
|
||||||
expect(found).to.equal(true, 'Did not find shared CSS module code');
|
expect(found).to.equal(true, 'Did not find shared CSS module code');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Hoisted scripts', () => {
|
||||||
|
it('Get bundled together on the page', async () => {
|
||||||
|
const html = await fixture.readFile('/hoisted/index.html');
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
expect($('script[type="module"]').length).to.equal(1, 'hoisted script added');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Do not get added to the wrong page', async () => {
|
||||||
|
const hoistedHTML = await fixture.readFile('/hoisted/index.html');
|
||||||
|
const $ = cheerio.load(hoistedHTML);
|
||||||
|
const href = $('script[type="module"]').attr('src');
|
||||||
|
const indexHTML = await fixture.readFile('/index.html');
|
||||||
|
const $$ = cheerio.load(indexHTML);
|
||||||
|
expect($$(`script[src="${href}"]`).length).to.equal(0, 'no script added to different page');
|
||||||
|
})
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue