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/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',

View file

@ -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';
}
};
</script>
<script type="module" defer>
import docsearch from 'https://cdn.skypack.dev/docsearch.js/dist/cdn/docsearch.min.js';
docsearch({
apiKey: '562139304880b94536fc53f5d65c5c19', indexName: 'snowpack', inputSelector: '#search-form-input', debug: true // Set debug to true if you want to inspect the dropdown
});
</script>
<script type="module" src="./docsearch.js"></script>
<doc-search api-key="562139304880b94536fc53f5d65c5c19" selector="#search-form-input"></doc-search>

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> {
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<string>();
const statics = new Set<string>();
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 });

View file

@ -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<Record<string, ValidExtensionPlugins>> = {
'.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>();
// 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<string, { plugin: ValidExtensionPlugins; type: string; specifier: string }> = {};
const plugins = new Set<ValidExtensionPlugins>();
@ -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 {

View file

@ -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<string, VisitorFn[]>;
@ -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<void>> = [];
const optimizers = [optimizeStyles(opts), optimizeDoctype(opts)];
const optimizers = [optimizeStyles(opts), optimizeDoctype(opts), optimizeModuleScripts(opts)];
for (const optimizer of optimizers) {
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 {
type: 'Text';
data: string;
raw: string;
}
export interface Attribute extends BaseNode {
type: 'Attribute';
name: string;
value: Text[];
}
export interface MustacheTag extends BaseNode {