Support for custom elements (#45)

* Support for custom elements

Now you can use custom elements like so in Astro components:

```html
<script type="module" src="./datepicker.js">
<date-picker></date-picker>
```

These will be resolve relative to the current astro component. In the build these modules are run through the same bundle/minify process as components.

* Remove component from public

* Formatting

* Disable empty fn rule
This commit is contained in:
Matthew Phillips 2021-03-31 16:46:09 -04:00 committed by GitHub
parent d9084ff4ad
commit d5b15a3851
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 144 additions and 13 deletions

View file

@ -8,6 +8,7 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'off',
'no-shadow': 'warn', 'no-shadow': 'warn',
'prettier/prettier': 'error', 'prettier/prettier': 'error',
'prefer-const': 'off', 'prefer-const': 'off',

View file

@ -123,6 +123,10 @@ export let version: string = '3.1.2';
flex-grow: 1; flex-grow: 1;
} }
> :global(.algolia-autocomplete) {
width: 100%;
}
@media (min-width: $breakpoint-m) { @media (min-width: $breakpoint-m) {
max-width: 600px; max-width: 600px;
} }
@ -344,9 +348,5 @@ export let version: string = '3.1.2';
} }
}; };
</script> </script>
<script type="module" defer> <script type="module" src="./docsearch.js"></script>
import docsearch from 'https://cdn.skypack.dev/docsearch.js/dist/cdn/docsearch.min.js'; <doc-search api-key="562139304880b94536fc53f5d65c5c19" selector="#search-form-input"></doc-search>
docsearch({
apiKey: '562139304880b94536fc53f5d65c5c19', indexName: 'snowpack', inputSelector: '#search-form-input', debug: true // Set debug to true if you want to inspect the dropdown
});
</script>

View file

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

23
src/ast.ts Normal file
View file

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

View file

@ -52,6 +52,7 @@ async function writeResult(result: LoadResult, outPath: URL, encoding: null | 'u
export async function build(astroConfig: AstroConfig): Promise<0 | 1> { export async function build(astroConfig: AstroConfig): Promise<0 | 1> {
const { projectRoot, astroRoot } = astroConfig; const { projectRoot, astroRoot } = astroConfig;
const pageRoot = new URL('./pages/', astroRoot); const pageRoot = new URL('./pages/', astroRoot);
const componentRoot = new URL('./components/', astroRoot);
const dist = new URL(astroConfig.dist + '/', projectRoot); const dist = new URL(astroConfig.dist + '/', projectRoot);
const runtimeLogging: LogOptions = { const runtimeLogging: LogOptions = {
@ -66,6 +67,7 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> {
const imports = new Set<string>(); const imports = new Set<string>();
const statics = new Set<string>(); const statics = new Set<string>();
const collectImportsOptions = { astroConfig, logging, resolve };
for (const pathname of await allPages(pageRoot)) { for (const pathname of await allPages(pageRoot)) {
const filepath = new URL(`file://${pathname}`); const filepath = new URL(`file://${pathname}`);
@ -90,7 +92,11 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> {
return 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 }); await bundle(imports, { dist, runtime, astroConfig });

View file

@ -2,10 +2,13 @@ import type { AstroConfig, ValidExtensionPlugins } from '../@types/astro';
import type { ImportDeclaration } from '@babel/types'; import type { ImportDeclaration } from '@babel/types';
import type { InputOptions, OutputOptions } from 'rollup'; import type { InputOptions, OutputOptions } from 'rollup';
import type { AstroRuntime } from '../runtime'; import type { AstroRuntime } from '../runtime';
import type { LogOptions } from '../logger';
import esbuild from 'esbuild'; import esbuild from 'esbuild';
import { promises as fsPromises } from 'fs'; import { promises as fsPromises } from 'fs';
import { parse } from '../parser/index.js'; 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 { walk } from 'estree-walker';
import babelParser from '@babel/parser'; import babelParser from '@babel/parser';
import path from 'path'; import path from 'path';
@ -55,11 +58,17 @@ const defaultExtensions: Readonly<Record<string, ValidExtensionPlugins>> = {
'.vue': 'vue', '.vue': 'vue',
}; };
export async function collectDynamicImports(filename: URL, astroConfig: AstroConfig, resolve: (s: string) => Promise<string>) { interface CollectDynamic {
astroConfig: AstroConfig;
resolve: (s: string) => Promise<string>;
logging: LogOptions;
}
export async function collectDynamicImports(filename: URL, { astroConfig, logging, resolve }: CollectDynamic) {
const imports = new Set<string>(); const imports = new Set<string>();
// No markdown for now // Only astro files
if (filename.pathname.endsWith('md')) { if (!filename.pathname.endsWith('astro')) {
return imports; return imports;
} }
@ -73,6 +82,16 @@ export async function collectDynamicImports(filename: URL, astroConfig: AstroCon
return imports; return imports;
} }
await optimize(ast, {
filename: filename.pathname,
fileID: '',
compileOptions: {
astroConfig,
resolve,
logging,
},
});
const componentImports: ImportDeclaration[] = []; const componentImports: ImportDeclaration[] = [];
const components: Record<string, { plugin: ValidExtensionPlugins; type: string; specifier: string }> = {}; const components: Record<string, { plugin: ValidExtensionPlugins; type: string; specifier: string }> = {};
const plugins = new Set<ValidExtensionPlugins>(); const plugins = new Set<ValidExtensionPlugins>();
@ -145,6 +164,18 @@ export async function collectDynamicImports(filename: URL, astroConfig: AstroCon
walk(ast.html, { walk(ast.html, {
enter(node) { enter(node) {
switch (node.type) { 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': { case 'MustacheTag': {
let code: string; let code: string;
try { try {

View file

@ -1,10 +1,13 @@
import { walk } from 'estree-walker';
import type { Ast, TemplateNode } from '../../parser/interfaces'; 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 // Optimizers
import optimizeStyles from './styles.js'; import optimizeStyles from './styles.js';
import optimizeDoctype from './doctype.js'; import optimizeDoctype from './doctype.js';
import optimizeModuleScripts from './module-scripts.js';
interface VisitorCollection { interface VisitorCollection {
enter: Map<string, VisitorFn[]>; enter: Map<string, VisitorFn[]>;
@ -67,6 +70,7 @@ function walkAstWithVisitors(tmpl: TemplateNode, collection: VisitorCollection)
} }
interface OptimizeOptions { interface OptimizeOptions {
compileOptions: CompileOptions;
filename: string; filename: string;
fileID: string; fileID: string;
} }
@ -76,7 +80,7 @@ export async function optimize(ast: Ast, opts: OptimizeOptions) {
const cssVisitors = createVisitorCollection(); const cssVisitors = createVisitorCollection();
const finalizers: Array<() => Promise<void>> = []; const finalizers: Array<() => Promise<void>> = [];
const optimizers = [optimizeStyles(opts), optimizeDoctype(opts)]; const optimizers = [optimizeStyles(opts), optimizeDoctype(opts), optimizeModuleScripts(opts)];
for (const optimizer of optimizers) { for (const optimizer of optimizers) {
collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers); collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers);

View file

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

View file

@ -17,6 +17,13 @@ export interface Fragment extends BaseNode {
export interface Text extends BaseNode { export interface Text extends BaseNode {
type: 'Text'; type: 'Text';
data: string; data: string;
raw: string;
}
export interface Attribute extends BaseNode {
type: 'Attribute';
name: string;
value: Text[];
} }
export interface MustacheTag extends BaseNode { export interface MustacheTag extends BaseNode {