diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 1ffaca73c..1072b1e9f 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -8,6 +8,7 @@ module.exports = { '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-empty-function': 'off', 'no-shadow': 'warn', 'prettier/prettier': 'error', 'prefer-const': 'off', diff --git a/examples/snowpack/astro/components/Nav.astro b/examples/snowpack/astro/components/Nav.astro index d2c0e943c..0c97dd425 100644 --- a/examples/snowpack/astro/components/Nav.astro +++ b/examples/snowpack/astro/components/Nav.astro @@ -123,6 +123,10 @@ export let version: string = '3.1.2'; flex-grow: 1; } + > :global(.algolia-autocomplete) { + width: 100%; + } + @media (min-width: $breakpoint-m) { max-width: 600px; } @@ -344,9 +348,5 @@ export let version: string = '3.1.2'; } }; - + + diff --git a/examples/snowpack/astro/components/docsearch.js b/examples/snowpack/astro/components/docsearch.js new file mode 100644 index 000000000..d7ae95f30 --- /dev/null +++ b/examples/snowpack/astro/components/docsearch.js @@ -0,0 +1,17 @@ +import docsearch from 'docsearch.js/dist/cdn/docsearch.min.js'; + +customElements.define('doc-search', class extends HTMLElement { + connectedCallback() { + if(!this._setup) { + const apiKey = this.getAttribute('api-key'); + const selector = this.getAttribute('selector'); + docsearch({ + apiKey: apiKey, + indexName: 'snowpack', + inputSelector: selector, + debug: true // Set debug to true if you want to inspect the dropdown + }); + this._setup = true; + } + } +}); \ No newline at end of file diff --git a/src/ast.ts b/src/ast.ts new file mode 100644 index 000000000..6c0bd7bd2 --- /dev/null +++ b/src/ast.ts @@ -0,0 +1,23 @@ +import type { Attribute } from './parser/interfaces'; + +// AST utility functions + +export function getAttr(attributes: Attribute[], name: string): Attribute | undefined { + const attr = attributes.find((a) => a.name === name); + return attr; +} + +export function getAttrValue(attributes: Attribute[], name: string): string | undefined { + const attr = getAttr(attributes, name); + if (attr) { + return attr.value[0]?.data; + } +} + +export function setAttrValue(attributes: Attribute[], name: string, value: string): void { + const attr = attributes.find((a) => a.name === name); + if (attr) { + attr.value[0]!.data = value; + attr.value[0]!.raw = value; + } +} diff --git a/src/build.ts b/src/build.ts index ffae6fac0..7f4fa713c 100644 --- a/src/build.ts +++ b/src/build.ts @@ -52,6 +52,7 @@ async function writeResult(result: LoadResult, outPath: URL, encoding: null | 'u export async function build(astroConfig: AstroConfig): Promise<0 | 1> { const { projectRoot, astroRoot } = astroConfig; const pageRoot = new URL('./pages/', astroRoot); + const componentRoot = new URL('./components/', astroRoot); const dist = new URL(astroConfig.dist + '/', projectRoot); const runtimeLogging: LogOptions = { @@ -66,6 +67,7 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> { const imports = new Set(); const statics = new Set(); + const collectImportsOptions = { astroConfig, logging, resolve }; for (const pathname of await allPages(pageRoot)) { const filepath = new URL(`file://${pathname}`); @@ -90,7 +92,11 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> { return 1; } - mergeSet(imports, await collectDynamicImports(filepath, astroConfig, resolve)); + mergeSet(imports, await collectDynamicImports(filepath, collectImportsOptions)); + } + + for (const pathname of await allPages(componentRoot)) { + mergeSet(imports, await collectDynamicImports(new URL(`file://${pathname}`), collectImportsOptions)); } await bundle(imports, { dist, runtime, astroConfig }); diff --git a/src/build/bundle.ts b/src/build/bundle.ts index af06ed8c6..82b6930d5 100644 --- a/src/build/bundle.ts +++ b/src/build/bundle.ts @@ -2,10 +2,13 @@ import type { AstroConfig, 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 { parse } from '../parser/index.js'; +import { optimize } from '../compiler/optimize/index.js'; +import { getAttrValue, setAttrValue } from '../ast.js'; import { walk } from 'estree-walker'; import babelParser from '@babel/parser'; import path from 'path'; @@ -55,11 +58,17 @@ const defaultExtensions: Readonly> = { '.vue': 'vue', }; -export async function collectDynamicImports(filename: URL, astroConfig: AstroConfig, resolve: (s: string) => Promise) { +interface CollectDynamic { + astroConfig: AstroConfig; + resolve: (s: string) => Promise; + logging: LogOptions; +} + +export async function collectDynamicImports(filename: URL, { astroConfig, logging, resolve }: CollectDynamic) { const imports = new Set(); - // No markdown for now - if (filename.pathname.endsWith('md')) { + // Only astro files + if (!filename.pathname.endsWith('astro')) { return imports; } @@ -73,6 +82,16 @@ export async function collectDynamicImports(filename: URL, astroConfig: AstroCon return imports; } + await optimize(ast, { + filename: filename.pathname, + fileID: '', + compileOptions: { + astroConfig, + resolve, + logging, + }, + }); + const componentImports: ImportDeclaration[] = []; const components: Record = {}; const plugins = new Set(); @@ -145,6 +164,18 @@ export async function collectDynamicImports(filename: URL, astroConfig: AstroCon 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 { diff --git a/src/compiler/optimize/index.ts b/src/compiler/optimize/index.ts index d86ce3c24..e73c93c7c 100644 --- a/src/compiler/optimize/index.ts +++ b/src/compiler/optimize/index.ts @@ -1,10 +1,13 @@ -import { walk } from 'estree-walker'; import type { Ast, TemplateNode } from '../../parser/interfaces'; -import { NodeVisitor, Optimizer, VisitorFn } from '../../@types/optimizer'; +import type { CompileOptions } from '../../@types/compiler'; +import type { NodeVisitor, Optimizer, VisitorFn } from '../../@types/optimizer'; + +import { walk } from 'estree-walker'; // Optimizers import optimizeStyles from './styles.js'; import optimizeDoctype from './doctype.js'; +import optimizeModuleScripts from './module-scripts.js'; interface VisitorCollection { enter: Map; @@ -67,6 +70,7 @@ function walkAstWithVisitors(tmpl: TemplateNode, collection: VisitorCollection) } interface OptimizeOptions { + compileOptions: CompileOptions; filename: string; fileID: string; } @@ -76,7 +80,7 @@ export async function optimize(ast: Ast, opts: OptimizeOptions) { const cssVisitors = createVisitorCollection(); const finalizers: Array<() => Promise> = []; - const optimizers = [optimizeStyles(opts), optimizeDoctype(opts)]; + const optimizers = [optimizeStyles(opts), optimizeDoctype(opts), optimizeModuleScripts(opts)]; for (const optimizer of optimizers) { collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers); diff --git a/src/compiler/optimize/module-scripts.ts b/src/compiler/optimize/module-scripts.ts new file mode 100644 index 000000000..713747fcb --- /dev/null +++ b/src/compiler/optimize/module-scripts.ts @@ -0,0 +1,42 @@ +import type { Optimizer } from '../../@types/optimizer'; +import type { CompileOptions } from '../../@types/compiler'; + +import path from 'path'; +import { getAttrValue, setAttrValue } from '../../ast.js'; + +export default function ({ compileOptions, filename }: { compileOptions: CompileOptions; filename: string; fileID: string }): Optimizer { + const { astroConfig } = compileOptions; + const { astroRoot } = astroConfig; + const fileUrl = new URL(`file://${filename}`); + + return { + visitors: { + html: { + Element: { + enter(node) { + let name = node.name; + if (name !== 'script') { + return; + } + + let type = getAttrValue(node.attributes, 'type'); + if (type !== 'module') { + return; + } + + let src = getAttrValue(node.attributes, 'src'); + if (!src || !src.startsWith('.')) { + return; + } + + const srcUrl = new URL(src, fileUrl); + const fromAstroRoot = path.posix.relative(astroRoot.pathname, srcUrl.pathname); + const absoluteUrl = `/_astro/${fromAstroRoot}`; + setAttrValue(node.attributes, 'src', absoluteUrl); + }, + }, + }, + }, + async finalize() {}, + }; +} diff --git a/src/parser/interfaces.ts b/src/parser/interfaces.ts index 848f48ec9..71b1812a3 100644 --- a/src/parser/interfaces.ts +++ b/src/parser/interfaces.ts @@ -17,6 +17,13 @@ export interface Fragment extends BaseNode { export interface Text extends BaseNode { type: 'Text'; data: string; + raw: string; +} + +export interface Attribute extends BaseNode { + type: 'Attribute'; + name: string; + value: Text[]; } export interface MustacheTag extends BaseNode {