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