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 {