diff options
Diffstat (limited to 'src')
78 files changed, 0 insertions, 9143 deletions
diff --git a/src/@types/astro.ts b/src/@types/astro.ts deleted file mode 100644 index 049105970..000000000 --- a/src/@types/astro.ts +++ /dev/null @@ -1,132 +0,0 @@ -export interface AstroConfigRaw { - dist: string; - projectRoot: string; - astroRoot: string; - public: string; - jsx?: string; -} - -export type ValidExtensionPlugins = 'astro' | 'react' | 'preact' | 'svelte' | 'vue'; - -export interface AstroConfig { - dist: string; - projectRoot: URL; - astroRoot: URL; - public: URL; - extensions?: Record<string, ValidExtensionPlugins>; - /** Options specific to `astro build` */ - buildOptions: { - /** Your public domain, e.g.: https://my-site.dev/. Used to generate sitemaps and canonical URLs. */ - site?: string; - /** Generate sitemap (set to "false" to disable) */ - sitemap: boolean; - }; - /** Options for the development server run with `astro dev`. */ - devOptions: { - /** The port to run the dev server on. */ - port: number; - projectRoot?: string; - }; -} - -export type AstroUserConfig = Omit<AstroConfig, 'buildOptions' | 'devOptions'> & { - buildOptions: { - sitemap: boolean; - }; - devOptions: { - port?: number; - projectRoot?: string; - }; -}; - -export interface JsxItem { - name: string; - jsx: string; -} - -export interface TransformResult { - script: string; - imports: string[]; - html: string; - css?: string; - /** If this page exports a collection, the JS to be executed as a string */ - createCollection?: string; -} - -export interface CompileResult { - result: TransformResult; - contents: string; - css?: string; -} - -export type RuntimeMode = 'development' | 'production'; - -export type Params = Record<string, string | number>; - -export interface CreateCollection<T = any> { - data: ({ params }: { params: Params }) => T[]; - routes?: Params[]; - /** tool for generating current page URL */ - permalink?: ({ params }: { params: Params }) => string; - /** page size */ - pageSize?: number; - /** Generate RSS feed from data() */ - rss?: CollectionRSS<T>; -} - -export interface CollectionRSS<T = any> { - /** (required) Title of the RSS Feed */ - title: string; - /** (required) Description of the RSS Feed */ - description: string; - /** Specify arbitrary metadata on opening <xml> tag */ - xmlns?: Record<string, string>; - /** Specify custom data in opening of file */ - customData?: string; - /** Return data about each item */ - item: ( - item: T - ) => { - /** (required) Title of item */ - title: string; - /** (required) Link to item */ - link: string; - /** Publication date of item */ - pubDate?: Date; - /** Item description */ - description?: string; - /** Append some other XML-valid data to this item */ - customData?: string; - }; -} - -export interface CollectionResult<T = any> { - /** result */ - data: T[]; - - /** metadata */ - /** the count of the first item on the page, starting from 0 */ - start: number; - /** the count of the last item on the page, starting from 0 */ - end: number; - /** total number of results */ - total: number; - page: { - /** the current page number, starting from 1 */ - current: number; - /** number of items per page (default: 25) */ - size: number; - /** number of last page */ - last: number; - }; - url: { - /** url of the current page */ - current: string; - /** url of the previous page (if there is one) */ - prev?: string; - /** url of the next page (if there is one) */ - next?: string; - }; - /** Matched parameters, if any */ - params: Params; -} diff --git a/src/@types/compiler.ts b/src/@types/compiler.ts deleted file mode 100644 index 7da0afaf2..000000000 --- a/src/@types/compiler.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { LogOptions } from '../logger'; -import type { AstroConfig, RuntimeMode, ValidExtensionPlugins } from './astro'; - -export interface CompileOptions { - logging: LogOptions; - resolvePackageUrl: (p: string) => Promise<string>; - astroConfig: AstroConfig; - extensions?: Record<string, ValidExtensionPlugins>; - mode: RuntimeMode; -} diff --git a/src/@types/estree-walker.d.ts b/src/@types/estree-walker.d.ts deleted file mode 100644 index a3b7da859..000000000 --- a/src/@types/estree-walker.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { BaseNode } from 'estree-walker'; - -declare module 'estree-walker' { - export function walk<T = BaseNode>( - ast: T, - { - enter, - leave, - }: { - enter?: (this: { skip: () => void; remove: () => void; replace: (node: T) => void }, node: T, parent: T, key: string, index: number) => void; - leave?: (this: { skip: () => void; remove: () => void; replace: (node: T) => void }, node: T, parent: T, key: string, index: number) => void; - } - ): T; - - export function asyncWalk<T = BaseNode>( - ast: T, - { - enter, - leave, - }: { - enter?: (this: { skip: () => void; remove: () => void; replace: (node: T) => void }, node: T, parent: T, key: string, index: number) => void; - leave?: (this: { skip: () => void; remove: () => void; replace: (node: T) => void }, node: T, parent: T, key: string, index: number) => void; - } - ): T; -} diff --git a/src/@types/micromark-extension-gfm.d.ts b/src/@types/micromark-extension-gfm.d.ts deleted file mode 100644 index ebdfe3b3a..000000000 --- a/src/@types/micromark-extension-gfm.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// TODO: add types (if helpful) -declare module 'micromark-extension-gfm'; -declare module 'micromark-extension-gfm/html.js'; diff --git a/src/@types/micromark.ts b/src/@types/micromark.ts deleted file mode 100644 index 9725aabb9..000000000 --- a/src/@types/micromark.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface MicromarkExtensionContext { - sliceSerialize(node: any): string; - raw(value: string): void; -} - -export type MicromarkExtensionCallback = (this: MicromarkExtensionContext, node: any) => void; - -export interface MicromarkExtension { - enter?: Record<string, MicromarkExtensionCallback>; - exit?: Record<string, MicromarkExtensionCallback>; -} diff --git a/src/@types/postcss-icss-keyframes.d.ts b/src/@types/postcss-icss-keyframes.d.ts deleted file mode 100644 index 14c330b6e..000000000 --- a/src/@types/postcss-icss-keyframes.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare module 'postcss-icss-keyframes' { - import type { Plugin } from 'postcss'; - - export default function (options: { generateScopedName(keyframesName: string, filepath: string, css: string): string }): Plugin; -} diff --git a/src/@types/renderer.ts b/src/@types/renderer.ts deleted file mode 100644 index f89cb6664..000000000 --- a/src/@types/renderer.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { Component as VueComponent } from 'vue'; -import type { ComponentType as PreactComponent } from 'preact'; -import type { ComponentType as ReactComponent } from 'react'; -import type { SvelteComponent } from 'svelte'; - -export interface DynamicRenderContext { - componentUrl: string; - componentExport: string; - frameworkUrls: string; -} - -export interface ComponentRenderer<T> { - renderStatic: StaticRendererGenerator<T>; - jsxPragma?: (...args: any) => any; - jsxPragmaName?: string; - render(context: { root: string; Component: string; props: string; [key: string]: string }): string; - imports?: Record<string, string[]>; -} - -export interface ComponentContext { - 'data-astro-id': string; - root: string; -} - -export type SupportedComponentRenderer = - | ComponentRenderer<VueComponent> - | ComponentRenderer<PreactComponent> - | ComponentRenderer<ReactComponent> - | ComponentRenderer<SvelteComponent>; -export type StaticRenderer = (props: Record<string, any>, ...children: any[]) => Promise<string>; -export type StaticRendererGenerator<T = any> = (Component: T) => StaticRenderer; -export type DynamicRenderer = (props: Record<string, any>, ...children: any[]) => Promise<string>; -export type DynamicRendererContext<T = any> = (Component: T, renderContext: DynamicRenderContext) => DynamicRenderer; -export type DynamicRendererGenerator = ( - wrapperStart: string | ((context: ComponentContext) => string), - wrapperEnd: string | ((context: ComponentContext) => string) -) => DynamicRendererContext; diff --git a/src/@types/tailwind.d.ts b/src/@types/tailwind.d.ts deleted file mode 100644 index d25eaae2f..000000000 --- a/src/@types/tailwind.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// we shouldn‘t have this as a dependency for Astro, but we may dynamically import it if a user requests it, so let TS know about it -declare module 'tailwindcss'; diff --git a/src/@types/transformer.ts b/src/@types/transformer.ts deleted file mode 100644 index 7f4167558..000000000 --- a/src/@types/transformer.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { TemplateNode } from '../parser/interfaces'; -import type { CompileOptions } from './compiler'; - -export type VisitorFn<T = TemplateNode> = (this: { skip: () => void; remove: () => void; replace: (node: T) => void }, node: T, parent: T, type: string, index: number) => void; - -export interface NodeVisitor { - enter?: VisitorFn; - leave?: VisitorFn; -} - -export interface Transformer { - visitors?: { - html?: Record<string, NodeVisitor>; - css?: Record<string, NodeVisitor>; - }; - finalize: () => Promise<void>; -} - -export interface TransformOptions { - compileOptions: CompileOptions; - filename: string; - fileID: string; -} diff --git a/src/ast.ts b/src/ast.ts deleted file mode 100644 index faf4b3eb6..000000000 --- a/src/ast.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { Attribute } from './parser/interfaces'; - -// AST utility functions - -/** Get TemplateNode attribute from name */ -export function getAttr(attributes: Attribute[], name: string): Attribute | undefined { - const attr = attributes.find((a) => a.name === name); - return attr; -} - -/** Get TemplateNode attribute by value */ -export function getAttrValue(attributes: Attribute[], name: string): string | undefined { - const attr = getAttr(attributes, name); - if (attr) { - return attr.value[0]?.data; - } -} - -/** Set TemplateNode attribute value */ -export function setAttrValue(attributes: Attribute[], name: string, value: string): void { - const attr = attributes.find((a) => a.name === name); - if (attr) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - attr.value[0]!.data = value; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - attr.value[0]!.raw = value; - } -} diff --git a/src/build.ts b/src/build.ts deleted file mode 100644 index 392b0a920..000000000 --- a/src/build.ts +++ /dev/null @@ -1,303 +0,0 @@ -import type { AstroConfig, RuntimeMode } from './@types/astro'; -import type { LogOptions } from './logger'; -import type { AstroRuntime, LoadResult } from './runtime'; - -import { existsSync, promises as fsPromises } from 'fs'; -import { bold, green, yellow, underline } from 'kleur/colors'; -import path from 'path'; -import cheerio from 'cheerio'; -import { fileURLToPath } from 'url'; -import { fdir } from 'fdir'; -import { defaultLogDestination, error, info, trapWarn } from './logger.js'; -import { createRuntime } from './runtime.js'; -import { bundle, collectDynamicImports } from './build/bundle.js'; -import { generateRSS } from './build/rss.js'; -import { generateSitemap } from './build/sitemap.js'; -import { collectStatics } from './build/static.js'; -import { canonicalURL } from './build/util.js'; - - -const { mkdir, readFile, writeFile } = fsPromises; - -interface PageBuildOptions { - astroRoot: URL; - dist: URL; - filepath: URL; - runtime: AstroRuntime; - site?: string; - sitemap: boolean; - statics: Set<string>; -} - -interface PageResult { - canonicalURLs: string[]; - rss?: string; - statusCode: number; -} - -const logging: LogOptions = { - level: 'debug', - dest: defaultLogDestination, -}; - -/** Return contents of src/pages */ -async function allPages(root: URL) { - const api = new fdir() - .filter((p) => /\.(astro|md)$/.test(p)) - .withFullPaths() - .crawl(fileURLToPath(root)); - const files = await api.withPromise(); - return files as string[]; -} - -/** Utility for merging two Set()s */ -function mergeSet(a: Set<string>, b: Set<string>) { - for (let str of b) { - a.add(str); - } - return a; -} - -/** Utility for writing to file (async) */ -async function writeFilep(outPath: URL, bytes: string | Buffer, encoding: 'utf8' | null) { - const outFolder = new URL('./', outPath); - await mkdir(outFolder, { recursive: true }); - await writeFile(outPath, bytes, encoding || 'binary'); -} - -/** Utility for writing a build result to disk */ -async function writeResult(result: LoadResult, outPath: URL, encoding: null | 'utf8') { - if (result.statusCode === 500 || result.statusCode === 404) { - error(logging, 'build', result.error || result.statusCode); - } else if (result.statusCode !== 200) { - error(logging, 'build', `Unexpected load result (${result.statusCode}) for ${fileURLToPath(outPath)}`); - } else { - const bytes = result.contents; - await writeFilep(outPath, bytes, encoding); - } -} - -/** Collection utility */ -function getPageType(filepath: URL): 'collection' | 'static' { - if (/\$[^.]+.astro$/.test(filepath.pathname)) return 'collection'; - return 'static'; -} - -/** Build collection */ -async function buildCollectionPage({ astroRoot, dist, filepath, runtime, site, statics }: PageBuildOptions): Promise<PageResult> { - const rel = path.relative(fileURLToPath(astroRoot) + '/pages', fileURLToPath(filepath)); // pages/index.astro - const pagePath = `/${rel.replace(/\$([^.]+)\.astro$/, '$1')}`; - const builtURLs = new Set<string>(); // !important: internal cache that prevents building the same URLs - - /** Recursively build collection URLs */ - async function loadCollection(url: string): Promise<LoadResult | undefined> { - if (builtURLs.has(url)) return; // this stops us from recursively building the same pages over and over - const result = await runtime.load(url); - builtURLs.add(url); - if (result.statusCode === 200) { - const outPath = new URL('./' + url + '/index.html', dist); - await writeResult(result, outPath, 'utf8'); - mergeSet(statics, collectStatics(result.contents.toString('utf8'))); - } - return result; - } - - const result = (await loadCollection(pagePath)) as LoadResult; - - if (result.statusCode >= 500) { - throw new Error((result as any).error); - } - if (result.statusCode === 200 && !result.collectionInfo) { - throw new Error(`[${rel}]: Collection page must export createCollection() function`); - } - - let rss: string | undefined; - - // note: for pages that require params (/tag/:tag), we will get a 404 but will still get back collectionInfo that tell us what the URLs should be - if (result.collectionInfo) { - // build subsequent pages - await Promise.all( - [...result.collectionInfo.additionalURLs].map(async (url) => { - // for the top set of additional URLs, we render every new URL generated - const addlResult = await loadCollection(url); - builtURLs.add(url); - if (addlResult && addlResult.collectionInfo) { - // believe it or not, we may still have a few unbuilt pages left. this is our last crawl: - await Promise.all([...addlResult.collectionInfo.additionalURLs].map(async (url2) => loadCollection(url2))); - } - }) - ); - - if (result.collectionInfo.rss) { - if (!site) throw new Error(`[${rel}] createCollection() tried to generate RSS but "buildOptions.site" missing in astro.config.mjs`); - rss = generateRSS({ ...(result.collectionInfo.rss as any), site }, rel.replace(/\$([^.]+)\.astro$/, '$1')); - } - } - - return { - canonicalURLs: [...builtURLs].filter((url) => !url.endsWith('/1')), // note: canonical URLs are controlled by the collection, so these are canonical (but exclude "/1" pages as those are duplicates of the index) - statusCode: result.statusCode, - rss, - }; -} - -/** Build static page */ -async function buildStaticPage({ astroRoot, dist, filepath, runtime, sitemap, statics }: PageBuildOptions): Promise<PageResult> { - const rel = path.relative(fileURLToPath(astroRoot) + '/pages', fileURLToPath(filepath)); // pages/index.astro - const pagePath = `/${rel.replace(/\.(astro|md)$/, '')}`; - let canonicalURLs: string[] = []; - - let relPath = './' + rel.replace(/\.(astro|md)$/, '.html'); - if (!relPath.endsWith('index.html')) { - relPath = relPath.replace(/\.html$/, '/index.html'); - } - - const outPath = new URL(relPath, dist); - const result = await runtime.load(pagePath); - - await writeResult(result, outPath, 'utf8'); - - if (result.statusCode === 200) { - mergeSet(statics, collectStatics(result.contents.toString('utf8'))); - - // get Canonical URL (if user has specified one manually, use that) - if (sitemap) { - const $ = cheerio.load(result.contents); - const canonicalTag = $('link[rel="canonical"]'); - canonicalURLs.push(canonicalTag.attr('href') || pagePath.replace(/index$/, '')); - } - } - - return { - canonicalURLs, - statusCode: result.statusCode, - }; -} - -/** The primary build action */ -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 = { - level: 'error', - dest: defaultLogDestination, - }; - - const mode: RuntimeMode = 'production'; - const runtime = await createRuntime(astroConfig, { mode, logging: runtimeLogging }); - const { runtimeConfig } = runtime; - const { backendSnowpack: snowpack } = runtimeConfig; - const resolvePackageUrl = (pkgName: string) => snowpack.getUrlForPackage(pkgName); - - const imports = new Set<string>(); - const statics = new Set<string>(); - const collectImportsOptions = { astroConfig, logging, resolvePackageUrl, mode }; - - const pages = await allPages(pageRoot); - let builtURLs: string[] = []; - - - try { - info(logging , 'build', yellow('! building pages...')); - // Vue also console.warns, this silences it. - const release = trapWarn(); - await Promise.all( - pages.map(async (pathname) => { - const filepath = new URL(`file://${pathname}`); - - const pageType = getPageType(filepath); - const pageOptions: PageBuildOptions = { astroRoot, dist, filepath, runtime, site: astroConfig.buildOptions.site, sitemap: astroConfig.buildOptions.sitemap, statics }; - if (pageType === 'collection') { - const { canonicalURLs, rss } = await buildCollectionPage(pageOptions); - builtURLs.push(...canonicalURLs); - if (rss) { - const basename = path - .relative(fileURLToPath(astroRoot) + '/pages', pathname) - .replace(/^\$/, '') - .replace(/\.astro$/, ''); - await writeFilep(new URL(`file://${path.join(fileURLToPath(dist), 'feed', basename + '.xml')}`), rss, 'utf8'); - } - } else { - const { canonicalURLs } = await buildStaticPage(pageOptions); - builtURLs.push(...canonicalURLs); - } - - mergeSet(imports, await collectDynamicImports(filepath, collectImportsOptions)); - }) - ); - info(logging, 'build', green('✔'), 'pages built.'); - release(); - } catch (err) { - error(logging, 'generate', err); - await runtime.shutdown(); - return 1; - } - - info(logging, 'build', yellow('! scanning pages...')); - for (const pathname of await allPages(componentRoot)) { - mergeSet(imports, await collectDynamicImports(new URL(`file://${pathname}`), collectImportsOptions)); - } - info(logging, 'build', green('✔'), 'pages scanned.'); - - if (imports.size > 0) { - try { - info(logging, 'build', yellow('! bundling client-side code.')); - await bundle(imports, { dist, runtime, astroConfig }); - info(logging, 'build', green('✔'), 'bundling complete.'); - } catch (err) { - error(logging, 'build', err); - await runtime.shutdown(); - return 1; - } - } - - for (let url of statics) { - const outPath = new URL('.' + url, dist); - const result = await runtime.load(url); - - await writeResult(result, outPath, null); - } - - if (existsSync(astroConfig.public)) { - info(logging, 'build', yellow(`! copying public folder...`)); - const pub = astroConfig.public; - const publicFiles = (await new fdir().withFullPaths().crawl(fileURLToPath(pub)).withPromise()) as string[]; - for (const filepath of publicFiles) { - const fileUrl = new URL(`file://${filepath}`); - const rel = path.relative(pub.pathname, fileUrl.pathname); - const outUrl = new URL('./' + rel, dist); - - const bytes = await readFile(fileUrl); - await writeFilep(outUrl, bytes, null); - } - info(logging, 'build', green('✔'), 'public folder copied.'); - } else { - if(path.basename(astroConfig.public.toString()) !=='public'){ - info(logging, 'tip', yellow(`! no public folder ${astroConfig.public} found...`)); - } - } - // build sitemap - if (astroConfig.buildOptions.sitemap && astroConfig.buildOptions.site) { - info(logging, 'build', yellow('! creating a sitemap...')); - const sitemap = generateSitemap(builtURLs.map((url) => ({ canonicalURL: canonicalURL(url, astroConfig.buildOptions.site) }))); - await writeFile(new URL('./sitemap.xml', dist), sitemap, 'utf8'); - info(logging, 'build', green('✔'), 'sitemap built.'); - } else if (astroConfig.buildOptions.sitemap) { - info(logging, 'tip', `Set "buildOptions.site" in astro.config.mjs to generate a sitemap.xml`); - } - - builtURLs.sort((a, b) => a.localeCompare(b, 'en', { numeric: true })); - info(logging, 'build', underline('Pages')); - const lastIndex = builtURLs.length - 1; - builtURLs.forEach((url, index) => { - const sep = index === 0 ? '┌' : index === lastIndex ? '└' : '├'; - info(logging, null, ' ' + sep, url === '/' ? url : url + '/'); - }); - - await runtime.shutdown(); - info(logging, 'build', bold(green('▶ Build Complete!'))); - return 0; -} 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); -} diff --git a/src/build/rss.ts b/src/build/rss.ts deleted file mode 100644 index b75ed908b..000000000 --- a/src/build/rss.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { CollectionRSS } from '../@types/astro'; -import parser from 'fast-xml-parser'; -import { canonicalURL } from './util.js'; - -/** Validates createCollection.rss */ -export function validateRSS(rss: CollectionRSS, filename: string): void { - if (!rss.title) throw new Error(`[${filename}] rss.title required`); - if (!rss.description) throw new Error(`[${filename}] rss.description required`); - if (typeof rss.item !== 'function') throw new Error(`[${filename}] rss.item() function required`); -} - -/** Generate RSS 2.0 feed */ -export function generateRSS<T>(input: { data: T[]; site: string } & CollectionRSS<T>, filename: string): string { - let xml = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"`; - - validateRSS(input as any, filename); - - // xmlns - if (input.xmlns) { - for (const [k, v] of Object.entries(input.xmlns)) { - xml += ` xmlns:${k}="${v}"`; - } - } - xml += `>`; - xml += `<channel>`; - - // title, description, customData - xml += `<title><![CDATA[${input.title}]]></title>`; - xml += `<description><![CDATA[${input.description}]]></description>`; - xml += `<link>${canonicalURL('/feed/' + filename + '.xml', input.site)}</link>`; - if (typeof input.customData === 'string') xml += input.customData; - - // items - if (!Array.isArray(input.data) || !input.data.length) throw new Error(`[${filename}] data() returned no items. Can’t generate RSS feed.`); - for (const item of input.data) { - xml += `<item>`; - const result = input.item(item); - // validate - if (typeof result !== 'object') throw new Error(`[${filename}] rss.item() expected to return an object, returned ${typeof result}.`); - if (!result.title) throw new Error(`[${filename}] rss.item() returned object but required "title" is missing.`); - if (!result.link) throw new Error(`[${filename}] rss.item() returned object but required "link" is missing.`); - xml += `<title><![CDATA[${result.title}]]></title>`; - xml += `<link>${canonicalURL(result.link, input.site)}</link>`; - if (result.description) xml += `<description><![CDATA[${result.description}]]></description>`; - if (result.pubDate) { - // note: this should be a Date, but if user provided a string or number, we can work with that, too. - if (typeof result.pubDate === 'number' || typeof result.pubDate === 'string') { - result.pubDate = new Date(result.pubDate); - } else if (result.pubDate instanceof Date === false) { - throw new Error('[${filename}] rss.item().pubDate must be a Date'); - } - xml += `<pubDate>${result.pubDate.toUTCString()}</pubDate>`; - } - if (typeof result.customData === 'string') xml += result.customData; - xml += `</item>`; - } - - xml += `</channel></rss>`; - - // validate user’s inputs to see if it’s valid XML - const isValid = parser.validate(xml); - if (isValid !== true) { - // If valid XML, isValid will be `true`. Otherwise, this will be an error object. Throw. - throw new Error(isValid as any); - } - - return xml; -} diff --git a/src/build/sitemap.ts b/src/build/sitemap.ts deleted file mode 100644 index 1cb3f3e40..000000000 --- a/src/build/sitemap.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface PageMeta { - /** (required) The canonical URL of the page */ - canonicalURL: string; -} - -/** Construct sitemap.xml given a set of URLs */ -export function generateSitemap(pages: PageMeta[]): string { - let sitemap = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`; - pages.sort((a, b) => a.canonicalURL.localeCompare(b.canonicalURL, 'en', { numeric: true })); // sort alphabetically - for (const page of pages) { - sitemap += `<url><loc>${page.canonicalURL}</loc></url>`; - } - sitemap += `</urlset>\n`; - return sitemap; -} diff --git a/src/build/static.ts b/src/build/static.ts deleted file mode 100644 index af99c33cb..000000000 --- a/src/build/static.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { Element } from 'domhandler'; -import cheerio from 'cheerio'; - -/** Given an HTML string, collect <link> and <img> tags */ -export function collectStatics(html: string) { - const statics = new Set<string>(); - - const $ = cheerio.load(html); - - const append = (el: Element, attr: string) => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const value: string = $(el).attr(attr)!; - if (value.startsWith('http') || $(el).attr('rel') === 'alternate') { - return; - } - statics.add(value); - }; - - $('link[href]').each((i, el) => { - append(el, 'href'); - }); - - $('img[src]').each((i, el) => { - append(el, 'src'); - }); - - return statics; -} diff --git a/src/build/util.ts b/src/build/util.ts deleted file mode 100644 index 505e6f183..000000000 --- a/src/build/util.ts +++ /dev/null @@ -1,9 +0,0 @@ -import path from 'path'; - -/** Normalize URL to its canonical form */ -export function canonicalURL(url: string, base?: string): string { - return new URL( - path.extname(url) ? url : url.replace(/(\/+)?$/, '/'), // add trailing slash if there’s no extension - base - ).href; -} diff --git a/src/cli.ts b/src/cli.ts deleted file mode 100644 index be0dfe27a..000000000 --- a/src/cli.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* eslint-disable no-console */ -import type { AstroConfig } from './@types/astro'; - -import * as colors from 'kleur/colors'; -import { promises as fsPromises } from 'fs'; -import yargs from 'yargs-parser'; - -import { loadConfig } from './config.js'; -import { build } from './build.js'; -import devServer from './dev.js'; - -const { readFile } = fsPromises; -const buildAndExit = async (...args: Parameters<typeof build>) => { - const ret = await build(...args); - process.exit(ret); -}; - -type Arguments = yargs.Arguments; -type cliCommand = 'help' | 'version' | 'dev' | 'build'; -interface CLIState { - cmd: cliCommand; - options: { - projectRoot?: string; - sitemap?: boolean; - port?: number; - config?: string; - }; -} - -/** Determine which action the user requested */ -function resolveArgs(flags: Arguments): CLIState { - const options: CLIState['options'] = { - projectRoot: typeof flags.projectRoot === 'string' ? flags.projectRoot: undefined, - sitemap: typeof flags.sitemap === 'boolean' ? flags.sitemap : undefined, - port: typeof flags.port === 'number' ? flags.port : undefined, - config: typeof flags.config === 'string' ? flags.config : undefined - }; - - if (flags.version) { - return { cmd: 'version', options }; - } else if (flags.help) { - return { cmd: 'help', options }; - } - - const cmd = flags._[2]; - switch (cmd) { - case 'dev': - return { cmd: 'dev', options }; - case 'build': - return { cmd: 'build', options }; - default: - return { cmd: 'help', options }; - } -} - -/** Display --help flag */ -function printHelp() { - console.error(` ${colors.bold('astro')} - Futuristic web development tool. - - ${colors.bold('Commands:')} - astro dev Run Astro in development mode. - astro build Build a pre-compiled production version of your site. - - ${colors.bold('Flags:')} - --config <path> Specify the path to the Astro config file. - --project-root <path> Specify the path to the project root folder. - --no-sitemap Disable sitemap generation (build only). - --version Show the version number and exit. - --help Show this help message. -`); -} - -/** Display --version flag */ -async function printVersion() { - const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf-8')); - console.error(pkg.version); -} - -/** Merge CLI flags & config options (CLI flags take priority) */ -function mergeCLIFlags(astroConfig: AstroConfig, flags: CLIState['options']) { - if (typeof flags.sitemap === 'boolean') astroConfig.buildOptions.sitemap = flags.sitemap; - if (typeof flags.port === 'number') astroConfig.devOptions.port = flags.port; -} - -/** Handle `astro run` command */ -async function runCommand(rawRoot: string, cmd: (a: AstroConfig) => Promise<void>, options: CLIState['options']) { - try { - const projectRoot = options.projectRoot || rawRoot; - const astroConfig = await loadConfig(projectRoot, options.config); - mergeCLIFlags(astroConfig, options); - - return cmd(astroConfig); - } catch (err) { - console.error(colors.red(err.toString() || err)); - process.exit(1); - } -} - -const cmdMap = new Map([ - ['build', buildAndExit], - ['dev', devServer], -]); - -/** The primary CLI action */ -export async function cli(args: string[]) { - const flags = yargs(args); - const state = resolveArgs(flags); - - switch (state.cmd) { - case 'help': { - printHelp(); - process.exit(1); - break; - } - case 'version': { - await printVersion(); - process.exit(0); - break; - } - case 'build': - case 'dev': { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const cmd = cmdMap.get(state.cmd)!; - runCommand(flags._[3], cmd, state.options); - } - } -} diff --git a/src/compiler/codegen/content.ts b/src/compiler/codegen/content.ts deleted file mode 100644 index fb8f9e307..000000000 --- a/src/compiler/codegen/content.ts +++ /dev/null @@ -1,78 +0,0 @@ -import path from 'path'; -import { fdir, PathsOutput } from 'fdir'; - -/** - * Handling for import.meta.glob and import.meta.globEager - */ - -interface GlobOptions { - namespace: string; - filename: string; -} - -interface GlobResult { - /** Array of import statements to inject */ - imports: Set<string>; - /** Replace original code with */ - code: string; -} - -const crawler = new fdir(); - -/** General glob handling */ -function globSearch(spec: string, { filename }: { filename: string }): string[] { - try { - // Note: fdir’s glob requires you to do some work finding the closest non-glob folder. - // For example, this fails: .glob("./post/*.md").crawl("/…/src/pages") ❌ - // …but this doesn’t: .glob("*.md").crawl("/…/src/pages/post") ✅ - let globDir = ''; - let glob = spec; - for (const part of spec.split('/')) { - if (!part.includes('*')) { - // iterate through spec until first '*' is reached - globDir = path.posix.join(globDir, part); // this must be POSIX-style - glob = glob.replace(`${part}/`, ''); // move parent dirs off spec, and onto globDir - } else { - // at first '*', exit - break; - } - } - - const cwd = path.join(path.dirname(filename), globDir.replace(/\//g, path.sep)); // this must match OS (could be '/' or '\') - let found = crawler.glob(glob).crawl(cwd).sync() as PathsOutput; - if (!found.length) { - throw new Error(`No files matched "${spec}" from ${filename}`); - } - return found.map((importPath) => { - if (importPath.startsWith('http') || importPath.startsWith('.')) return importPath; - return `./` + globDir + '/' + importPath; - }); - } catch (err) { - throw new Error(`No files matched "${spec}" from ${filename}`); - } -} - -/** Astro.fetchContent() */ -export function fetchContent(spec: string, { namespace, filename }: GlobOptions): GlobResult { - let code = ''; - const imports = new Set<string>(); - const importPaths = globSearch(spec, { filename }); - - // gather imports - importPaths.forEach((importPath, j) => { - const id = `${namespace}_${j}`; - imports.add(`import { __content as ${id} } from '${importPath}';`); - - // add URL if this appears within the /pages/ directory (probably can be improved) - const fullPath = path.resolve(path.dirname(filename), importPath); - if (fullPath.includes(`${path.sep}pages${path.sep}`)) { - const url = importPath.replace(/^\./, '').replace(/\.md$/, ''); - imports.add(`${id}.url = '${url}';`); - } - }); - - // generate replacement code - code += `${namespace} = [${importPaths.map((_, j) => `${namespace}_${j}`).join(',')}];\n`; - - return { imports, code }; -} diff --git a/src/compiler/codegen/index.ts b/src/compiler/codegen/index.ts deleted file mode 100644 index 5113799d6..000000000 --- a/src/compiler/codegen/index.ts +++ /dev/null @@ -1,686 +0,0 @@ -import type { CompileOptions } from '../../@types/compiler'; -import type { AstroConfig, ValidExtensionPlugins } from '../../@types/astro'; -import type { Ast, Script, Style, TemplateNode } from '../../parser/interfaces'; -import type { TransformResult } from '../../@types/astro'; - -import eslexer from 'es-module-lexer'; -import esbuild from 'esbuild'; -import path from 'path'; -import { walk } from 'estree-walker'; -import _babelGenerator from '@babel/generator'; -import babelParser from '@babel/parser'; -import { codeFrameColumns } from '@babel/code-frame'; -import * as babelTraverse from '@babel/traverse'; -import { ImportDeclaration, ExportNamedDeclaration, VariableDeclarator, Identifier } from '@babel/types'; -import { warn } from '../../logger.js'; -import { fetchContent } from './content.js'; -import { isFetchContent } from './utils.js'; -import { yellow } from 'kleur/colors'; - -const traverse: typeof babelTraverse.default = (babelTraverse.default as any).default; -const babelGenerator: typeof _babelGenerator = - // @ts-ignore - _babelGenerator.default; -const { transformSync } = esbuild; - -interface Attribute { - start: number; - end: number; - type: 'Attribute'; - name: string; - value: TemplateNode[] | boolean; -} - -interface CodeGenOptions { - compileOptions: CompileOptions; - filename: string; - fileID: string; -} - -/** Format Astro internal import URL */ -function internalImport(internalPath: string) { - return `/_astro_internal/${internalPath}`; -} - -/** Retrieve attributes from TemplateNode */ -function getAttributes(attrs: Attribute[]): Record<string, string> { - let result: Record<string, string> = {}; - for (const attr of attrs) { - if (attr.value === true) { - result[attr.name] = JSON.stringify(attr.value); - continue; - } - if (attr.value === false || attr.value === undefined) { - // note: attr.value shouldn’t be `undefined`, but a bad transform would cause a compile error here, so prevent that - continue; - } - if (attr.value.length > 1) { - result[attr.name] = - '(' + - attr.value - .map((v: TemplateNode) => { - if (v.content) { - return v.content; - } else { - return JSON.stringify(getTextFromAttribute(v)); - } - }) - .join('+') + - ')'; - continue; - } - const val = attr.value[0]; - if (!val) { - result[attr.name] = '(' + val + ')'; - continue; - } - switch (val.type) { - case 'MustacheTag': { - // FIXME: this won't work when JSX element can appear in attributes (rare but possible). - result[attr.name] = '(' + val.expression.codeChunks[0] + ')'; - continue; - } - case 'Text': - result[attr.name] = JSON.stringify(getTextFromAttribute(val)); - continue; - default: - throw new Error(`UNKNOWN: ${val.type}`); - } - } - return result; -} - -/** Get value from a TemplateNode Attribute (text attributes only!) */ -function getTextFromAttribute(attr: any): string { - switch (attr.type) { - case 'Text': { - if (attr.raw !== undefined) { - return attr.raw; - } - if (attr.data !== undefined) { - return attr.data; - } - break; - } - case 'MustacheTag': { - // FIXME: this won't work when JSX element can appear in attributes (rare but possible). - return attr.expression.codeChunks[0]; - } - } - throw new Error(`Unknown attribute type ${attr.type}`); -} - -/** Convert TemplateNode attributes to string */ -function generateAttributes(attrs: Record<string, string>): string { - let result = '{'; - for (const [key, val] of Object.entries(attrs)) { - result += JSON.stringify(key) + ':' + val + ','; - } - return result + '}'; -} - -interface ComponentInfo { - type: string; - url: string; - plugin: string | undefined; -} - -const defaultExtensions: Readonly<Record<string, ValidExtensionPlugins>> = { - '.astro': 'astro', - '.jsx': 'react', - '.tsx': 'react', - '.vue': 'vue', - '.svelte': 'svelte', -}; - -type DynamicImportMap = Map<'vue' | 'react' | 'react-dom' | 'preact' | 'svelte', string>; - -interface GetComponentWrapperOptions { - filename: string; - astroConfig: AstroConfig; - dynamicImports: DynamicImportMap; -} - -/** Generate Astro-friendly component import */ -function getComponentWrapper(_name: string, { type, plugin, url }: ComponentInfo, opts: GetComponentWrapperOptions) { - const { astroConfig, dynamicImports, filename } = opts; - const { astroRoot } = astroConfig; - const [name, kind] = _name.split(':'); - const currFileUrl = new URL(`file://${filename}`); - - if (!plugin) { - throw new Error(`No supported plugin found for ${type ? `extension ${type}` : `${url} (try adding an extension)`}`); - } - - const getComponentUrl = (ext = '.js') => { - const outUrl = new URL(url, currFileUrl); - return '/_astro/' + path.posix.relative(astroRoot.pathname, outUrl.pathname).replace(/\.[^.]+$/, ext); - }; - - switch (plugin) { - case 'astro': { - if (kind) { - throw new Error(`Astro does not support :${kind}`); - } - return { - wrapper: name, - wrapperImport: ``, - }; - } - case 'preact': { - if (['load', 'idle', 'visible'].includes(kind)) { - return { - wrapper: `__preact_${kind}(${name}, ${JSON.stringify({ - componentUrl: getComponentUrl(), - componentExport: 'default', - frameworkUrls: { - preact: dynamicImports.get('preact'), - }, - })})`, - wrapperImport: `import {__preact_${kind}} from '${internalImport('render/preact.js')}';`, - }; - } - - return { - wrapper: `__preact_static(${name})`, - wrapperImport: `import {__preact_static} from '${internalImport('render/preact.js')}';`, - }; - } - case 'react': { - if (['load', 'idle', 'visible'].includes(kind)) { - return { - wrapper: `__react_${kind}(${name}, ${JSON.stringify({ - componentUrl: getComponentUrl(), - componentExport: 'default', - frameworkUrls: { - react: dynamicImports.get('react'), - 'react-dom': dynamicImports.get('react-dom'), - }, - })})`, - wrapperImport: `import {__react_${kind}} from '${internalImport('render/react.js')}';`, - }; - } - - return { - wrapper: `__react_static(${name})`, - wrapperImport: `import {__react_static} from '${internalImport('render/react.js')}';`, - }; - } - case 'svelte': { - if (['load', 'idle', 'visible'].includes(kind)) { - return { - wrapper: `__svelte_${kind}(${name}, ${JSON.stringify({ - componentUrl: getComponentUrl('.svelte.js'), - componentExport: 'default', - frameworkUrls: { - 'svelte-runtime': internalImport('runtime/svelte.js'), - }, - })})`, - wrapperImport: `import {__svelte_${kind}} from '${internalImport('render/svelte.js')}';`, - }; - } - - return { - wrapper: `__svelte_static(${name})`, - wrapperImport: `import {__svelte_static} from '${internalImport('render/svelte.js')}';`, - }; - } - case 'vue': { - if (['load', 'idle', 'visible'].includes(kind)) { - return { - wrapper: `__vue_${kind}(${name}, ${JSON.stringify({ - componentUrl: getComponentUrl('.vue.js'), - componentExport: 'default', - frameworkUrls: { - vue: dynamicImports.get('vue'), - }, - })})`, - wrapperImport: `import {__vue_${kind}} from '${internalImport('render/vue.js')}';`, - }; - } - - return { - wrapper: `__vue_static(${name})`, - wrapperImport: `import {__vue_static} from '${internalImport('render/vue.js')}';`, - }; - } - default: { - throw new Error(`Unknown component type`); - } - } -} - -/** Evaluate expression (safely) */ -function compileExpressionSafe(raw: string): string { - let { code } = transformSync(raw, { - loader: 'tsx', - jsxFactory: 'h', - jsxFragment: 'Fragment', - charset: 'utf8', - }); - return code; -} - -/** Build dependency map of dynamic component runtime frameworks */ -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 '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; - } - case 'svelte': { - importMap.set('svelte', await resolvePackageUrl('svelte')); - break; - } - } - } - return importMap; -} - -type Components = Record<string, { type: string; url: string; plugin: string | undefined }>; - -interface CompileResult { - script: string; - componentPlugins: Set<ValidExtensionPlugins>; - createCollection?: string; -} - -interface CodegenState { - filename: string; - components: Components; - css: string[]; - importExportStatements: Set<string>; - dynamicImports: DynamicImportMap; -} - -/** Compile/prepare Astro frontmatter scripts */ -function compileModule(module: Script, state: CodegenState, compileOptions: CompileOptions): CompileResult { - const { extensions = defaultExtensions } = compileOptions; - - const componentImports: ImportDeclaration[] = []; - const componentProps: VariableDeclarator[] = []; - const componentExports: ExportNamedDeclaration[] = []; - - const contentImports = new Map<string, { spec: string; declarator: string }>(); - - let script = ''; - let propsStatement = ''; - let contentCode = ''; // code for handling Astro.fetchContent(), if any; - let createCollection = ''; // function for executing collection - const componentPlugins = new Set<ValidExtensionPlugins>(); - - if (module) { - const parseOptions: babelParser.ParserOptions = { - sourceType: 'module', - plugins: ['jsx', 'typescript', 'topLevelAwait'], - }; - let parseResult; - try { - parseResult = babelParser.parse(module.content, parseOptions); - } catch (err) { - const location = { start: err.loc }; - const frame = codeFrameColumns(module.content, location); - err.frame = frame; - err.filename = state.filename; - err.start = err.loc; - throw err; - } - const program = parseResult.program; - - const { body } = program; - let i = body.length; - while (--i >= 0) { - const node = body[i]; - switch (node.type) { - case 'ExportNamedDeclaration': { - if (!node.declaration) break; - // const replacement = extract_exports(node); - - if (node.declaration.type === 'VariableDeclaration') { - // case 1: prop (export let title) - - const declaration = node.declaration.declarations[0]; - if ((declaration.id as Identifier).name === '__layout' || (declaration.id as Identifier).name === '__content') { - componentExports.push(node); - } else { - componentProps.push(declaration); - } - body.splice(i, 1); - } else if (node.declaration.type === 'FunctionDeclaration') { - // case 2: createCollection (export async function) - if (!node.declaration.id || node.declaration.id.name !== 'createCollection') break; - createCollection = module.content.substring(node.declaration.start || 0, node.declaration.end || 0); - - // remove node - body.splice(i, 1); - } - break; - } - case 'FunctionDeclaration': { - break; - } - case 'ImportDeclaration': { - componentImports.push(node); - body.splice(i, 1); // remove node - break; - } - case 'VariableDeclaration': { - for (const declaration of node.declarations) { - // only select Astro.fetchContent() calls here. this utility filters those out for us. - if (!isFetchContent(declaration)) continue; - - // remove node - body.splice(i, 1); - - // a bit of munging - let { id, init } = declaration; - if (!id || !init || id.type !== 'Identifier') continue; - if (init.type === 'AwaitExpression') { - init = init.argument; - const shortname = path.relative(compileOptions.astroConfig.projectRoot.pathname, state.filename); - warn(compileOptions.logging, shortname, yellow('awaiting Astro.fetchContent() not necessary')); - } - if (init.type !== 'CallExpression') continue; - - // gather data - const namespace = id.name; - - if ((init as any).arguments[0].type !== 'StringLiteral') { - throw new Error(`[Astro.fetchContent] Only string literals allowed, ex: \`Astro.fetchContent('./post/*.md')\`\n ${state.filename}`); - } - const spec = (init as any).arguments[0].value; - if (typeof spec === 'string') contentImports.set(namespace, { spec, declarator: node.kind }); - } - break; - } - } - } - - for (const componentImport of componentImports) { - const importUrl = componentImport.source.value; - const componentType = path.posix.extname(importUrl); - const specifier = componentImport.specifiers[0]; - if (!specifier) continue; // this is unused - // set componentName to default import if used (user), or use filename if no default import (mostly internal use) - const componentName = specifier.type === 'ImportDefaultSpecifier' ? specifier.local.name : path.posix.basename(importUrl, componentType); - const plugin = extensions[componentType] || defaultExtensions[componentType]; - state.components[componentName] = { - type: componentType, - plugin, - url: importUrl, - }; - if (plugin) { - componentPlugins.add(plugin); - } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - state.importExportStatements.add(module.content.slice(componentImport.start!, componentImport.end!)); - } - for (const componentImport of componentExports) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - state.importExportStatements.add(module.content.slice(componentImport.start!, componentImport.end!)); - } - - if (componentProps.length > 0) { - propsStatement = 'let {'; - for (const componentExport of componentProps) { - propsStatement += `${(componentExport.id as Identifier).name}`; - if (componentExport.init) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - propsStatement += `= ${babelGenerator(componentExport.init!).code}`; - } - propsStatement += `,`; - } - propsStatement += `} = props;\n`; - } - - // handle createCollection, if any - if (createCollection) { - // TODO: improve this? while transforming in-place isn’t great, this happens at most once per-route - const ast = babelParser.parse(createCollection, { - sourceType: 'module', - }); - traverse(ast, { - enter({ node }) { - switch (node.type) { - case 'VariableDeclaration': { - for (const declaration of node.declarations) { - // only select Astro.fetchContent() calls here. this utility filters those out for us. - if (!isFetchContent(declaration)) continue; - - // a bit of munging - let { id, init } = declaration; - if (!id || !init || id.type !== 'Identifier') continue; - if (init.type === 'AwaitExpression') { - init = init.argument; - const shortname = path.relative(compileOptions.astroConfig.projectRoot.pathname, state.filename); - warn(compileOptions.logging, shortname, yellow('awaiting Astro.fetchContent() not necessary')); - } - if (init.type !== 'CallExpression') continue; - - // gather data - const namespace = id.name; - - if ((init as any).arguments[0].type !== 'StringLiteral') { - throw new Error(`[Astro.fetchContent] Only string literals allowed, ex: \`Astro.fetchContent('./post/*.md')\`\n ${state.filename}`); - } - const spec = (init as any).arguments[0].value; - if (typeof spec !== 'string') break; - - const globResult = fetchContent(spec, { namespace, filename: state.filename }); - - let imports = ''; - for (const importStatement of globResult.imports) { - imports += importStatement + '\n'; - } - - createCollection = - imports + '\nexport ' + createCollection.substring(0, declaration.start || 0) + globResult.code + createCollection.substring(declaration.end || 0); - } - break; - } - } - }, - }); - } - - // Astro.fetchContent() - for (const [namespace, { spec }] of contentImports.entries()) { - const globResult = fetchContent(spec, { namespace, filename: state.filename }); - for (const importStatement of globResult.imports) { - state.importExportStatements.add(importStatement); - } - contentCode += globResult.code; - } - - script = propsStatement + contentCode + babelGenerator(program).code; - } - - return { - script, - componentPlugins, - createCollection: createCollection || undefined, - }; -} - -/** Compile styles */ -function compileCss(style: Style, state: CodegenState) { - walk(style, { - enter(node: TemplateNode) { - if (node.type === 'Style') { - state.css.push(node.content.styles); // if multiple <style> tags, combine together - this.skip(); - } - }, - leave(node: TemplateNode) { - if (node.type === 'Style') { - this.remove(); // this will be optimized in a global CSS file; remove so it‘s not accidentally inlined - } - }, - }); -} - -/** Compile page markup */ -function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOptions: CompileOptions) { - const { components, css, importExportStatements, dynamicImports, filename } = state; - const { astroConfig } = compileOptions; - - let outSource = ''; - walk(enterNode, { - enter(node: TemplateNode) { - switch (node.type) { - case 'Expression': { - let children: string[] = []; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - for (const child of node.children!) { - children.push(compileHtml(child, state, compileOptions)); - } - let raw = ''; - let nextChildIndex = 0; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - for (const chunk of node.codeChunks!) { - raw += chunk; - if (nextChildIndex < children.length) { - raw += children[nextChildIndex++]; - } - } - // TODO Do we need to compile this now, or should we compile the entire module at the end? - let code = compileExpressionSafe(raw).trim().replace(/\;$/, ''); - outSource += `,(${code})`; - this.skip(); - break; - } - case 'MustacheTag': - case 'Comment': - return; - case 'Fragment': - break; - case 'Slot': - case 'Head': - case 'InlineComponent': - case 'Title': - case 'Element': { - const name: string = node.name; - if (!name) { - throw new Error('AHHHH'); - } - const attributes = getAttributes(node.attributes); - - outSource += outSource === '' ? '' : ','; - if (node.type === 'Slot') { - outSource += `(children`; - return; - } - const COMPONENT_NAME_SCANNER = /^[A-Z]/; - if (!COMPONENT_NAME_SCANNER.test(name)) { - outSource += `h("${name}", ${attributes ? generateAttributes(attributes) : 'null'}`; - return; - } - const [componentName, componentKind] = name.split(':'); - const componentImportData = components[componentName]; - if (!componentImportData) { - throw new Error(`Unknown Component: ${componentName}`); - } - const { wrapper, wrapperImport } = getComponentWrapper(name, components[componentName], { astroConfig, dynamicImports, filename }); - if (wrapperImport) { - importExportStatements.add(wrapperImport); - } - - outSource += `h(${wrapper}, ${attributes ? generateAttributes(attributes) : 'null'}`; - return; - } - case 'Attribute': { - this.skip(); - return; - } - case 'Style': { - css.push(node.content.styles); // if multiple <style> tags, combine together - this.skip(); - return; - } - case 'Text': { - const text = getTextFromAttribute(node); - if (!text.trim()) { - return; - } - outSource += ',' + JSON.stringify(text); - return; - } - default: - throw new Error('Unexpected (enter) node type: ' + node.type); - } - }, - leave(node, parent, prop, index) { - switch (node.type) { - case 'Text': - case 'Attribute': - case 'Comment': - case 'Fragment': - case 'Expression': - case 'MustacheTag': - return; - case 'Slot': - case 'Head': - case 'Body': - case 'Title': - case 'Element': - case 'InlineComponent': - outSource += ')'; - return; - case 'Style': { - this.remove(); // this will be optimized in a global CSS file; remove so it‘s not accidentally inlined - return; - } - default: - throw new Error('Unexpected (leave) node type: ' + node.type); - } - }, - }); - - return outSource; -} - -/** - * Codegen - * Step 3/3 in Astro SSR. - * This is the final pass over a document AST before it‘s converted to an h() function - * and handed off to Snowpack to build. - * @param {Ast} AST The parsed AST to crawl - * @param {object} CodeGenOptions - */ -export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOptions): Promise<TransformResult> { - await eslexer.init; - - const state: CodegenState = { - filename, - components: {}, - css: [], - importExportStatements: new Set(), - dynamicImports: new Map(), - }; - - const { script, componentPlugins, createCollection } = compileModule(ast.module, state, compileOptions); - state.dynamicImports = await acquireDynamicComponentImports(componentPlugins, compileOptions.resolvePackageUrl); - - compileCss(ast.css, state); - - const html = compileHtml(ast.html, state, compileOptions); - - return { - script: script, - imports: Array.from(state.importExportStatements), - html, - css: state.css.length ? state.css.join('\n\n') : undefined, - createCollection, - }; -} diff --git a/src/compiler/codegen/utils.ts b/src/compiler/codegen/utils.ts deleted file mode 100644 index e1c558bc4..000000000 --- a/src/compiler/codegen/utils.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Codegen utils - */ - -import type { VariableDeclarator } from '@babel/types'; - -/** Is this an import.meta.* built-in? You can pass an optional 2nd param to see if the name matches as well. */ -export function isImportMetaDeclaration(declaration: VariableDeclarator, metaName?: string): boolean { - let { init } = declaration; - if (!init) return false; // definitely not import.meta - // this could be `await import.meta`; if so, evaluate that: - if (init.type === 'AwaitExpression') { - init = init.argument; - } - // continue evaluating - if (init.type !== 'CallExpression' || init.callee.type !== 'MemberExpression' || init.callee.object.type !== 'MetaProperty') return false; - // optional: if metaName specified, match that - if (metaName && (init.callee.property.type !== 'Identifier' || init.callee.property.name !== metaName)) return false; - return true; -} - -/** Is this an Astro.fetchContent() call? */ -export function isFetchContent(declaration: VariableDeclarator): boolean { - let { init } = declaration; - if (!init) return false; // definitely not import.meta - // this could be `await import.meta`; if so, evaluate that: - if (init.type === 'AwaitExpression') { - init = init.argument; - } - // continue evaluating - if ( - init.type !== 'CallExpression' || - init.callee.type !== 'MemberExpression' || - (init.callee.object as any).name !== 'Astro' || - (init.callee.property as any).name !== 'fetchContent' - ) - return false; - return true; -} diff --git a/src/compiler/index.ts b/src/compiler/index.ts deleted file mode 100644 index 7e7bfc4c6..000000000 --- a/src/compiler/index.ts +++ /dev/null @@ -1,176 +0,0 @@ -import type { CompileResult, TransformResult } from '../@types/astro'; -import type { CompileOptions } from '../@types/compiler.js'; - -import path from 'path'; -import micromark from 'micromark'; -import gfmSyntax from 'micromark-extension-gfm'; -import matter from 'gray-matter'; -import gfmHtml from 'micromark-extension-gfm/html.js'; - -import { parse } from '../parser/index.js'; -import { createMarkdownHeadersCollector } from './markdown/micromark-collect-headers.js'; -import { encodeMarkdown } from './markdown/micromark-encode.js'; -import { encodeAstroMdx } from './markdown/micromark-mdx-astro.js'; -import { transform } from './transform/index.js'; -import { codegen } from './codegen/index.js'; - -/** Return Astro internal import URL */ -function internalImport(internalPath: string) { - return `/_astro_internal/${internalPath}`; -} - -interface ConvertAstroOptions { - compileOptions: CompileOptions; - filename: string; - fileID: string; -} - -/** - * .astro -> .jsx - * Core function processing .astro files. Initiates all 3 phases of compilation: - * 1. Parse - * 2. Transform - * 3. Codegen - */ -async function convertAstroToJsx(template: string, opts: ConvertAstroOptions): Promise<TransformResult> { - const { filename } = opts; - - // 1. Parse - const ast = parse(template, { - filename, - }); - - // 2. Transform the AST - await transform(ast, opts); - - // 3. Turn AST into JSX - return await codegen(ast, opts); -} - -/** - * .md -> .astro source - */ -export async function convertMdToAstroSource(contents: string): Promise<string> { - const { data: frontmatterData, content } = matter(contents); - const { headers, headersExtension } = createMarkdownHeadersCollector(); - const { htmlAstro, mdAstro } = encodeAstroMdx(); - const mdHtml = micromark(content, { - allowDangerousHtml: true, - extensions: [gfmSyntax(), ...htmlAstro], - htmlExtensions: [gfmHtml, encodeMarkdown, headersExtension, mdAstro], - }); - - // TODO: Warn if reserved word is used in "frontmatterData" - const contentData: any = { - ...frontmatterData, - headers, - source: content, - }; - - let imports = ''; - for (let [ComponentName, specifier] of Object.entries(frontmatterData.import || {})) { - imports += `import ${ComponentName} from '${specifier}';\n`; - } - - // </script> can't be anywhere inside of a JS string, otherwise the HTML parser fails. - // Break it up here so that the HTML parser won't detect it. - const stringifiedSetupContext = JSON.stringify(contentData).replace(/\<\/script\>/g, `</scrip" + "t>`); - - return `--- - ${imports} - ${frontmatterData.layout ? `import {__renderPage as __layout} from '${frontmatterData.layout}';` : 'const __layout = undefined;'} - export const __content = ${stringifiedSetupContext}; ---- -<section>${mdHtml}</section>`; -} - -/** - * .md -> .jsx - * Core function processing Markdown, but along the way also calls convertAstroToJsx(). - */ -async function convertMdToJsx( - contents: string, - { compileOptions, filename, fileID }: { compileOptions: CompileOptions; filename: string; fileID: string } -): Promise<TransformResult> { - const raw = await convertMdToAstroSource(contents); - const convertOptions = { compileOptions, filename, fileID }; - return await convertAstroToJsx(raw, convertOptions); -} - -type SupportedExtensions = '.astro' | '.md'; - -/** Given a file, process it either as .astro or .md. */ -async function transformFromSource( - contents: string, - { compileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string } -): Promise<TransformResult> { - const fileID = path.relative(projectRoot, filename); - switch (path.extname(filename) as SupportedExtensions) { - case '.astro': - return await convertAstroToJsx(contents, { compileOptions, filename, fileID }); - case '.md': - return await convertMdToJsx(contents, { compileOptions, filename, fileID }); - default: - throw new Error('Not Supported!'); - } -} - -/** Return internal code that gets processed in Snowpack */ -export async function compileComponent( - source: string, - { compileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string } -): Promise<CompileResult> { - const result = await transformFromSource(source, { compileOptions, filename, projectRoot }); - - // return template - let modJsx = ` -import fetch from 'node-fetch'; - -// <script astro></script> -${result.imports.join('\n')} - -// \`__render()\`: Render the contents of the Astro module. -import { h, Fragment } from '${internalImport('h.js')}'; -const __astroRequestSymbol = Symbol('astro.request'); -async function __render(props, ...children) { - const Astro = { - request: props[__astroRequestSymbol] - }; - - ${result.script} - return h(Fragment, null, ${result.html}); -} -export default __render; - -${result.createCollection || ''} - -// \`__renderPage()\`: Render the contents of the Astro module as a page. This is a special flow, -// triggered by loading a component directly by URL. -export async function __renderPage({request, children, props}) { - const currentChild = { - layout: typeof __layout === 'undefined' ? undefined : __layout, - content: typeof __content === 'undefined' ? undefined : __content, - __render, - }; - - props[__astroRequestSymbol] = request; - const childBodyResult = await currentChild.__render(props, children); - - // find layout, if one was given. - if (currentChild.layout) { - return currentChild.layout({ - request, - props: {content: currentChild.content}, - children: [childBodyResult], - }); - } - - return childBodyResult; -};\n`; - - return { - result, - contents: modJsx, - css: result.css, - }; -} diff --git a/src/compiler/markdown/micromark-collect-headers.ts b/src/compiler/markdown/micromark-collect-headers.ts deleted file mode 100644 index 69781231a..000000000 --- a/src/compiler/markdown/micromark-collect-headers.ts +++ /dev/null @@ -1,38 +0,0 @@ -import slugger from 'github-slugger'; - -/** - * Create Markdown Headers Collector - * NOTE: micromark has terrible TS types. Instead of fighting with the - * limited/broken TS types that they ship, we just reach for our good friend, "any". - */ -export function createMarkdownHeadersCollector() { - const headers: any[] = []; - let currentHeader: any; - return { - headers, - headersExtension: { - enter: { - atxHeading(node: any) { - currentHeader = {}; - headers.push(currentHeader); - this.buffer(); - }, - atxHeadingSequence(node: any) { - currentHeader.depth = this.sliceSerialize(node).length; - }, - atxHeadingText(node: any) { - currentHeader.text = this.sliceSerialize(node); - }, - } as any, - exit: { - atxHeading(node: any) { - currentHeader.slug = slugger.slug(currentHeader.text); - this.resume(); - this.tag(`<h${currentHeader.depth} id="${currentHeader.slug}">`); - this.raw(currentHeader.text); - this.tag(`</h${currentHeader.depth}>`); - }, - } as any, - } as any, - }; -} diff --git a/src/compiler/markdown/micromark-encode.ts b/src/compiler/markdown/micromark-encode.ts deleted file mode 100644 index 635ab3b54..000000000 --- a/src/compiler/markdown/micromark-encode.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Token } from 'micromark/dist/shared-types'; -import type { MicromarkExtension, MicromarkExtensionContext } from '../../@types/micromark'; - -const characterReferences = { - '"': 'quot', - '&': 'amp', - '<': 'lt', - '>': 'gt', - '{': 'lbrace', - '}': 'rbrace', -}; - -type EncodedChars = '"' | '&' | '<' | '>' | '{' | '}'; - -/** Encode HTML entity */ -function encode(value: string): string { - return value.replace(/["&<>{}]/g, (raw: string) => { - return '&' + characterReferences[raw as EncodedChars] + ';'; - }); -} - -/** Encode Markdown node */ -function encodeToken(this: MicromarkExtensionContext) { - const token: Token = arguments[0]; - const value = this.sliceSerialize(token); - this.raw(encode(value)); -} - -const plugin: MicromarkExtension = { - exit: { - codeTextData: encodeToken, - codeFlowValue: encodeToken, - }, -}; - -export { plugin as encodeMarkdown }; diff --git a/src/compiler/markdown/micromark-mdx-astro.ts b/src/compiler/markdown/micromark-mdx-astro.ts deleted file mode 100644 index b978ad407..000000000 --- a/src/compiler/markdown/micromark-mdx-astro.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { MicromarkExtension } from '../../@types/micromark'; -import mdxExpression from 'micromark-extension-mdx-expression'; -import mdxJsx from 'micromark-extension-mdx-jsx'; - -/** - * Keep MDX. - */ -export function encodeAstroMdx() { - const extension: MicromarkExtension = { - enter: { - mdxJsxFlowTag(node: any) { - const mdx = this.sliceSerialize(node); - this.raw(mdx); - }, - }, - }; - - return { - htmlAstro: [mdxExpression(), mdxJsx()], - mdAstro: extension, - }; -} diff --git a/src/compiler/markdown/micromark.d.ts b/src/compiler/markdown/micromark.d.ts deleted file mode 100644 index fd094306e..000000000 --- a/src/compiler/markdown/micromark.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -declare module 'micromark-extension-mdx-expression' { - import type { HtmlExtension } from 'micromark/dist/shared-types'; - - export default function (): HtmlExtension; -} - -declare module 'micromark-extension-mdx-jsx' { - import type { HtmlExtension } from 'micromark/dist/shared-types'; - - export default function (): HtmlExtension; -} diff --git a/src/compiler/transform/doctype.ts b/src/compiler/transform/doctype.ts deleted file mode 100644 index e871f5b48..000000000 --- a/src/compiler/transform/doctype.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Transformer } from '../../@types/transformer'; - -/** Transform <!doctype> tg */ -export default function (_opts: { filename: string; fileID: string }): Transformer { - let hasDoctype = false; - - return { - visitors: { - html: { - Element: { - enter(node, parent, _key, index) { - if (node.name === '!doctype') { - hasDoctype = true; - } - if (node.name === 'html' && !hasDoctype) { - const dtNode = { - start: 0, - end: 0, - attributes: [{ type: 'Attribute', name: 'html', value: true, start: 0, end: 0 }], - children: [], - name: '!doctype', - type: 'Element', - }; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - parent.children!.splice(index, 0, dtNode); - hasDoctype = true; - } - }, - }, - }, - }, - async finalize() { - // Nothing happening here. - }, - }; -} diff --git a/src/compiler/transform/index.ts b/src/compiler/transform/index.ts deleted file mode 100644 index 02a98709b..000000000 --- a/src/compiler/transform/index.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type { Ast, TemplateNode } from '../../parser/interfaces'; -import type { NodeVisitor, TransformOptions, Transformer, VisitorFn } from '../../@types/transformer'; - -import { walk } from 'estree-walker'; - -// Transformers -import transformStyles from './styles.js'; -import transformDoctype from './doctype.js'; -import transformModuleScripts from './module-scripts.js'; -import transformCodeBlocks from './prism.js'; - -interface VisitorCollection { - enter: Map<string, VisitorFn[]>; - leave: Map<string, VisitorFn[]>; -} - -/** Add visitors to given collection */ -function addVisitor(visitor: NodeVisitor, collection: VisitorCollection, nodeName: string, event: 'enter' | 'leave') { - if (typeof visitor[event] !== 'function') return; - if (!collection[event]) collection[event] = new Map<string, VisitorFn[]>(); - - const visitors = collection[event].get(nodeName) || []; - visitors.push(visitor[event] as any); - collection[event].set(nodeName, visitors); -} - -/** Compile visitor actions from transformer */ -function collectVisitors(transformer: Transformer, htmlVisitors: VisitorCollection, cssVisitors: VisitorCollection, finalizers: Array<() => Promise<void>>) { - if (transformer.visitors) { - if (transformer.visitors.html) { - for (const [nodeName, visitor] of Object.entries(transformer.visitors.html)) { - addVisitor(visitor, htmlVisitors, nodeName, 'enter'); - addVisitor(visitor, htmlVisitors, nodeName, 'leave'); - } - } - if (transformer.visitors.css) { - for (const [nodeName, visitor] of Object.entries(transformer.visitors.css)) { - addVisitor(visitor, cssVisitors, nodeName, 'enter'); - addVisitor(visitor, cssVisitors, nodeName, 'leave'); - } - } - } - finalizers.push(transformer.finalize); -} - -/** Utility for formatting visitors */ -function createVisitorCollection() { - return { - enter: new Map<string, VisitorFn[]>(), - leave: new Map<string, VisitorFn[]>(), - }; -} - -/** Walk AST with collected visitors */ -function walkAstWithVisitors(tmpl: TemplateNode, collection: VisitorCollection) { - walk(tmpl, { - enter(node, parent, key, index) { - if (collection.enter.has(node.type)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const fns = collection.enter.get(node.type)!; - for (let fn of fns) { - fn.call(this, node, parent, key, index); - } - } - }, - leave(node, parent, key, index) { - if (collection.leave.has(node.type)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const fns = collection.leave.get(node.type)!; - for (let fn of fns) { - fn.call(this, node, parent, key, index); - } - } - }, - }); -} - -/** - * Transform - * Step 2/3 in Astro SSR. - * Transform is the point at which we mutate the AST before sending off to - * Codegen, and then to Snowpack. In some ways, it‘s a preprocessor. - */ -export async function transform(ast: Ast, opts: TransformOptions) { - const htmlVisitors = createVisitorCollection(); - const cssVisitors = createVisitorCollection(); - const finalizers: Array<() => Promise<void>> = []; - - const optimizers = [transformStyles(opts), transformDoctype(opts), transformModuleScripts(opts), transformCodeBlocks(ast.module)]; - - for (const optimizer of optimizers) { - collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers); - } - - walkAstWithVisitors(ast.css, cssVisitors); - walkAstWithVisitors(ast.html, htmlVisitors); - - // Run all of the finalizer functions in parallel because why not. - await Promise.all(finalizers.map((fn) => fn())); -} diff --git a/src/compiler/transform/module-scripts.ts b/src/compiler/transform/module-scripts.ts deleted file mode 100644 index aff1ec4f6..000000000 --- a/src/compiler/transform/module-scripts.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { Transformer } from '../../@types/transformer'; -import type { CompileOptions } from '../../@types/compiler'; - -import path from 'path'; -import { getAttrValue, setAttrValue } from '../../ast.js'; - -/** Transform <script type="module"> */ -export default function ({ compileOptions, filename }: { compileOptions: CompileOptions; filename: string; fileID: string }): Transformer { - 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/compiler/transform/postcss-scoped-styles/index.ts b/src/compiler/transform/postcss-scoped-styles/index.ts deleted file mode 100644 index 23350869c..000000000 --- a/src/compiler/transform/postcss-scoped-styles/index.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { Declaration, Plugin } from 'postcss'; - -interface AstroScopedOptions { - className: string; -} - -interface Selector { - start: number; - end: number; - value: string; -} - -const CSS_SEPARATORS = new Set([' ', ',', '+', '>', '~']); -const KEYFRAME_PERCENT = /\d+\.?\d*%/; - -/** HTML tags that should never get scoped classes */ -export const NEVER_SCOPED_TAGS = new Set<string>(['base', 'body', 'font', 'frame', 'frameset', 'head', 'html', 'link', 'meta', 'noframes', 'noscript', 'script', 'style', 'title']); - -/** - * Scope Rules - * Given a selector string (`.btn>span,.nav>span`), add an additional CSS class to every selector (`.btn.myClass>span.myClass,.nav.myClass>span.myClass`) - * @param {string} selector The minified selector string to parse. Cannot contain arbitrary whitespace (other than child selector syntax). - * @param {string} className The CSS class to apply. - */ -export function scopeRule(selector: string, className: string) { - // if this is a keyframe keyword, return original selector - if (selector === 'from' || selector === 'to' || KEYFRAME_PERCENT.test(selector)) { - return selector; - } - - // For everything else, parse & scope - const c = className.replace(/^\.?/, '.'); // make sure class always has leading '.' - const selectors: Selector[] = []; - let ss = selector; // final output - - // Pass 1: parse selector string; extract top-level selectors - { - let start = 0; - let lastValue = ''; - let parensOpen = false; - for (let n = 0; n < ss.length; n++) { - const isEnd = n === selector.length - 1; - if (selector[n] === '(') parensOpen = true; - if (selector[n] === ')') parensOpen = false; - if (isEnd || (parensOpen === false && CSS_SEPARATORS.has(selector[n]))) { - lastValue = selector.substring(start, isEnd ? undefined : n); - if (!lastValue) continue; - selectors.push({ start, end: isEnd ? n + 1 : n, value: lastValue }); - start = n + 1; - } - } - } - - // Pass 2: starting from end, transform selectors w/ scoped class - for (let i = selectors.length - 1; i >= 0; i--) { - const { start, end, value } = selectors[i]; - const head = ss.substring(0, start); - const tail = ss.substring(end); - - // replace '*' with className - if (value === '*') { - ss = head + c + tail; - continue; - } - - // leave :global() alone! - if (value.startsWith(':global(')) { - ss = - head + - ss - .substring(start, end) - .replace(/^:global\(/, '') - .replace(/\)$/, '') + - tail; - continue; - } - - // don‘t scope body, title, etc. - if (NEVER_SCOPED_TAGS.has(value)) { - ss = head + value + tail; - continue; - } - - // scope everything else - let newSelector = ss.substring(start, end); - const pseudoIndex = newSelector.indexOf(':'); - if (pseudoIndex > 0) { - // if there‘s a pseudoclass (:focus) - ss = head + newSelector.substring(start, pseudoIndex) + c + newSelector.substr(pseudoIndex) + tail; - } else { - ss = head + newSelector + c + tail; - } - } - - return ss; -} - -/** PostCSS Scope plugin */ -export default function astroScopedStyles(options: AstroScopedOptions): Plugin { - return { - postcssPlugin: '@astro/postcss-scoped-styles', - Rule(rule) { - rule.selector = scopeRule(rule.selector, options.className); - }, - }; -} diff --git a/src/compiler/transform/prism.ts b/src/compiler/transform/prism.ts deleted file mode 100644 index 1bb024a84..000000000 --- a/src/compiler/transform/prism.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { Transformer } from '../../@types/transformer'; -import type { Script } from '../../parser/interfaces'; -import { getAttrValue } from '../../ast.js'; - -const PRISM_IMPORT = `import Prism from 'astro/components/Prism.astro';\n`; -const prismImportExp = /import Prism from ['"]astro\/components\/Prism.astro['"]/; -/** escaping code samples that contain template string replacement parts, ${foo} or example. */ -function escape(code: string) { - return code.replace(/[`$]/g, (match) => { - return '\\' + match; - }); -} -/** default export - Transform prism */ -export default function (module: Script): Transformer { - let usesPrism = false; - - return { - visitors: { - html: { - Element: { - enter(node) { - if (node.name !== 'code') return; - const className = getAttrValue(node.attributes, 'class') || ''; - const classes = className.split(' '); - - let lang; - for (let cn of classes) { - const matches = /language-(.+)/.exec(cn); - if (matches) { - lang = matches[1]; - } - } - - if (!lang) return; - - let code; - if (node.children?.length) { - code = node.children[0].data; - } - - const repl = { - start: 0, - end: 0, - type: 'InlineComponent', - name: 'Prism', - attributes: [ - { - type: 'Attribute', - name: 'lang', - value: [ - { - type: 'Text', - raw: lang, - data: lang, - }, - ], - }, - { - type: 'Attribute', - name: 'code', - value: [ - { - type: 'MustacheTag', - expression: { - type: 'Expression', - codeChunks: ['`' + escape(code) + '`'], - children: [], - }, - }, - ], - }, - ], - children: [], - }; - - this.replace(repl); - usesPrism = true; - }, - }, - }, - }, - async finalize() { - // Add the Prism import if needed. - if (usesPrism && !prismImportExp.test(module.content)) { - module.content = PRISM_IMPORT + module.content; - } - }, - }; -} diff --git a/src/compiler/transform/styles.ts b/src/compiler/transform/styles.ts deleted file mode 100644 index 53585651f..000000000 --- a/src/compiler/transform/styles.ts +++ /dev/null @@ -1,290 +0,0 @@ -import crypto from 'crypto'; -import fs from 'fs'; -import { createRequire } from 'module'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import autoprefixer from 'autoprefixer'; -import postcss, { Plugin } from 'postcss'; -import postcssKeyframes from 'postcss-icss-keyframes'; -import findUp from 'find-up'; -import sass from 'sass'; -import type { RuntimeMode } from '../../@types/astro'; -import type { TransformOptions, Transformer } from '../../@types/transformer'; -import type { TemplateNode } from '../../parser/interfaces'; -import { debug } from '../../logger.js'; -import astroScopedStyles, { NEVER_SCOPED_TAGS } from './postcss-scoped-styles/index.js'; - -type StyleType = 'css' | 'scss' | 'sass' | 'postcss'; - -declare global { - interface ImportMeta { - /** https://nodejs.org/api/esm.html#esm_import_meta_resolve_specifier_parent */ - resolve(specifier: string, parent?: string): Promise<any>; - } -} - -const getStyleType: Map<string, StyleType> = new Map([ - ['.css', 'css'], - ['.pcss', 'postcss'], - ['.sass', 'sass'], - ['.scss', 'scss'], - ['css', 'css'], - ['sass', 'sass'], - ['scss', 'scss'], - ['text/css', 'css'], - ['text/sass', 'sass'], - ['text/scss', 'scss'], -]); - -/** Should be deterministic, given a unique filename */ -function hashFromFilename(filename: string): string { - const hash = crypto.createHash('sha256'); - return hash - .update(filename.replace(/\\/g, '/')) - .digest('base64') - .toString() - .replace(/[^A-Za-z0-9-]/g, '') - .substr(0, 8); -} - -export interface StyleTransformResult { - css: string; - type: StyleType; -} - -interface StylesMiniCache { - nodeModules: Map<string, string>; // filename: node_modules location - tailwindEnabled?: boolean; // cache once per-run -} - -/** Simple cache that only exists in memory per-run. Prevents the same lookups from happening over and over again within the same build or dev server session. */ -const miniCache: StylesMiniCache = { - nodeModules: new Map<string, string>(), -}; - -export interface TransformStyleOptions { - type?: string; - filename: string; - scopedClass: string; - mode: RuntimeMode; -} - -/** given a class="" string, does it contain a given class? */ -function hasClass(classList: string, className: string): boolean { - if (!className) return false; - for (const c of classList.split(' ')) { - if (className === c.trim()) return true; - } - return false; -} - -/** Convert styles to scoped CSS */ -async function transformStyle(code: string, { type, filename, scopedClass, mode }: TransformStyleOptions): Promise<StyleTransformResult> { - let styleType: StyleType = 'css'; // important: assume CSS as default - if (type) { - styleType = getStyleType.get(type) || styleType; - } - - // add file path to includePaths - let includePaths: string[] = [path.dirname(filename)]; - - // include node_modules to includePaths (allows @use-ing node modules, if it can be located) - const cachedNodeModulesDir = miniCache.nodeModules.get(filename); - if (cachedNodeModulesDir) { - includePaths.push(cachedNodeModulesDir); - } else { - const nodeModulesDir = await findUp('node_modules', { type: 'directory', cwd: path.dirname(filename) }); - if (nodeModulesDir) { - miniCache.nodeModules.set(filename, nodeModulesDir); - includePaths.push(nodeModulesDir); - } - } - - // 1. Preprocess (currently only Sass supported) - let css = ''; - switch (styleType) { - case 'css': { - css = code; - break; - } - case 'sass': - case 'scss': { - css = sass.renderSync({ data: code, includePaths }).css.toString('utf8'); - break; - } - default: { - throw new Error(`Unsupported: <style lang="${styleType}">`); - } - } - - // 2. Post-process (PostCSS) - const postcssPlugins: Plugin[] = []; - - // 2a. Tailwind (only if project uses Tailwind) - if (miniCache.tailwindEnabled) { - try { - const require = createRequire(import.meta.url); - const tw = require.resolve('tailwindcss', { paths: [import.meta.url, process.cwd()] }); - postcssPlugins.push(require(tw) as any); - } catch (err) { - // eslint-disable-next-line no-console - console.error(err); - throw new Error(`tailwindcss not installed. Try running \`npm install tailwindcss\` and trying again.`); - } - } - - // 2b. Astro scoped styles (always on) - postcssPlugins.push(astroScopedStyles({ className: scopedClass })); - - // 2c. Scoped @keyframes - postcssPlugins.push( - postcssKeyframes({ - generateScopedName(keyframesName) { - return `${keyframesName}-${scopedClass}`; - }, - }) - ); - - // 2d. Autoprefixer (always on) - postcssPlugins.push(autoprefixer()); - - // 2e. Run PostCSS - css = await postcss(postcssPlugins) - .process(css, { from: filename, to: undefined }) - .then((result) => result.css); - - return { css, type: styleType }; -} - -/** Transform <style> tags */ -export default function transformStyles({ compileOptions, filename, fileID }: TransformOptions): Transformer { - const styleNodes: TemplateNode[] = []; // <style> tags to be updated - const styleTransformPromises: Promise<StyleTransformResult>[] = []; // async style transform results to be finished in finalize(); - const scopedClass = `astro-${hashFromFilename(fileID)}`; // this *should* generate same hash from fileID every time - - // find Tailwind config, if first run (cache for subsequent runs) - if (miniCache.tailwindEnabled === undefined) { - const tailwindNames = ['tailwind.config.js', 'tailwind.config.mjs']; - for (const loc of tailwindNames) { - const tailwindLoc = path.join(fileURLToPath(compileOptions.astroConfig.projectRoot), loc); - if (fs.existsSync(tailwindLoc)) { - miniCache.tailwindEnabled = true; // Success! We have a Tailwind config file. - debug(compileOptions.logging, 'tailwind', 'Found config. Enabling.'); - break; - } - } - if (miniCache.tailwindEnabled !== true) miniCache.tailwindEnabled = false; // We couldn‘t find one; mark as false - debug(compileOptions.logging, 'tailwind', 'No config found. Skipping.'); - } - - return { - visitors: { - html: { - Element: { - enter(node) { - // 1. if <style> tag, transform it and continue to next node - if (node.name === 'style') { - // Same as ast.css (below) - const code = Array.isArray(node.children) ? node.children.map(({ data }: any) => data).join('\n') : ''; - if (!code) return; - const langAttr = (node.attributes || []).find(({ name }: any) => name === 'lang'); - styleNodes.push(node); - styleTransformPromises.push( - transformStyle(code, { - type: (langAttr && langAttr.value[0] && langAttr.value[0].data) || undefined, - filename, - scopedClass, - mode: compileOptions.mode, - }) - ); - return; - } - - // 2. add scoped HTML classes - if (NEVER_SCOPED_TAGS.has(node.name)) return; // only continue if this is NOT a <script> tag, etc. - // Note: currently we _do_ scope web components/custom elements. This seems correct? - - if (!node.attributes) node.attributes = []; - const classIndex = node.attributes.findIndex(({ name }: any) => name === 'class'); - if (classIndex === -1) { - // 3a. element has no class="" attribute; add one and append scopedClass - node.attributes.push({ start: -1, end: -1, type: 'Attribute', name: 'class', value: [{ type: 'Text', raw: scopedClass, data: scopedClass }] }); - } else { - // 3b. element has class=""; append scopedClass - const attr = node.attributes[classIndex]; - for (let k = 0; k < attr.value.length; k++) { - if (attr.value[k].type === 'Text') { - // don‘t add same scopedClass twice - if (!hasClass(attr.value[k].data, scopedClass)) { - // string literal - attr.value[k].raw += ' ' + scopedClass; - attr.value[k].data += ' ' + scopedClass; - } - } else if (attr.value[k].type === 'MustacheTag' && attr.value[k]) { - // don‘t add same scopedClass twice (this check is a little more basic, but should suffice) - if (!attr.value[k].expression.codeChunks[0].includes(`' ${scopedClass}'`)) { - // MustacheTag - // FIXME: this won't work when JSX element can appear in attributes (rare but possible). - attr.value[k].expression.codeChunks[0] = `(${attr.value[k].expression.codeChunks[0]}) + ' ${scopedClass}'`; - } - } - } - } - }, - }, - }, - // CSS: compile styles, apply CSS Modules scoping - css: { - Style: { - enter(node) { - // Same as ast.html (above) - // Note: this is duplicated from html because of the compiler we‘re using; in a future version we should combine these - if (!node.content || !node.content.styles) return; - const code = node.content.styles; - const langAttr = (node.attributes || []).find(({ name }: any) => name === 'lang'); - styleNodes.push(node); - styleTransformPromises.push( - transformStyle(code, { - type: (langAttr && langAttr.value[0] && langAttr.value[0].data) || undefined, - filename, - scopedClass, - mode: compileOptions.mode, - }) - ); - }, - }, - }, - }, - async finalize() { - const styleTransforms = await Promise.all(styleTransformPromises); - - styleTransforms.forEach((result, n) => { - if (styleNodes[n].attributes) { - // 1. Replace with final CSS - const isHeadStyle = !styleNodes[n].content; - if (isHeadStyle) { - // Note: <style> tags in <head> have different attributes/rules, because of the parser. Unknown why - (styleNodes[n].children as any) = [{ ...(styleNodes[n].children as any)[0], data: result.css }]; - } else { - styleNodes[n].content.styles = result.css; - } - - // 2. Update <style> attributes - const styleTypeIndex = styleNodes[n].attributes.findIndex(({ name }: any) => name === 'type'); - // add type="text/css" - if (styleTypeIndex !== -1) { - styleNodes[n].attributes[styleTypeIndex].value[0].raw = 'text/css'; - styleNodes[n].attributes[styleTypeIndex].value[0].data = 'text/css'; - } else { - styleNodes[n].attributes.push({ name: 'type', type: 'Attribute', value: [{ type: 'Text', raw: 'text/css', data: 'text/css' }] }); - } - // remove lang="*" - const styleLangIndex = styleNodes[n].attributes.findIndex(({ name }: any) => name === 'lang'); - if (styleLangIndex !== -1) styleNodes[n].attributes.splice(styleLangIndex, 1); - // TODO: add data-astro for later - // styleNodes[n].attributes.push({ name: 'data-astro', type: 'Attribute', value: true }); - } - }); - }, - }; -} diff --git a/src/config.ts b/src/config.ts deleted file mode 100644 index 8f3ebaf5a..000000000 --- a/src/config.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { AstroConfig } from './@types/astro'; -import { join as pathJoin, resolve as pathResolve } from 'path'; -import { existsSync } from 'fs'; - -/** Type util */ -const type = (thing: any): string => (Array.isArray(thing) ? 'Array' : typeof thing); - -/** Throws error if a user provided an invalid config. Manually-implemented to avoid a heavy validation library. */ -function validateConfig(config: any): void { - // basic - if (config === undefined || config === null) throw new Error(`[astro config] Config empty!`); - if (typeof config !== 'object') throw new Error(`[astro config] Expected object, received ${typeof config}`); - - // strings - for (const key of ['projectRoot', 'astroRoot', 'dist', 'public', 'site']) { - if (config[key] !== undefined && config[key] !== null && typeof config[key] !== 'string') { - throw new Error(`[astro config] ${key}: ${JSON.stringify(config[key])}\n Expected string, received ${type(config[key])}.`); - } - } - - // booleans - for (const key of ['sitemap']) { - if (config[key] !== undefined && config[key] !== null && typeof config[key] !== 'boolean') { - throw new Error(`[astro config] ${key}: ${JSON.stringify(config[key])}\n Expected boolean, received ${type(config[key])}.`); - } - } - - if(typeof config.devOptions?.port !== 'number') { - throw new Error(`[astro config] devOptions.port: Expected number, received ${type(config.devOptions?.port)}`) - } -} - -/** Set default config values */ -function configDefaults(userConfig?: any): any { - const config: any = { ...(userConfig || {}) }; - - if (!config.projectRoot) config.projectRoot = '.'; - if (!config.astroRoot) config.astroRoot = './src'; - if (!config.dist) config.dist = './dist'; - if (!config.public) config.public = './public'; - if (!config.devOptions) config.devOptions = {}; - if (!config.devOptions.port) config.devOptions.port = 3000; - if (!config.buildOptions) config.buildOptions = {}; - if (typeof config.buildOptions.sitemap === 'undefined') config.buildOptions.sitemap = true; - - return config; -} - -/** Turn raw config values into normalized values */ -function normalizeConfig(userConfig: any, root: string): AstroConfig { - const config: any = { ...(userConfig || {}) }; - - const fileProtocolRoot = `file://${root}/`; - config.projectRoot = new URL(config.projectRoot + '/', fileProtocolRoot); - config.astroRoot = new URL(config.astroRoot + '/', fileProtocolRoot); - config.public = new URL(config.public + '/', fileProtocolRoot); - - return config as AstroConfig; -} - -/** Attempt to load an `astro.config.mjs` file */ -export async function loadConfig(rawRoot: string | undefined, configFileName = 'astro.config.mjs'): Promise<AstroConfig> { - if (typeof rawRoot === 'undefined') { - rawRoot = process.cwd(); - } - - const root = pathResolve(rawRoot); - const astroConfigPath = pathJoin(root, configFileName); - - // load - let config: any; - if (existsSync(astroConfigPath)) { - config = configDefaults((await import(astroConfigPath)).default); - } else { - config = configDefaults(); - } - - // validate - validateConfig(config); - - // normalize - config = normalizeConfig(config, root); - - return config as AstroConfig; -} diff --git a/src/dev.ts b/src/dev.ts deleted file mode 100644 index 4ca8e28e9..000000000 --- a/src/dev.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { AstroConfig } from './@types/astro'; -import type { LogOptions } from './logger.js'; - -import { logger as snowpackLogger } from 'snowpack'; -import { bold, green } from 'kleur/colors'; -import http from 'http'; -import { relative as pathRelative } from 'path'; -import { performance } from 'perf_hooks'; -import { defaultLogDestination, error, info, parseError } from './logger.js'; -import { createRuntime } from './runtime.js'; - -const hostname = '127.0.0.1'; - -// Disable snowpack from writing to stdout/err. -snowpackLogger.level = 'silent'; - -const logging: LogOptions = { - level: 'debug', - dest: defaultLogDestination, -}; - -/** The primary dev action */ -export default async function dev(astroConfig: AstroConfig) { - const startServerTime = performance.now(); - const { projectRoot } = astroConfig; - - const runtime = await createRuntime(astroConfig, { mode: 'development', logging }); - - const server = http.createServer(async (req, res) => { - const result = await runtime.load(req.url); - - switch (result.statusCode) { - case 200: { - if (result.contentType) { - res.setHeader('Content-Type', result.contentType); - } - res.statusCode = 200; - res.write(result.contents); - res.end(); - break; - } - case 404: { - const fullurl = new URL(req.url || '/', 'https://example.org/'); - const reqPath = decodeURI(fullurl.pathname); - error(logging, 'static', 'Not found', reqPath); - res.statusCode = 404; - - const fourOhFourResult = await runtime.load('/404'); - if (fourOhFourResult.statusCode === 200) { - if (fourOhFourResult.contentType) { - res.setHeader('Content-Type', fourOhFourResult.contentType); - } - res.write(fourOhFourResult.contents); - } else { - res.setHeader('Content-Type', 'text/plain'); - res.write('Not Found'); - } - res.end(); - break; - } - case 500: { - switch (result.type) { - case 'parse-error': { - const err = result.error; - err.filename = pathRelative(projectRoot.pathname, err.filename); - parseError(logging, err); - break; - } - default: { - error(logging, 'executing astro', result.error); - break; - } - } - res.statusCode = 500; - - let errorResult = await runtime.load(`/500?error=${encodeURIComponent(result.error.stack || result.error.toString())}`); - if(errorResult.statusCode === 200) { - if (errorResult.contentType) { - res.setHeader('Content-Type', errorResult.contentType); - } - res.write(errorResult.contents); - } else { - res.write(result.error.toString()); - } - res.end(); - break; - } - } - }); - - const port = astroConfig.devOptions.port; - server.listen(port, hostname, () => { - const endServerTime = performance.now(); - info(logging, 'dev server', green(`Server started in ${Math.floor(endServerTime - startServerTime)}ms.`)); - info(logging, 'dev server', `${green('Local:')} http://${hostname}:${port}/`); - }); -} diff --git a/src/frontend/500.astro b/src/frontend/500.astro deleted file mode 100644 index 01fab8bea..000000000 --- a/src/frontend/500.astro +++ /dev/null @@ -1,128 +0,0 @@ ---- -import Prism from 'astro/components/Prism.astro'; -let title = 'Uh oh...'; - -const error = Astro.request.url.searchParams.get('error'); ---- - -<!doctype html> -<html lang="en"> - <head> - <title>Error 500</title> - <link rel="preconnect"href="https://fonts.gstatic.com"> - <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&family=IBM+Plex+Sans&display=swap"> - <link rel="stylesheet" href="http://cdn.skypack.dev/prism-themes/themes/prism-material-dark.css"> - - <style> - * { - box-sizing: border-box; - margin: 0; - } - - :global(:root) { - --font-sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; - --font-mono: "IBM Plex Mono", Consolas, "Andale Mono WT", "Andale Mono", - "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", - "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, - "Courier New", Courier, monospace; - --color-gray-800: #1F2937; - --color-gray-500: #6B7280; - --color-gray-400: #9CA3AF; - --color-gray-100: #F3F4F6; - --color-red: #FF1639; - } - - html, body { - width: 100vw; - height: 100%; - min-height: 100vh; - - font-family: var(--font-sans); - font-weight: 400; - background: var(--color-gray-100); - text-align: center; - } - - body { - display: grid; - place-content: center; - } - - header { - display: flex; - flex-direction: column; - align-items: center; - font-family: var(--font-sans); - font-size: 2.5rem; - font-size: clamp(24px, calc(2vw + 1rem), 2.5rem); - } - - header h1 { - margin: 0.25em; - margin-right: 0; - font-weight: 400; - letter-spacing: -2px; - line-height: 1; - } - - header h1 .title { - color: var(--color-gray-400); - white-space: nowrap; - } - - header svg { - margin-bottom: -0.125em; - color: var(--color-red); - } - - p { - font-size: 1.75rem; - font-size: clamp(14px, calc(2vw + 0.5rem), 1.75rem); - flex: 1; - } - - .error-message { - display: grid; - justify-content: center; - margin-top: 4rem; - } - - .error-message :global(code[class*="language-"]) { - background: var(--color-gray-800); - } - .error-message :global(pre) { - margin: 0; - font-family: var(--font-mono); - font-size: 0.85rem; - background: var(--color-gray-800); - border-radius: 8px; - } - - .error-message :global(.token.punctuation) { - color: var(--color-gray-400); - } - - .error-message :global(.token.operator) { - color: var(--color-gray-400); - } - </style> - </head> - <body> - <main> - <header> - <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" width="1.75em" height="1.75em"> - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> - </svg> - <h1><span class="error">500 Error </span><span class="title">{title}</span></h1> - </header> - - <article> - <p>Astro had some trouble loading this page.</p> - - <div class="error-message"> - <Prism lang="shell" code={error} /> - </div> - </article> - </main> - </body> -</html> diff --git a/src/frontend/SvelteWrapper.svelte b/src/frontend/SvelteWrapper.svelte deleted file mode 100644 index eb4cbb7d9..000000000 --- a/src/frontend/SvelteWrapper.svelte +++ /dev/null @@ -1,7 +0,0 @@ -<script> -const { __astro_component: Component, __astro_children, ...props } = $$props; -</script> - -<Component {...props}> - {@html __astro_children} -</Component> diff --git a/src/frontend/SvelteWrapper.svelte.client.ts b/src/frontend/SvelteWrapper.svelte.client.ts deleted file mode 100644 index 9df168895..000000000 --- a/src/frontend/SvelteWrapper.svelte.client.ts +++ /dev/null @@ -1,166 +0,0 @@ -/* eslint-disable */ -// @ts-nocheck -// TODO: don't precompile this, but it works for now -import { - HtmlTag, - SvelteComponentDev, - assign, - claim_component, - create_component, - destroy_component, - detach_dev, - dispatch_dev, - empty, - exclude_internal_props, - get_spread_object, - get_spread_update, - init, - insert_dev, - mount_component, - noop, - not_equal, - transition_in, - transition_out, - validate_slots, -} from 'svelte/internal'; - -const file = 'App.svelte'; - -// (5:0) <Component {...props}> -function create_default_slot(ctx) { - let html_tag; - let html_anchor; - - const block = { - c: function create() { - html_anchor = empty(); - this.h(); - }, - l: function claim(nodes) { - html_anchor = empty(); - this.h(); - }, - h: function hydrate() { - html_tag = new HtmlTag(html_anchor); - }, - m: function mount(target, anchor) { - html_tag.m(/*__astro_children*/ ctx[1], target, anchor); - insert_dev(target, html_anchor, anchor); - }, - p: noop, - d: function destroy(detaching) { - if (detaching) detach_dev(html_anchor); - if (detaching) html_tag.d(); - }, - }; - - dispatch_dev('SvelteRegisterBlock', { - block, - id: create_default_slot.name, - type: 'slot', - source: '(5:0) <Component {...props}>', - ctx, - }); - - return block; -} - -function create_fragment(ctx) { - let component; - let current; - const component_spread_levels = [/*props*/ ctx[2]]; - - let component_props = { - $$slots: { default: [create_default_slot] }, - $$scope: { ctx }, - }; - - for (let i = 0; i < component_spread_levels.length; i += 1) { - component_props = assign(component_props, component_spread_levels[i]); - } - - component = new /*Component*/ ctx[0]({ props: component_props, $$inline: true }); - - const block = { - c: function create() { - create_component(component.$$.fragment); - }, - l: function claim(nodes) { - claim_component(component.$$.fragment, nodes); - }, - m: function mount(target, anchor) { - mount_component(component, target, anchor); - current = true; - }, - p: function update(ctx, [dirty]) { - const component_changes = dirty & /*props*/ 4 ? get_spread_update(component_spread_levels, [get_spread_object(/*props*/ ctx[2])]) : {}; - - if (dirty & /*$$scope*/ 16) { - component_changes.$$scope = { dirty, ctx }; - } - - component.$set(component_changes); - }, - i: function intro(local) { - if (current) return; - transition_in(component.$$.fragment, local); - current = true; - }, - o: function outro(local) { - transition_out(component.$$.fragment, local); - current = false; - }, - d: function destroy(detaching) { - destroy_component(component, detaching); - }, - }; - - dispatch_dev('SvelteRegisterBlock', { - block, - id: create_fragment.name, - type: 'component', - source: '', - ctx, - }); - - return block; -} - -function instance($$self, $$props, $$invalidate) { - let { $$slots: slots = {}, $$scope } = $$props; - validate_slots('App', slots, []); - const { __astro_component: Component, __astro_children, ...props } = $$props; - - $$self.$$set = ($$new_props) => { - $$invalidate(3, ($$props = assign(assign({}, $$props), exclude_internal_props($$new_props)))); - }; - - $$self.$capture_state = () => ({ Component, __astro_children, props }); - - $$self.$inject_state = ($$new_props) => { - $$invalidate(3, ($$props = assign(assign({}, $$props), $$new_props))); - }; - - if ($$props && '$$inject' in $$props) { - $$self.$inject_state($$props.$$inject); - } - - $$props = exclude_internal_props($$props); - return [Component, __astro_children, props]; -} - -class App extends SvelteComponentDev { - constructor(options) { - super(options); - init(this, options, instance, create_fragment, not_equal, {}); - - dispatch_dev('SvelteRegisterComponent', { - component: this, - tagName: 'App', - options, - id: create_fragment.name, - }); - } -} - -export default App; diff --git a/src/frontend/SvelteWrapper.svelte.server.ts b/src/frontend/SvelteWrapper.svelte.server.ts deleted file mode 100644 index c5a25ff03..000000000 --- a/src/frontend/SvelteWrapper.svelte.server.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-disable */ -// @ts-nocheck -// TODO: don't precompile this, but it works for now -/* App.svelte generated by Svelte v3.37.0 */ -import { create_ssr_component, validate_component } from 'svelte/internal'; - -const App = create_ssr_component(($$result, $$props, $$bindings, slots) => { - const { __astro_component: Component, __astro_children, ...props } = $$props; - return `${validate_component(Component, 'Component').$$render($$result, Object.assign(props), {}, { default: () => `${__astro_children}` })}`; -}); - -export default App; diff --git a/src/frontend/h.ts b/src/frontend/h.ts deleted file mode 100644 index c1e21dc95..000000000 --- a/src/frontend/h.ts +++ /dev/null @@ -1,65 +0,0 @@ -export type HProps = Record<string, string> | null | undefined; -export type HChild = string | undefined | (() => string); -export type AstroComponent = (props: HProps, ...children: Array<HChild>) => string; -export type HTag = string | AstroComponent; - -const voidTags = new Set(['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']); - -/** Generator for primary h() function */ -function* _h(tag: string, attrs: HProps, children: Array<HChild>) { - if (tag === '!doctype') { - yield '<!doctype '; - if (attrs) { - yield Object.keys(attrs).join(' '); - } - yield '>'; - return; - } - - yield `<${tag}`; - if (attrs) { - yield ' '; - for (let [key, value] of Object.entries(attrs)) { - yield `${key}="${value}"`; - } - } - yield '>'; - - // Void tags have no children. - if (voidTags.has(tag)) { - return; - } - - for (let child of children) { - // Special: If a child is a function, call it automatically. - // This lets you do {() => ...} without the extra boilerplate - // of wrapping it in a function and calling it. - if (typeof child === 'function') { - yield child(); - } else if (typeof child === 'string') { - yield child; - } else if (!child) { - // do nothing, safe to ignore falsey values. - } else { - yield child; - } - } - - yield `</${tag}>`; -} - -/** Astro‘s primary h() function. Allows it to use JSX-like syntax. */ -export async function h(tag: HTag, attrs: HProps, ...pChildren: Array<Promise<HChild>>) { - const children = await Promise.all(pChildren.flat(Infinity)); - if (typeof tag === 'function') { - // We assume it's an astro component - return tag(attrs, ...children); - } - - return Array.from(_h(tag, attrs, children)).join(''); -} - -/** Fragment helper, similar to React.Fragment */ -export function Fragment(_: HProps, ...children: Array<string>) { - return children.join(''); -} diff --git a/src/frontend/render/preact.ts b/src/frontend/render/preact.ts deleted file mode 100644 index 5c50b6fe3..000000000 --- a/src/frontend/render/preact.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { h, render, ComponentType } from 'preact'; -import { renderToString } from 'preact-render-to-string'; -import { childrenToVnodes } from './utils'; -import type { ComponentRenderer } from '../../@types/renderer'; -import { createRenderer } from './renderer'; - -// This prevents tree-shaking of render. -Function.prototype(render); - -const Preact: ComponentRenderer<ComponentType> = { - jsxPragma: h, - jsxPragmaName: 'h', - renderStatic(Component) { - return async (props, ...children) => { - return renderToString(h(Component, props, childrenToVnodes(h, children))); - }; - }, - imports: { - preact: ['render', 'Fragment', 'h'], - }, - render({ Component, root, props, children }) { - return `render(h(${Component}, ${props}, h(Fragment, null, ...${children})), ${root})`; - }, -}; - -const renderer = createRenderer(Preact); - -export const __preact_static = renderer.static; -export const __preact_load = renderer.load; -export const __preact_idle = renderer.idle; -export const __preact_visible = renderer.visible; diff --git a/src/frontend/render/react.ts b/src/frontend/render/react.ts deleted file mode 100644 index 51c0c9729..000000000 --- a/src/frontend/render/react.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { ComponentRenderer } from '../../@types/renderer'; -import React, { ComponentType } from 'react'; -import ReactDOMServer from 'react-dom/server'; -import { createRenderer } from './renderer'; -import { childrenToVnodes } from './utils'; - -const ReactRenderer: ComponentRenderer<ComponentType> = { - jsxPragma: React.createElement, - jsxPragmaName: 'React.createElement', - renderStatic(Component) { - return async (props, ...children) => { - return ReactDOMServer.renderToString(React.createElement(Component, props, childrenToVnodes(React.createElement, children))); - }; - }, - imports: { - react: ['default: React'], - 'react-dom': ['default: ReactDOM'], - }, - render({ Component, root, children, props }) { - return `ReactDOM.hydrate(React.createElement(${Component}, ${props}, React.createElement(React.Fragment, null, ...${children})), ${root})`; - }, -}; - -const renderer = createRenderer(ReactRenderer); - -export const __react_static = renderer.static; -export const __react_load = renderer.load; -export const __react_idle = renderer.idle; -export const __react_visible = renderer.visible; diff --git a/src/frontend/render/renderer.ts b/src/frontend/render/renderer.ts deleted file mode 100644 index a5cc9d581..000000000 --- a/src/frontend/render/renderer.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { DynamicRenderContext, DynamicRendererGenerator, SupportedComponentRenderer, StaticRendererGenerator } from '../../@types/renderer'; -import { childrenToH } from './utils'; - -/** Initialize Astro Component renderer for Static and Dynamic components */ -export function createRenderer(renderer: SupportedComponentRenderer) { - const _static: StaticRendererGenerator = (Component) => renderer.renderStatic(Component); - const _imports = (context: DynamicRenderContext) => { - const values = Object.values(renderer.imports ?? {}) - .reduce((acc, v) => { - return [...acc, `{ ${v.join(', ')} }`]; - }, []) - .join(', '); - const libs = Object.keys(renderer.imports ?? {}) - .reduce((acc: string[], lib: string) => { - return [...acc, `import("${context.frameworkUrls[lib as any]}")`]; - }, []) - .join(','); - return `const [{${context.componentExport}: Component}, ${values}] = await Promise.all([import("${context.componentUrl}")${renderer.imports ? ', ' + libs : ''}]);`; - }; - const serializeProps = ({ children: _, ...props }: Record<string, any>) => JSON.stringify(props); - const createContext = () => { - const astroId = `${Math.floor(Math.random() * 1e16)}`; - return { ['data-astro-id']: astroId, root: `document.querySelector('[data-astro-id="${astroId}"]')`, Component: 'Component' }; - }; - const createDynamicRender: DynamicRendererGenerator = (wrapperStart, wrapperEnd) => (Component, renderContext) => { - const innerContext = createContext(); - return async (props, ...children) => { - let value: string; - try { - value = await _static(Component)(props, ...children); - } catch (e) { - value = ''; - } - value = `<div data-astro-id="${innerContext['data-astro-id']}" style="display:contents">${value}</div>`; - - const script = ` - ${typeof wrapperStart === 'function' ? wrapperStart(innerContext) : wrapperStart} - ${_imports(renderContext)} - ${renderer.render({ - ...innerContext, - props: serializeProps(props), - children: `[${childrenToH(renderer, children) ?? ''}]`, - childrenAsString: `\`${children}\``, - })} - ${typeof wrapperEnd === 'function' ? wrapperEnd(innerContext) : wrapperEnd} - `; - - return [value, `<script type="module">${script.trim()}</script>`].join('\n'); - }; - }; - - return { - static: _static, - load: createDynamicRender('(async () => {', '})()'), - idle: createDynamicRender('requestIdleCallback(async () => {', '})'), - visible: createDynamicRender( - 'const o = new IntersectionObserver(async ([entry]) => { if (!entry.isIntersecting) { return; } o.disconnect();', - ({ root }) => `}); Array.from(${root}.children).forEach(child => o.observe(child))` - ), - }; -} diff --git a/src/frontend/render/svelte.ts b/src/frontend/render/svelte.ts deleted file mode 100644 index 13e2b8f58..000000000 --- a/src/frontend/render/svelte.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ComponentRenderer } from '../../@types/renderer'; -import type { SvelteComponent } from 'svelte'; -import { createRenderer } from './renderer'; -import SvelteWrapper from '../SvelteWrapper.svelte.server'; - -const SvelteRenderer: ComponentRenderer<SvelteComponent> = { - renderStatic(Component) { - return async (props, ...children) => { - const { html } = SvelteWrapper.render({ __astro_component: Component, __astro_children: children.join('\n'), ...props }); - return html; - }; - }, - imports: { - 'svelte-runtime': ['default: render'], - }, - render({ Component, root, props, childrenAsString }) { - return `render(${root}, ${Component}, ${props}, ${childrenAsString});`; - }, -}; - -const renderer = createRenderer(SvelteRenderer); - -export const __svelte_static = renderer.static; -export const __svelte_load = renderer.load; -export const __svelte_idle = renderer.idle; -export const __svelte_visible = renderer.visible; diff --git a/src/frontend/render/utils.ts b/src/frontend/render/utils.ts deleted file mode 100644 index 9d55626fe..000000000 --- a/src/frontend/render/utils.ts +++ /dev/null @@ -1,52 +0,0 @@ -import unified from 'unified'; -import parse from 'rehype-parse'; -import toH from 'hast-to-hyperscript'; -import { ComponentRenderer } from '../../@types/renderer'; -import moize from 'moize'; - -/** @internal */ -function childrenToTree(children: string[]) { - return children.map((child) => (unified().use(parse, { fragment: true }).parse(child) as any).children.pop()); -} - -/** - * Converts an HTML fragment string into vnodes for rendering via provided framework - * @param h framework's `createElement` function - * @param children the HTML string children - */ -export const childrenToVnodes = moize.deep(function childrenToVnodes(h: any, children: string[]) { - const tree = childrenToTree(children); - const vnodes = tree.map((subtree) => { - if (subtree.type === 'text') return subtree.value; - return toH(h, subtree); - }); - return vnodes; -}); - -/** - * Converts an HTML fragment string into h function calls as a string - * @param h framework's `createElement` function - * @param children the HTML string children - */ -export const childrenToH = moize.deep(function childrenToH(renderer: ComponentRenderer<any>, children: string[]): any { - if (!renderer.jsxPragma) return; - const tree = childrenToTree(children); - const innerH = (name: any, attrs: Record<string, any> | null = null, _children: string[] | null = null) => { - const vnode = renderer.jsxPragma?.(name, attrs, _children); - const childStr = _children ? `, [${_children.map((child) => serializeChild(child)).join(',')}]` : ''; - /* fix(react): avoid hard-coding keys into the serialized tree */ - if (attrs && attrs.key) attrs.key = undefined; - const __SERIALIZED = `${renderer.jsxPragmaName}("${name}", ${attrs ? JSON.stringify(attrs) : 'null'}${childStr})` as string; - return { ...vnode, __SERIALIZED }; - }; - const serializeChild = (child: unknown) => { - if (['string', 'number', 'boolean'].includes(typeof child)) return JSON.stringify(child); - if (child === null) return `null`; - if ((child as any).__SERIALIZED) return (child as any).__SERIALIZED; - return innerH(child).__SERIALIZED; - }; - return tree.map((subtree) => { - if (subtree.type === 'text') return JSON.stringify(subtree.value); - return toH(innerH, subtree).__SERIALIZED - }); -}); diff --git a/src/frontend/render/vue.ts b/src/frontend/render/vue.ts deleted file mode 100644 index 57c3c8276..000000000 --- a/src/frontend/render/vue.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { ComponentRenderer } from '../../@types/renderer'; -import type { Component as VueComponent } from 'vue'; -import { renderToString } from '@vue/server-renderer'; -import { defineComponent, createSSRApp, h as createElement } from 'vue'; -import { createRenderer } from './renderer'; - -// This prevents tree-shaking of render. -Function.prototype(renderToString); - -/** - * Users might attempt to use :vueAttribute syntax to pass primitive values. - * If so, try to JSON.parse them to get the primitives - */ -function cleanPropsForVue(obj: Record<string, any>) { - let cleaned = {} as any; - for (let [key, value] of Object.entries(obj)) { - if (key.startsWith(':')) { - key = key.slice(1); - if (typeof value === 'string') { - try { - value = JSON.parse(value); - } catch (e) {} - } - } - cleaned[key] = value; - } - return cleaned; -} - -const Vue: ComponentRenderer<VueComponent> = { - jsxPragma: createElement, - jsxPragmaName: 'createElement', - renderStatic(Component) { - return async (props, ...children) => { - const App = defineComponent({ - components: { - Component, - }, - data() { - return { props }; - }, - template: `<Component v-bind="props">${children.join('\n')}</Component>`, - }); - - const app = createSSRApp(App); - const html = await renderToString(app); - return html; - }; - }, - imports: { - vue: ['createApp', 'h: createElement'], - }, - render({ Component, root, props, children }) { - const vueProps = cleanPropsForVue(JSON.parse(props)); - return `const App = { render: () => createElement(${Component}, ${JSON.stringify(vueProps)}, { default: () => ${children} }) }; -createApp(App).mount(${root});`; - }, -}; - -const renderer = createRenderer(Vue); - -export const __vue_static = renderer.static; -export const __vue_load = renderer.load; -export const __vue_idle = renderer.idle; -export const __vue_visible = renderer.visible; diff --git a/src/frontend/runtime/svelte.ts b/src/frontend/runtime/svelte.ts deleted file mode 100644 index 78b6af6b6..000000000 --- a/src/frontend/runtime/svelte.ts +++ /dev/null @@ -1,10 +0,0 @@ -import SvelteWrapper from '../SvelteWrapper.svelte.client'; -import type { SvelteComponent } from 'svelte'; - -export default (target: Element, component: SvelteComponent, props: any, children: string) => { - new SvelteWrapper({ - target, - props: { __astro_component: component, __astro_children: children, ...props }, - hydrate: true, - }); -}; diff --git a/src/logger.ts b/src/logger.ts deleted file mode 100644 index 7bfde67d9..000000000 --- a/src/logger.ts +++ /dev/null @@ -1,143 +0,0 @@ -import type { CompileError } from './parser/utils/error.js'; -import { bold, blue, red, grey, underline } from 'kleur/colors'; -import { Writable } from 'stream'; -import { format as utilFormat } from 'util'; - -type ConsoleStream = Writable & { - fd: 1 | 2; -}; - -export const defaultLogDestination = new Writable({ - objectMode: true, - write(event: LogMessage, _, callback) { - let dest: ConsoleStream = process.stderr; - if (levels[event.level] < levels['error']) { - dest = process.stdout; - } - let type = event.type; - if(type !== null) { - if (event.level === 'info') { - type = bold(blue(type)); - } else if (event.level === 'error') { - type = bold(red(type)); - } - - dest.write(`[${type}] `); - } - - dest.write(utilFormat(...event.args)); - dest.write('\n'); - - callback(); - }, -}); - -interface LogWritable<T> extends Writable { - write: (chunk: T) => boolean; -} - -export type LoggerLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'; // same as Pino -export type LoggerEvent = 'debug' | 'info' | 'warn' | 'error'; - -export interface LogOptions { - dest: LogWritable<LogMessage>; - level: LoggerLevel; -} - -export const defaultLogOptions: LogOptions = { - dest: defaultLogDestination, - level: 'info', -}; - -export interface LogMessage { - type: string | null; - level: LoggerLevel; - message: string; - args: Array<any>; -} - -const levels: Record<LoggerLevel, number> = { - debug: 20, - info: 30, - warn: 40, - error: 50, - silent: 90, -}; - -/** Full logging API */ -export function log(opts: LogOptions = defaultLogOptions, level: LoggerLevel, type: string | null, ...args: Array<any>) { - const event: LogMessage = { - type, - level, - args, - message: '', - }; - - // test if this level is enabled or not - if (levels[opts.level] > levels[level]) { - return; // do nothing - } - - opts.dest.write(event); -} - -/** Emit a message only shown in debug mode */ -export function debug(opts: LogOptions, type: string | null, ...messages: Array<any>) { - return log(opts, 'debug', type, ...messages); -} - -/** Emit a general info message (be careful using this too much!) */ -export function info(opts: LogOptions, type: string | null, ...messages: Array<any>) { - return log(opts, 'info', type, ...messages); -} - -/** Emit a warning a user should be aware of */ -export function warn(opts: LogOptions, type: string | null, ...messages: Array<any>) { - return log(opts, 'warn', type, ...messages); -} - -/** Emit a fatal error message the user should address. */ -export function error(opts: LogOptions, type: string | null, ...messages: Array<any>) { - return log(opts, 'error', type, ...messages); -} - -/** Pretty format error for display */ -export function parseError(opts: LogOptions, err: CompileError) { - let frame = err.frame - // Switch colons for pipes - .replace(/^([0-9]+)(:)/gm, `${bold('$1')} │`) - // Make the caret red. - .replace(/(?<=^\s+)(\^)/gm, bold(red(' ^'))) - // Add identation - .replace(/^/gm, ' '); - - error( - opts, - 'parse-error', - ` - - ${underline(bold(grey(`${err.filename}:${err.start.line}:${err.start.column}`)))} - - ${bold(red(`𝘅 ${err.message}`))} - -${frame} -` - ); -} - -// A default logger for when too lazy to pass LogOptions around. -export const logger = { - debug: debug.bind(null, defaultLogOptions), - info: info.bind(null, defaultLogOptions), - warn: warn.bind(null, defaultLogOptions), - error: error.bind(null, defaultLogOptions), -}; - -// For silencing libraries that go directly to console.warn -export function trapWarn(cb: (...args: any[]) => void = () =>{}) { - const warn = console.warn; - console.warn = function(...args: any[]) { - cb(...args); - }; - return () => console.warn = warn; -}
\ No newline at end of file diff --git a/src/parser/README.md b/src/parser/README.md deleted file mode 100644 index f44d45ecf..000000000 --- a/src/parser/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `astro/compiler` - -This directory is a fork of `svelte/compiler`. It is meant to stay as close to the original source as possible, so that upstream changes are easy to integrate. Everything svelte-specific and unrelated to parsing (compiler, preprocess, etc) has been removed.
\ No newline at end of file diff --git a/src/parser/Stats.ts b/src/parser/Stats.ts deleted file mode 100644 index 5f7f991f8..000000000 --- a/src/parser/Stats.ts +++ /dev/null @@ -1,83 +0,0 @@ -// @ts-nocheck - -const now = - typeof process !== 'undefined' && process.hrtime - ? () => { - const t = process.hrtime(); - return t[0] * 1e3 + t[1] / 1e6; - } - : () => self.performance.now(); - -interface Timing { - label: string; - start: number; - end: number; - children: Timing[]; -} - -/** Format benchmarks */ -function collapse_timings(timings) { - const result = {}; - timings.forEach((timing) => { - result[timing.label] = Object.assign( - { - total: timing.end - timing.start, - }, - timing.children && collapse_timings(timing.children) - ); - }); - return result; -} - -export default class Stats { - start_time: number; - current_timing: Timing; - current_children: Timing[]; - timings: Timing[]; - stack: Timing[]; - - constructor() { - this.start_time = now(); - this.stack = []; - this.current_children = this.timings = []; - } - - start(label) { - const timing = { - label, - start: now(), - end: null, - children: [], - }; - - this.current_children.push(timing); - this.stack.push(timing); - - this.current_timing = timing; - this.current_children = timing.children; - } - - stop(label) { - if (label !== this.current_timing.label) { - throw new Error(`Mismatched timing labels (expected ${this.current_timing.label}, got ${label})`); - } - - this.current_timing.end = now(); - this.stack.pop(); - this.current_timing = this.stack[this.stack.length - 1]; - this.current_children = this.current_timing ? this.current_timing.children : this.timings; - } - - render() { - const timings = Object.assign( - { - total: now() - this.start_time, - }, - collapse_timings(this.timings) - ); - - return { - timings, - }; - } -} diff --git a/src/parser/config.ts b/src/parser/config.ts deleted file mode 100644 index e6d0f65a7..000000000 --- a/src/parser/config.ts +++ /dev/null @@ -1 +0,0 @@ -export const test = typeof process !== 'undefined' && process.env.TEST; diff --git a/src/parser/index.ts b/src/parser/index.ts deleted file mode 100644 index 718199c94..000000000 --- a/src/parser/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as parse } from './parse/index.js'; diff --git a/src/parser/interfaces.ts b/src/parser/interfaces.ts deleted file mode 100644 index 3273b8be1..000000000 --- a/src/parser/interfaces.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type { SourceMap } from 'magic-string'; - -export interface BaseNode { - start: number; - end: number; - type: string; - children?: TemplateNode[]; - [prop_name: string]: any; -} - -export interface Fragment extends BaseNode { - type: 'Fragment'; - children: TemplateNode[]; -} - -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 { - type: 'MustacheTag'; - content: string; -} - -export type DirectiveType = 'Action' | 'Animation' | 'Binding' | 'Class' | 'EventHandler' | 'Let' | 'Ref' | 'Transition'; - -interface BaseDirective extends BaseNode { - type: DirectiveType; - expression: null | Node; - name: string; - modifiers: string[]; -} - -export interface Transition extends BaseDirective { - type: 'Transition'; - intro: boolean; - outro: boolean; -} - -export type Directive = BaseDirective | Transition; - -export type TemplateNode = Text | MustacheTag | BaseNode | Directive | Transition; - -export interface Expression { - type: 'Expression'; - start: number; - end: number; - codeChunks: string[]; - children: BaseNode[]; -} - -export interface Parser { - readonly template: string; - readonly filename?: string; - - index: number; - stack: Node[]; - - html: Node; - css: Node; - js: Node; - meta_tags: Map<string, string>; -} - -export interface Script extends BaseNode { - type: 'Script'; - context: 'runtime' | 'setup'; - content: string; -} - -export interface Style extends BaseNode { - type: 'Style'; - attributes: any[]; // TODO - content: { - start: number; - end: number; - styles: string; - }; -} - -export interface Ast { - html: TemplateNode; - css: Style; - module: Script; - // instance: Script; -} - -export interface Warning { - start?: { line: number; column: number; pos?: number }; - end?: { line: number; column: number }; - pos?: number; - code: string; - message: string; - filename?: string; - frame?: string; - toString: () => string; -} - -export type ModuleFormat = 'esm' | 'cjs'; - -export type CssHashGetter = (args: { name: string; filename: string | undefined; css: string; hash: (input: string) => string }) => string; - -export interface Visitor { - enter: (node: Node) => void; - leave?: (node: Node) => void; -} - -export interface AppendTarget { - slots: Record<string, string>; - slot_stack: string[]; -} - -export interface Var { - name: string; - export_name?: string; // the `bar` in `export { foo as bar }` - injected?: boolean; - module?: boolean; - mutated?: boolean; - reassigned?: boolean; - referenced?: boolean; // referenced from template scope - referenced_from_script?: boolean; // referenced from script - writable?: boolean; - - // used internally, but not exposed - global?: boolean; - internal?: boolean; // event handlers, bindings - initialised?: boolean; - hoistable?: boolean; - subscribable?: boolean; - is_reactive_dependency?: boolean; - imported?: boolean; -} - -export interface CssResult { - code: string; - map: SourceMap; -} diff --git a/src/parser/parse/index.ts b/src/parser/parse/index.ts deleted file mode 100644 index 124e125ef..000000000 --- a/src/parser/parse/index.ts +++ /dev/null @@ -1,270 +0,0 @@ -// @ts-nocheck - -import { isIdentifierStart, isIdentifierChar } from 'acorn'; -import fragment from './state/fragment.js'; -import { whitespace } from '../utils/patterns.js'; -import { reserved } from '../utils/names.js'; -import full_char_code_at from '../utils/full_char_code_at.js'; -import { TemplateNode, Ast, ParserOptions, Fragment, Style, Script } from '../interfaces.js'; -import error from '../utils/error.js'; - -type ParserState = (parser: Parser) => ParserState | void; - -interface LastAutoClosedTag { - tag: string; - reason: string; - depth: number; -} - -export class Parser { - readonly template: string; - readonly filename?: string; - readonly customElement: boolean; - - index = 0; - stack: TemplateNode[] = []; - - html: Fragment; - css: Style[] = []; - js: Script[] = []; - meta_tags = {}; - last_auto_closed_tag?: LastAutoClosedTag; - - constructor(template: string, options: ParserOptions) { - if (typeof template !== 'string') { - throw new TypeError('Template must be a string'); - } - - this.template = template.replace(/\s+$/, ''); - this.filename = options.filename; - this.customElement = options.customElement; - - this.html = { - start: null, - end: null, - type: 'Fragment', - children: [], - }; - - this.stack.push(this.html); - - let state: ParserState = fragment; - - while (this.index < this.template.length) { - state = state(this) || fragment; - } - - if (this.stack.length > 1) { - const current = this.current(); - - const type = current.type === 'Element' ? `<${current.name}>` : 'Block'; - const slug = current.type === 'Element' ? 'element' : 'block'; - - this.error( - { - code: `unclosed-${slug}`, - message: `${type} was left open`, - }, - current.start - ); - } - - if (state !== fragment) { - this.error({ - code: 'unexpected-eof', - message: 'Unexpected end of input', - }); - } - - if (this.html.children.length) { - let start = this.html.children[0].start; - while (whitespace.test(template[start])) start += 1; - - let end = this.html.children[this.html.children.length - 1].end; - while (whitespace.test(template[end - 1])) end -= 1; - - this.html.start = start; - this.html.end = end; - } else { - this.html.start = this.html.end = null; - } - } - - current() { - return this.stack[this.stack.length - 1]; - } - - acorn_error(err: any) { - this.error( - { - code: 'parse-error', - message: err.message.replace(/ \(\d+:\d+\)$/, ''), - }, - err.pos - ); - } - - error({ code, message }: { code: string; message: string }, index = this.index) { - error(message, { - name: 'ParseError', - code, - source: this.template, - start: index, - filename: this.filename, - }); - } - - eat(str: string, required?: boolean, message?: string) { - if (this.match(str)) { - this.index += str.length; - return true; - } - - if (required) { - this.error({ - code: `unexpected-${this.index === this.template.length ? 'eof' : 'token'}`, - message: message || `Expected ${str}`, - }); - } - - return false; - } - - match(str: string) { - return this.template.slice(this.index, this.index + str.length) === str; - } - - match_regex(pattern: RegExp) { - const match = pattern.exec(this.template.slice(this.index)); - if (!match || match.index !== 0) return null; - - return match[0]; - } - - allow_whitespace() { - while (this.index < this.template.length && whitespace.test(this.template[this.index])) { - this.index++; - } - } - - read(pattern: RegExp) { - const result = this.match_regex(pattern); - if (result) this.index += result.length; - return result; - } - - read_identifier(allow_reserved = false) { - const start = this.index; - - let i = this.index; - - const code = full_char_code_at(this.template, i); - if (!isIdentifierStart(code, true)) return null; - - i += code <= 0xffff ? 1 : 2; - - while (i < this.template.length) { - const code = full_char_code_at(this.template, i); - - if (!isIdentifierChar(code, true)) break; - i += code <= 0xffff ? 1 : 2; - } - - const identifier = this.template.slice(this.index, (this.index = i)); - - if (!allow_reserved && reserved.has(identifier)) { - this.error( - { - code: 'unexpected-reserved-word', - message: `'${identifier}' is a reserved word in JavaScript and cannot be used here`, - }, - start - ); - } - - return identifier; - } - - read_until(pattern: RegExp) { - if (this.index >= this.template.length) { - this.error({ - code: 'unexpected-eof', - message: 'Unexpected end of input', - }); - } - - const start = this.index; - const match = pattern.exec(this.template.slice(start)); - - if (match) { - this.index = start + match.index; - return this.template.slice(start, this.index); - } - - this.index = this.template.length; - return this.template.slice(start); - } - - require_whitespace() { - if (!whitespace.test(this.template[this.index])) { - this.error({ - code: 'missing-whitespace', - message: 'Expected whitespace', - }); - } - - this.allow_whitespace(); - } -} - -/** - * Parse - * Step 1/3 in Astro SSR. - * This is the first pass over .astro files and the step at which we convert a string to an AST for us to crawl. - */ -export default function parse(template: string, options: ParserOptions = {}): Ast { - const parser = new Parser(template, options); - - // TODO we may want to allow multiple <style> tags — - // one scoped, one global. for now, only allow one - if (parser.css.length > 1) { - parser.error( - { - code: 'duplicate-style', - message: 'You can only have one <style> tag per Astro file', - }, - parser.css[1].start - ); - } - - // const instance_scripts = parser.js.filter((script) => script.context === 'default'); - // const module_scripts = parser.js.filter((script) => script.context === 'module'); - const astro_scripts = parser.js.filter((script) => script.context === 'setup'); - - if (astro_scripts.length > 1) { - parser.error( - { - code: 'invalid-script', - message: 'A component can only have one frontmatter (---) script', - }, - astro_scripts[1].start - ); - } - - // if (module_scripts.length > 1) { - // parser.error( - // { - // code: 'invalid-script', - // message: 'A component can only have one <script context="module"> element', - // }, - // module_scripts[1].start - // ); - // } - - return { - html: parser.html, - css: parser.css[0], - // instance: instance_scripts[0], - module: astro_scripts[0], - }; -} diff --git a/src/parser/parse/read/context.ts b/src/parser/parse/read/context.ts deleted file mode 100644 index 565c66d18..000000000 --- a/src/parser/parse/read/context.ts +++ /dev/null @@ -1,72 +0,0 @@ -// @ts-nocheck - -import { Parser } from '../index.js'; -import { isIdentifierStart } from 'acorn'; -import full_char_code_at from '../../utils/full_char_code_at.js'; -import { is_bracket_open, is_bracket_close, is_bracket_pair, get_bracket_close } from '../utils/bracket.js'; -import { parse_expression_at } from './expression.js'; -import { Pattern } from 'estree'; - -export default function read_context(parser: Parser): Pattern & { start: number; end: number } { - const start = parser.index; - let i = parser.index; - - const code = full_char_code_at(parser.template, i); - if (isIdentifierStart(code, true)) { - return { - type: 'Identifier', - name: parser.read_identifier(), - start, - end: parser.index, - }; - } - - if (!is_bracket_open(code)) { - parser.error({ - code: 'unexpected-token', - message: 'Expected identifier or destructure pattern', - }); - } - - const bracket_stack = [code]; - i += code <= 0xffff ? 1 : 2; - - while (i < parser.template.length) { - const code = full_char_code_at(parser.template, i); - if (is_bracket_open(code)) { - bracket_stack.push(code); - } else if (is_bracket_close(code)) { - if (!is_bracket_pair(bracket_stack[bracket_stack.length - 1], code)) { - parser.error({ - code: 'unexpected-token', - message: `Expected ${String.fromCharCode(get_bracket_close(bracket_stack[bracket_stack.length - 1]))}`, - }); - } - bracket_stack.pop(); - if (bracket_stack.length === 0) { - i += code <= 0xffff ? 1 : 2; - break; - } - } - i += code <= 0xffff ? 1 : 2; - } - - parser.index = i; - - const pattern_string = parser.template.slice(start, i); - try { - // the length of the `space_with_newline` has to be start - 1 - // because we added a `(` in front of the pattern_string, - // which shifted the entire string to right by 1 - // so we offset it by removing 1 character in the `space_with_newline` - // to achieve that, we remove the 1st space encountered, - // so it will not affect the `column` of the node - let space_with_newline = parser.template.slice(0, start).replace(/[^\n]/g, ' '); - const first_space = space_with_newline.indexOf(' '); - space_with_newline = space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1); - - return (parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, start - 1) as any).left; - } catch (error) { - parser.acorn_error(error); - } -} diff --git a/src/parser/parse/read/expression.ts b/src/parser/parse/read/expression.ts deleted file mode 100644 index 9d0d09175..000000000 --- a/src/parser/parse/read/expression.ts +++ /dev/null @@ -1,251 +0,0 @@ -import type { BaseNode, Expression } from '../../interfaces'; -import { Parser } from '../index.js'; -import parseAstro from '../index.js'; - -interface ParseState { - source: string; - start: number; - index: number; - curlyCount: number; - bracketCount: number; - root: Expression; -} - -function peek_char(state: ParseState) { - return state.source[state.index]; -} - -function peek_nonwhitespace(state: ParseState) { - let index = state.index; - do { - let char = state.source[index]; - if (!/\s/.test(char)) { - return char; - } - index++; - } while (index < state.source.length); -} - -function next_char(state: ParseState) { - return state.source[state.index++]; -} - -function in_bounds(state: ParseState) { - return state.index < state.source.length; -} - -function consume_string(state: ParseState, stringChar: string) { - let inEscape; - do { - const char = next_char(state); - - if (inEscape) { - inEscape = false; - } else if (char === '\\') { - inEscape = true; - } else if (char === stringChar) { - break; - } - } while (in_bounds(state)); -} - -function consume_multiline_comment(state: ParseState) { - do { - const char = next_char(state); - - if (char === '*' && peek_char(state) === '/') { - break; - } - } while (in_bounds(state)); -} - -function consume_line_comment(state: ParseState) { - do { - const char = next_char(state); - if (char === '\n') { - break; - } - } while (in_bounds(state)); -} - -const voidElements = new Set(['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']); - -function consume_tag(state: ParseState) { - const start = state.index - 1; - let tagName = ''; - let inTag = false; - let inStart = true; - let selfClosed = false; - let inClose = false; - - let bracketIndex = 1; - do { - const char = next_char(state); - - switch (char) { - case "'": - case '"': { - consume_string(state, char); - break; - } - case '<': { - inTag = false; - tagName = ''; - - if (peek_nonwhitespace(state) === '/') { - inClose = true; - bracketIndex--; - } else { - inStart = true; - bracketIndex++; - } - break; - } - case '>': { - // An arrow function, probably - if (!inStart && !inClose) { - break; - } - - bracketIndex--; - - const addExpectedBrackets = - // Void elements don't need a closing - !voidElements.has(tagName.toLowerCase()) && - // Self-closing don't need a closing - !selfClosed && - // If we're in a start tag, we expect to find 2 more brackets - !inClose; - - if (addExpectedBrackets) { - bracketIndex += 2; - } - - inTag = false; - selfClosed = false; - inStart = false; - inClose = false; - break; - } - case ' ': { - inTag = true; - break; - } - case '/': { - if (inStart) { - selfClosed = true; - } - break; - } - default: { - if (!inTag) { - tagName += char; - } - break; - } - } - - // Unclosed tags - if (state.curlyCount <= 0) { - break; - } - - if (bracketIndex === 0) { - break; - } - } while (in_bounds(state)); - - const source = state.source.substring(start, state.index); - - const ast = parseAstro(source); - const fragment = ast.html; - - return fragment; -} - -function consume_expression(source: string, start: number): Expression { - const expr: Expression = { - type: 'Expression', - start, - end: Number.NaN, - codeChunks: [], - children: [], - }; - - let codeStart: number = start; - - const state: ParseState = { - source, - start, - index: start, - curlyCount: 1, - bracketCount: 0, - root: expr, - }; - - do { - const char = next_char(state); - - switch (char) { - case '{': { - state.curlyCount++; - break; - } - case '}': { - state.curlyCount--; - break; - } - case '<': { - const chunk = source.substring(codeStart, state.index - 1); - expr.codeChunks.push(chunk); - const tag = consume_tag(state); - expr.children.push(tag); - codeStart = state.index; - break; - } - case "'": - case '"': - case '`': { - consume_string(state, char); - break; - } - case '/': { - switch (peek_char(state)) { - case '/': { - consume_line_comment(state); - break; - } - case '*': { - consume_multiline_comment(state); - break; - } - } - } - } - } while (in_bounds(state) && state.curlyCount > 0); - - expr.end = state.index - 1; - - if (expr.children.length || !expr.codeChunks.length) { - expr.codeChunks.push(source.substring(codeStart, expr.end)); - } - - return expr; -} - -export const parse_expression_at = (source: string, index: number): Expression => { - const expression = consume_expression(source, index); - - return expression; -}; - -// @ts-ignore -export default function read_expression(parser: Parser) { - try { - const expression = parse_expression_at(parser.template, parser.index); - parser.index = expression.end; - return expression; - } catch (err) { - parser.acorn_error(err); - } -} diff --git a/src/parser/parse/read/script.ts b/src/parser/parse/read/script.ts deleted file mode 100644 index 9b8d71110..000000000 --- a/src/parser/parse/read/script.ts +++ /dev/null @@ -1,60 +0,0 @@ -// @ts-nocheck - -import type { Node } from 'estree'; -import { Parser } from '../index.js'; -import { Script } from '../../interfaces.js'; - -const script_closing_tag = '</script>'; - -function get_context(parser: Parser, attributes: any[], start: number): 'runtime' | 'setup' { - const context = attributes.find((attribute) => attribute.name === 'astro'); - if (!context) return 'runtime'; - if (context.value === true) return 'setup'; - - if (context.value.length !== 1 || context.value[0].type !== 'Text') { - parser.error( - { - code: 'invalid-script', - message: 'astro attribute must be static', - }, - start - ); - } - - const value = context.value[0].data; - - if (value !== 'setup') { - parser.error( - { - code: 'invalid-script', - message: 'If the "astro" attribute has a value, its value must be "setup"', - }, - context.start - ); - } - - return value; -} - -export default function read_script(parser: Parser, start: number, attributes: Node[]): Script { - const script_start = parser.index; - const script_end = parser.template.indexOf(script_closing_tag, script_start); - - if (script_end === -1) { - parser.error({ - code: 'unclosed-script', - message: '<script> must have a closing tag', - }); - } - - const source = parser.template.slice(0, script_start).replace(/[^\n]/g, ' ') + parser.template.slice(script_start, script_end); - parser.index = script_end + script_closing_tag.length; - - return { - type: 'Script', - start, - end: parser.index, - context: get_context(parser, attributes, start), - content: source, - }; -} diff --git a/src/parser/parse/read/style.ts b/src/parser/parse/read/style.ts deleted file mode 100644 index f23d7b10e..000000000 --- a/src/parser/parse/read/style.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Parser } from '../index.js'; -import { Style } from '../../interfaces.js'; - -interface Attribute { - start: number; - end: number; - type: 'Attribute'; - name: string; - value: { - raw: string; - data: string; - }[]; -} - -export default function read_style(parser: Parser, start: number, attributes: Attribute[]): Style { - const content_start = parser.index; - const styles = parser.read_until(/<\/style>/); - const content_end = parser.index; - parser.eat('</style>', true); - const end = parser.index; - - return { - type: 'Style', - start, - end, - attributes, - content: { - start: content_start, - end: content_end, - styles, - }, - }; -} - -function is_ref_selector(a: any, b: any) { - // TODO add CSS node types - if (!b) return false; - - return a.type === 'TypeSelector' && a.name === 'ref' && b.type === 'PseudoClassSelector'; -} diff --git a/src/parser/parse/state/fragment.ts b/src/parser/parse/state/fragment.ts deleted file mode 100644 index 97398b227..000000000 --- a/src/parser/parse/state/fragment.ts +++ /dev/null @@ -1,21 +0,0 @@ -import tag from './tag.js'; -import setup from './setup.js'; -import mustache from './mustache.js'; -import text from './text.js'; -import { Parser } from '../index.js'; - -export default function fragment(parser: Parser) { - if (parser.html.children.length === 0 && parser.match_regex(/^---/m)) { - return setup; - } - - if (parser.match('<')) { - return tag; - } - - if (parser.match('{')) { - return mustache; - } - - return text; -} diff --git a/src/parser/parse/state/mustache.ts b/src/parser/parse/state/mustache.ts deleted file mode 100644 index 79372d8d9..000000000 --- a/src/parser/parse/state/mustache.ts +++ /dev/null @@ -1,413 +0,0 @@ -import read_context from '../read/context.js'; -import read_expression from '../read/expression.js'; -import { closing_tag_omitted } from '../utils/html.js'; -import { whitespace } from '../../utils/patterns.js'; -import { trim_start, trim_end } from '../../utils/trim.js'; -import { to_string } from '../utils/node.js'; -import { Parser } from '../index.js'; -import { TemplateNode } from '../../interfaces.js'; - -type TODO = any; - -function trim_whitespace(block: TemplateNode, trim_before: boolean, trim_after: boolean) { - if (!block.children || block.children.length === 0) return; // AwaitBlock - - const first_child = block.children[0]; - const last_child = block.children[block.children.length - 1]; - - if (first_child.type === 'Text' && trim_before) { - first_child.data = trim_start(first_child.data); - if (!first_child.data) block.children.shift(); - } - - if (last_child.type === 'Text' && trim_after) { - last_child.data = trim_end(last_child.data); - if (!last_child.data) block.children.pop(); - } - - if (block.else) { - trim_whitespace(block.else, trim_before, trim_after); - } - - if (first_child.elseif) { - trim_whitespace(first_child, trim_before, trim_after); - } -} - -export default function mustache(parser: Parser) { - const start = parser.index; - parser.index += 1; - - parser.allow_whitespace(); - - // {/if}, {/each}, {/await} or {/key} - if (parser.eat('/')) { - let block = parser.current(); - let expected: TODO; - - if (closing_tag_omitted(block.name)) { - block.end = start; - parser.stack.pop(); - block = parser.current(); - } - - if (block.type === 'ElseBlock' || block.type === 'PendingBlock' || block.type === 'ThenBlock' || block.type === 'CatchBlock') { - block.end = start; - parser.stack.pop(); - block = parser.current(); - - expected = 'await'; - } - - if (block.type === 'IfBlock') { - expected = 'if'; - } else if (block.type === 'EachBlock') { - expected = 'each'; - } else if (block.type === 'AwaitBlock') { - expected = 'await'; - } else if (block.type === 'KeyBlock') { - expected = 'key'; - } else { - parser.error({ - code: 'unexpected-block-close', - message: 'Unexpected block closing tag', - }); - } - - parser.eat(expected, true); - parser.allow_whitespace(); - parser.eat('}', true); - - while (block.elseif) { - block.end = parser.index; - parser.stack.pop(); - block = parser.current(); - - if (block.else) { - block.else.end = start; - } - } - - // strip leading/trailing whitespace as necessary - const char_before = parser.template[block.start - 1]; - const char_after = parser.template[parser.index]; - const trim_before = !char_before || whitespace.test(char_before); - const trim_after = !char_after || whitespace.test(char_after); - - trim_whitespace(block, trim_before, trim_after); - - block.end = parser.index; - parser.stack.pop(); - } else if (parser.eat(':else')) { - if (parser.eat('if')) { - parser.error({ - code: 'invalid-elseif', - message: "'elseif' should be 'else if'", - }); - } - - parser.allow_whitespace(); - - // :else if - if (parser.eat('if')) { - const block = parser.current(); - if (block.type !== 'IfBlock') { - parser.error({ - code: 'invalid-elseif-placement', - message: parser.stack.some((block) => block.type === 'IfBlock') - ? `Expected to close ${to_string(block)} before seeing {:else if ...} block` - : 'Cannot have an {:else if ...} block outside an {#if ...} block', - }); - } - - parser.require_whitespace(); - - const expression = read_expression(parser); - - parser.allow_whitespace(); - parser.eat('}', true); - - block.else = { - start: parser.index, - end: null, - type: 'ElseBlock', - children: [ - { - start: parser.index, - end: null, - type: 'IfBlock', - elseif: true, - expression, - children: [], - }, - ], - }; - - parser.stack.push(block.else.children[0]); - } else { - // :else - const block = parser.current(); - if (block.type !== 'IfBlock' && block.type !== 'EachBlock') { - parser.error({ - code: 'invalid-else-placement', - message: parser.stack.some((block) => block.type === 'IfBlock' || block.type === 'EachBlock') - ? `Expected to close ${to_string(block)} before seeing {:else} block` - : 'Cannot have an {:else} block outside an {#if ...} or {#each ...} block', - }); - } - - parser.allow_whitespace(); - parser.eat('}', true); - - block.else = { - start: parser.index, - end: null, - type: 'ElseBlock', - children: [], - }; - - parser.stack.push(block.else); - } - } else if (parser.match(':then') || parser.match(':catch')) { - const block = parser.current(); - const is_then = parser.eat(':then') || !parser.eat(':catch'); - - if (is_then) { - if (block.type !== 'PendingBlock') { - parser.error({ - code: 'invalid-then-placement', - message: parser.stack.some((block) => block.type === 'PendingBlock') - ? `Expected to close ${to_string(block)} before seeing {:then} block` - : 'Cannot have an {:then} block outside an {#await ...} block', - }); - } - } else { - if (block.type !== 'ThenBlock' && block.type !== 'PendingBlock') { - parser.error({ - code: 'invalid-catch-placement', - message: parser.stack.some((block) => block.type === 'ThenBlock' || block.type === 'PendingBlock') - ? `Expected to close ${to_string(block)} before seeing {:catch} block` - : 'Cannot have an {:catch} block outside an {#await ...} block', - }); - } - } - - block.end = start; - parser.stack.pop(); - const await_block = parser.current(); - - if (!parser.eat('}')) { - parser.require_whitespace(); - await_block[is_then ? 'value' : 'error'] = read_context(parser); - parser.allow_whitespace(); - parser.eat('}', true); - } - - const new_block: TemplateNode = { - start, - // @ts-ignore - end: null, - type: is_then ? 'ThenBlock' : 'CatchBlock', - children: [], - skip: false, - }; - - await_block[is_then ? 'then' : 'catch'] = new_block; - parser.stack.push(new_block); - } else if (parser.eat('#')) { - // {#if foo}, {#each foo} or {#await foo} - let type; - - if (parser.eat('if')) { - type = 'IfBlock'; - } else if (parser.eat('each')) { - type = 'EachBlock'; - } else if (parser.eat('await')) { - type = 'AwaitBlock'; - } else if (parser.eat('key')) { - type = 'KeyBlock'; - } else { - parser.error({ - code: 'expected-block-type', - message: 'Expected if, each, await or key', - }); - } - - parser.require_whitespace(); - - const expression = read_expression(parser); - - // @ts-ignore - const block: TemplateNode = - type === 'AwaitBlock' - ? { - start, - end: null, - type, - expression, - value: null, - error: null, - pending: { - start: null, - end: null, - type: 'PendingBlock', - children: [], - skip: true, - }, - then: { - start: null, - end: null, - type: 'ThenBlock', - children: [], - skip: true, - }, - catch: { - start: null, - end: null, - type: 'CatchBlock', - children: [], - skip: true, - }, - } - : { - start, - end: null, - type, - expression, - children: [], - }; - - parser.allow_whitespace(); - - // {#each} blocks must declare a context – {#each list as item} - if (type === 'EachBlock') { - parser.eat('as', true); - parser.require_whitespace(); - - block.context = read_context(parser); - - parser.allow_whitespace(); - - if (parser.eat(',')) { - parser.allow_whitespace(); - block.index = parser.read_identifier(); - if (!block.index) { - parser.error({ - code: 'expected-name', - message: 'Expected name', - }); - } - - parser.allow_whitespace(); - } - - if (parser.eat('(')) { - parser.allow_whitespace(); - - block.key = read_expression(parser); - parser.allow_whitespace(); - parser.eat(')', true); - parser.allow_whitespace(); - } - } - - const await_block_shorthand = type === 'AwaitBlock' && parser.eat('then'); - if (await_block_shorthand) { - parser.require_whitespace(); - block.value = read_context(parser); - parser.allow_whitespace(); - } - - const await_block_catch_shorthand = !await_block_shorthand && type === 'AwaitBlock' && parser.eat('catch'); - if (await_block_catch_shorthand) { - parser.require_whitespace(); - block.error = read_context(parser); - parser.allow_whitespace(); - } - - parser.eat('}', true); - - // @ts-ignore - parser.current().children.push(block); - parser.stack.push(block); - - if (type === 'AwaitBlock') { - let child_block; - if (await_block_shorthand) { - block.then.skip = false; - child_block = block.then; - } else if (await_block_catch_shorthand) { - block.catch.skip = false; - child_block = block.catch; - } else { - block.pending.skip = false; - child_block = block.pending; - } - - child_block.start = parser.index; - parser.stack.push(child_block); - } - } else if (parser.eat('@html')) { - // {@html content} tag - parser.require_whitespace(); - - const expression = read_expression(parser); - - parser.allow_whitespace(); - parser.eat('}', true); - - // @ts-ignore - parser.current().children.push({ - start, - end: parser.index, - type: 'RawMustacheTag', - expression, - }); - } else if (parser.eat('@debug')) { - // let identifiers; - - // // Implies {@debug} which indicates "debug all" - // if (parser.read(/\s*}/)) { - // identifiers = []; - // } else { - // const expression = read_expression(parser); - - // identifiers = expression.type === 'SequenceExpression' - // ? expression.expressions - // : [expression]; - - // identifiers.forEach(node => { - // if (node.type !== 'Identifier') { - // parser.error({ - // code: 'invalid-debug-args', - // message: '{@debug ...} arguments must be identifiers, not arbitrary expressions' - // }, node.start); - // } - // }); - - // parser.allow_whitespace(); - // parser.eat('}', true); - // } - - // parser.current().children.push({ - // start, - // end: parser.index, - // type: 'DebugTag', - // identifiers - // }); - throw new Error('@debug not yet supported'); - } else { - const expression = read_expression(parser); - - parser.allow_whitespace(); - parser.eat('}', true); - - // @ts-ignore - parser.current().children.push({ - start, - end: parser.index, - type: 'MustacheTag', - expression, - }); - } -} diff --git a/src/parser/parse/state/setup.ts b/src/parser/parse/state/setup.ts deleted file mode 100644 index f64d8c52b..000000000 --- a/src/parser/parse/state/setup.ts +++ /dev/null @@ -1,35 +0,0 @@ -// @ts-nocheck - -import { Parser } from '../index.js'; - -export default function setup(parser: Parser): void { - // TODO: Error if not at top of file? currently, we ignore / just treat as text. - // if (parser.html.children.length > 0) { - // parser.error({ - // code: 'unexpected-token', - // message: 'Frontmatter scripts only supported at the top of file.', - // }); - // } - - const start = parser.index; - parser.index += 3; - const content_start = parser.index; - const setupScriptContent = parser.read_until(/^---/m); - const content_end = parser.index; - parser.eat('---', true); - const end = parser.index; - parser.js.push({ - type: 'Script', - context: 'setup', - start, - end, - content: setupScriptContent, - // attributes, - // content: { - // start: content_start, - // end: content_end, - // styles, - // }, - }); - return; -} diff --git a/src/parser/parse/state/tag.ts b/src/parser/parse/state/tag.ts deleted file mode 100644 index a8b919a49..000000000 --- a/src/parser/parse/state/tag.ts +++ /dev/null @@ -1,579 +0,0 @@ -// @ts-nocheck - -import read_expression from '../read/expression.js'; -import read_script from '../read/script.js'; -import read_style from '../read/style.js'; -import { decode_character_references, closing_tag_omitted } from '../utils/html.js'; -import { is_void } from '../../utils/names.js'; -import { Parser } from '../index.js'; -import { Directive, DirectiveType, TemplateNode, Text } from '../../interfaces.js'; -import fuzzymatch from '../../utils/fuzzymatch.js'; -import list from '../../utils/list.js'; - -// eslint-disable-next-line no-useless-escape -const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/; - -const meta_tags = new Map([ - ['astro:head', 'Head'], - // ['slot:body', 'Body'], - // ['astro:options', 'Options'], - // ['astro:window', 'Window'], - // ['astro:body', 'Body'], -]); - -const valid_meta_tags = Array.from(meta_tags.keys()); //.concat('astro:self', 'astro:component', 'astro:fragment'); - -const specials = new Map([ - // Now handled as "setup" in setup.ts - // [ - // 'script', - // { - // read: read_script, - // property: 'js', - // }, - // ], - [ - 'style', - { - read: read_style, - property: 'css', - }, - ], -]); - -const SELF = /^astro:self(?=[\s/>])/; -const COMPONENT = /^astro:component(?=[\s/>])/; -const SLOT = /^astro:fragment(?=[\s/>])/; -const HEAD = /^head(?=[\s/>])/; - -function parent_is_head(stack) { - let i = stack.length; - while (i--) { - const { type } = stack[i]; - if (type === 'Head') return true; - if (type === 'Element' || type === 'InlineComponent') return false; - } - return false; -} - -export default function tag(parser: Parser) { - const start = parser.index++; - - let parent = parser.current(); - - if (parser.eat('!--')) { - const data = parser.read_until(/-->/); - parser.eat('-->', true, 'comment was left open, expected -->'); - - parser.current().children.push({ - start, - end: parser.index, - type: 'Comment', - data, - }); - - return; - } - - const is_closing_tag = parser.eat('/'); - - const name = read_tag_name(parser); - - if (meta_tags.has(name)) { - const slug = meta_tags.get(name).toLowerCase(); - if (is_closing_tag) { - if ((name === 'astro:window' || name === 'astro:body') && parser.current().children.length) { - parser.error( - { - code: `invalid-${slug}-content`, - message: `<${name}> cannot have children`, - }, - parser.current().children[0].start - ); - } - } else { - if (name in parser.meta_tags) { - parser.error( - { - code: `duplicate-${slug}`, - message: `A component can only have one <${name}> tag`, - }, - start - ); - } - - if (parser.stack.length > 1) { - parser.error( - { - code: `invalid-${slug}-placement`, - message: `<${name}> tags cannot be inside elements or blocks`, - }, - start - ); - } - - parser.meta_tags[name] = true; - } - } - - const type = meta_tags.has(name) - ? meta_tags.get(name) - : /[A-Z]/.test(name[0]) || name === 'astro:self' || name === 'astro:component' - ? 'InlineComponent' - : name === 'astro:fragment' - ? 'SlotTemplate' - : name === 'title' && parent_is_head(parser.stack) - ? 'Title' - : name === 'slot' && !parser.customElement - ? 'Slot' - : 'Element'; - - const element: TemplateNode = { - start, - end: null, // filled in later - type, - name, - attributes: [], - children: [], - }; - - parser.allow_whitespace(); - - if (is_closing_tag) { - if (is_void(name)) { - parser.error( - { - code: 'invalid-void-content', - message: `<${name}> is a void element and cannot have children, or a closing tag`, - }, - start - ); - } - - parser.eat('>', true); - - // close any elements that don't have their own closing tags, e.g. <div><p></div> - while (parent.name !== name) { - if (parent.type !== 'Element') { - const message = - parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name - ? `</${name}> attempted to close <${name}> that was already automatically closed by <${parser.last_auto_closed_tag.reason}>` - : `</${name}> attempted to close an element that was not open`; - parser.error( - { - code: 'invalid-closing-tag', - message, - }, - start - ); - } - - parent.end = start; - parser.stack.pop(); - - parent = parser.current(); - } - - parent.end = parser.index; - parser.stack.pop(); - - if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) { - parser.last_auto_closed_tag = null; - } - - return; - } else if (closing_tag_omitted(parent.name, name)) { - parent.end = start; - parser.stack.pop(); - parser.last_auto_closed_tag = { - tag: parent.name, - reason: name, - depth: parser.stack.length, - }; - } - - const unique_names: Set<string> = new Set(); - - let attribute; - while ((attribute = read_attribute(parser, unique_names))) { - element.attributes.push(attribute); - parser.allow_whitespace(); - } - - if (name === 'astro:component') { - const index = element.attributes.findIndex((attr) => attr.type === 'Attribute' && attr.name === 'this'); - if (!~index) { - parser.error( - { - code: 'missing-component-definition', - message: "<astro:component> must have a 'this' attribute", - }, - start - ); - } - - const definition = element.attributes.splice(index, 1)[0]; - if (definition.value === true || definition.value.length !== 1 || definition.value[0].type === 'Text') { - parser.error( - { - code: 'invalid-component-definition', - message: 'invalid component definition', - }, - definition.start - ); - } - - element.expression = definition.value[0].expression; - } - - // special cases – top-level <script> and <style> - if (specials.has(name) && parser.stack.length === 1) { - const special = specials.get(name); - - parser.eat('>', true); - const content = special.read(parser, start, element.attributes); - if (content) parser[special.property].push(content); - return; - } - - parser.current().children.push(element); - - const self_closing = parser.eat('/') || is_void(name); - - parser.eat('>', true); - - if (self_closing) { - // don't push self-closing elements onto the stack - element.end = parser.index; - } else if (name === 'textarea') { - // special case - element.children = read_sequence(parser, () => parser.template.slice(parser.index, parser.index + 11) === '</textarea>'); - parser.read(/<\/textarea>/); - element.end = parser.index; - } else if (name === 'script' || name === 'style') { - // special case - const start = parser.index; - const data = parser.read_until(new RegExp(`</${name}>`)); - const end = parser.index; - element.children.push({ start, end, type: 'Text', data }); - parser.eat(`</${name}>`, true); - element.end = parser.index; - } else { - parser.stack.push(element); - } -} - -function read_tag_name(parser: Parser) { - const start = parser.index; - - if (parser.read(SELF)) { - // check we're inside a block, otherwise this - // will cause infinite recursion - let i = parser.stack.length; - let legal = false; - - while (i--) { - const fragment = parser.stack[i]; - if (fragment.type === 'IfBlock' || fragment.type === 'EachBlock' || fragment.type === 'InlineComponent') { - legal = true; - break; - } - } - - if (!legal) { - parser.error( - { - code: 'invalid-self-placement', - message: '<astro:self> components can only exist inside {#if} blocks, {#each} blocks, or slots passed to components', - }, - start - ); - } - - return 'astro:self'; - } - - if (parser.read(COMPONENT)) return 'astro:component'; - - if (parser.read(SLOT)) return 'astro:fragment'; - - if (parser.read(HEAD)) return 'head'; - - const name = parser.read_until(/(\s|\/|>)/); - - if (meta_tags.has(name)) return name; - - if (name.startsWith('astro:')) { - const match = fuzzymatch(name.slice(7), valid_meta_tags); - - let message = `Valid <astro:...> tag names are ${list(valid_meta_tags)}`; - if (match) message += ` (did you mean '${match}'?)`; - - parser.error( - { - code: 'invalid-tag-name', - message, - }, - start - ); - } - - if (!valid_tag_name.test(name)) { - parser.error( - { - code: 'invalid-tag-name', - message: 'Expected valid tag name', - }, - start - ); - } - - return name; -} - -function read_attribute(parser: Parser, unique_names: Set<string>) { - const start = parser.index; - - function check_unique(name: string) { - if (unique_names.has(name)) { - parser.error( - { - code: 'duplicate-attribute', - message: 'Attributes need to be unique', - }, - start - ); - } - unique_names.add(name); - } - - if (parser.eat('{')) { - parser.allow_whitespace(); - - if (parser.eat('...')) { - const { expression } = read_expression(parser); - - parser.allow_whitespace(); - parser.eat('}', true); - - return { - start, - end: parser.index, - type: 'Spread', - expression, - }; - } else { - const value_start = parser.index; - - const name = parser.read_identifier(); - parser.allow_whitespace(); - parser.eat('}', true); - - check_unique(name); - - return { - start, - end: parser.index, - type: 'Attribute', - name, - value: [ - { - start: value_start, - end: value_start + name.length, - type: 'AttributeShorthand', - expression: { - start: value_start, - end: value_start + name.length, - type: 'Identifier', - name, - }, - }, - ], - }; - } - } - - // eslint-disable-next-line no-useless-escape - const name = parser.read_until(/[\s=\/>"']/); - if (!name) return null; - - let end = parser.index; - - parser.allow_whitespace(); - - const colon_index = name.indexOf(':'); - const type = colon_index !== -1 && get_directive_type(name.slice(0, colon_index)); - - let value: any[] | true = true; - if (parser.eat('=')) { - parser.allow_whitespace(); - value = read_attribute_value(parser); - end = parser.index; - } else if (parser.match_regex(/["']/)) { - parser.error( - { - code: 'unexpected-token', - message: 'Expected =', - }, - parser.index - ); - } - - if (type) { - const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|'); - - if (type === 'Binding' && directive_name !== 'this') { - check_unique(directive_name); - } else if (type !== 'EventHandler' && type !== 'Action') { - check_unique(name); - } - - if (type === 'Ref') { - parser.error( - { - code: 'invalid-ref-directive', - message: `The ref directive is no longer supported — use \`bind:this={${directive_name}}\` instead`, - }, - start - ); - } - - if (type === 'Class' && directive_name === '') { - parser.error( - { - code: 'invalid-class-directive', - message: 'Class binding name cannot be empty', - }, - start + colon_index + 1 - ); - } - - if (value[0]) { - if ((value as any[]).length > 1 || value[0].type === 'Text') { - parser.error( - { - code: 'invalid-directive-value', - message: 'Directive value must be a JavaScript expression enclosed in curly braces', - }, - value[0].start - ); - } - } - - const directive: Directive = { - start, - end, - type, - name: directive_name, - modifiers, - expression: (value[0] && value[0].expression) || null, - }; - - if (type === 'Transition') { - const direction = name.slice(0, colon_index); - directive.intro = direction === 'in' || direction === 'transition'; - directive.outro = direction === 'out' || direction === 'transition'; - } - - if (!directive.expression && (type === 'Binding' || type === 'Class')) { - directive.expression = { - start: directive.start + colon_index + 1, - end: directive.end, - type: 'Identifier', - name: directive.name, - } as any; - } - - return directive; - } - - check_unique(name); - - return { - start, - end, - type: 'Attribute', - name, - value, - }; -} - -function get_directive_type(name: string): DirectiveType { - if (name === 'use') return 'Action'; - if (name === 'animate') return 'Animation'; - if (name === 'bind') return 'Binding'; - if (name === 'class') return 'Class'; - if (name === 'on') return 'EventHandler'; - if (name === 'let') return 'Let'; - if (name === 'ref') return 'Ref'; - if (name === 'in' || name === 'out' || name === 'transition') return 'Transition'; -} - -function read_attribute_value(parser: Parser) { - const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null; - - const regex = quote_mark === "'" ? /'/ : quote_mark === '"' ? /"/ : /(\/>|[\s"'=<>`])/; - - const value = read_sequence(parser, () => !!parser.match_regex(regex)); - - if (quote_mark) parser.index += 1; - return value; -} - -function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] { - let current_chunk: Text = { - start: parser.index, - end: null, - type: 'Text', - raw: '', - data: null, - }; - - function flush() { - if (current_chunk.raw) { - current_chunk.data = decode_character_references(current_chunk.raw); - current_chunk.end = parser.index; - chunks.push(current_chunk); - } - } - - const chunks: TemplateNode[] = []; - - while (parser.index < parser.template.length) { - const index = parser.index; - - if (done()) { - flush(); - return chunks; - } else if (parser.eat('{')) { - flush(); - - parser.allow_whitespace(); - const expression = read_expression(parser); - parser.allow_whitespace(); - parser.eat('}', true); - - chunks.push({ - start: index, - end: parser.index, - type: 'MustacheTag', - expression, - }); - - current_chunk = { - start: parser.index, - end: null, - type: 'Text', - raw: '', - data: null, - }; - } else { - current_chunk.raw += parser.template[parser.index++]; - } - } - - parser.error({ - code: 'unexpected-eof', - message: 'Unexpected end of input', - }); -} diff --git a/src/parser/parse/state/text.ts b/src/parser/parse/state/text.ts deleted file mode 100644 index cca83f2d4..000000000 --- a/src/parser/parse/state/text.ts +++ /dev/null @@ -1,24 +0,0 @@ -// @ts-nocheck - -import { decode_character_references } from '../utils/html.js'; -import { Parser } from '../index.js'; - -export default function text(parser: Parser) { - const start = parser.index; - - let data = ''; - - while (parser.index < parser.template.length && !parser.match('---') && !parser.match('<') && !parser.match('{')) { - data += parser.template[parser.index++]; - } - - const node = { - start, - end: parser.index, - type: 'Text', - raw: data, - data: decode_character_references(data), - }; - - parser.current().children.push(node); -} diff --git a/src/parser/parse/utils/bracket.ts b/src/parser/parse/utils/bracket.ts deleted file mode 100644 index 7e885ad78..000000000 --- a/src/parser/parse/utils/bracket.ts +++ /dev/null @@ -1,27 +0,0 @@ -// @ts-nocheck - -const SQUARE_BRACKET_OPEN = '['.charCodeAt(0); -const SQUARE_BRACKET_CLOSE = ']'.charCodeAt(0); -const CURLY_BRACKET_OPEN = '{'.charCodeAt(0); -const CURLY_BRACKET_CLOSE = '}'.charCodeAt(0); - -export function is_bracket_open(code) { - return code === SQUARE_BRACKET_OPEN || code === CURLY_BRACKET_OPEN; -} - -export function is_bracket_close(code) { - return code === SQUARE_BRACKET_CLOSE || code === CURLY_BRACKET_CLOSE; -} - -export function is_bracket_pair(open, close) { - return (open === SQUARE_BRACKET_OPEN && close === SQUARE_BRACKET_CLOSE) || (open === CURLY_BRACKET_OPEN && close === CURLY_BRACKET_CLOSE); -} - -export function get_bracket_close(open) { - if (open === SQUARE_BRACKET_OPEN) { - return SQUARE_BRACKET_CLOSE; - } - if (open === CURLY_BRACKET_OPEN) { - return CURLY_BRACKET_CLOSE; - } -} diff --git a/src/parser/parse/utils/entities.ts b/src/parser/parse/utils/entities.ts deleted file mode 100644 index e554664eb..000000000 --- a/src/parser/parse/utils/entities.ts +++ /dev/null @@ -1,2034 +0,0 @@ -// https://dev.w3.org/html5/html-author/charref -export default { - CounterClockwiseContourIntegral: 8755, - ClockwiseContourIntegral: 8754, - DoubleLongLeftRightArrow: 10234, - DiacriticalDoubleAcute: 733, - NotSquareSupersetEqual: 8931, - CloseCurlyDoubleQuote: 8221, - DoubleContourIntegral: 8751, - FilledVerySmallSquare: 9642, - NegativeVeryThinSpace: 8203, - NotPrecedesSlantEqual: 8928, - NotRightTriangleEqual: 8941, - NotSucceedsSlantEqual: 8929, - CapitalDifferentialD: 8517, - DoubleLeftRightArrow: 8660, - DoubleLongRightArrow: 10233, - EmptyVerySmallSquare: 9643, - NestedGreaterGreater: 8811, - NotDoubleVerticalBar: 8742, - NotLeftTriangleEqual: 8940, - NotSquareSubsetEqual: 8930, - OpenCurlyDoubleQuote: 8220, - ReverseUpEquilibrium: 10607, - DoubleLongLeftArrow: 10232, - DownLeftRightVector: 10576, - LeftArrowRightArrow: 8646, - NegativeMediumSpace: 8203, - RightArrowLeftArrow: 8644, - SquareSupersetEqual: 8850, - leftrightsquigarrow: 8621, - DownRightTeeVector: 10591, - DownRightVectorBar: 10583, - LongLeftRightArrow: 10231, - Longleftrightarrow: 10234, - NegativeThickSpace: 8203, - PrecedesSlantEqual: 8828, - ReverseEquilibrium: 8651, - RightDoubleBracket: 10215, - RightDownTeeVector: 10589, - RightDownVectorBar: 10581, - RightTriangleEqual: 8885, - SquareIntersection: 8851, - SucceedsSlantEqual: 8829, - blacktriangleright: 9656, - longleftrightarrow: 10231, - DoubleUpDownArrow: 8661, - DoubleVerticalBar: 8741, - DownLeftTeeVector: 10590, - DownLeftVectorBar: 10582, - FilledSmallSquare: 9724, - GreaterSlantEqual: 10878, - LeftDoubleBracket: 10214, - LeftDownTeeVector: 10593, - LeftDownVectorBar: 10585, - LeftTriangleEqual: 8884, - NegativeThinSpace: 8203, - NotReverseElement: 8716, - NotTildeFullEqual: 8775, - RightAngleBracket: 10217, - RightUpDownVector: 10575, - SquareSubsetEqual: 8849, - VerticalSeparator: 10072, - blacktriangledown: 9662, - blacktriangleleft: 9666, - leftrightharpoons: 8651, - rightleftharpoons: 8652, - twoheadrightarrow: 8608, - DiacriticalAcute: 180, - DiacriticalGrave: 96, - DiacriticalTilde: 732, - DoubleRightArrow: 8658, - DownArrowUpArrow: 8693, - EmptySmallSquare: 9723, - GreaterEqualLess: 8923, - GreaterFullEqual: 8807, - LeftAngleBracket: 10216, - LeftUpDownVector: 10577, - LessEqualGreater: 8922, - NonBreakingSpace: 160, - NotRightTriangle: 8939, - NotSupersetEqual: 8841, - RightTriangleBar: 10704, - RightUpTeeVector: 10588, - RightUpVectorBar: 10580, - UnderParenthesis: 9181, - UpArrowDownArrow: 8645, - circlearrowright: 8635, - downharpoonright: 8642, - ntrianglerighteq: 8941, - rightharpoondown: 8641, - rightrightarrows: 8649, - twoheadleftarrow: 8606, - vartriangleright: 8883, - CloseCurlyQuote: 8217, - ContourIntegral: 8750, - DoubleDownArrow: 8659, - DoubleLeftArrow: 8656, - DownRightVector: 8641, - LeftRightVector: 10574, - LeftTriangleBar: 10703, - LeftUpTeeVector: 10592, - LeftUpVectorBar: 10584, - LowerRightArrow: 8600, - NotGreaterEqual: 8817, - NotGreaterTilde: 8821, - NotLeftTriangle: 8938, - OverParenthesis: 9180, - RightDownVector: 8642, - ShortRightArrow: 8594, - UpperRightArrow: 8599, - bigtriangledown: 9661, - circlearrowleft: 8634, - curvearrowright: 8631, - downharpoonleft: 8643, - leftharpoondown: 8637, - leftrightarrows: 8646, - nLeftrightarrow: 8654, - nleftrightarrow: 8622, - ntrianglelefteq: 8940, - rightleftarrows: 8644, - rightsquigarrow: 8605, - rightthreetimes: 8908, - straightepsilon: 1013, - trianglerighteq: 8885, - vartriangleleft: 8882, - DiacriticalDot: 729, - DoubleRightTee: 8872, - DownLeftVector: 8637, - GreaterGreater: 10914, - HorizontalLine: 9472, - InvisibleComma: 8291, - InvisibleTimes: 8290, - LeftDownVector: 8643, - LeftRightArrow: 8596, - Leftrightarrow: 8660, - LessSlantEqual: 10877, - LongRightArrow: 10230, - Longrightarrow: 10233, - LowerLeftArrow: 8601, - NestedLessLess: 8810, - NotGreaterLess: 8825, - NotLessGreater: 8824, - NotSubsetEqual: 8840, - NotVerticalBar: 8740, - OpenCurlyQuote: 8216, - ReverseElement: 8715, - RightTeeVector: 10587, - RightVectorBar: 10579, - ShortDownArrow: 8595, - ShortLeftArrow: 8592, - SquareSuperset: 8848, - TildeFullEqual: 8773, - UpperLeftArrow: 8598, - ZeroWidthSpace: 8203, - curvearrowleft: 8630, - doublebarwedge: 8966, - downdownarrows: 8650, - hookrightarrow: 8618, - leftleftarrows: 8647, - leftrightarrow: 8596, - leftthreetimes: 8907, - longrightarrow: 10230, - looparrowright: 8620, - nshortparallel: 8742, - ntriangleright: 8939, - rightarrowtail: 8611, - rightharpoonup: 8640, - trianglelefteq: 8884, - upharpoonright: 8638, - ApplyFunction: 8289, - DifferentialD: 8518, - DoubleLeftTee: 10980, - DoubleUpArrow: 8657, - LeftTeeVector: 10586, - LeftVectorBar: 10578, - LessFullEqual: 8806, - LongLeftArrow: 10229, - Longleftarrow: 10232, - NotTildeEqual: 8772, - NotTildeTilde: 8777, - Poincareplane: 8460, - PrecedesEqual: 10927, - PrecedesTilde: 8830, - RightArrowBar: 8677, - RightTeeArrow: 8614, - RightTriangle: 8883, - RightUpVector: 8638, - SucceedsEqual: 10928, - SucceedsTilde: 8831, - SupersetEqual: 8839, - UpEquilibrium: 10606, - VerticalTilde: 8768, - VeryThinSpace: 8202, - bigtriangleup: 9651, - blacktriangle: 9652, - divideontimes: 8903, - fallingdotseq: 8786, - hookleftarrow: 8617, - leftarrowtail: 8610, - leftharpoonup: 8636, - longleftarrow: 10229, - looparrowleft: 8619, - measuredangle: 8737, - ntriangleleft: 8938, - shortparallel: 8741, - smallsetminus: 8726, - triangleright: 9657, - upharpoonleft: 8639, - DownArrowBar: 10515, - DownTeeArrow: 8615, - ExponentialE: 8519, - GreaterEqual: 8805, - GreaterTilde: 8819, - HilbertSpace: 8459, - HumpDownHump: 8782, - Intersection: 8898, - LeftArrowBar: 8676, - LeftTeeArrow: 8612, - LeftTriangle: 8882, - LeftUpVector: 8639, - NotCongruent: 8802, - NotLessEqual: 8816, - NotLessTilde: 8820, - Proportional: 8733, - RightCeiling: 8969, - RoundImplies: 10608, - ShortUpArrow: 8593, - SquareSubset: 8847, - UnderBracket: 9141, - VerticalLine: 124, - blacklozenge: 10731, - exponentiale: 8519, - risingdotseq: 8787, - triangledown: 9663, - triangleleft: 9667, - CircleMinus: 8854, - CircleTimes: 8855, - Equilibrium: 8652, - GreaterLess: 8823, - LeftCeiling: 8968, - LessGreater: 8822, - MediumSpace: 8287, - NotPrecedes: 8832, - NotSucceeds: 8833, - OverBracket: 9140, - RightVector: 8640, - Rrightarrow: 8667, - RuleDelayed: 10740, - SmallCircle: 8728, - SquareUnion: 8852, - SubsetEqual: 8838, - UpDownArrow: 8597, - Updownarrow: 8661, - VerticalBar: 8739, - backepsilon: 1014, - blacksquare: 9642, - circledcirc: 8858, - circleddash: 8861, - curlyeqprec: 8926, - curlyeqsucc: 8927, - diamondsuit: 9830, - eqslantless: 10901, - expectation: 8496, - nRightarrow: 8655, - nrightarrow: 8603, - preccurlyeq: 8828, - precnapprox: 10937, - quaternions: 8461, - straightphi: 981, - succcurlyeq: 8829, - succnapprox: 10938, - thickapprox: 8776, - updownarrow: 8597, - Bernoullis: 8492, - CirclePlus: 8853, - EqualTilde: 8770, - Fouriertrf: 8497, - ImaginaryI: 8520, - Laplacetrf: 8466, - LeftVector: 8636, - Lleftarrow: 8666, - NotElement: 8713, - NotGreater: 8815, - Proportion: 8759, - RightArrow: 8594, - RightFloor: 8971, - Rightarrow: 8658, - TildeEqual: 8771, - TildeTilde: 8776, - UnderBrace: 9183, - UpArrowBar: 10514, - UpTeeArrow: 8613, - circledast: 8859, - complement: 8705, - curlywedge: 8911, - eqslantgtr: 10902, - gtreqqless: 10892, - lessapprox: 10885, - lesseqqgtr: 10891, - lmoustache: 9136, - longmapsto: 10236, - mapstodown: 8615, - mapstoleft: 8612, - nLeftarrow: 8653, - nleftarrow: 8602, - precapprox: 10935, - rightarrow: 8594, - rmoustache: 9137, - sqsubseteq: 8849, - sqsupseteq: 8850, - subsetneqq: 10955, - succapprox: 10936, - supsetneqq: 10956, - upuparrows: 8648, - varepsilon: 949, - varnothing: 8709, - Backslash: 8726, - CenterDot: 183, - CircleDot: 8857, - Congruent: 8801, - Coproduct: 8720, - DoubleDot: 168, - DownArrow: 8595, - DownBreve: 785, - Downarrow: 8659, - HumpEqual: 8783, - LeftArrow: 8592, - LeftFloor: 8970, - Leftarrow: 8656, - LessTilde: 8818, - Mellintrf: 8499, - MinusPlus: 8723, - NotCupCap: 8813, - NotExists: 8708, - OverBrace: 9182, - PlusMinus: 177, - Therefore: 8756, - ThinSpace: 8201, - TripleDot: 8411, - UnionPlus: 8846, - backprime: 8245, - backsimeq: 8909, - bigotimes: 10754, - centerdot: 183, - checkmark: 10003, - complexes: 8450, - dotsquare: 8865, - downarrow: 8595, - gtrapprox: 10886, - gtreqless: 8923, - heartsuit: 9829, - leftarrow: 8592, - lesseqgtr: 8922, - nparallel: 8742, - nshortmid: 8740, - nsubseteq: 8840, - nsupseteq: 8841, - pitchfork: 8916, - rationals: 8474, - spadesuit: 9824, - subseteqq: 10949, - subsetneq: 8842, - supseteqq: 10950, - supsetneq: 8843, - therefore: 8756, - triangleq: 8796, - varpropto: 8733, - DDotrahd: 10513, - DotEqual: 8784, - Integral: 8747, - LessLess: 10913, - NotEqual: 8800, - NotTilde: 8769, - PartialD: 8706, - Precedes: 8826, - RightTee: 8866, - Succeeds: 8827, - SuchThat: 8715, - Superset: 8835, - Uarrocir: 10569, - UnderBar: 818, - andslope: 10840, - angmsdaa: 10664, - angmsdab: 10665, - angmsdac: 10666, - angmsdad: 10667, - angmsdae: 10668, - angmsdaf: 10669, - angmsdag: 10670, - angmsdah: 10671, - angrtvbd: 10653, - approxeq: 8778, - awconint: 8755, - backcong: 8780, - barwedge: 8965, - bbrktbrk: 9142, - bigoplus: 10753, - bigsqcup: 10758, - biguplus: 10756, - bigwedge: 8896, - boxminus: 8863, - boxtimes: 8864, - capbrcup: 10825, - circledR: 174, - circledS: 9416, - cirfnint: 10768, - clubsuit: 9827, - cupbrcap: 10824, - curlyvee: 8910, - cwconint: 8754, - doteqdot: 8785, - dotminus: 8760, - drbkarow: 10512, - dzigrarr: 10239, - elinters: 9191, - emptyset: 8709, - eqvparsl: 10725, - fpartint: 10765, - geqslant: 10878, - gesdotol: 10884, - gnapprox: 10890, - hksearow: 10533, - hkswarow: 10534, - imagline: 8464, - imagpart: 8465, - infintie: 10717, - integers: 8484, - intercal: 8890, - intlarhk: 10775, - laemptyv: 10676, - ldrushar: 10571, - leqslant: 10877, - lesdotor: 10883, - llcorner: 8990, - lnapprox: 10889, - lrcorner: 8991, - lurdshar: 10570, - mapstoup: 8613, - multimap: 8888, - naturals: 8469, - otimesas: 10806, - parallel: 8741, - plusacir: 10787, - pointint: 10773, - precneqq: 10933, - precnsim: 8936, - profalar: 9006, - profline: 8978, - profsurf: 8979, - raemptyv: 10675, - realpart: 8476, - rppolint: 10770, - rtriltri: 10702, - scpolint: 10771, - setminus: 8726, - shortmid: 8739, - smeparsl: 10724, - sqsubset: 8847, - sqsupset: 8848, - subseteq: 8838, - succneqq: 10934, - succnsim: 8937, - supseteq: 8839, - thetasym: 977, - thicksim: 8764, - timesbar: 10801, - triangle: 9653, - triminus: 10810, - trpezium: 9186, - ulcorner: 8988, - urcorner: 8989, - varkappa: 1008, - varsigma: 962, - vartheta: 977, - Because: 8757, - Cayleys: 8493, - Cconint: 8752, - Cedilla: 184, - Diamond: 8900, - DownTee: 8868, - Element: 8712, - Epsilon: 917, - Implies: 8658, - LeftTee: 8867, - NewLine: 10, - NoBreak: 8288, - NotLess: 8814, - Omicron: 927, - OverBar: 175, - Product: 8719, - UpArrow: 8593, - Uparrow: 8657, - Upsilon: 933, - alefsym: 8501, - angrtvb: 8894, - angzarr: 9084, - asympeq: 8781, - backsim: 8765, - because: 8757, - bemptyv: 10672, - between: 8812, - bigcirc: 9711, - bigodot: 10752, - bigstar: 9733, - boxplus: 8862, - ccupssm: 10832, - cemptyv: 10674, - cirscir: 10690, - coloneq: 8788, - congdot: 10861, - cudarrl: 10552, - cudarrr: 10549, - cularrp: 10557, - curarrm: 10556, - dbkarow: 10511, - ddagger: 8225, - ddotseq: 10871, - demptyv: 10673, - diamond: 8900, - digamma: 989, - dotplus: 8724, - dwangle: 10662, - epsilon: 949, - eqcolon: 8789, - equivDD: 10872, - gesdoto: 10882, - gtquest: 10876, - gtrless: 8823, - harrcir: 10568, - intprod: 10812, - isindot: 8949, - larrbfs: 10527, - larrsim: 10611, - lbrksld: 10639, - lbrkslu: 10637, - ldrdhar: 10599, - lesdoto: 10881, - lessdot: 8918, - lessgtr: 8822, - lesssim: 8818, - lotimes: 10804, - lozenge: 9674, - ltquest: 10875, - luruhar: 10598, - maltese: 10016, - minusdu: 10794, - napprox: 8777, - natural: 9838, - nearrow: 8599, - nexists: 8708, - notinva: 8713, - notinvb: 8951, - notinvc: 8950, - notniva: 8716, - notnivb: 8958, - notnivc: 8957, - npolint: 10772, - nsqsube: 8930, - nsqsupe: 8931, - nvinfin: 10718, - nwarrow: 8598, - olcross: 10683, - omicron: 959, - orderof: 8500, - orslope: 10839, - pertenk: 8241, - planckh: 8462, - pluscir: 10786, - plussim: 10790, - plustwo: 10791, - precsim: 8830, - quatint: 10774, - questeq: 8799, - rarrbfs: 10528, - rarrsim: 10612, - rbrksld: 10638, - rbrkslu: 10640, - rdldhar: 10601, - realine: 8475, - rotimes: 10805, - ruluhar: 10600, - searrow: 8600, - simplus: 10788, - simrarr: 10610, - subedot: 10947, - submult: 10945, - subplus: 10943, - subrarr: 10617, - succsim: 8831, - supdsub: 10968, - supedot: 10948, - suphsub: 10967, - suplarr: 10619, - supmult: 10946, - supplus: 10944, - swarrow: 8601, - topfork: 10970, - triplus: 10809, - tritime: 10811, - uparrow: 8593, - upsilon: 965, - uwangle: 10663, - vzigzag: 10650, - zigrarr: 8669, - Aacute: 193, - Abreve: 258, - Agrave: 192, - Assign: 8788, - Atilde: 195, - Barwed: 8966, - Bumpeq: 8782, - Cacute: 262, - Ccaron: 268, - Ccedil: 199, - Colone: 10868, - Conint: 8751, - CupCap: 8781, - Dagger: 8225, - Dcaron: 270, - DotDot: 8412, - Dstrok: 272, - Eacute: 201, - Ecaron: 282, - Egrave: 200, - Exists: 8707, - ForAll: 8704, - Gammad: 988, - Gbreve: 286, - Gcedil: 290, - HARDcy: 1066, - Hstrok: 294, - Iacute: 205, - Igrave: 204, - Itilde: 296, - Jsercy: 1032, - Kcedil: 310, - Lacute: 313, - Lambda: 923, - Lcaron: 317, - Lcedil: 315, - Lmidot: 319, - Lstrok: 321, - Nacute: 323, - Ncaron: 327, - Ncedil: 325, - Ntilde: 209, - Oacute: 211, - Odblac: 336, - Ograve: 210, - Oslash: 216, - Otilde: 213, - Otimes: 10807, - Racute: 340, - Rarrtl: 10518, - Rcaron: 344, - Rcedil: 342, - SHCHcy: 1065, - SOFTcy: 1068, - Sacute: 346, - Scaron: 352, - Scedil: 350, - Square: 9633, - Subset: 8912, - Supset: 8913, - Tcaron: 356, - Tcedil: 354, - Tstrok: 358, - Uacute: 218, - Ubreve: 364, - Udblac: 368, - Ugrave: 217, - Utilde: 360, - Vdashl: 10982, - Verbar: 8214, - Vvdash: 8874, - Yacute: 221, - Zacute: 377, - Zcaron: 381, - aacute: 225, - abreve: 259, - agrave: 224, - andand: 10837, - angmsd: 8737, - angsph: 8738, - apacir: 10863, - approx: 8776, - atilde: 227, - barvee: 8893, - barwed: 8965, - becaus: 8757, - bernou: 8492, - bigcap: 8898, - bigcup: 8899, - bigvee: 8897, - bkarow: 10509, - bottom: 8869, - bowtie: 8904, - boxbox: 10697, - bprime: 8245, - brvbar: 166, - bullet: 8226, - bumpeq: 8783, - cacute: 263, - capand: 10820, - capcap: 10827, - capcup: 10823, - capdot: 10816, - ccaron: 269, - ccedil: 231, - circeq: 8791, - cirmid: 10991, - colone: 8788, - commat: 64, - compfn: 8728, - conint: 8750, - coprod: 8720, - copysr: 8471, - cularr: 8630, - cupcap: 10822, - cupcup: 10826, - cupdot: 8845, - curarr: 8631, - curren: 164, - cylcty: 9005, - dagger: 8224, - daleth: 8504, - dcaron: 271, - dfisht: 10623, - divide: 247, - divonx: 8903, - dlcorn: 8990, - dlcrop: 8973, - dollar: 36, - drcorn: 8991, - drcrop: 8972, - dstrok: 273, - eacute: 233, - easter: 10862, - ecaron: 283, - ecolon: 8789, - egrave: 232, - egsdot: 10904, - elsdot: 10903, - emptyv: 8709, - emsp13: 8196, - emsp14: 8197, - eparsl: 10723, - eqcirc: 8790, - equals: 61, - equest: 8799, - female: 9792, - ffilig: 64259, - ffllig: 64260, - forall: 8704, - frac12: 189, - frac13: 8531, - frac14: 188, - frac15: 8533, - frac16: 8537, - frac18: 8539, - frac23: 8532, - frac25: 8534, - frac34: 190, - frac35: 8535, - frac38: 8540, - frac45: 8536, - frac56: 8538, - frac58: 8541, - frac78: 8542, - gacute: 501, - gammad: 989, - gbreve: 287, - gesdot: 10880, - gesles: 10900, - gtlPar: 10645, - gtrarr: 10616, - gtrdot: 8919, - gtrsim: 8819, - hairsp: 8202, - hamilt: 8459, - hardcy: 1098, - hearts: 9829, - hellip: 8230, - hercon: 8889, - homtht: 8763, - horbar: 8213, - hslash: 8463, - hstrok: 295, - hybull: 8259, - hyphen: 8208, - iacute: 237, - igrave: 236, - iiiint: 10764, - iinfin: 10716, - incare: 8453, - inodot: 305, - intcal: 8890, - iquest: 191, - isinsv: 8947, - itilde: 297, - jsercy: 1112, - kappav: 1008, - kcedil: 311, - kgreen: 312, - lAtail: 10523, - lacute: 314, - lagran: 8466, - lambda: 955, - langle: 10216, - larrfs: 10525, - larrhk: 8617, - larrlp: 8619, - larrpl: 10553, - larrtl: 8610, - latail: 10521, - lbrace: 123, - lbrack: 91, - lcaron: 318, - lcedil: 316, - ldquor: 8222, - lesdot: 10879, - lesges: 10899, - lfisht: 10620, - lfloor: 8970, - lharul: 10602, - llhard: 10603, - lmidot: 320, - lmoust: 9136, - loplus: 10797, - lowast: 8727, - lowbar: 95, - lparlt: 10643, - lrhard: 10605, - lsaquo: 8249, - lsquor: 8218, - lstrok: 322, - lthree: 8907, - ltimes: 8905, - ltlarr: 10614, - ltrPar: 10646, - mapsto: 8614, - marker: 9646, - mcomma: 10793, - midast: 42, - midcir: 10992, - middot: 183, - minusb: 8863, - minusd: 8760, - mnplus: 8723, - models: 8871, - mstpos: 8766, - nVDash: 8879, - nVdash: 8878, - nacute: 324, - ncaron: 328, - ncedil: 326, - nearhk: 10532, - nequiv: 8802, - nesear: 10536, - nexist: 8708, - nltrie: 8940, - nprcue: 8928, - nrtrie: 8941, - nsccue: 8929, - nsimeq: 8772, - ntilde: 241, - numero: 8470, - nvDash: 8877, - nvHarr: 10500, - nvdash: 8876, - nvlArr: 10498, - nvrArr: 10499, - nwarhk: 10531, - nwnear: 10535, - oacute: 243, - odblac: 337, - odsold: 10684, - ograve: 242, - ominus: 8854, - origof: 8886, - oslash: 248, - otilde: 245, - otimes: 8855, - parsim: 10995, - percnt: 37, - period: 46, - permil: 8240, - phmmat: 8499, - planck: 8463, - plankv: 8463, - plusdo: 8724, - plusdu: 10789, - plusmn: 177, - preceq: 10927, - primes: 8473, - prnsim: 8936, - propto: 8733, - prurel: 8880, - puncsp: 8200, - qprime: 8279, - rAtail: 10524, - racute: 341, - rangle: 10217, - rarrap: 10613, - rarrfs: 10526, - rarrhk: 8618, - rarrlp: 8620, - rarrpl: 10565, - rarrtl: 8611, - ratail: 10522, - rbrace: 125, - rbrack: 93, - rcaron: 345, - rcedil: 343, - rdquor: 8221, - rfisht: 10621, - rfloor: 8971, - rharul: 10604, - rmoust: 9137, - roplus: 10798, - rpargt: 10644, - rsaquo: 8250, - rsquor: 8217, - rthree: 8908, - rtimes: 8906, - sacute: 347, - scaron: 353, - scedil: 351, - scnsim: 8937, - searhk: 10533, - seswar: 10537, - sfrown: 8994, - shchcy: 1097, - sigmaf: 962, - sigmav: 962, - simdot: 10858, - smashp: 10803, - softcy: 1100, - solbar: 9023, - spades: 9824, - sqsube: 8849, - sqsupe: 8850, - square: 9633, - squarf: 9642, - ssetmn: 8726, - ssmile: 8995, - sstarf: 8902, - subdot: 10941, - subset: 8834, - subsim: 10951, - subsub: 10965, - subsup: 10963, - succeq: 10928, - supdot: 10942, - supset: 8835, - supsim: 10952, - supsub: 10964, - supsup: 10966, - swarhk: 10534, - swnwar: 10538, - target: 8982, - tcaron: 357, - tcedil: 355, - telrec: 8981, - there4: 8756, - thetav: 977, - thinsp: 8201, - thksim: 8764, - timesb: 8864, - timesd: 10800, - topbot: 9014, - topcir: 10993, - tprime: 8244, - tridot: 9708, - tstrok: 359, - uacute: 250, - ubreve: 365, - udblac: 369, - ufisht: 10622, - ugrave: 249, - ulcorn: 8988, - ulcrop: 8975, - urcorn: 8989, - urcrop: 8974, - utilde: 361, - vangrt: 10652, - varphi: 966, - varrho: 1009, - veebar: 8891, - vellip: 8942, - verbar: 124, - wedbar: 10847, - wedgeq: 8793, - weierp: 8472, - wreath: 8768, - xoplus: 10753, - xotime: 10754, - xsqcup: 10758, - xuplus: 10756, - xwedge: 8896, - yacute: 253, - zacute: 378, - zcaron: 382, - zeetrf: 8488, - AElig: 198, - Acirc: 194, - Alpha: 913, - Amacr: 256, - Aogon: 260, - Aring: 197, - Breve: 728, - Ccirc: 264, - Colon: 8759, - Cross: 10799, - Dashv: 10980, - Delta: 916, - Ecirc: 202, - Emacr: 274, - Eogon: 280, - Equal: 10869, - Gamma: 915, - Gcirc: 284, - Hacek: 711, - Hcirc: 292, - IJlig: 306, - Icirc: 206, - Imacr: 298, - Iogon: 302, - Iukcy: 1030, - Jcirc: 308, - Jukcy: 1028, - Kappa: 922, - OElig: 338, - Ocirc: 212, - Omacr: 332, - Omega: 937, - Prime: 8243, - RBarr: 10512, - Scirc: 348, - Sigma: 931, - THORN: 222, - TRADE: 8482, - TSHcy: 1035, - Theta: 920, - Tilde: 8764, - Ubrcy: 1038, - Ucirc: 219, - Umacr: 362, - Union: 8899, - Uogon: 370, - UpTee: 8869, - Uring: 366, - VDash: 8875, - Vdash: 8873, - Wcirc: 372, - Wedge: 8896, - Ycirc: 374, - acirc: 226, - acute: 180, - aelig: 230, - aleph: 8501, - alpha: 945, - amacr: 257, - amalg: 10815, - angle: 8736, - angrt: 8735, - angst: 8491, - aogon: 261, - aring: 229, - asymp: 8776, - awint: 10769, - bcong: 8780, - bdquo: 8222, - bepsi: 1014, - blank: 9251, - blk12: 9618, - blk14: 9617, - blk34: 9619, - block: 9608, - boxDL: 9559, - boxDR: 9556, - boxDl: 9558, - boxDr: 9555, - boxHD: 9574, - boxHU: 9577, - boxHd: 9572, - boxHu: 9575, - boxUL: 9565, - boxUR: 9562, - boxUl: 9564, - boxUr: 9561, - boxVH: 9580, - boxVL: 9571, - boxVR: 9568, - boxVh: 9579, - boxVl: 9570, - boxVr: 9567, - boxdL: 9557, - boxdR: 9554, - boxdl: 9488, - boxdr: 9484, - boxhD: 9573, - boxhU: 9576, - boxhd: 9516, - boxhu: 9524, - boxuL: 9563, - boxuR: 9560, - boxul: 9496, - boxur: 9492, - boxvH: 9578, - boxvL: 9569, - boxvR: 9566, - boxvh: 9532, - boxvl: 9508, - boxvr: 9500, - breve: 728, - bsemi: 8271, - bsime: 8909, - bsolb: 10693, - bumpE: 10926, - bumpe: 8783, - caret: 8257, - caron: 711, - ccaps: 10829, - ccirc: 265, - ccups: 10828, - cedil: 184, - check: 10003, - clubs: 9827, - colon: 58, - comma: 44, - crarr: 8629, - cross: 10007, - csube: 10961, - csupe: 10962, - ctdot: 8943, - cuepr: 8926, - cuesc: 8927, - cupor: 10821, - cuvee: 8910, - cuwed: 8911, - cwint: 8753, - dashv: 8867, - dblac: 733, - ddarr: 8650, - delta: 948, - dharl: 8643, - dharr: 8642, - diams: 9830, - disin: 8946, - doteq: 8784, - dtdot: 8945, - dtrif: 9662, - duarr: 8693, - duhar: 10607, - eDDot: 10871, - ecirc: 234, - efDot: 8786, - emacr: 275, - empty: 8709, - eogon: 281, - eplus: 10865, - epsiv: 949, - eqsim: 8770, - equiv: 8801, - erDot: 8787, - erarr: 10609, - esdot: 8784, - exist: 8707, - fflig: 64256, - filig: 64257, - fllig: 64258, - fltns: 9649, - forkv: 10969, - frasl: 8260, - frown: 8994, - gamma: 947, - gcirc: 285, - gescc: 10921, - gimel: 8503, - gneqq: 8809, - gnsim: 8935, - grave: 96, - gsime: 10894, - gsiml: 10896, - gtcir: 10874, - gtdot: 8919, - harrw: 8621, - hcirc: 293, - hoarr: 8703, - icirc: 238, - iexcl: 161, - iiint: 8749, - iiota: 8489, - ijlig: 307, - imacr: 299, - image: 8465, - imath: 305, - imped: 437, - infin: 8734, - iogon: 303, - iprod: 10812, - isinE: 8953, - isins: 8948, - isinv: 8712, - iukcy: 1110, - jcirc: 309, - jmath: 567, - jukcy: 1108, - kappa: 954, - lAarr: 8666, - lBarr: 10510, - langd: 10641, - laquo: 171, - larrb: 8676, - lbarr: 10508, - lbbrk: 10098, - lbrke: 10635, - lceil: 8968, - ldquo: 8220, - lescc: 10920, - lhard: 8637, - lharu: 8636, - lhblk: 9604, - llarr: 8647, - lltri: 9722, - lneqq: 8808, - lnsim: 8934, - loang: 10220, - loarr: 8701, - lobrk: 10214, - lopar: 10629, - lrarr: 8646, - lrhar: 8651, - lrtri: 8895, - lsime: 10893, - lsimg: 10895, - lsquo: 8216, - ltcir: 10873, - ltdot: 8918, - ltrie: 8884, - ltrif: 9666, - mDDot: 8762, - mdash: 8212, - micro: 181, - minus: 8722, - mumap: 8888, - nabla: 8711, - napos: 329, - natur: 9838, - ncong: 8775, - ndash: 8211, - neArr: 8663, - nearr: 8599, - ngsim: 8821, - nhArr: 8654, - nharr: 8622, - nhpar: 10994, - nlArr: 8653, - nlarr: 8602, - nless: 8814, - nlsim: 8820, - nltri: 8938, - notin: 8713, - notni: 8716, - nprec: 8832, - nrArr: 8655, - nrarr: 8603, - nrtri: 8939, - nsime: 8772, - nsmid: 8740, - nspar: 8742, - nsube: 8840, - nsucc: 8833, - nsupe: 8841, - numsp: 8199, - nwArr: 8662, - nwarr: 8598, - ocirc: 244, - odash: 8861, - oelig: 339, - ofcir: 10687, - ohbar: 10677, - olarr: 8634, - olcir: 10686, - oline: 8254, - omacr: 333, - omega: 969, - operp: 10681, - oplus: 8853, - orarr: 8635, - order: 8500, - ovbar: 9021, - parsl: 11005, - phone: 9742, - plusb: 8862, - pluse: 10866, - pound: 163, - prcue: 8828, - prime: 8242, - prnap: 10937, - prsim: 8830, - quest: 63, - rAarr: 8667, - rBarr: 10511, - radic: 8730, - rangd: 10642, - range: 10661, - raquo: 187, - rarrb: 8677, - rarrc: 10547, - rarrw: 8605, - ratio: 8758, - rbarr: 10509, - rbbrk: 10099, - rbrke: 10636, - rceil: 8969, - rdquo: 8221, - reals: 8477, - rhard: 8641, - rharu: 8640, - rlarr: 8644, - rlhar: 8652, - rnmid: 10990, - roang: 10221, - roarr: 8702, - robrk: 10215, - ropar: 10630, - rrarr: 8649, - rsquo: 8217, - rtrie: 8885, - rtrif: 9656, - sbquo: 8218, - sccue: 8829, - scirc: 349, - scnap: 10938, - scsim: 8831, - sdotb: 8865, - sdote: 10854, - seArr: 8664, - searr: 8600, - setmn: 8726, - sharp: 9839, - sigma: 963, - simeq: 8771, - simgE: 10912, - simlE: 10911, - simne: 8774, - slarr: 8592, - smile: 8995, - sqcap: 8851, - sqcup: 8852, - sqsub: 8847, - sqsup: 8848, - srarr: 8594, - starf: 9733, - strns: 175, - subnE: 10955, - subne: 8842, - supnE: 10956, - supne: 8843, - swArr: 8665, - swarr: 8601, - szlig: 223, - theta: 952, - thkap: 8776, - thorn: 254, - tilde: 732, - times: 215, - trade: 8482, - trisb: 10701, - tshcy: 1115, - twixt: 8812, - ubrcy: 1118, - ucirc: 251, - udarr: 8645, - udhar: 10606, - uharl: 8639, - uharr: 8638, - uhblk: 9600, - ultri: 9720, - umacr: 363, - uogon: 371, - uplus: 8846, - upsih: 978, - uring: 367, - urtri: 9721, - utdot: 8944, - utrif: 9652, - uuarr: 8648, - vBarv: 10985, - vDash: 8872, - varpi: 982, - vdash: 8866, - veeeq: 8794, - vltri: 8882, - vprop: 8733, - vrtri: 8883, - wcirc: 373, - wedge: 8743, - xcirc: 9711, - xdtri: 9661, - xhArr: 10234, - xharr: 10231, - xlArr: 10232, - xlarr: 10229, - xodot: 10752, - xrArr: 10233, - xrarr: 10230, - xutri: 9651, - ycirc: 375, - Aopf: 120120, - Ascr: 119964, - Auml: 196, - Barv: 10983, - Beta: 914, - Bopf: 120121, - Bscr: 8492, - CHcy: 1063, - COPY: 169, - Cdot: 266, - Copf: 8450, - Cscr: 119966, - DJcy: 1026, - DScy: 1029, - DZcy: 1039, - Darr: 8609, - Dopf: 120123, - Dscr: 119967, - Edot: 278, - Eopf: 120124, - Escr: 8496, - Esim: 10867, - Euml: 203, - Fopf: 120125, - Fscr: 8497, - GJcy: 1027, - Gdot: 288, - Gopf: 120126, - Gscr: 119970, - Hopf: 8461, - Hscr: 8459, - IEcy: 1045, - IOcy: 1025, - Idot: 304, - Iopf: 120128, - Iota: 921, - Iscr: 8464, - Iuml: 207, - Jopf: 120129, - Jscr: 119973, - KHcy: 1061, - KJcy: 1036, - Kopf: 120130, - Kscr: 119974, - LJcy: 1033, - Lang: 10218, - Larr: 8606, - Lopf: 120131, - Lscr: 8466, - Mopf: 120132, - Mscr: 8499, - NJcy: 1034, - Nopf: 8469, - Nscr: 119977, - Oopf: 120134, - Oscr: 119978, - Ouml: 214, - Popf: 8473, - Pscr: 119979, - QUOT: 34, - Qopf: 8474, - Qscr: 119980, - Rang: 10219, - Rarr: 8608, - Ropf: 8477, - Rscr: 8475, - SHcy: 1064, - Sopf: 120138, - Sqrt: 8730, - Sscr: 119982, - Star: 8902, - TScy: 1062, - Topf: 120139, - Tscr: 119983, - Uarr: 8607, - Uopf: 120140, - Upsi: 978, - Uscr: 119984, - Uuml: 220, - Vbar: 10987, - Vert: 8214, - Vopf: 120141, - Vscr: 119985, - Wopf: 120142, - Wscr: 119986, - Xopf: 120143, - Xscr: 119987, - YAcy: 1071, - YIcy: 1031, - YUcy: 1070, - Yopf: 120144, - Yscr: 119988, - Yuml: 376, - ZHcy: 1046, - Zdot: 379, - Zeta: 918, - Zopf: 8484, - Zscr: 119989, - andd: 10844, - andv: 10842, - ange: 10660, - aopf: 120146, - apid: 8779, - apos: 39, - ascr: 119990, - auml: 228, - bNot: 10989, - bbrk: 9141, - beta: 946, - beth: 8502, - bnot: 8976, - bopf: 120147, - boxH: 9552, - boxV: 9553, - boxh: 9472, - boxv: 9474, - bscr: 119991, - bsim: 8765, - bsol: 92, - bull: 8226, - bump: 8782, - cdot: 267, - cent: 162, - chcy: 1095, - cirE: 10691, - circ: 710, - cire: 8791, - comp: 8705, - cong: 8773, - copf: 120148, - copy: 169, - cscr: 119992, - csub: 10959, - csup: 10960, - dArr: 8659, - dHar: 10597, - darr: 8595, - dash: 8208, - diam: 8900, - djcy: 1106, - dopf: 120149, - dscr: 119993, - dscy: 1109, - dsol: 10742, - dtri: 9663, - dzcy: 1119, - eDot: 8785, - ecir: 8790, - edot: 279, - emsp: 8195, - ensp: 8194, - eopf: 120150, - epar: 8917, - epsi: 1013, - escr: 8495, - esim: 8770, - euml: 235, - euro: 8364, - excl: 33, - flat: 9837, - fnof: 402, - fopf: 120151, - fork: 8916, - fscr: 119995, - gdot: 289, - geqq: 8807, - gjcy: 1107, - gnap: 10890, - gneq: 10888, - gopf: 120152, - gscr: 8458, - gsim: 8819, - gtcc: 10919, - hArr: 8660, - half: 189, - harr: 8596, - hbar: 8463, - hopf: 120153, - hscr: 119997, - iecy: 1077, - imof: 8887, - iocy: 1105, - iopf: 120154, - iota: 953, - iscr: 119998, - isin: 8712, - iuml: 239, - jopf: 120155, - jscr: 119999, - khcy: 1093, - kjcy: 1116, - kopf: 120156, - kscr: 120000, - lArr: 8656, - lHar: 10594, - lang: 10216, - larr: 8592, - late: 10925, - lcub: 123, - ldca: 10550, - ldsh: 8626, - leqq: 8806, - ljcy: 1113, - lnap: 10889, - lneq: 10887, - lopf: 120157, - lozf: 10731, - lpar: 40, - lscr: 120001, - lsim: 8818, - lsqb: 91, - ltcc: 10918, - ltri: 9667, - macr: 175, - male: 9794, - malt: 10016, - mlcp: 10971, - mldr: 8230, - mopf: 120158, - mscr: 120002, - nbsp: 160, - ncap: 10819, - ncup: 10818, - ngeq: 8817, - ngtr: 8815, - nisd: 8954, - njcy: 1114, - nldr: 8229, - nleq: 8816, - nmid: 8740, - nopf: 120159, - npar: 8742, - nscr: 120003, - nsim: 8769, - nsub: 8836, - nsup: 8837, - ntgl: 8825, - ntlg: 8824, - oast: 8859, - ocir: 8858, - odiv: 10808, - odot: 8857, - ogon: 731, - oint: 8750, - omid: 10678, - oopf: 120160, - opar: 10679, - ordf: 170, - ordm: 186, - oror: 10838, - oscr: 8500, - osol: 8856, - ouml: 246, - para: 182, - part: 8706, - perp: 8869, - phiv: 966, - plus: 43, - popf: 120161, - prap: 10935, - prec: 8826, - prnE: 10933, - prod: 8719, - prop: 8733, - pscr: 120005, - qint: 10764, - qopf: 120162, - qscr: 120006, - quot: 34, - rArr: 8658, - rHar: 10596, - race: 10714, - rang: 10217, - rarr: 8594, - rcub: 125, - rdca: 10551, - rdsh: 8627, - real: 8476, - rect: 9645, - rhov: 1009, - ring: 730, - ropf: 120163, - rpar: 41, - rscr: 120007, - rsqb: 93, - rtri: 9657, - scap: 10936, - scnE: 10934, - sdot: 8901, - sect: 167, - semi: 59, - sext: 10038, - shcy: 1096, - sime: 8771, - simg: 10910, - siml: 10909, - smid: 8739, - smte: 10924, - solb: 10692, - sopf: 120164, - spar: 8741, - squf: 9642, - sscr: 120008, - star: 9734, - subE: 10949, - sube: 8838, - succ: 8827, - sung: 9834, - sup1: 185, - sup2: 178, - sup3: 179, - supE: 10950, - supe: 8839, - tbrk: 9140, - tdot: 8411, - tint: 8749, - toea: 10536, - topf: 120165, - tosa: 10537, - trie: 8796, - tscr: 120009, - tscy: 1094, - uArr: 8657, - uHar: 10595, - uarr: 8593, - uopf: 120166, - upsi: 965, - uscr: 120010, - utri: 9653, - uuml: 252, - vArr: 8661, - vBar: 10984, - varr: 8597, - vert: 124, - vopf: 120167, - vscr: 120011, - wopf: 120168, - wscr: 120012, - xcap: 8898, - xcup: 8899, - xmap: 10236, - xnis: 8955, - xopf: 120169, - xscr: 120013, - xvee: 8897, - yacy: 1103, - yicy: 1111, - yopf: 120170, - yscr: 120014, - yucy: 1102, - yuml: 255, - zdot: 380, - zeta: 950, - zhcy: 1078, - zopf: 120171, - zscr: 120015, - zwnj: 8204, - AMP: 38, - Acy: 1040, - Afr: 120068, - And: 10835, - Bcy: 1041, - Bfr: 120069, - Cap: 8914, - Cfr: 8493, - Chi: 935, - Cup: 8915, - Dcy: 1044, - Del: 8711, - Dfr: 120071, - Dot: 168, - ENG: 330, - ETH: 208, - Ecy: 1069, - Efr: 120072, - Eta: 919, - Fcy: 1060, - Ffr: 120073, - Gcy: 1043, - Gfr: 120074, - Hat: 94, - Hfr: 8460, - Icy: 1048, - Ifr: 8465, - Int: 8748, - Jcy: 1049, - Jfr: 120077, - Kcy: 1050, - Kfr: 120078, - Lcy: 1051, - Lfr: 120079, - Lsh: 8624, - Map: 10501, - Mcy: 1052, - Mfr: 120080, - Ncy: 1053, - Nfr: 120081, - Not: 10988, - Ocy: 1054, - Ofr: 120082, - Pcy: 1055, - Pfr: 120083, - Phi: 934, - Psi: 936, - Qfr: 120084, - REG: 174, - Rcy: 1056, - Rfr: 8476, - Rho: 929, - Rsh: 8625, - Scy: 1057, - Sfr: 120086, - Sub: 8912, - Sum: 8721, - Sup: 8913, - Tab: 9, - Tau: 932, - Tcy: 1058, - Tfr: 120087, - Ucy: 1059, - Ufr: 120088, - Vcy: 1042, - Vee: 8897, - Vfr: 120089, - Wfr: 120090, - Xfr: 120091, - Ycy: 1067, - Yfr: 120092, - Zcy: 1047, - Zfr: 8488, - acd: 8767, - acy: 1072, - afr: 120094, - amp: 38, - and: 8743, - ang: 8736, - apE: 10864, - ape: 8778, - ast: 42, - bcy: 1073, - bfr: 120095, - bot: 8869, - cap: 8745, - cfr: 120096, - chi: 967, - cir: 9675, - cup: 8746, - dcy: 1076, - deg: 176, - dfr: 120097, - die: 168, - div: 247, - dot: 729, - ecy: 1101, - efr: 120098, - egs: 10902, - ell: 8467, - els: 10901, - eng: 331, - eta: 951, - eth: 240, - fcy: 1092, - ffr: 120099, - gEl: 10892, - gap: 10886, - gcy: 1075, - gel: 8923, - geq: 8805, - ges: 10878, - gfr: 120100, - ggg: 8921, - glE: 10898, - gla: 10917, - glj: 10916, - gnE: 8809, - gne: 10888, - hfr: 120101, - icy: 1080, - iff: 8660, - ifr: 120102, - int: 8747, - jcy: 1081, - jfr: 120103, - kcy: 1082, - kfr: 120104, - lEg: 10891, - lap: 10885, - lat: 10923, - lcy: 1083, - leg: 8922, - leq: 8804, - les: 10877, - lfr: 120105, - lgE: 10897, - lnE: 8808, - lne: 10887, - loz: 9674, - lrm: 8206, - lsh: 8624, - map: 8614, - mcy: 1084, - mfr: 120106, - mho: 8487, - mid: 8739, - nap: 8777, - ncy: 1085, - nfr: 120107, - nge: 8817, - ngt: 8815, - nis: 8956, - niv: 8715, - nle: 8816, - nlt: 8814, - not: 172, - npr: 8832, - nsc: 8833, - num: 35, - ocy: 1086, - ofr: 120108, - ogt: 10689, - ohm: 8486, - olt: 10688, - ord: 10845, - orv: 10843, - par: 8741, - pcy: 1087, - pfr: 120109, - phi: 966, - piv: 982, - prE: 10931, - pre: 10927, - psi: 968, - qfr: 120110, - rcy: 1088, - reg: 174, - rfr: 120111, - rho: 961, - rlm: 8207, - rsh: 8625, - scE: 10932, - sce: 10928, - scy: 1089, - sfr: 120112, - shy: 173, - sim: 8764, - smt: 10922, - sol: 47, - squ: 9633, - sub: 8834, - sum: 8721, - sup: 8835, - tau: 964, - tcy: 1090, - tfr: 120113, - top: 8868, - ucy: 1091, - ufr: 120114, - uml: 168, - vcy: 1074, - vee: 8744, - vfr: 120115, - wfr: 120116, - xfr: 120117, - ycy: 1099, - yen: 165, - yfr: 120118, - zcy: 1079, - zfr: 120119, - zwj: 8205, - DD: 8517, - GT: 62, - Gg: 8921, - Gt: 8811, - Im: 8465, - LT: 60, - Ll: 8920, - Lt: 8810, - Mu: 924, - Nu: 925, - Or: 10836, - Pi: 928, - Pr: 10939, - Re: 8476, - Sc: 10940, - Xi: 926, - ac: 8766, - af: 8289, - ap: 8776, - dd: 8518, - ee: 8519, - eg: 10906, - el: 10905, - gE: 8807, - ge: 8805, - gg: 8811, - gl: 8823, - gt: 62, - ic: 8291, - ii: 8520, - in: 8712, - it: 8290, - lE: 8806, - le: 8804, - lg: 8822, - ll: 8810, - lt: 60, - mp: 8723, - mu: 956, - ne: 8800, - ni: 8715, - nu: 957, - oS: 9416, - or: 8744, - pi: 960, - pm: 177, - pr: 8826, - rx: 8478, - sc: 8827, - wp: 8472, - wr: 8768, - xi: 958, -}; diff --git a/src/parser/parse/utils/html.ts b/src/parser/parse/utils/html.ts deleted file mode 100644 index 3b406c9cc..000000000 --- a/src/parser/parse/utils/html.ts +++ /dev/null @@ -1,143 +0,0 @@ -// @ts-nocheck - -import entities from './entities.js'; - -const windows_1252 = [ - 8364, - 129, - 8218, - 402, - 8222, - 8230, - 8224, - 8225, - 710, - 8240, - 352, - 8249, - 338, - 141, - 381, - 143, - 144, - 8216, - 8217, - 8220, - 8221, - 8226, - 8211, - 8212, - 732, - 8482, - 353, - 8250, - 339, - 157, - 382, - 376, -]; - -const entity_pattern = new RegExp(`&(#?(?:x[\\w\\d]+|\\d+|${Object.keys(entities).join('|')}))(?:;|\\b)`, 'g'); - -export function decode_character_references(html: string) { - return html.replace(entity_pattern, (match, entity) => { - let code; - - // Handle named entities - if (entity[0] !== '#') { - code = entities[entity]; - } else if (entity[1] === 'x') { - code = parseInt(entity.substring(2), 16); - } else { - code = parseInt(entity.substring(1), 10); - } - - if (!code) { - return match; - } - - return String.fromCodePoint(validate_code(code)); - }); -} - -const NUL = 0; - -// some code points are verboten. If we were inserting HTML, the browser would replace the illegal -// code points with alternatives in some cases - since we're bypassing that mechanism, we need -// to replace them ourselves -// -// Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters -function validate_code(code: number) { - // line feed becomes generic whitespace - if (code === 10) { - return 32; - } - - // ASCII range. (Why someone would use HTML entities for ASCII characters I don't know, but...) - if (code < 128) { - return code; - } - - // code points 128-159 are dealt with leniently by browsers, but they're incorrect. We need - // to correct the mistake or we'll end up with missing € signs and so on - if (code <= 159) { - return windows_1252[code - 128]; - } - - // basic multilingual plane - if (code < 55296) { - return code; - } - - // UTF-16 surrogate halves - if (code <= 57343) { - return NUL; - } - - // rest of the basic multilingual plane - if (code <= 65535) { - return code; - } - - // supplementary multilingual plane 0x10000 - 0x1ffff - if (code >= 65536 && code <= 131071) { - return code; - } - - // supplementary ideographic plane 0x20000 - 0x2ffff - if (code >= 131072 && code <= 196607) { - return code; - } - - return NUL; -} - -// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission -const disallowed_contents = new Map([ - ['li', new Set(['li'])], - ['dt', new Set(['dt', 'dd'])], - ['dd', new Set(['dt', 'dd'])], - ['p', new Set('address article aside blockquote div dl fieldset footer form h1 h2 h3 h4 h5 h6 header hgroup hr main menu nav ol p pre section table ul'.split(' '))], - ['rt', new Set(['rt', 'rp'])], - ['rp', new Set(['rt', 'rp'])], - ['optgroup', new Set(['optgroup'])], - ['option', new Set(['option', 'optgroup'])], - ['thead', new Set(['tbody', 'tfoot'])], - ['tbody', new Set(['tbody', 'tfoot'])], - ['tfoot', new Set(['tbody'])], - ['tr', new Set(['tr', 'tbody'])], - ['td', new Set(['td', 'th', 'tr'])], - ['th', new Set(['td', 'th', 'tr'])], -]); - -// can this be a child of the parent element, or does it implicitly -// close it, like `<li>one<li>two`? -export function closing_tag_omitted(current: string, next?: string) { - if (disallowed_contents.has(current)) { - if (!next || disallowed_contents.get(current).has(next)) { - return true; - } - } - - return false; -} diff --git a/src/parser/parse/utils/node.ts b/src/parser/parse/utils/node.ts deleted file mode 100644 index 45769f96e..000000000 --- a/src/parser/parse/utils/node.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { TemplateNode } from '../../interfaces.js'; - -export function to_string(node: TemplateNode) { - switch (node.type) { - case 'IfBlock': - return '{#if} block'; - case 'ThenBlock': - return '{:then} block'; - case 'ElseBlock': - return '{:else} block'; - case 'PendingBlock': - case 'AwaitBlock': - return '{#await} block'; - case 'CatchBlock': - return '{:catch} block'; - case 'EachBlock': - return '{#each} block'; - case 'RawMustacheTag': - return '{@html} block'; - case 'DebugTag': - return '{@debug} block'; - case 'Element': - case 'InlineComponent': - case 'Slot': - case 'Title': - return `<${node.name}> tag`; - default: - return node.type; - } -} diff --git a/src/parser/utils/error.ts b/src/parser/utils/error.ts deleted file mode 100644 index 8ebb5b093..000000000 --- a/src/parser/utils/error.ts +++ /dev/null @@ -1,46 +0,0 @@ -// @ts-nocheck - -import { locate } from 'locate-character'; -import get_code_frame from './get_code_frame.js'; - -export class CompileError extends Error { - code: string; - start: { line: number; column: number }; - end: { line: number; column: number }; - pos: number; - filename: string; - frame: string; - - toString() { - return `${this.message} (${this.start.line}:${this.start.column})\n${this.frame}`; - } -} - -/** Throw CompileError */ -export default function error( - message: string, - props: { - name: string; - code: string; - source: string; - filename: string; - start: number; - end?: number; - } -): never { - const err = new CompileError(message); - err.name = props.name; - - const start = locate(props.source, props.start, { offsetLine: 1 }); - const end = locate(props.source, props.end || props.start, { offsetLine: 1 }); - - err.code = props.code; - err.start = start; - err.end = end; - err.pos = props.start; - err.filename = props.filename; - - err.frame = get_code_frame(props.source, start.line - 1, start.column); - - throw err; -} diff --git a/src/parser/utils/full_char_code_at.ts b/src/parser/utils/full_char_code_at.ts deleted file mode 100644 index b62b2c77a..000000000 --- a/src/parser/utils/full_char_code_at.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Adapted from https://github.com/acornjs/acorn/blob/6584815dca7440e00de841d1dad152302fdd7ca5/src/tokenize.js -// Reproduced under MIT License https://github.com/acornjs/acorn/blob/master/LICENSE - -/** @url https://github.com/acornjs/acorn/blob/6584815dca7440e00de841d1dad152302fdd7ca5/src/tokenize.js */ -export default function full_char_code_at(str: string, i: number): number { - const code = str.charCodeAt(i); - if (code <= 0xd7ff || code >= 0xe000) return code; - - const next = str.charCodeAt(i + 1); - return (code << 10) + next - 0x35fdc00; -} diff --git a/src/parser/utils/fuzzymatch.ts b/src/parser/utils/fuzzymatch.ts deleted file mode 100644 index 4d17aafdf..000000000 --- a/src/parser/utils/fuzzymatch.ts +++ /dev/null @@ -1,233 +0,0 @@ -// @ts-nocheck - -/** Utility for accessing FuzzySet */ -export default function fuzzymatch(name: string, names: string[]) { - const set = new FuzzySet(names); - const matches = set.get(name); - - return matches && matches[0] && matches[0][0] > 0.7 ? matches[0][1] : null; -} - -// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js -// BSD Licensed - -const GRAM_SIZE_LOWER = 2; -const GRAM_SIZE_UPPER = 3; - -/** Return an edit distance from 0 to 1 */ -function _distance(str1: string, str2: string) { - if (str1 === null && str2 === null) { - throw 'Trying to compare two null values'; - } - if (str1 === null || str2 === null) return 0; - str1 = String(str1); - str2 = String(str2); - - const distance = levenshtein(str1, str2); - if (str1.length > str2.length) { - return 1 - distance / str1.length; - } else { - return 1 - distance / str2.length; - } -} - -/** @url https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js#L18 */ -function levenshtein(str1: string, str2: string) { - const current: number[] = []; - let prev; - let value; - - for (let i = 0; i <= str2.length; i++) { - for (let j = 0; j <= str1.length; j++) { - if (i && j) { - if (str1.charAt(j - 1) === str2.charAt(i - 1)) { - value = prev; - } else { - value = Math.min(current[j], current[j - 1], prev) + 1; - } - } else { - value = i + j; - } - - prev = current[j]; - current[j] = value; - } - } - - return current.pop(); -} - -const non_word_regex = /[^\w, ]+/; - -/** @url https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js#L53 */ -function iterate_grams(value: string, gram_size = 2) { - const simplified = '-' + value.toLowerCase().replace(non_word_regex, '') + '-'; - const len_diff = gram_size - simplified.length; - const results = []; - - if (len_diff > 0) { - for (let i = 0; i < len_diff; ++i) { - value += '-'; - } - } - for (let i = 0; i < simplified.length - gram_size + 1; ++i) { - results.push(simplified.slice(i, i + gram_size)); - } - return results; -} - -/** @url https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js#L69 */ -function gram_counter(value: string, gram_size = 2) { - // return an object where key=gram, value=number of occurrences - const result = {}; - const grams = iterate_grams(value, gram_size); - let i = 0; - - for (i; i < grams.length; ++i) { - if (grams[i] in result) { - result[grams[i]] += 1; - } else { - result[grams[i]] = 1; - } - } - return result; -} - -/** @url https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js#L158 */ -function sort_descending(a, b) { - return b[0] - a[0]; -} - -class FuzzySet { - exact_set = {}; - match_dict = {}; - items = {}; - - constructor(arr: string[]) { - // initialization - for (let i = GRAM_SIZE_LOWER; i < GRAM_SIZE_UPPER + 1; ++i) { - this.items[i] = []; - } - - // add all the items to the set - for (let i = 0; i < arr.length; ++i) { - this.add(arr[i]); - } - } - - add(value: string) { - const normalized_value = value.toLowerCase(); - if (normalized_value in this.exact_set) { - return false; - } - - let i = GRAM_SIZE_LOWER; - for (i; i < GRAM_SIZE_UPPER + 1; ++i) { - this._add(value, i); - } - } - - _add(value: string, gram_size: number) { - const normalized_value = value.toLowerCase(); - const items = this.items[gram_size] || []; - const index = items.length; - - items.push(0); - const gram_counts = gram_counter(normalized_value, gram_size); - let sum_of_square_gram_counts = 0; - let gram; - let gram_count; - - for (gram in gram_counts) { - gram_count = gram_counts[gram]; - sum_of_square_gram_counts += Math.pow(gram_count, 2); - if (gram in this.match_dict) { - this.match_dict[gram].push([index, gram_count]); - } else { - this.match_dict[gram] = [[index, gram_count]]; - } - } - const vector_normal = Math.sqrt(sum_of_square_gram_counts); - items[index] = [vector_normal, normalized_value]; - this.items[gram_size] = items; - this.exact_set[normalized_value] = value; - } - - get(value: string) { - const normalized_value = value.toLowerCase(); - const result = this.exact_set[normalized_value]; - - if (result) { - return [[1, result]]; - } - - let results = []; - // start with high gram size and if there are no results, go to lower gram sizes - for (let gram_size = GRAM_SIZE_UPPER; gram_size >= GRAM_SIZE_LOWER; --gram_size) { - results = this.__get(value, gram_size); - if (results) { - return results; - } - } - return null; - } - - __get(value: string, gram_size: number) { - const normalized_value = value.toLowerCase(); - const matches = {}; - const gram_counts = gram_counter(normalized_value, gram_size); - const items = this.items[gram_size]; - let sum_of_square_gram_counts = 0; - let gram; - let gram_count; - let i; - let index; - let other_gram_count; - - for (gram in gram_counts) { - gram_count = gram_counts[gram]; - sum_of_square_gram_counts += Math.pow(gram_count, 2); - if (gram in this.match_dict) { - for (i = 0; i < this.match_dict[gram].length; ++i) { - index = this.match_dict[gram][i][0]; - other_gram_count = this.match_dict[gram][i][1]; - if (index in matches) { - matches[index] += gram_count * other_gram_count; - } else { - matches[index] = gram_count * other_gram_count; - } - } - } - } - - const vector_normal = Math.sqrt(sum_of_square_gram_counts); - let results = []; - let match_score; - - // build a results list of [score, str] - for (const match_index in matches) { - match_score = matches[match_index]; - results.push([match_score / (vector_normal * items[match_index][0]), items[match_index][1]]); - } - - results.sort(sort_descending); - - let new_results = []; - const end_index = Math.min(50, results.length); - // truncate somewhat arbitrarily to 50 - for (let j = 0; j < end_index; ++j) { - new_results.push([_distance(results[j][1], normalized_value), results[j][1]]); - } - results = new_results; - results.sort(sort_descending); - - new_results = []; - for (let j = 0; j < results.length; ++j) { - if (results[j][0] == results[0][0]) { - new_results.push([results[j][0], this.exact_set[results[j][1]]]); - } - } - - return new_results; - } -} diff --git a/src/parser/utils/get_code_frame.ts b/src/parser/utils/get_code_frame.ts deleted file mode 100644 index e4f1834fd..000000000 --- a/src/parser/utils/get_code_frame.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** Die you stupid tabs */ -function tabs_to_spaces(str: string) { - return str.replace(/^\t+/, (match) => match.split('\t').join(' ')); -} - -/** Display syntax error in pretty format in logs */ -export default function get_code_frame(source: string, line: number, column: number) { - const lines = source.split('\n'); - - const frame_start = Math.max(0, line - 2); - const frame_end = Math.min(line + 3, lines.length); - - const digits = String(frame_end + 1).length; - - return lines - .slice(frame_start, frame_end) - .map((str, i) => { - const isErrorLine = frame_start + i === line; - const line_num = String(i + frame_start + 1).padStart(digits, ' '); - - if (isErrorLine) { - const indicator = ' '.repeat(digits + 2 + tabs_to_spaces(str.slice(0, column)).length) + '^'; - return `${line_num}: ${tabs_to_spaces(str)}\n${indicator}`; - } - - return `${line_num}: ${tabs_to_spaces(str)}`; - }) - .join('\n'); -} diff --git a/src/parser/utils/link.ts b/src/parser/utils/link.ts deleted file mode 100644 index 4e2ed662f..000000000 --- a/src/parser/utils/link.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** Linked list */ -export function link<T extends { next?: T; prev?: T }>(next: T, prev: T) { - prev.next = next; - if (next) next.prev = prev; -} diff --git a/src/parser/utils/list.ts b/src/parser/utils/list.ts deleted file mode 100644 index 9388adb14..000000000 --- a/src/parser/utils/list.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** Display an array of strings in a human-readable format */ -export default function list(items: string[], conjunction = 'or') { - if (items.length === 1) return items[0]; - return `${items.slice(0, -1).join(', ')} ${conjunction} ${items[items.length - 1]}`; -} diff --git a/src/parser/utils/names.ts b/src/parser/utils/names.ts deleted file mode 100644 index f041d20ce..000000000 --- a/src/parser/utils/names.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { isIdentifierStart, isIdentifierChar } from 'acorn'; -import full_char_code_at from './full_char_code_at.js'; - -export const globals = new Set([ - 'alert', - 'Array', - 'Boolean', - 'clearInterval', - 'clearTimeout', - 'confirm', - 'console', - 'Date', - 'decodeURI', - 'decodeURIComponent', - 'document', - 'Element', - 'encodeURI', - 'encodeURIComponent', - 'Error', - 'EvalError', - 'Event', - 'EventSource', - 'fetch', - 'global', - 'globalThis', - 'history', - 'Infinity', - 'InternalError', - 'Intl', - 'isFinite', - 'isNaN', - 'JSON', - 'localStorage', - 'location', - 'Map', - 'Math', - 'NaN', - 'navigator', - 'Number', - 'Node', - 'Object', - 'parseFloat', - 'parseInt', - 'process', - 'Promise', - 'prompt', - 'RangeError', - 'ReferenceError', - 'RegExp', - 'sessionStorage', - 'Set', - 'setInterval', - 'setTimeout', - 'String', - 'SyntaxError', - 'TypeError', - 'undefined', - 'URIError', - 'URL', - 'window', -]); - -export const reserved = new Set([ - 'arguments', - 'await', - 'break', - 'case', - 'catch', - 'class', - 'const', - 'continue', - 'debugger', - 'default', - 'delete', - 'do', - 'else', - 'enum', - 'eval', - 'export', - 'extends', - 'false', - 'finally', - 'for', - 'function', - 'if', - 'implements', - 'import', - 'in', - 'instanceof', - 'interface', - 'let', - 'new', - 'null', - 'package', - 'private', - 'protected', - 'public', - 'return', - 'static', - 'super', - 'switch', - 'this', - 'throw', - 'true', - 'try', - 'typeof', - 'var', - 'void', - 'while', - 'with', - 'yield', -]); - -const void_element_names = /^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/; - -/** Is this a void HTML element? */ -export function is_void(name: string) { - return void_element_names.test(name) || name.toLowerCase() === '!doctype'; -} - -/** Is this a valid HTML element? */ -export function is_valid(str: string): boolean { - let i = 0; - - while (i < str.length) { - const code = full_char_code_at(str, i); - if (!(i === 0 ? isIdentifierStart : isIdentifierChar)(code, true)) return false; - - i += code <= 0xffff ? 1 : 2; - } - - return true; -} - -/** Utility to normalize HTML */ -export function sanitize(name: string) { - return name - .replace(/[^a-zA-Z0-9_]+/g, '_') - .replace(/^_/, '') - .replace(/_$/, '') - .replace(/^[0-9]/, '_$&'); -} diff --git a/src/parser/utils/namespaces.ts b/src/parser/utils/namespaces.ts deleted file mode 100644 index 5f61beff9..000000000 --- a/src/parser/utils/namespaces.ts +++ /dev/null @@ -1,13 +0,0 @@ -// The `foreign` namespace covers all DOM implementations that aren't HTML5. -// It opts out of HTML5-specific a11y checks and case-insensitive attribute names. -export const foreign = 'https://svelte.dev/docs#svelte_options'; -export const html = 'http://www.w3.org/1999/xhtml'; -export const mathml = 'http://www.w3.org/1998/Math/MathML'; -export const svg = 'http://www.w3.org/2000/svg'; -export const xlink = 'http://www.w3.org/1999/xlink'; -export const xml = 'http://www.w3.org/XML/1998/namespace'; -export const xmlns = 'http://www.w3.org/2000/xmlns'; - -export const valid_namespaces = ['foreign', 'html', 'mathml', 'svg', 'xlink', 'xml', 'xmlns', foreign, html, mathml, svg, xlink, xml, xmlns]; - -export const namespaces: Record<string, string> = { foreign, html, mathml, svg, xlink, xml, xmlns }; diff --git a/src/parser/utils/nodes_match.ts b/src/parser/utils/nodes_match.ts deleted file mode 100644 index 7e4093994..000000000 --- a/src/parser/utils/nodes_match.ts +++ /dev/null @@ -1,35 +0,0 @@ -// @ts-nocheck - -/** Compare two TemplateNodes to determine if they are equivalent */ -export function nodes_match(a, b) { - if (!!a !== !!b) return false; - if (Array.isArray(a) !== Array.isArray(b)) return false; - - if (a && typeof a === 'object') { - if (Array.isArray(a)) { - if (a.length !== b.length) return false; - return a.every((child, i) => nodes_match(child, b[i])); - } - - const a_keys = Object.keys(a).sort(); - const b_keys = Object.keys(b).sort(); - - if (a_keys.length !== b_keys.length) return false; - - let i = a_keys.length; - while (i--) { - const key = a_keys[i]; - if (b_keys[i] !== key) return false; - - if (key === 'start' || key === 'end') continue; - - if (!nodes_match(a[key], b[key])) { - return false; - } - } - - return true; - } - - return a === b; -} diff --git a/src/parser/utils/patterns.ts b/src/parser/utils/patterns.ts deleted file mode 100644 index 317a7c199..000000000 --- a/src/parser/utils/patterns.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const whitespace = /[ \t\r\n]/; - -export const dimensions = /^(?:offset|client)(?:Width|Height)$/; diff --git a/src/parser/utils/trim.ts b/src/parser/utils/trim.ts deleted file mode 100644 index 480cc99a8..000000000 --- a/src/parser/utils/trim.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { whitespace } from './patterns.js'; - -/** Trim whitespace from start of string */ -export function trim_start(str: string) { - let i = 0; - while (whitespace.test(str[i])) i += 1; - - return str.slice(i); -} - -/** Trim whitespace from end of string */ -export function trim_end(str: string) { - let i = str.length; - while (whitespace.test(str[i - 1])) i -= 1; - - return str.slice(0, i); -} diff --git a/src/runtime.ts b/src/runtime.ts deleted file mode 100644 index 66a0657cf..000000000 --- a/src/runtime.ts +++ /dev/null @@ -1,365 +0,0 @@ -import { fileURLToPath } from 'url'; -import type { SnowpackDevServer, ServerRuntime as SnowpackServerRuntime, SnowpackConfig } from 'snowpack'; -import type { AstroConfig, CollectionResult, CollectionRSS, CreateCollection, Params, RuntimeMode } from './@types/astro'; -import type { LogOptions } from './logger'; -import type { CompileError } from './parser/utils/error.js'; -import { debug, info } from './logger.js'; -import { searchForPage } from './search.js'; - -import { existsSync } from 'fs'; -import { loadConfiguration, logger as snowpackLogger, startServer as startSnowpackServer } from 'snowpack'; - -// We need to use require.resolve for snowpack plugins, so create a require function here. -import { createRequire } from 'module'; -const require = createRequire(import.meta.url); - -interface RuntimeConfig { - astroConfig: AstroConfig; - logging: LogOptions; - mode: RuntimeMode; - backendSnowpack: SnowpackDevServer; - backendSnowpackRuntime: SnowpackServerRuntime; - backendSnowpackConfig: SnowpackConfig; - frontendSnowpack: SnowpackDevServer; - frontendSnowpackRuntime: SnowpackServerRuntime; - frontendSnowpackConfig: SnowpackConfig; -} - -// info needed for collection generation -interface CollectionInfo { - additionalURLs: Set<string>; - rss?: { data: any[] & CollectionRSS }; -} - -type LoadResultSuccess = { - statusCode: 200; - contents: string | Buffer; - contentType?: string | false; -}; -type LoadResultNotFound = { statusCode: 404; error: Error; collectionInfo?: CollectionInfo }; -type LoadResultRedirect = { statusCode: 301 | 302; location: string; collectionInfo?: CollectionInfo }; -type LoadResultError = { statusCode: 500 } & ({ type: 'parse-error'; error: CompileError } | { type: 'unknown'; error: Error }); - -export type LoadResult = (LoadResultSuccess | LoadResultNotFound | LoadResultRedirect | LoadResultError) & { collectionInfo?: CollectionInfo }; - -// Disable snowpack from writing to stdout/err. -snowpackLogger.level = 'silent'; - -/** Pass a URL to Astro to resolve and build */ -async function load(config: RuntimeConfig, rawPathname: string | undefined): Promise<LoadResult> { - const { logging, backendSnowpackRuntime, frontendSnowpack } = config; - const { astroRoot } = config.astroConfig; - - const fullurl = new URL(rawPathname || '/', 'https://example.org/'); - - const reqPath = decodeURI(fullurl.pathname); - info(logging, 'access', reqPath); - - const searchResult = searchForPage(fullurl, astroRoot); - if (searchResult.statusCode === 404) { - try { - const result = await frontendSnowpack.loadUrl(reqPath); - if (!result) throw new Error(`Unable to load ${reqPath}`); - // success - return { - statusCode: 200, - ...result, - }; - } catch (err) { - // build error - if (err.failed) { - return { statusCode: 500, type: 'unknown', error: err }; - } - - // not found - return { statusCode: 404, error: err }; - } - } - - if (searchResult.statusCode === 301) { - return { statusCode: 301, location: searchResult.pathname }; - } - - const snowpackURL = searchResult.location.snowpackURL; - let rss: { data: any[] & CollectionRSS } = {} as any; - - try { - const mod = await backendSnowpackRuntime.importModule(snowpackURL); - debug(logging, 'resolve', `${reqPath} -> ${snowpackURL}`); - - // handle collection - let collection = {} as CollectionResult; - let additionalURLs = new Set<string>(); - - if (mod.exports.createCollection) { - const createCollection: CreateCollection = await mod.exports.createCollection(); - for (const key of Object.keys(createCollection)) { - if (key !== 'data' && key !== 'routes' && key !== 'permalink' && key !== 'pageSize' && key !== 'rss') { - throw new Error(`[createCollection] unknown option: "${key}"`); - } - } - let { data: loadData, routes, permalink, pageSize, rss: createRSS } = createCollection; - if (!pageSize) pageSize = 25; // can’t be 0 - let currentParams: Params = {}; - - // params - if (routes || permalink) { - if (!routes || !permalink) { - throw new Error('createCollection() must have both routes and permalink options. Include both together, or omit both.'); - } - let requestedParams = routes.find((p) => { - const baseURL = (permalink as any)({ params: p }); - additionalURLs.add(baseURL); - return baseURL === reqPath || `${baseURL}/${searchResult.currentPage || 1}` === reqPath; - }); - if (requestedParams) { - currentParams = requestedParams; - collection.params = requestedParams; - } - } - - let data: any[] = await loadData({ params: currentParams }); - - // handle RSS - if (createRSS) { - rss = { - ...createRSS, - data: [...data] as any, - }; - } - - collection.start = 0; - collection.end = data.length - 1; - collection.total = data.length; - collection.page = { current: 1, size: pageSize, last: 1 }; - collection.url = { current: reqPath }; - - // paginate - if (searchResult.currentPage) { - const start = (searchResult.currentPage - 1) * pageSize; // currentPage is 1-indexed - const end = Math.min(start + pageSize, data.length); - - collection.start = start; - collection.end = end - 1; - collection.page.current = searchResult.currentPage; - collection.page.last = Math.ceil(data.length / pageSize); - // TODO: fix the .replace() hack - if (end < data.length) { - collection.url.next = collection.url.current.replace(/(\/\d+)?$/, `/${searchResult.currentPage + 1}`); - } - if (searchResult.currentPage > 1) { - collection.url.prev = collection.url.current - .replace(/\d+$/, `${searchResult.currentPage - 1 || 1}`) // update page # - .replace(/\/1$/, ''); // if end is `/1`, then just omit - } - - // from page 2 to the end, add all pages as additional URLs (needed for build) - for (let n = 1; n <= collection.page.last; n++) { - if (additionalURLs.size) { - // if this is a param-based collection, paginate all params - additionalURLs.forEach((url) => { - additionalURLs.add(url.replace(/(\/\d+)?$/, `/${n}`)); - }); - } else { - // if this has no params, simply add page - additionalURLs.add(reqPath.replace(/(\/\d+)?$/, `/${n}`)); - } - } - - data = data.slice(start, end); - } else if (createCollection.pageSize) { - // TODO: fix bug where redirect doesn’t happen - // This happens because a pageSize is set, but the user isn’t on a paginated route. Redirect: - return { - statusCode: 301, - location: reqPath + '/1', - collectionInfo: { - additionalURLs, - rss: rss.data ? rss : undefined, - }, - }; - } - - // if we’ve paginated too far, this is a 404 - if (!data.length) { - return { - statusCode: 404, - error: new Error('Not Found'), - collectionInfo: { - additionalURLs, - rss: rss.data ? rss : undefined, - }, - }; - } - - collection.data = data; - } - - const requestURL = new URL(fullurl.toString()); - - // For first release query params are not passed to components. - // An exception is made for dev server specific routes. - if(reqPath !== '/500') { - requestURL.search = ''; - } - - let html = (await mod.exports.__renderPage({ - request: { - // params should go here when implemented - url: requestURL - }, - children: [], - props: { collection }, - })) as string; - - // inject styles - // TODO: handle this in compiler - const styleTags = Array.isArray(mod.css) && mod.css.length ? mod.css.reduce((markup, href) => `${markup}\n<link rel="stylesheet" type="text/css" href="${href}" />`, '') : ``; - if (html.indexOf('</head>') !== -1) { - html = html.replace('</head>', `${styleTags}</head>`); - } else { - html = styleTags + html; - } - - return { - statusCode: 200, - contentType: 'text/html; charset=utf-8', - contents: html, - collectionInfo: { - additionalURLs, - rss: rss.data ? rss : undefined, - }, - }; - } catch (err) { - if (err.code === 'parse-error' || err instanceof SyntaxError) { - return { - statusCode: 500, - type: 'parse-error', - error: err, - }; - } - return { - statusCode: 500, - type: 'unknown', - error: err, - }; - } -} - -export interface AstroRuntime { - runtimeConfig: RuntimeConfig; - load: (rawPathname: string | undefined) => Promise<LoadResult>; - shutdown: () => Promise<void>; -} - -interface RuntimeOptions { - mode: RuntimeMode; - logging: LogOptions; -} - -interface CreateSnowpackOptions { - env: Record<string, any>; - mode: RuntimeMode; - resolvePackageUrl?: (pkgName: string) => Promise<string>; -} - -/** Create a new Snowpack instance to power Astro */ -async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackOptions) { - const { projectRoot, astroRoot, extensions } = astroConfig; - const { env, mode, resolvePackageUrl } = options; - - const internalPath = new URL('./frontend/', import.meta.url); - - let snowpack: SnowpackDevServer; - const astroPlugOptions: { - resolvePackageUrl?: (s: string) => Promise<string>; - extensions?: Record<string, string>; - astroConfig: AstroConfig; - } = { - astroConfig, - extensions, - resolvePackageUrl, - }; - - const mountOptions = { - [fileURLToPath(astroRoot)]: '/_astro', - [fileURLToPath(internalPath)]: '/_astro_internal', - }; - - if (existsSync(astroConfig.public)) { - mountOptions[fileURLToPath(astroConfig.public)] = '/'; - } - - const snowpackConfig = await loadConfiguration({ - root: fileURLToPath(projectRoot), - mount: mountOptions, - mode, - plugins: [ - [fileURLToPath(new URL('../snowpack-plugin.cjs', import.meta.url)), astroPlugOptions], - require.resolve('@snowpack/plugin-sass'), - [require.resolve('@snowpack/plugin-svelte'), { compilerOptions: { hydratable: true } }], - require.resolve('@snowpack/plugin-vue'), - ], - devOptions: { - open: 'none', - output: 'stream', - port: 0, - }, - buildOptions: { - out: astroConfig.dist, - }, - packageOptions: { - knownEntrypoints: ['preact-render-to-string'], - external: ['@vue/server-renderer', 'node-fetch', 'prismjs/components/index.js'], - }, - }); - - const envConfig = snowpackConfig.env || (snowpackConfig.env = {}); - Object.assign(envConfig, env); - - snowpack = await startSnowpackServer({ - config: snowpackConfig, - lockfile: null, - }); - const snowpackRuntime = snowpack.getServerRuntime(); - - return { snowpack, snowpackRuntime, snowpackConfig }; -} - -/** Core Astro runtime */ -export async function createRuntime(astroConfig: AstroConfig, { mode, logging }: RuntimeOptions): Promise<AstroRuntime> { - const resolvePackageUrl = async (pkgName: string) => frontendSnowpack.getUrlForPackage(pkgName); - - const { snowpack: backendSnowpack, snowpackRuntime: backendSnowpackRuntime, snowpackConfig: backendSnowpackConfig } = await createSnowpack(astroConfig, { - env: { - astro: true, - }, - mode, - resolvePackageUrl, - }); - - const { snowpack: frontendSnowpack, snowpackRuntime: frontendSnowpackRuntime, snowpackConfig: frontendSnowpackConfig } = await createSnowpack(astroConfig, { - env: { - astro: false, - }, - mode, - }); - - const runtimeConfig: RuntimeConfig = { - astroConfig, - logging, - mode, - backendSnowpack, - backendSnowpackRuntime, - backendSnowpackConfig, - frontendSnowpack, - frontendSnowpackRuntime, - frontendSnowpackConfig, - }; - - return { - runtimeConfig, - load: load.bind(null, runtimeConfig), - shutdown: () => Promise.all([backendSnowpack.shutdown(), frontendSnowpack.shutdown()]).then(() => void 0), - }; -} diff --git a/src/search.ts b/src/search.ts deleted file mode 100644 index c141e4a77..000000000 --- a/src/search.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { existsSync } from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { fdir, PathsOutput } from 'fdir'; - -interface PageLocation { - fileURL: URL; - snowpackURL: string; -} -/** findAnyPage and return the _astro candidate for snowpack */ -function findAnyPage(candidates: Array<string>, astroRoot: URL): PageLocation | false { - for (let candidate of candidates) { - const url = new URL(`./pages/${candidate}`, astroRoot); - if (existsSync(url)) { - return { - fileURL: url, - snowpackURL: `/_astro/pages/${candidate}.js`, - }; - } - } - return false; -} - -type SearchResult = - | { - statusCode: 200; - location: PageLocation; - pathname: string; - currentPage?: number; - } - | { - statusCode: 301; - location: null; - pathname: string; - } - | { - statusCode: 404; - }; - -/** Given a URL, attempt to locate its source file (similar to Snowpack’s load()) */ -export function searchForPage(url: URL, astroRoot: URL): SearchResult { - const reqPath = decodeURI(url.pathname); - const base = reqPath.substr(1); - - // Try to find index.astro/md paths - if (reqPath.endsWith('/')) { - const candidates = [`${base}index.astro`, `${base}index.md`]; - const location = findAnyPage(candidates, astroRoot); - if (location) { - return { - statusCode: 200, - location, - pathname: reqPath, - }; - } - } else { - // Try to find the page by its name. - const candidates = [`${base}.astro`, `${base}.md`]; - let location = findAnyPage(candidates, astroRoot); - if (location) { - return { - statusCode: 200, - location, - pathname: reqPath, - }; - } - } - - // Try to find name/index.astro/md - const candidates = [`${base}/index.astro`, `${base}/index.md`]; - const location = findAnyPage(candidates, astroRoot); - if (location) { - return { - statusCode: 301, - location: null, - pathname: reqPath + '/', - }; - } - - // Try and load collections (but only for non-extension files) - const hasExt = !!path.extname(reqPath); - if (!location && !hasExt) { - const collection = loadCollection(reqPath, astroRoot); - if (collection) { - return { - statusCode: 200, - location: collection.location, - pathname: reqPath, - currentPage: collection.currentPage || 1, - }; - } - } - - if(reqPath === '/500') { - return { - statusCode: 200, - location: { - fileURL: new URL('./frontend/500.astro', import.meta.url), - snowpackURL: `/_astro_internal/500.astro.js` - }, - pathname: reqPath - }; - } - - return { - statusCode: 404, - }; -} - -const crawler = new fdir(); - -/** load a collection route */ -function loadCollection(url: string, astroRoot: URL): { currentPage?: number; location: PageLocation } | undefined { - const pages = (crawler - .glob('**/*') - .crawl(path.join(fileURLToPath(astroRoot), 'pages')) - .sync() as PathsOutput).filter((filepath) => filepath.startsWith('$') || filepath.includes('/$')); - for (const pageURL of pages) { - const reqURL = new RegExp('^/' + pageURL.replace(/\$([^/]+)\.astro/, '$1') + '/?(.*)'); - const match = url.match(reqURL); - if (match) { - let currentPage: number | undefined; - if (match[1]) { - const segments = match[1].split('/').filter((s) => !!s); - if (segments.length) { - const last = segments.pop() as string; - if (parseInt(last, 10)) { - currentPage = parseInt(last, 10); - } - } - } - return { - location: { - fileURL: new URL(`./pages/${pageURL}`, astroRoot), - snowpackURL: `/_astro/pages/${pageURL}.js`, - }, - currentPage, - }; - } - } -} |