parent
f6a7ac67be
commit
c26c244ca2
32 changed files with 156 additions and 49 deletions
1
.eslintignore
Normal file
1
.eslintignore
Normal file
|
@ -0,0 +1 @@
|
|||
src/parser/parse/**/*.ts
|
|
@ -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',
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<string>, b: Set<string>) {
|
||||
for (let str of b) {
|
||||
a.add(str);
|
||||
|
@ -33,12 +35,14 @@ function mergeSet(a: Set<string>, b: Set<string>) {
|
|||
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);
|
||||
|
|
|
@ -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<ValidExtensionPlugins>, resolve: (s: string) => Promise<string>): Promise<DynamicImportMap> {
|
||||
const importMap: DynamicImportMap = new Map();
|
||||
for (let plugin of plugins) {
|
||||
|
@ -42,6 +43,7 @@ async function acquireDynamicComponentImports(plugins: Set<ValidExtensionPlugins
|
|||
return importMap;
|
||||
}
|
||||
|
||||
/** Evaluate mustache expression (safely) */
|
||||
function compileExpressionSafe(raw: string): string {
|
||||
let { code } = transformSync(raw, {
|
||||
loader: 'tsx',
|
||||
|
@ -65,6 +67,7 @@ interface CollectDynamic {
|
|||
mode: RuntimeMode;
|
||||
}
|
||||
|
||||
/** Gather necessary framework runtimes for dynamic components */
|
||||
export async function collectDynamicImports(filename: URL, { astroConfig, logging, resolve, mode }: CollectDynamic) {
|
||||
const imports = new Set<string>();
|
||||
|
||||
|
@ -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<string>, { runtime, dist }: BundleOptions) {
|
||||
const ROOT = 'astro:root';
|
||||
const root = `
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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>();
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ const buildAndExit = async (...args: Parameters<typeof build>) => {
|
|||
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<void>) {
|
||||
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);
|
||||
|
|
|
@ -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<string, string> {
|
||||
let result: Record<string, string> = {};
|
||||
for (const attr of attrs) {
|
||||
|
@ -79,6 +81,7 @@ function getAttributes(attrs: Attribute[]): Record<string, string> {
|
|||
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, string>): 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<ValidExtensionPlugins>, resolve: (s: string) => Promise<string>): Promise<DynamicImportMap> {
|
||||
const importMap: DynamicImportMap = new Map();
|
||||
for (let plugin of plugins) {
|
||||
|
@ -254,7 +262,15 @@ async function acquireDynamicComponentImports(plugins: Set<ValidExtensionPlugins
|
|||
return importMap;
|
||||
}
|
||||
|
||||
export async function codegen(ast: Ast, { compileOptions, filename, fileID }: CodeGenOptions): Promise<TransformResult> {
|
||||
/**
|
||||
* 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> {
|
||||
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(/\;$/, '')})`;
|
||||
|
|
|
@ -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<TransformResult> {
|
||||
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 }
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Optimizer } from '../../@types/optimizer';
|
||||
|
||||
/** Optimize <!doctype> tg */
|
||||
export default function (_opts: { filename: string; fileID: string }): Optimizer {
|
||||
let hasDoctype = false;
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ interface VisitorCollection {
|
|||
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[]>();
|
||||
|
@ -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<void>>) {
|
||||
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<string, VisitorFn[]>(),
|
||||
|
@ -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();
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { CompileOptions } from '../../@types/compiler';
|
|||
import path from 'path';
|
||||
import { getAttrValue, setAttrValue } from '../../ast.js';
|
||||
|
||||
/** Optimize <script type="module"> */
|
||||
export default function ({ compileOptions, filename }: { compileOptions: CompileOptions; filename: string; fileID: string }): Optimizer {
|
||||
const { astroConfig } = compileOptions;
|
||||
const { astroRoot } = astroConfig;
|
||||
|
|
|
@ -24,15 +24,17 @@ export function scopeSelectors(selector: string, className: string) {
|
|||
let ss = selector; // final output
|
||||
|
||||
// Pass 1: parse selector string; extract top-level selectors
|
||||
let start = 0;
|
||||
let lastValue = '';
|
||||
for (let n = 0; n < ss.length; n++) {
|
||||
const isEnd = n === selector.length - 1;
|
||||
if (isEnd || 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;
|
||||
{
|
||||
let start = 0;
|
||||
let lastValue = '';
|
||||
for (let n = 0; n < ss.length; n++) {
|
||||
const isEnd = n === selector.length - 1;
|
||||
if (isEnd || 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ async function transformStyle(code: string, { type, filename, scopedClass, mode
|
|||
return { css, type: styleType };
|
||||
}
|
||||
|
||||
/** Style optimizer */
|
||||
/** Optimize <style> tags */
|
||||
export default function optimizeStyles({ compileOptions, filename, fileID }: OptimizeOptions): Optimizer {
|
||||
const styleNodes: TemplateNode[] = []; // <style> tags to be updated
|
||||
const styleTransformPromises: Promise<StyleTransformResult>[] = []; // async style transform results to be finished in finalize();
|
||||
|
|
|
@ -2,6 +2,7 @@ import type { AstroConfig } from './@types/astro';
|
|||
import { join as pathJoin, resolve as pathResolve } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
/** Attempt to load an `astro.config.mjs` file */
|
||||
export async function loadConfig(rawRoot: string | undefined): Promise<AstroConfig | undefined> {
|
||||
if (typeof rawRoot === 'undefined') {
|
||||
rawRoot = process.cwd();
|
||||
|
|
|
@ -18,7 +18,8 @@ const logging: LogOptions = {
|
|||
dest: defaultLogDestination,
|
||||
};
|
||||
|
||||
export default async function (astroConfig: AstroConfig) {
|
||||
/** The primary dev action */
|
||||
export default async function dev(astroConfig: AstroConfig) {
|
||||
const { projectRoot } = astroConfig;
|
||||
|
||||
const runtime = await createRuntime(astroConfig, { mode: 'development', logging });
|
||||
|
@ -69,7 +70,8 @@ export default async function (astroConfig: AstroConfig) {
|
|||
});
|
||||
}
|
||||
|
||||
function formatErrorForBrowser(error: Error) {
|
||||
/** Format error message */
|
||||
function formatErrorForBrowser(err: Error) {
|
||||
// TODO make this pretty.
|
||||
return error.toString();
|
||||
return err.toString();
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ 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 ';
|
||||
|
@ -47,6 +48,7 @@ function* _h(tag: string, attrs: HProps, children: Array<HChild>) {
|
|||
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') {
|
||||
|
@ -57,6 +59,7 @@ export async function h(tag: HTag, attrs: HProps, ...pChildren: Array<Promise<HC
|
|||
return Array.from(_h(tag, attrs, children)).join('');
|
||||
}
|
||||
|
||||
/** Fragment helper, similar to React.Fragment */
|
||||
export function Fragment(_: HProps, ...children: Array<string>) {
|
||||
return children.join('');
|
||||
}
|
||||
|
|
|
@ -10,12 +10,13 @@ export interface Renderer {
|
|||
imports?: Record<string, string[]>;
|
||||
}
|
||||
|
||||
/** Initialize Astro Component renderer for Static and Dynamic components */
|
||||
export function createRenderer(renderer: Renderer) {
|
||||
const _static: Renderer['renderStatic'] = (Component: any) => renderer.renderStatic(Component);
|
||||
const _imports = (context: DynamicRenderContext) => {
|
||||
const values = Object.values(renderer.imports ?? {})
|
||||
.reduce((acc, values) => {
|
||||
return [...acc, `{ ${values.join(', ')} }`];
|
||||
.reduce((acc, v) => {
|
||||
return [...acc, `{ ${v.join(', ')} }`];
|
||||
}, [])
|
||||
.join(', ');
|
||||
const libs = Object.keys(renderer.imports ?? {})
|
||||
|
|
|
@ -61,6 +61,7 @@ const levels: Record<LoggerLevel, number> = {
|
|||
silent: 90,
|
||||
};
|
||||
|
||||
/** Full logging API */
|
||||
export function log(opts: LogOptions = defaultLogOptions, level: LoggerLevel, type: string, ...args: Array<any>) {
|
||||
const event: LogMessage = {
|
||||
type,
|
||||
|
@ -77,22 +78,27 @@ export function log(opts: LogOptions = defaultLogOptions, level: LoggerLevel, ty
|
|||
opts.dest.write(event);
|
||||
}
|
||||
|
||||
/** Emit a message only shown in debug mode */
|
||||
export function debug(opts: LogOptions, type: string, ...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, ...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, ...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, ...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
|
||||
|
@ -108,7 +114,7 @@ export function parseError(opts: LogOptions, err: CompileError) {
|
|||
`
|
||||
|
||||
${underline(bold(grey(`${err.filename}:${err.start.line}:${err.start.column}`)))}
|
||||
|
||||
|
||||
${bold(red(`𝘅 ${err.message}`))}
|
||||
|
||||
${frame}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import slugger from 'github-slugger';
|
||||
|
||||
// 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".
|
||||
/**
|
||||
* 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;
|
||||
|
|
|
@ -11,12 +11,14 @@ const characterReferences = {
|
|||
|
||||
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: Record<string, () => void>) {
|
||||
const token: Token = arguments[0];
|
||||
const serialize = (this.sliceSerialize as unknown) as (t: Token) => string;
|
||||
|
|
|
@ -15,6 +15,7 @@ interface Timing {
|
|||
children: Timing[];
|
||||
}
|
||||
|
||||
/** Format benchmarks */
|
||||
function collapse_timings(timings) {
|
||||
const result = {};
|
||||
timings.forEach((timing) => {
|
||||
|
|
|
@ -217,6 +217,11 @@ export class Parser {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ export class CompileError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
/** Throw CompileError */
|
||||
export default function error(
|
||||
message: string,
|
||||
props: {
|
||||
|
@ -27,19 +28,19 @@ export default function error(
|
|||
end?: number;
|
||||
}
|
||||
): never {
|
||||
const error = new CompileError(message);
|
||||
error.name = props.name;
|
||||
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 });
|
||||
|
||||
error.code = props.code;
|
||||
error.start = start;
|
||||
error.end = end;
|
||||
error.pos = props.start;
|
||||
error.filename = props.filename;
|
||||
err.code = props.code;
|
||||
err.start = start;
|
||||
err.end = end;
|
||||
err.pos = props.start;
|
||||
err.filename = props.filename;
|
||||
|
||||
error.frame = get_code_frame(props.source, start.line - 1, start.column);
|
||||
err.frame = get_code_frame(props.source, start.line - 1, start.column);
|
||||
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// 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;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// @ts-nocheck
|
||||
|
||||
/** Utility for accessing FuzzySet */
|
||||
export default function fuzzymatch(name: string, names: string[]) {
|
||||
const set = new FuzzySet(names);
|
||||
const matches = set.get(name);
|
||||
|
@ -13,7 +14,7 @@ export default function fuzzymatch(name: string, names: string[]) {
|
|||
const GRAM_SIZE_LOWER = 2;
|
||||
const GRAM_SIZE_UPPER = 3;
|
||||
|
||||
// return an edit distance from 0 to 1
|
||||
/** 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';
|
||||
|
@ -30,7 +31,7 @@ function _distance(str1: string, str2: string) {
|
|||
}
|
||||
}
|
||||
|
||||
// helper functions
|
||||
/** @url https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js#L18 */
|
||||
function levenshtein(str1: string, str2: string) {
|
||||
const current: number[] = [];
|
||||
let prev;
|
||||
|
@ -58,6 +59,7 @@ function levenshtein(str1: string, str2: string) {
|
|||
|
||||
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;
|
||||
|
@ -74,6 +76,7 @@ function iterate_grams(value: string, gram_size = 2) {
|
|||
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 = {};
|
||||
|
@ -90,6 +93,7 @@ function gram_counter(value: string, gram_size = 2) {
|
|||
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];
|
||||
}
|
||||
|
@ -211,16 +215,16 @@ class FuzzySet {
|
|||
let new_results = [];
|
||||
const end_index = Math.min(50, results.length);
|
||||
// truncate somewhat arbitrarily to 50
|
||||
for (let i = 0; i < end_index; ++i) {
|
||||
new_results.push([_distance(results[i][1], normalized_value), results[i][1]]);
|
||||
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 i = 0; i < results.length; ++i) {
|
||||
if (results[i][0] == results[0][0]) {
|
||||
new_results.push([results[i][0], this.exact_set[results[i][1]]]);
|
||||
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]]]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
/** 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');
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/** 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]}`;
|
||||
|
|
|
@ -113,10 +113,12 @@ export const reserved = new Set([
|
|||
|
||||
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;
|
||||
|
||||
|
@ -130,6 +132,7 @@ export function is_valid(str: string): boolean {
|
|||
return true;
|
||||
}
|
||||
|
||||
/** Utility to normalize HTML */
|
||||
export function sanitize(name: string) {
|
||||
return name
|
||||
.replace(/[^a-zA-Z0-9_]+/g, '_')
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// @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;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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;
|
||||
|
@ -7,6 +8,7 @@ export function trim_start(str: string) {
|
|||
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;
|
||||
|
|
|
@ -32,6 +32,7 @@ export type LoadResult = LoadResultSuccess | LoadResultNotFound | LoadResultErro
|
|||
// 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;
|
||||
|
@ -134,6 +135,7 @@ interface RuntimeOptions {
|
|||
logging: LogOptions;
|
||||
}
|
||||
|
||||
/** Create a new Snowpack instance to power Astro */
|
||||
async function createSnowpack(astroConfig: AstroConfig, env: Record<string, any>) {
|
||||
const { projectRoot, astroRoot, extensions } = astroConfig;
|
||||
|
||||
|
@ -189,6 +191,7 @@ async function createSnowpack(astroConfig: AstroConfig, env: Record<string, any>
|
|||
return { snowpack, snowpackRuntime, snowpackConfig };
|
||||
}
|
||||
|
||||
/** Core Astro runtime */
|
||||
export async function createRuntime(astroConfig: AstroConfig, { mode, logging }: RuntimeOptions): Promise<AstroRuntime> {
|
||||
const { snowpack: backendSnowpack, snowpackRuntime: backendSnowpackRuntime, snowpackConfig: backendSnowpackConfig } = await createSnowpack(astroConfig, {
|
||||
astro: true,
|
||||
|
|
Loading…
Reference in a new issue