diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..d6b8f1b2f --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +src/parser/parse/**/*.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 1072b1e9f..e0c03858c 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -6,11 +6,12 @@ module.exports = { '@typescript-eslint/ban-ts-comment': 'warn', '@typescript-eslint/camelcase': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': 'off', '@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/no-var-requires': 'off', - '@typescript-eslint/no-empty-function': 'off', - 'no-shadow': 'warn', - 'prettier/prettier': 'error', + 'no-shadow': 'error', 'prefer-const': 'off', 'prefer-rest-params': 'off', 'require-jsdoc': 'warn', diff --git a/src/ast.ts b/src/ast.ts index 6c0bd7bd2..88178d399 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -2,11 +2,13 @@ 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) { @@ -14,6 +16,7 @@ export function getAttrValue(attributes: Attribute[], name: string): string | un } } +/** Set TemplateNode attribute value */ export function setAttrValue(attributes: Attribute[], name: string, value: string): void { const attr = attributes.find((a) => a.name === name); if (attr) { diff --git a/src/build.ts b/src/build.ts index 39b778600..831d0be0e 100644 --- a/src/build.ts +++ b/src/build.ts @@ -17,6 +17,7 @@ const logging: LogOptions = { dest: defaultLogDestination, }; +/** Return contents of astro/pages */ async function allPages(root: URL) { const api = new fdir() .filter((p) => /\.(astro|md)$/.test(p)) @@ -26,6 +27,7 @@ async function allPages(root: URL) { return files as string[]; } +/** Utility for merging two Set()s */ function mergeSet(a: Set, b: Set) { for (let str of b) { a.add(str); @@ -33,12 +35,14 @@ function mergeSet(a: Set, b: Set) { return a; } +/** Utility for writing to file (async) */ async function writeFilep(outPath: URL, bytes: string | Buffer, encoding: 'utf-8' | 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 | 'utf-8') { if (result.statusCode !== 200) { error(logging, 'build', result.error || result.statusCode); @@ -49,6 +53,7 @@ async function writeResult(result: LoadResult, outPath: URL, encoding: null | 'u } } +/** The primary build action */ export async function build(astroConfig: AstroConfig): Promise<0 | 1> { const { projectRoot, astroRoot } = astroConfig; const pageRoot = new URL('./pages/', astroRoot); diff --git a/src/build/bundle.ts b/src/build/bundle.ts index 143be0521..ba1b8f2c2 100644 --- a/src/build/bundle.ts +++ b/src/build/bundle.ts @@ -8,7 +8,7 @@ import esbuild from 'esbuild'; import { promises as fsPromises } from 'fs'; import { parse } from '../parser/index.js'; import { optimize } from '../compiler/optimize/index.js'; -import { getAttrValue, setAttrValue } from '../ast.js'; +import { getAttrValue } from '../ast.js'; import { walk } from 'estree-walker'; import babelParser from '@babel/parser'; import path from 'path'; @@ -20,6 +20,7 @@ const { readFile } = fsPromises; type DynamicImportMap = Map<'vue' | 'react' | 'react-dom' | 'preact', string>; +/** Add framework runtimes when needed */ async function acquireDynamicComponentImports(plugins: Set, resolve: (s: string) => Promise): Promise { const importMap: DynamicImportMap = new Map(); for (let plugin of plugins) { @@ -42,6 +43,7 @@ async function acquireDynamicComponentImports(plugins: Set(); @@ -127,7 +130,8 @@ export async function collectDynamicImports(filename: URL, { astroConfig, loggin const dynamic = await acquireDynamicComponentImports(plugins, resolve); - function appendImports(rawName: string, filename: URL, astroConfig: AstroConfig) { + /** Add dynamic component runtimes to imports */ + function appendImports(rawName: string, importUrl: URL) { const [componentName, componentType] = rawName.split(':'); if (!componentType) { return; @@ -138,7 +142,7 @@ export async function collectDynamicImports(filename: URL, { astroConfig, loggin } const defn = components[componentName]; - const fileUrl = new URL(defn.specifier, filename); + const fileUrl = new URL(defn.specifier, importUrl); let rel = path.posix.relative(astroConfig.astroRoot.pathname, fileUrl.pathname); switch (defn.plugin) { @@ -193,15 +197,15 @@ export async function collectDynamicImports(filename: URL, { astroConfig, loggin while ((match = regex.exec(code))) { matches.push(match); } - for (const match of matches.reverse()) { - const name = match[1]; - appendImports(name, filename, astroConfig); + 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, astroConfig); + appendImports(node.name, filename); return; } @@ -220,6 +224,7 @@ interface BundleOptions { astroConfig: AstroConfig; } +/** The primary bundling/optimization action */ export async function bundle(imports: Set, { runtime, dist }: BundleOptions) { const ROOT = 'astro:root'; const root = ` diff --git a/src/build/static.ts b/src/build/static.ts index a10f4c33f..b24ce7729 100644 --- a/src/build/static.ts +++ b/src/build/static.ts @@ -1,6 +1,7 @@ import type { Element } from 'domhandler'; import cheerio from 'cheerio'; +/** Given an HTML string, collect and tags */ export function collectStatics(html: string) { const statics = new Set(); diff --git a/src/cli.ts b/src/cli.ts index d74656615..e0f0c0dc3 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,6 +17,7 @@ const buildAndExit = async (...args: Parameters) => { type Arguments = yargs.Arguments; type cliState = 'help' | 'version' | 'dev' | 'build'; +/** Determine which action the user requested */ function resolveArgs(flags: Arguments): cliState { if (flags.version) { return 'version'; @@ -35,6 +36,7 @@ function resolveArgs(flags: Arguments): cliState { } } +/** Display --help flag */ function printHelp() { console.error(` ${colors.bold('astro')} - Futuristic web development tool. @@ -48,11 +50,13 @@ function printHelp() { `); } +/** 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); } +/** Handle `astro run` command */ async function runCommand(rawRoot: string, cmd: (a: AstroConfig) => Promise) { const astroConfig = await loadConfig(rawRoot); if (typeof astroConfig === 'undefined') { @@ -69,6 +73,7 @@ const cmdMap = new Map([ ['dev', devServer], ]); +/** The primary CLI action */ export async function cli(args: string[]) { const flags = yargs(args); const state = resolveArgs(flags); diff --git a/src/compiler/codegen.ts b/src/compiler/codegen.ts index 63fc44dfb..2486ef92f 100644 --- a/src/compiler/codegen.ts +++ b/src/compiler/codegen.ts @@ -30,10 +30,12 @@ interface CodeGenOptions { fileID: string; } +/** Format Astro internal import URL */ function internalImport(internalPath: string) { return `/_astro_internal/${internalPath}`; } +/** Retrieve attributes from TemplateNode */ function getAttributes(attrs: Attribute[]): Record { let result: Record = {}; for (const attr of attrs) { @@ -79,6 +81,7 @@ function getAttributes(attrs: Attribute[]): Record { return result; } +/** Get value from a TemplateNode Attribute (text attributes only!) */ function getTextFromAttribute(attr: any): string { if (attr.raw !== undefined) { return attr.raw; @@ -89,6 +92,7 @@ function getTextFromAttribute(attr: any): string { throw new Error('UNKNOWN attr'); } +/** Convert TemplateNode attributes to string */ function generateAttributes(attrs: Record): string { let result = '{'; for (const [key, val] of Object.entries(attrs)) { @@ -117,6 +121,8 @@ interface GetComponentWrapperOptions { 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; @@ -222,6 +228,7 @@ function getComponentWrapper(_name: string, { type, plugin, url }: ComponentInfo } } +/** Evaluate mustache expression (safely) */ function compileExpressionSafe(raw: string): string { let { code } = transformSync(raw, { loader: 'tsx', @@ -232,6 +239,7 @@ function compileExpressionSafe(raw: string): string { return code; } +/** Build dependency map of dynamic component runtime frameworks */ async function acquireDynamicComponentImports(plugins: Set, resolve: (s: string) => Promise): Promise { const importMap: DynamicImportMap = new Map(); for (let plugin of plugins) { @@ -254,7 +262,15 @@ async function acquireDynamicComponentImports(plugins: Set { +/** + * 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 { const { extensions = defaultExtensions, astroConfig } = compileOptions; await eslexer.init; @@ -364,8 +380,8 @@ export async function codegen(ast: Ast, { compileOptions, filename, fileID }: Co while ((match = regex.exec(code))) { matches.push(match); } - for (const match of matches.reverse()) { - const name = match[1]; + for (const astroComponent of matches.reverse()) { + const name = astroComponent[1]; const [componentName, componentKind] = name.split(':'); if (!components[componentName]) { throw new Error(`Unknown Component: ${componentName}`); @@ -375,7 +391,7 @@ export async function codegen(ast: Ast, { compileOptions, filename, fileID }: Co importExportStatements.add(wrapperImport); } if (wrapper !== name) { - code = code.slice(0, match.index + 2) + wrapper + code.slice(match.index + match[0].length - 1); + code = code.slice(0, astroComponent.index + 2) + wrapper + code.slice(astroComponent.index + astroComponent[0].length - 1); } } collectionItem!.jsx += `,(${code.trim().replace(/\;$/, '')})`; diff --git a/src/compiler/index.ts b/src/compiler/index.ts index 88ea5caf9..541bae21e 100644 --- a/src/compiler/index.ts +++ b/src/compiler/index.ts @@ -13,6 +13,7 @@ import { encodeMarkdown } from '../micromark-encode.js'; import { optimize } from './optimize/index.js'; import { codegen } from './codegen.js'; +/** Return Astro internal import URL */ function internalImport(internalPath: string) { return `/_astro_internal/${internalPath}`; } @@ -23,6 +24,13 @@ interface ConvertAstroOptions { fileID: string; } +/** + * .astro -> .jsx + * Core function processing .astro files. Initiates all 3 phases of compilation: + * 1. Parse + * 2. Optimize + * 3. Codegen + */ async function convertAstroToJsx(template: string, opts: ConvertAstroOptions): Promise { const { filename } = opts; @@ -34,10 +42,14 @@ async function convertAstroToJsx(template: string, opts: ConvertAstroOptions): P // 2. Optimize the AST await optimize(ast, opts); - // Turn AST into JSX + // 3. Turn AST into JSX return await codegen(ast, opts); } +/** + * .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 } @@ -80,6 +92,7 @@ async function convertMdToJsx( 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 } @@ -95,6 +108,7 @@ async function transformFromSource( } } +/** Return internal code that gets processed in Snowpack */ export async function compileComponent( source: string, { compileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string } diff --git a/src/compiler/optimize/doctype.ts b/src/compiler/optimize/doctype.ts index fdf6c4078..176880c08 100644 --- a/src/compiler/optimize/doctype.ts +++ b/src/compiler/optimize/doctype.ts @@ -1,5 +1,6 @@ import { Optimizer } from '../../@types/optimizer'; +/** Optimize tg */ export default function (_opts: { filename: string; fileID: string }): Optimizer { let hasDoctype = false; diff --git a/src/compiler/optimize/index.ts b/src/compiler/optimize/index.ts index 53dd3f2d6..a7bf828e0 100644 --- a/src/compiler/optimize/index.ts +++ b/src/compiler/optimize/index.ts @@ -13,6 +13,7 @@ interface VisitorCollection { leave: Map; } +/** 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(); @@ -22,6 +23,7 @@ function addVisitor(visitor: NodeVisitor, collection: VisitorCollection, nodeNam collection[event].set(nodeName, visitors); } +/** Compile visitor actions from optimizer */ function collectVisitors(optimizer: Optimizer, htmlVisitors: VisitorCollection, cssVisitors: VisitorCollection, finalizers: Array<() => Promise>) { if (optimizer.visitors) { if (optimizer.visitors.html) { @@ -40,6 +42,7 @@ function collectVisitors(optimizer: Optimizer, htmlVisitors: VisitorCollection, finalizers.push(optimizer.finalize); } +/** Utility for formatting visitors */ function createVisitorCollection() { return { enter: new Map(), @@ -47,6 +50,7 @@ function createVisitorCollection() { }; } +/** Walk AST with collected visitors */ function walkAstWithVisitors(tmpl: TemplateNode, collection: VisitorCollection) { walk(tmpl, { enter(node, parent, key, index) { @@ -68,6 +72,12 @@ function walkAstWithVisitors(tmpl: TemplateNode, collection: VisitorCollection) }); } +/** + * Optimize + * Step 2/3 in Astro SSR. + * Optimize 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 optimize(ast: Ast, opts: OptimizeOptions) { const htmlVisitors = createVisitorCollection(); const cssVisitors = createVisitorCollection(); diff --git a/src/compiler/optimize/module-scripts.ts b/src/compiler/optimize/module-scripts.ts index 713747fcb..9d4949215 100644 --- a/src/compiler/optimize/module-scripts.ts +++ b/src/compiler/optimize/module-scripts.ts @@ -4,6 +4,7 @@ import type { CompileOptions } from '../../@types/compiler'; import path from 'path'; import { getAttrValue, setAttrValue } from '../../ast.js'; +/** Optimize