Implementation of hoisted scripts (#1178)

* Implementation of hoisted scripts

* Use the facade id

* Adds docs on hoisted scripts

* Don't try to run rollup if there are no hoisted scripts

* Handle scripts possibly being undefined (client:only)

* Get rid of changes to the portfolio example

* Adds a changeset

* Remove a todo

* Fix lint errors

* Rename TransformResult property to hoistedScripts

* Move Hoisted Scripts docs to astro-components page

* Fixes lint errors

* Fix path join for windows
This commit is contained in:
Matthew Phillips 2021-08-27 07:12:27 -07:00 committed by GitHub
parent dc493b39f7
commit 788c769d78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 530 additions and 12 deletions

View file

@ -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
<script hoist>
// Anything goes here!
</script>
```

View file

@ -275,6 +275,39 @@ const items = ["Dog", "Cat", "Platipus"];
</ul> </ul>
``` ```
### Hoisted scripts
By default Astro does not make any assumptions on how you want scripts to be served, so if you add a `<script>` tag in a page or a component it will be left alone.
However if you'd like all of your scripts to be hoisted out of components and moved to the top of the page, and then later bundled together in production, you can achieve this with hoisted scripts.
A __hoisted script__ looks like this:
```astro
<script hoist>
// An inline script
</script>
```
Or it can link to an external JavaScript file:
```astro
<script src={Astro.resolve('./my-component.js')} hoist></script>
```
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. -->
<TwitterTimeline />
<TwitterTimeline />
<TwitterTimeline />
```
## Comparing `.astro` versus `.jsx` ## 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. `.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.

View file

@ -20,12 +20,24 @@ export interface JsxItem {
jsx: string; jsx: string;
} }
export interface InlineScriptInfo {
content: string;
}
export interface ExternalScriptInfo {
src: string;
}
export type ScriptInfo = InlineScriptInfo | ExternalScriptInfo;
export interface TransformResult { export interface TransformResult {
script: string; script: string;
imports: string[]; imports: string[];
exports: string[]; exports: string[];
components: string[];
html: string; html: string;
css?: string; css?: string;
hoistedScripts: ScriptInfo[];
getStaticPaths?: string; getStaticPaths?: string;
hasCustomElements: boolean; hasCustomElements: boolean;
customElementCandidates: Map<string, string>; customElementCandidates: Map<string, string>;
@ -56,6 +68,8 @@ export interface BuildFile {
contentType: string; contentType: string;
/** Encoding */ /** Encoding */
encoding?: 'utf8'; encoding?: 'utf8';
/** Extracted scripts */
hoistedScripts?: ScriptInfo[];
} }
/** Mapping of every URL and its required assets. All URLs are absolute relative to the project. */ /** 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<string>; css: Set<string>;
/** Images needed for page. Can be loaded via CSS, <link>, or otherwise. */ /** Images needed for page. Can be loaded via CSS, <link>, or otherwise. */
images: Set<string>; images: Set<string>;
/** Async hoisted Javascript */
hoistedJS: Map<string, ScriptInfo>;
} }
export interface RSSFunctionArgs { export interface RSSFunctionArgs {

View file

@ -7,10 +7,11 @@ import mime from 'mime';
import path from 'path'; import path from 'path';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import glob from 'tiny-glob'; import glob from 'tiny-glob';
import hash from 'shorthash';
import { fileURLToPath } from 'url'; 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 { 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 { buildStaticPage, getStaticPathsForPage } from './build/page.js';
import { generateSitemap } from './build/sitemap.js'; import { generateSitemap } from './build/sitemap.js';
import { collectBundleStats, logURLStats, mapBundleStatsToURLStats } from './build/stats.js'; import { collectBundleStats, logURLStats, mapBundleStatsToURLStats } from './build/stats.js';
@ -139,6 +140,7 @@ ${stack}
const pageDeps = findDeps(buildState[id].contents as string, { const pageDeps = findDeps(buildState[id].contents as string, {
astroConfig, astroConfig,
srcPath: buildState[id].srcPath, srcPath: buildState[id].srcPath,
id
}); });
depTree[id] = pageDeps; 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) * 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...')); info(logging, 'build', yellow('! optimizing css...'));
timer.prebundle = performance.now(); timer.prebundleCSS = performance.now();
await Promise.all([ await Promise.all([
bundleCSS({ buildState, astroConfig, logging, depTree }).then(() => { 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: optimize images?
]); ]);
// TODO: minify HTML? // TODO: minify HTML?
@ -269,18 +272,31 @@ ${stack}
} }
/** Given an HTML string, collect <link> and <img> tags */ /** Given an HTML string, collect <link> and <img> 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 = { const pageDeps: PageDependencies = {
js: new Set<string>(), js: new Set<string>(),
css: new Set<string>(), css: new Set<string>(),
images: new Set<string>(), images: new Set<string>(),
hoistedJS: new Map<string, ScriptInfo>(),
}; };
const $ = cheerio.load(html); const $ = cheerio.load(html);
$('script').each((_i, el) => { $('script').each((_i, el) => {
const src = $(el).attr('src'); 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; if (isRemoteOrEmbedded(src)) return;
pageDeps.js.add(getDistPath(src, { astroConfig, srcPath })); pageDeps.js.add(getDistPath(src, { astroConfig, srcPath }));
} else { } else {

View file

@ -1,12 +1,15 @@
import type { InputOptions, OutputOptions, OutputChunk } from 'rollup'; 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 { AstroRuntime } from '../../runtime';
import type { LogOptions } from '../../logger.js';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { rollup } from 'rollup'; import { rollup } from 'rollup';
import { terser } from 'rollup-plugin-terser'; 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 { IS_ASTRO_FILE_URL } from '../util.js';
import cheerio from 'cheerio';
import path from 'path';
interface BundleOptions { interface BundleOptions {
dist: URL; dist: URL;
@ -22,6 +25,161 @@ export function collectJSImports(buildState: BuildOutput): Set<string> {
return imports; 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<string, ScriptInfo>();
const pageToEntryMap = new Map<string, string>();
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<string, string>();
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 */ /** Bundle JS action */
export async function bundleJS(imports: Set<string>, { astroRuntime, dist }: BundleOptions): Promise<BundleStatsMap> { export async function bundleJS(imports: Set<string>, { astroRuntime, dist }: BundleOptions): Promise<BundleStatsMap> {
const ROOT = 'astro:root'; const ROOT = 'astro:root';
@ -43,6 +201,7 @@ export async function bundleJS(imports: Set<string>, { astroRuntime, dist }: Bun
return source; return source;
} }
if (imported) { if (imported) {
const outUrl = new URL(source, 'http://example.com' + imported); const outUrl = new URL(source, 'http://example.com' + imported);
return outUrl.pathname; return outUrl.pathname;

View file

@ -1,6 +1,6 @@
import type { Ast, Script, Style, TemplateNode, Expression } from '@astrojs/parser'; import type { Ast, Script, Style, TemplateNode, Expression } from '@astrojs/parser';
import type { CompileOptions } from '../../@types/compiler'; 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 { ImportDeclaration, ExportNamedDeclaration, VariableDeclarator, Identifier, ImportDefaultSpecifier } from '@babel/types';
import type { Attribute } from './interfaces'; import type { Attribute } from './interfaces';
import eslexer from 'es-module-lexer'; import eslexer from 'es-module-lexer';
@ -316,6 +316,7 @@ interface CompileResult {
interface CodegenState { interface CodegenState {
components: Components; components: Components;
css: string[]; css: string[];
hoistedScripts: ScriptInfo[];
filename: string; filename: string;
fileID: string; fileID: string;
markers: { markers: {
@ -672,6 +673,19 @@ async function compileHtml(enterNode: TemplateNode, state: CodegenState, compile
buffers[curr] += `h(__astro_slot_content, { name: ${attributes.slot} },`; buffers[curr] += `h(__astro_slot_content, { name: ${attributes.slot} },`;
paren++; 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)},`; buffers[curr] += `h("${name}", ${generateAttributes(attributes)},`;
paren++; paren++;
return; return;
@ -887,6 +901,7 @@ export async function codegen(ast: Ast, { compileOptions, filename, fileID }: Co
fileID, fileID,
components: new Map(), components: new Map(),
css: [], css: [],
hoistedScripts: [],
markers: { markers: {
insideMarkdown: false, insideMarkdown: false,
}, },
@ -909,6 +924,8 @@ export async function codegen(ast: Ast, { compileOptions, filename, fileID }: Co
exports: Array.from(state.exportStatements), exports: Array.from(state.exportStatements),
html, html,
css: state.css.length ? state.css.join('\n\n') : undefined, css: state.css.length ? state.css.join('\n\n') : undefined,
hoistedScripts: state.hoistedScripts,
components: Array.from(state.components.keys()),
getStaticPaths, getStaticPaths,
hasCustomElements: Boolean(ast.meta.features & FEATURE_CUSTOM_ELEMENT), hasCustomElements: Boolean(ast.meta.features & FEATURE_CUSTOM_ELEMENT),
customElementCandidates: state.customElementCandidates, customElementCandidates: state.customElementCandidates,

View file

@ -153,6 +153,9 @@ ${result.getStaticPaths || ''}
// \`__render()\`: Render the contents of the Astro module. // \`__render()\`: Render the contents of the Astro module.
import { h, Fragment } from 'astro/dist/internal/h.js'; 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 __astroInternal = Symbol('astro.internal');
const __astroContext = Symbol.for('astro.context'); const __astroContext = Symbol.for('astro.context');
async function __render(props, ...children) { async function __render(props, ...children) {
@ -165,6 +168,10 @@ async function __render(props, ...children) {
value: (props[__astroContext] && props[__astroContext].pageCSS) || [], value: (props[__astroContext] && props[__astroContext].pageCSS) || [],
enumerable: true enumerable: true
}, },
pageScripts: {
value: (props[__astroContext] && props[__astroContext].pageScripts) || [],
enumerable: true
},
isPage: { isPage: {
value: (props[__astroInternal] && props[__astroInternal].isPage) || false, value: (props[__astroInternal] && props[__astroInternal].isPage) || false,
enumerable: true enumerable: true
@ -178,11 +185,11 @@ async function __render(props, ...children) {
${result.script} ${result.script}
return h(Fragment, null, ${result.html}); 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, // \`__renderPage()\`: Render the contents of the Astro module as a page. This is a special flow,
// triggered by loading a component directly by URL. // 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 = { const currentChild = {
isAstroComponent: true, isAstroComponent: true,
layout: typeof __layout === 'undefined' ? undefined : __layout, layout: typeof __layout === 'undefined' ? undefined : __layout,
@ -198,6 +205,7 @@ export async function __renderPage({request, children, props, css}) {
pageCSS: css, pageCSS: css,
request, request,
createAstroRootUID(seed) { return seed + astroRootUIDCounter++; }, createAstroRootUID(seed) { return seed + astroRootUIDCounter++; },
pageScripts: scripts,
}, },
writable: false, writable: false,
enumerable: false enumerable: false
@ -227,7 +235,6 @@ export async function __renderPage({request, children, props, css}) {
}; };
${result.exports.join('\n')} ${result.exports.join('\n')}
`; `;
return { return {

View file

@ -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: [],
},
}
],
},
]
}
],
},
], ],
}); });

View file

@ -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
};

View file

@ -163,6 +163,7 @@ async function load(config: AstroRuntimeConfig, rawPathname: string | undefined)
children: [], children: [],
props: pageProps, props: pageProps,
css: Array.isArray(mod.css) ? mod.css : typeof mod.css === 'string' ? [mod.css] : [], css: Array.isArray(mod.css) ? mod.css : typeof mod.css === 'string' ? [mod.css] : [],
scripts: mod.exports.default[Symbol.for('astro.hoistedScripts')]
})) as string; })) as string;
return { return {

View file

@ -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();

View file

@ -0,0 +1,2 @@
let variable = 'foo';
console.log(`${variable} bar`);

View file

@ -0,0 +1 @@
console.log('here i am');

View file

@ -0,0 +1 @@
console.log('this is a widget');

View file

@ -0,0 +1,3 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -0,0 +1,3 @@
<script hoist type="module">
console.log('some content here.');
</script>

View file

@ -0,0 +1 @@
<script hoist type="module" src="/something.js"></script>

View file

@ -0,0 +1 @@
<script hoist type="module" src="/another_external.js"></script>

View file

@ -0,0 +1,19 @@
---
import Widget from '../components/Widget.astro';
import Widget2 from '../components/Widget2.astro';
---
<html lang="en">
<head>
<title>My Page</title>
<script type="module" src="/regular_script.js"></script>
</head>
<body>
<Widget />
<Widget />
<Widget />
<Widget />
<Widget />
<Widget2 />
</body>
</html>

View file

@ -0,0 +1,16 @@
---
import Inline from '../components/Inline.astro';
---
<html lang="en">
<head>
<title>My Page</title>
</head>
<body>
<Inline />
<Inline />
<Inline />
<Inline />
<Inline />
</body>
</html>