diff options
Diffstat (limited to 'src/build/bundle.ts')
-rw-r--r-- | src/build/bundle.ts | 313 |
1 files changed, 0 insertions, 313 deletions
diff --git a/src/build/bundle.ts b/src/build/bundle.ts deleted file mode 100644 index d590e85df..000000000 --- a/src/build/bundle.ts +++ /dev/null @@ -1,313 +0,0 @@ -import type { AstroConfig, RuntimeMode, ValidExtensionPlugins } from '../@types/astro'; -import type { ImportDeclaration } from '@babel/types'; -import type { InputOptions, OutputOptions } from 'rollup'; -import type { AstroRuntime } from '../runtime'; -import type { LogOptions } from '../logger'; - -import esbuild from 'esbuild'; -import { promises as fsPromises } from 'fs'; -import { fileURLToPath } from 'url'; -import { parse } from '../parser/index.js'; -import { transform } from '../compiler/transform/index.js'; -import { convertMdToAstroSource } from '../compiler/index.js'; -import { getAttrValue } from '../ast.js'; -import { walk } from 'estree-walker'; -import babelParser from '@babel/parser'; -import path from 'path'; -import { rollup } from 'rollup'; -import { terser } from 'rollup-plugin-terser'; - -const { transformSync } = esbuild; -const { readFile } = fsPromises; - -type DynamicImportMap = Map<'vue' | 'react' | 'react-dom' | 'preact' | 'svelte', string>; - -/** Add framework runtimes when needed */ -async function acquireDynamicComponentImports(plugins: Set<ValidExtensionPlugins>, resolvePackageUrl: (s: string) => Promise<string>): Promise<DynamicImportMap> { - const importMap: DynamicImportMap = new Map(); - for (let plugin of plugins) { - switch (plugin) { - case 'svelte': { - importMap.set('svelte', await resolvePackageUrl('svelte')); - break; - } - case 'vue': { - importMap.set('vue', await resolvePackageUrl('vue')); - break; - } - case 'react': { - importMap.set('react', await resolvePackageUrl('react')); - importMap.set('react-dom', await resolvePackageUrl('react-dom')); - break; - } - case 'preact': { - importMap.set('preact', await resolvePackageUrl('preact')); - break; - } - } - } - return importMap; -} - -/** Evaluate mustache expression (safely) */ -function compileExpressionSafe(raw: string): string { - let { code } = transformSync(raw, { - loader: 'tsx', - jsxFactory: 'h', - jsxFragment: 'Fragment', - charset: 'utf8', - }); - return code; -} - -const defaultExtensions: Readonly<Record<string, ValidExtensionPlugins>> = { - '.jsx': 'react', - '.tsx': 'react', - '.svelte': 'svelte', - '.vue': 'vue', -}; - -interface CollectDynamic { - astroConfig: AstroConfig; - resolvePackageUrl: (s: string) => Promise<string>; - logging: LogOptions; - mode: RuntimeMode; -} - -/** Gather necessary framework runtimes for dynamic components */ -export async function collectDynamicImports(filename: URL, { astroConfig, logging, resolvePackageUrl, mode }: CollectDynamic) { - const imports = new Set<string>(); - - // Only astro files - if (!filename.pathname.endsWith('.astro') && !filename.pathname.endsWith('.md')) { - return imports; - } - - const extensions = astroConfig.extensions || defaultExtensions; - - let source = await readFile(filename, 'utf-8'); - if (filename.pathname.endsWith('.md')) { - source = await convertMdToAstroSource(source); - } - - const ast = parse(source, { - filename, - }); - - if (!ast.module) { - return imports; - } - - await transform(ast, { - filename: fileURLToPath(filename), - fileID: '', - compileOptions: { - astroConfig, - resolvePackageUrl, - logging, - mode, - }, - }); - - const componentImports: ImportDeclaration[] = []; - const components: Record<string, { plugin: ValidExtensionPlugins; type: string; specifier: string }> = {}; - const plugins = new Set<ValidExtensionPlugins>(); - - const program = babelParser.parse(ast.module.content, { - sourceType: 'module', - plugins: ['jsx', 'typescript', 'topLevelAwait'], - }).program; - - const { body } = program; - let i = body.length; - while (--i >= 0) { - const node = body[i]; - if (node.type === 'ImportDeclaration') { - componentImports.push(node); - } - } - - for (const componentImport of componentImports) { - const importUrl = componentImport.source.value; - const componentType = path.posix.extname(importUrl); - const componentName = path.posix.basename(importUrl, componentType); - const plugin = extensions[componentType] || defaultExtensions[componentType]; - plugins.add(plugin); - components[componentName] = { - plugin, - type: componentType, - specifier: importUrl, - }; - } - - const dynamic = await acquireDynamicComponentImports(plugins, resolvePackageUrl); - - /** Add dynamic component runtimes to imports */ - function appendImports(rawName: string, importUrl: URL) { - const [componentName, componentType] = rawName.split(':'); - if (!componentType) { - return; - } - - if (!components[componentName]) { - throw new Error(`Unknown Component: ${componentName}`); - } - - const defn = components[componentName]; - const fileUrl = new URL(defn.specifier, importUrl); - let rel = path.posix.relative(astroConfig.astroRoot.pathname, fileUrl.pathname); - - switch (defn.plugin) { - case 'preact': { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - imports.add(dynamic.get('preact')!); - rel = rel.replace(/\.[^.]+$/, '.js'); - break; - } - case 'react': { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - imports.add(dynamic.get('react')!); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - imports.add(dynamic.get('react-dom')!); - rel = rel.replace(/\.[^.]+$/, '.js'); - break; - } - case 'vue': { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - imports.add(dynamic.get('vue')!); - rel = rel.replace(/\.[^.]+$/, '.vue.js'); - break; - } - case 'svelte': { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - imports.add(dynamic.get('svelte')!); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - imports.add('/_astro_internal/runtime/svelte.js'); - rel = rel.replace(/\.[^.]+$/, '.svelte.js'); - break; - } - } - - imports.add(`/_astro/${rel}`); - } - - walk(ast.html, { - enter(node) { - switch (node.type) { - case 'Element': { - if (node.name !== 'script') return; - if (getAttrValue(node.attributes, 'type') !== 'module') return; - - const src = getAttrValue(node.attributes, 'src'); - - if (src && src.startsWith('/')) { - imports.add(src); - } - break; - } - - case 'MustacheTag': { - let code: string; - try { - code = compileExpressionSafe(node.content); - } catch { - return; - } - - let matches: RegExpExecArray[] = []; - let match: RegExpExecArray | null | undefined; - const H_COMPONENT_SCANNER = /h\(['"]?([A-Z].*?)['"]?,/gs; - const regex = new RegExp(H_COMPONENT_SCANNER); - while ((match = regex.exec(code))) { - matches.push(match); - } - for (const foundImport of matches.reverse()) { - const name = foundImport[1]; - appendImports(name, filename); - } - break; - } - case 'InlineComponent': { - if (/^[A-Z]/.test(node.name)) { - appendImports(node.name, filename); - return; - } - - break; - } - } - }, - }); - - return imports; -} - -interface BundleOptions { - runtime: AstroRuntime; - dist: URL; - astroConfig: AstroConfig; -} - -/** The primary bundling/optimization action */ -export async function bundle(imports: Set<string>, { runtime, dist }: BundleOptions) { - const ROOT = 'astro:root'; - const root = ` - ${[...imports].map((url) => `import '${url}';`).join('\n')} - `; - - const inputOptions: InputOptions = { - input: [...imports], - plugins: [ - { - name: 'astro:build', - resolveId(source: string, imported?: string) { - if (source === ROOT) { - 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 (id === ROOT) { - return root; - } - - 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) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return chunk.facadeModuleId!.substr(1); - }, - 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(), - ], - }; - - await build.write(outputOptions); -} |