diff --git a/.changeset/young-trainers-rule.md b/.changeset/young-trainers-rule.md
new file mode 100644
index 000000000..fb248edeb
--- /dev/null
+++ b/.changeset/young-trainers-rule.md
@@ -0,0 +1,14 @@
+---
+'docs': patch
+'astro': patch
+---
+
+# Hoisted scripts
+
+This change adds support for hoisted scripts, allowing you to bundle scripts together for a page and hoist them to the top (in the head):
+
+```astro
+
+```
\ No newline at end of file
diff --git a/docs/src/pages/core-concepts/astro-components.md b/docs/src/pages/core-concepts/astro-components.md
index 1faa65d0f..2747b33cb 100644
--- a/docs/src/pages/core-concepts/astro-components.md
+++ b/docs/src/pages/core-concepts/astro-components.md
@@ -275,6 +275,39 @@ const items = ["Dog", "Cat", "Platipus"];
```
+### Hoisted scripts
+
+By default Astro does not make any assumptions on how you want scripts to be served, so if you add a `
+```
+
+Or it can link to an external JavaScript file:
+
+```astro
+
+```
+
+A hoisted script can be within a page or a component, and no matter how many times the component is used, the script will only be added once:
+
+```astro
+---
+import TwitterTimeline from '../components/TwitterTimeline.astro';
+---
+
+<-- The script will only be injected into the head once. -->
+
+
+
+```
+
## Comparing `.astro` versus `.jsx`
`.astro` files can end up looking very similar to `.jsx` files, but there are a few key differences. Here's a comparison between the two formats.
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 2e4e018c0..0d1f1c0de 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -20,12 +20,24 @@ export interface JsxItem {
jsx: string;
}
+export interface InlineScriptInfo {
+ content: string;
+}
+
+export interface ExternalScriptInfo {
+ src: string;
+}
+
+export type ScriptInfo = InlineScriptInfo | ExternalScriptInfo;
+
export interface TransformResult {
script: string;
imports: string[];
exports: string[];
+ components: string[];
html: string;
css?: string;
+ hoistedScripts: ScriptInfo[];
getStaticPaths?: string;
hasCustomElements: boolean;
customElementCandidates: Map;
@@ -56,6 +68,8 @@ export interface BuildFile {
contentType: string;
/** Encoding */
encoding?: 'utf8';
+ /** Extracted scripts */
+ hoistedScripts?: ScriptInfo[];
}
/** Mapping of every URL and its required assets. All URLs are absolute relative to the project. */
@@ -70,6 +84,8 @@ export interface PageDependencies {
css: Set;
/** Images needed for page. Can be loaded via CSS, , or otherwise. */
images: Set;
+ /** Async hoisted Javascript */
+ hoistedJS: Map;
}
export interface RSSFunctionArgs {
diff --git a/packages/astro/src/build.ts b/packages/astro/src/build.ts
index 28c9c90b7..64a7d8d86 100644
--- a/packages/astro/src/build.ts
+++ b/packages/astro/src/build.ts
@@ -7,10 +7,11 @@ import mime from 'mime';
import path from 'path';
import { performance } from 'perf_hooks';
import glob from 'tiny-glob';
+import hash from 'shorthash';
import { fileURLToPath } from 'url';
-import type { AstroConfig, BuildOutput, BundleMap, PageDependencies, RouteData, RuntimeMode } from './@types/astro';
+import type { AstroConfig, BuildOutput, BundleMap, PageDependencies, RouteData, RuntimeMode, ScriptInfo } from './@types/astro';
import { bundleCSS } from './build/bundle/css.js';
-import { bundleJS, collectJSImports } from './build/bundle/js.js';
+import { bundleJS, bundleHoistedJS, collectJSImports } from './build/bundle/js.js';
import { buildStaticPage, getStaticPathsForPage } from './build/page.js';
import { generateSitemap } from './build/sitemap.js';
import { collectBundleStats, logURLStats, mapBundleStatsToURLStats } from './build/stats.js';
@@ -139,6 +140,7 @@ ${stack}
const pageDeps = findDeps(buildState[id].contents as string, {
astroConfig,
srcPath: buildState[id].srcPath,
+ id
});
depTree[id] = pageDeps;
@@ -171,11 +173,12 @@ ${stack}
* Bundle CSS, and anything else that can happen in memory (for now, JS bundling happens after writing to disk)
*/
info(logging, 'build', yellow('! optimizing css...'));
- timer.prebundle = performance.now();
+ timer.prebundleCSS = performance.now();
await Promise.all([
bundleCSS({ buildState, astroConfig, logging, depTree }).then(() => {
- debug(logging, 'build', `bundled CSS [${stopTimer(timer.prebundle)}]`);
+ debug(logging, 'build', `bundled CSS [${stopTimer(timer.prebundleCSS)}]`);
}),
+ bundleHoistedJS({ buildState, astroConfig, logging, depTree, runtime: astroRuntime, dist: astroConfig.dist })
// TODO: optimize images?
]);
// TODO: minify HTML?
@@ -269,18 +272,31 @@ ${stack}
}
/** Given an HTML string, collect and
tags */
-export function findDeps(html: string, { astroConfig, srcPath }: { astroConfig: AstroConfig; srcPath: URL }): PageDependencies {
+export function findDeps(html: string, { astroConfig, srcPath }: { astroConfig: AstroConfig; srcPath: URL, id: string }): PageDependencies {
const pageDeps: PageDependencies = {
js: new Set(),
css: new Set(),
images: new Set(),
+ hoistedJS: new Map(),
};
const $ = cheerio.load(html);
$('script').each((_i, el) => {
const src = $(el).attr('src');
- if (src) {
+ const hoist = $(el).attr('data-astro') === 'hoist';
+ if(hoist) {
+ if(src) {
+ pageDeps.hoistedJS.set(src, {
+ src
+ });
+ } else {
+ let content = $(el).html() || '';
+ pageDeps.hoistedJS.set(`astro-virtual:${hash.unique(content)}`, {
+ content
+ });
+ }
+ } else if (src) {
if (isRemoteOrEmbedded(src)) return;
pageDeps.js.add(getDistPath(src, { astroConfig, srcPath }));
} else {
diff --git a/packages/astro/src/build/bundle/js.ts b/packages/astro/src/build/bundle/js.ts
index 4deecb30a..dfab05b1d 100644
--- a/packages/astro/src/build/bundle/js.ts
+++ b/packages/astro/src/build/bundle/js.ts
@@ -1,12 +1,15 @@
import type { InputOptions, OutputOptions, OutputChunk } from 'rollup';
-import type { BuildOutput } from '../../@types/astro';
+import type { AstroConfig, BundleMap, BuildOutput, ScriptInfo, InlineScriptInfo } from '../../@types/astro';
import type { AstroRuntime } from '../../runtime';
+import type { LogOptions } from '../../logger.js';
import { fileURLToPath } from 'url';
import { rollup } from 'rollup';
import { terser } from 'rollup-plugin-terser';
-import { createBundleStats, addBundleStats, BundleStatsMap } from '../stats.js';
+import { createBundleStats, addBundleStats, BundleStatsMap } from '../stats.js'
import { IS_ASTRO_FILE_URL } from '../util.js';
+import cheerio from 'cheerio';
+import path from 'path';
interface BundleOptions {
dist: URL;
@@ -22,6 +25,161 @@ export function collectJSImports(buildState: BuildOutput): Set {
return imports;
}
+function pageUrlToVirtualJSEntry(pageUrl: string) {
+ return 'astro-virtual:' + pageUrl.replace(/.html$/, '').replace(/^\./, '') + '.js';
+}
+
+export async function bundleHoistedJS({
+ buildState,
+ astroConfig,
+ logging,
+ depTree,
+ dist,
+ runtime
+}: {
+ astroConfig: AstroConfig;
+ buildState: BuildOutput;
+ logging: LogOptions;
+ depTree: BundleMap;
+ dist: URL;
+ runtime: AstroRuntime;
+}) {
+ const sortedPages = Object.keys(depTree); // these were scanned in parallel; sort to create somewhat deterministic order
+ sortedPages.sort((a, b) => a.localeCompare(b, 'en', { numeric: true }));
+
+ /**
+ * 1. Go over sorted pages and create a virtual module for all of its dependencies
+ */
+ const entryImports: string[] = [];
+ const virtualScripts = new Map();
+ const pageToEntryMap = new Map();
+
+ for(let pageUrl of sortedPages) {
+ const hoistedJS = depTree[pageUrl].hoistedJS;
+ if(hoistedJS.size) {
+ for(let [url, scriptInfo] of hoistedJS) {
+ if(virtualScripts.has(url) || !url.startsWith('astro-virtual:')) continue;
+ virtualScripts.set(url, scriptInfo);
+ }
+ const entryURL = pageUrlToVirtualJSEntry(pageUrl);
+ const entryJS = Array.from(hoistedJS.keys()).map(url => `import '${url}';`).join('\n');
+ virtualScripts.set(entryURL, {
+ content: entryJS
+ });
+ entryImports.push(entryURL);
+ pageToEntryMap.set(pageUrl, entryURL);
+ }
+ }
+
+ if(!entryImports.length) {
+ // There are no hoisted scripts, bail
+ return;
+ }
+
+ /**
+ * 2. Run the bundle to bundle each pages JS into a single bundle (with shared content)
+ */
+ const inputOptions: InputOptions = {
+ input: entryImports,
+ plugins: [
+ {
+ name: 'astro:build',
+ resolveId(source: string, imported?: string) {
+ if(virtualScripts.has(source)) {
+ return source;
+ }
+ if (source.startsWith('/')) {
+ return source;
+ }
+
+
+ if (imported) {
+ const outUrl = new URL(source, 'http://example.com' + imported);
+ return outUrl.pathname;
+ }
+
+ return null;
+ },
+ async load(id: string) {
+ if(virtualScripts.has(id)) {
+ let info = virtualScripts.get(id) as InlineScriptInfo;
+ return info.content;
+ }
+
+
+ const result = await runtime.load(id);
+
+ if (result.statusCode !== 200) {
+ return null;
+ }
+
+ return result.contents.toString('utf-8');
+ },
+ },
+ ],
+ };
+
+ const build = await rollup(inputOptions);
+
+ const outputOptions: OutputOptions = {
+ dir: fileURLToPath(dist),
+ format: 'esm',
+ exports: 'named',
+ entryFileNames(chunk) {
+ const { facadeModuleId } = chunk;
+ if (!facadeModuleId) throw new Error(`facadeModuleId missing: ${chunk.name}`);
+ return facadeModuleId.substr('astro-virtual:/'.length, facadeModuleId.length - 'astro-virtual:/'.length - 3 /* .js */)
+ + '-[hash].js';
+ },
+ plugins: [
+ // We are using terser for the demo, but might switch to something else long term
+ // Look into that rather than adding options here.
+ terser(),
+ ],
+ };
+
+ const { output } = await build.write(outputOptions);
+
+ /**
+ * 3. Get a mapping of the virtual filename to the chunk file name
+ */
+ const entryToChunkFileName = new Map();
+ output.forEach((chunk) => {
+ const { fileName, facadeModuleId, isEntry } = chunk as OutputChunk;
+ if(!facadeModuleId || !isEntry) return;
+ entryToChunkFileName.set(facadeModuleId, fileName);
+ });
+
+ /**
+ * 4. Update the original HTML with the new chunk scripts
+ */
+ Object.keys(buildState).forEach((id) => {
+ if (buildState[id].contentType !== 'text/html') return;
+
+ const entryVirtualURL = pageUrlToVirtualJSEntry(id);
+ let hasHoisted = false;
+ const $ = cheerio.load(buildState[id].contents);
+ $('script[data-astro="hoist"]').each((i, el) => {
+ hasHoisted = true;
+ if(i === 0) {
+ let chunkName = entryToChunkFileName.get(entryVirtualURL);
+ if (!chunkName) return;
+ let chunkPathname = '/' + chunkName;
+ let relLink = path.relative(path.dirname(id), chunkPathname);
+ $(el).attr('src', relLink.startsWith('.') ? relLink : './' + relLink);
+ $(el).removeAttr('data-astro');
+ $(el).html('');
+ } else {
+ $(el).remove();
+ }
+ });
+
+ if(hasHoisted) {
+ (buildState[id] as any).contents = $.html(); // save updated HTML in global buildState
+ }
+ });
+}
+
/** Bundle JS action */
export async function bundleJS(imports: Set, { astroRuntime, dist }: BundleOptions): Promise {
const ROOT = 'astro:root';
@@ -42,6 +200,7 @@ export async function bundleJS(imports: Set, { astroRuntime, dist }: Bun
if (source.startsWith('/')) {
return source;
}
+
if (imported) {
const outUrl = new URL(source, 'http://example.com' + imported);
diff --git a/packages/astro/src/compiler/codegen/index.ts b/packages/astro/src/compiler/codegen/index.ts
index 446fa760b..f915a39bd 100644
--- a/packages/astro/src/compiler/codegen/index.ts
+++ b/packages/astro/src/compiler/codegen/index.ts
@@ -1,6 +1,6 @@
import type { Ast, Script, Style, TemplateNode, Expression } from '@astrojs/parser';
import type { CompileOptions } from '../../@types/compiler';
-import type { AstroConfig, TransformResult, ComponentInfo, Components } from '../../@types/astro';
+import type { AstroConfig, TransformResult, ComponentInfo, Components, ScriptInfo } from '../../@types/astro';
import type { ImportDeclaration, ExportNamedDeclaration, VariableDeclarator, Identifier, ImportDefaultSpecifier } from '@babel/types';
import type { Attribute } from './interfaces';
import eslexer from 'es-module-lexer';
@@ -316,6 +316,7 @@ interface CompileResult {
interface CodegenState {
components: Components;
css: string[];
+ hoistedScripts: ScriptInfo[];
filename: string;
fileID: string;
markers: {
@@ -672,6 +673,19 @@ async function compileHtml(enterNode: TemplateNode, state: CodegenState, compile
buffers[curr] += `h(__astro_slot_content, { name: ${attributes.slot} },`;
paren++;
}
+ if(attributes.hoist) {
+ if(attributes.src) {
+ state.hoistedScripts.push({
+ src: attributes.src.substr(1, attributes.src.length - 2)
+ });
+ } else if(node.children && node.children.length === 1 && node.children[0].type === 'Text') {
+ state.hoistedScripts.push({
+ content: node.children[0].data
+ });
+ }
+ this.skip();
+ return;
+ }
buffers[curr] += `h("${name}", ${generateAttributes(attributes)},`;
paren++;
return;
@@ -887,6 +901,7 @@ export async function codegen(ast: Ast, { compileOptions, filename, fileID }: Co
fileID,
components: new Map(),
css: [],
+ hoistedScripts: [],
markers: {
insideMarkdown: false,
},
@@ -909,6 +924,8 @@ export async function codegen(ast: Ast, { compileOptions, filename, fileID }: Co
exports: Array.from(state.exportStatements),
html,
css: state.css.length ? state.css.join('\n\n') : undefined,
+ hoistedScripts: state.hoistedScripts,
+ components: Array.from(state.components.keys()),
getStaticPaths,
hasCustomElements: Boolean(ast.meta.features & FEATURE_CUSTOM_ELEMENT),
customElementCandidates: state.customElementCandidates,
diff --git a/packages/astro/src/compiler/index.ts b/packages/astro/src/compiler/index.ts
index ede2a62f2..6409c5825 100644
--- a/packages/astro/src/compiler/index.ts
+++ b/packages/astro/src/compiler/index.ts
@@ -153,6 +153,9 @@ ${result.getStaticPaths || ''}
// \`__render()\`: Render the contents of the Astro module.
import { h, Fragment } from 'astro/dist/internal/h.js';
+import { __astro_hoisted_scripts } from 'astro/dist/internal/__astro_hoisted_scripts.js';
+
+const __astroScripts = __astro_hoisted_scripts([${result.components.map(n => `typeof ${n} !== 'undefined' && ${n}`)}], ${JSON.stringify(result.hoistedScripts)});
const __astroInternal = Symbol('astro.internal');
const __astroContext = Symbol.for('astro.context');
async function __render(props, ...children) {
@@ -165,6 +168,10 @@ async function __render(props, ...children) {
value: (props[__astroContext] && props[__astroContext].pageCSS) || [],
enumerable: true
},
+ pageScripts: {
+ value: (props[__astroContext] && props[__astroContext].pageScripts) || [],
+ enumerable: true
+ },
isPage: {
value: (props[__astroInternal] && props[__astroInternal].isPage) || false,
enumerable: true
@@ -178,11 +185,11 @@ async function __render(props, ...children) {
${result.script}
return h(Fragment, null, ${result.html});
}
-export default { isAstroComponent: true, __render };
+export default { isAstroComponent: true, __render, [Symbol.for('astro.hoistedScripts')]: __astroScripts };
// \`__renderPage()\`: Render the contents of the Astro module as a page. This is a special flow,
// triggered by loading a component directly by URL.
-export async function __renderPage({request, children, props, css}) {
+export async function __renderPage({request, children, props, css, scripts}) {
const currentChild = {
isAstroComponent: true,
layout: typeof __layout === 'undefined' ? undefined : __layout,
@@ -198,6 +205,7 @@ export async function __renderPage({request, children, props, css}) {
pageCSS: css,
request,
createAstroRootUID(seed) { return seed + astroRootUIDCounter++; },
+ pageScripts: scripts,
},
writable: false,
enumerable: false
@@ -227,7 +235,6 @@ export async function __renderPage({request, children, props, css}) {
};
${result.exports.join('\n')}
-
`;
return {
diff --git a/packages/astro/src/compiler/transform/head.ts b/packages/astro/src/compiler/transform/head.ts
index f277b56f1..ff707547a 100644
--- a/packages/astro/src/compiler/transform/head.ts
+++ b/packages/astro/src/compiler/transform/head.ts
@@ -115,6 +115,115 @@ export default function (opts: TransformOptions): Transformer {
},
],
},
+ {
+ start: 0,
+ end: 0,
+ type: 'Expression',
+ codeChunks: ['Astro.pageScripts.map(script => (', '))'],
+ children: [
+ {
+ start: 0,
+ end: 0,
+ type: 'Expression',
+ codeChunks: ['script.src ? (', ') : (', ')'],
+ children: [
+ {
+ type: 'Element',
+ name: 'script',
+ attributes: [
+ {
+ type: 'Attribute',
+ name: 'type',
+ value: [
+ {
+ type: 'Text',
+ raw: 'module',
+ data: 'module'
+ }
+ ]
+ },
+ {
+ type: 'Attribute',
+ name: 'src',
+ value: [
+ {
+ start: 0,
+ end: 0,
+ type: 'MustacheTag',
+ expression: {
+ start: 0,
+ end: 0,
+ type: 'Expression',
+ codeChunks: ['script.src'],
+ children: [],
+ },
+ }
+ ]
+ },
+ {
+ type: 'Attribute',
+ name: 'data-astro',
+ value: [
+ {
+ type: 'Text',
+ raw: 'hoist',
+ data: 'hoist'
+ }
+ ]
+ }
+ ],
+ start: 0,
+ end: 0,
+ children: [],
+ },
+ {
+ type: 'Element',
+ name: 'script',
+ attributes: [
+ {
+ type: 'Attribute',
+ name: 'type',
+ value: [
+ {
+ type: 'Text',
+ raw: 'module',
+ data: 'module'
+ }
+ ]
+ },
+ {
+ type: 'Attribute',
+ name: 'data-astro',
+ value: [
+ {
+ type: 'Text',
+ raw: 'hoist',
+ data: 'hoist'
+ }
+ ]
+ }
+ ],
+ start: 0,
+ end: 0,
+ children: [
+ {
+ start: 0,
+ end: 0,
+ type: 'MustacheTag',
+ expression: {
+ start: 0,
+ end: 0,
+ type: 'Expression',
+ codeChunks: ['script.content'],
+ children: [],
+ },
+ }
+ ],
+ },
+ ]
+ }
+ ],
+ },
],
});
diff --git a/packages/astro/src/internal/__astro_hoisted_scripts.ts b/packages/astro/src/internal/__astro_hoisted_scripts.ts
new file mode 100644
index 000000000..4899ca60b
--- /dev/null
+++ b/packages/astro/src/internal/__astro_hoisted_scripts.ts
@@ -0,0 +1,37 @@
+import type { ScriptInfo } from '../@types/astro';
+
+const sym = Symbol.for('astro.hoistedScripts');
+
+interface ComponentThatMaybeHasHoistedScripts {
+ [sym]: ScriptInfo[]
+}
+
+/**
+ * Takes all of the components this component uses and combines them with its
+ * own scripts and flattens it to a deduped list.
+ * The page component will have an array of all scripts used by all child components and itself.
+ */
+function hoistedScripts(Components: ComponentThatMaybeHasHoistedScripts[], scripts: ScriptInfo[]) {
+ const flatScripts = [];
+
+ const allScripts: ScriptInfo[] = Components.map(c => c && c[sym])
+ .filter(a => a)
+ .concat(scripts)
+ .flatMap(a => a);
+
+ const visitedSource = new Set();
+ for(let script of allScripts) {
+ if(!('src' in script)) {
+ flatScripts.push(script);
+ } else if(!visitedSource.has(script.src)) {
+ flatScripts.push(script);
+ visitedSource.add(script.src);
+ }
+ }
+
+ return flatScripts;
+}
+
+export {
+ hoistedScripts as __astro_hoisted_scripts
+};
\ No newline at end of file
diff --git a/packages/astro/src/runtime.ts b/packages/astro/src/runtime.ts
index 4145753ed..ed8f96e9e 100644
--- a/packages/astro/src/runtime.ts
+++ b/packages/astro/src/runtime.ts
@@ -163,6 +163,7 @@ async function load(config: AstroRuntimeConfig, rawPathname: string | undefined)
children: [],
props: pageProps,
css: Array.isArray(mod.css) ? mod.css : typeof mod.css === 'string' ? [mod.css] : [],
+ scripts: mod.exports.default[Symbol.for('astro.hoistedScripts')]
})) as string;
return {
diff --git a/packages/astro/test/astro-scripts.test.js b/packages/astro/test/astro-scripts.test.js
new file mode 100644
index 000000000..4bc1118df
--- /dev/null
+++ b/packages/astro/test/astro-scripts.test.js
@@ -0,0 +1,62 @@
+import { suite } from 'uvu';
+import * as assert from 'uvu/assert';
+import { setup, setupBuild } from './helpers.js';
+import { doc } from './test-utils.js';
+import path from 'path';
+
+const Scripts = suite('Hoisted scripts');
+
+setup(Scripts, './fixtures/astro-scripts');
+setupBuild(Scripts, './fixtures/astro-scripts');
+
+Scripts('Moves external scripts up', async ({ runtime }) => {
+ const result = await runtime.load('/external');
+ if (result.error) throw new Error(result.error);
+ assert.equal(result.statusCode, 200);
+ const html = result.contents;
+
+ const $ = doc(html);
+ assert.equal($('head script[type="module"][data-astro="hoist"]').length, 2);
+ assert.equal($('body script').length, 0);
+});
+
+Scripts('Moves inline scripts up', async ({ runtime }) => {
+ const result = await runtime.load('/inline');
+ if (result.error) throw new Error(result.error);
+ assert.equal(result.statusCode, 200);
+ const html = result.contents;
+
+ const $ = doc(html);
+ assert.equal($('head script[type="module"][data-astro="hoist"]').length, 1);
+ assert.equal($('body script').length, 0);
+});
+
+Scripts('Builds the scripts to a single bundle', async({ build, readFile }) => {
+ try {
+ await build();
+ } catch(err) {
+ console.error(err.stack);
+ assert.ok(!err);
+ return;
+ }
+
+ /* Inline page */
+ let inline = await readFile('/inline/index.html');
+ let $ = doc(inline);
+ assert.equal($('script').length, 1, 'Just one entry module');
+ assert.equal($('script').attr('data-astro'), undefined, 'attr removed');
+ let entryURL = path.join('inline', $('script').attr('src'));
+ let inlineEntryJS = await readFile(entryURL);
+ assert.ok(inlineEntryJS, 'The JS exists');
+
+ /* External page */
+ let external = await readFile('/external/index.html');
+ $ = doc(external);
+ assert.equal($('script').length, 2, 'There are two scripts');
+ let el = $('script').get(1);
+ entryURL = path.join('external', $(el).attr('src'));
+ let externalEntryJS = await readFile(entryURL);
+ assert.ok(externalEntryJS, 'got JS');
+});
+
+Scripts.run();
diff --git a/packages/astro/test/fixtures/astro-scripts/public/another_external.js b/packages/astro/test/fixtures/astro-scripts/public/another_external.js
new file mode 100644
index 000000000..d0665036b
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-scripts/public/another_external.js
@@ -0,0 +1,2 @@
+let variable = 'foo';
+console.log(`${variable} bar`);
\ No newline at end of file
diff --git a/packages/astro/test/fixtures/astro-scripts/public/regular_script.js b/packages/astro/test/fixtures/astro-scripts/public/regular_script.js
new file mode 100644
index 000000000..7c457fb56
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-scripts/public/regular_script.js
@@ -0,0 +1 @@
+console.log('here i am');
\ No newline at end of file
diff --git a/packages/astro/test/fixtures/astro-scripts/public/something.js b/packages/astro/test/fixtures/astro-scripts/public/something.js
new file mode 100644
index 000000000..f79e0a992
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-scripts/public/something.js
@@ -0,0 +1 @@
+console.log('this is a widget');
\ No newline at end of file
diff --git a/packages/astro/test/fixtures/astro-scripts/snowpack.config.json b/packages/astro/test/fixtures/astro-scripts/snowpack.config.json
new file mode 100644
index 000000000..8f034781d
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-scripts/snowpack.config.json
@@ -0,0 +1,3 @@
+{
+ "workspaceRoot": "../../../../../"
+}
diff --git a/packages/astro/test/fixtures/astro-scripts/src/components/Inline.astro b/packages/astro/test/fixtures/astro-scripts/src/components/Inline.astro
new file mode 100644
index 000000000..3dac7f270
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-scripts/src/components/Inline.astro
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/packages/astro/test/fixtures/astro-scripts/src/components/Widget.astro b/packages/astro/test/fixtures/astro-scripts/src/components/Widget.astro
new file mode 100644
index 000000000..56fff46c4
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-scripts/src/components/Widget.astro
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/astro/test/fixtures/astro-scripts/src/components/Widget2.astro b/packages/astro/test/fixtures/astro-scripts/src/components/Widget2.astro
new file mode 100644
index 000000000..a87763ef2
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-scripts/src/components/Widget2.astro
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/astro/test/fixtures/astro-scripts/src/pages/external.astro b/packages/astro/test/fixtures/astro-scripts/src/pages/external.astro
new file mode 100644
index 000000000..2fbdc02b3
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-scripts/src/pages/external.astro
@@ -0,0 +1,19 @@
+---
+import Widget from '../components/Widget.astro';
+import Widget2 from '../components/Widget2.astro';
+---
+
+
+
+ My Page
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/astro/test/fixtures/astro-scripts/src/pages/inline.astro b/packages/astro/test/fixtures/astro-scripts/src/pages/inline.astro
new file mode 100644
index 000000000..e3de6198a
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-scripts/src/pages/inline.astro
@@ -0,0 +1,16 @@
+---
+import Inline from '../components/Inline.astro';
+---
+
+
+
+ My Page
+
+
+
+
+
+
+
+
+
\ No newline at end of file