UNPKG

@lynx-js/rspeedy

Version:

A webpack/rspack-based frontend toolchain for Lynx

560 lines (559 loc) 19.4 kB
import { URL } from "node:url"; import typescript from "typescript"; const FLAG_REPLACE_WITH_OPEN_PAREN = 1; const FLAG_REPLACE_WITH_CLOSE_PAREN = 2; const FLAG_REPLACE_WITH_SEMI = 3; function getSpace(input, start, end) { let out = ""; for(let i = start; i < end; i++){ const charCode = input.charCodeAt(i); switch(charCode){ case 10: out += "\n"; break; case 13: out += "\r"; break; default: out += " "; } } return out; } class BlankString { constructor(input){ this.__input = input; this.__ranges = []; } blankButStartWithOpenParen(start, end) { this.__ranges.push(FLAG_REPLACE_WITH_OPEN_PAREN, start, end); } blankButEndWithCloseParen(start, end) { this.__ranges.push(0, start, end - 1); this.__ranges.push(FLAG_REPLACE_WITH_CLOSE_PAREN, end - 1, end); } blankButStartWithSemi(start, end) { this.__ranges.push(FLAG_REPLACE_WITH_SEMI, start, end); } blank(start, end) { this.__ranges.push(0, start, end); } toString() { const ranges = this.__ranges; const input = this.__input; if (0 === ranges.length) return input; let out = ""; let previousEnd = 0; const max = Math.max; for(let i = 0; i < ranges.length; i += 3){ const flags = ranges[i]; let rangeStart = ranges[i + 1]; const rangeEnd = ranges[i + 2]; rangeStart = max(rangeStart, previousEnd); out += input.slice(previousEnd, rangeStart); if (flags === FLAG_REPLACE_WITH_CLOSE_PAREN) { out += ")"; rangeStart += 1; } else if (flags === FLAG_REPLACE_WITH_SEMI) { out += ";"; rangeStart += 1; } else if (flags === FLAG_REPLACE_WITH_OPEN_PAREN) { out += "("; rangeStart += 1; } previousEnd = rangeEnd; out += getSpace(input, rangeStart, previousEnd); } return out + input.slice(previousEnd); } } const SK = typescript.SyntaxKind; const VISIT_BLANKED = ""; const VISITED_JS = null; const languageOptions = { languageVersion: typescript.ScriptTarget.ESNext, impliedNodeFormat: typescript.ModuleKind.ESNext }; const scanner = typescript.createScanner(typescript.ScriptTarget.ESNext, true, typescript.LanguageVariant.Standard); if (typescript.JSDocParsingMode) { languageOptions.jsDocParsingMode = typescript.JSDocParsingMode.ParseNone; scanner.setJSDocParsingMode(typescript.JSDocParsingMode.ParseNone); } let src = ""; let str = new BlankString(""); let ast; let onError; let seenJS = false; let parentStatement; function tsBlankSpace(input, onErrorArg) { return blankSourceFile(typescript.createSourceFile("input.ts", input, languageOptions, false, typescript.ScriptKind.TS), onErrorArg); } function blankSourceFile(source, onErrorArg) { try { const input = source.getFullText(source); src = input; str = new BlankString(input); onError = onErrorArg; scanner.setText(input); ast = source; visitNodeArray(ast.statements, true, false); return str.toString(); } finally{ scanner.setText(""); onError = void 0; ast = void 0; str = void 0; src = ""; seenJS = false; parentStatement = void 0; } } function visitUnknownNodeArray(nodes) { if (0 === nodes.length) return VISITED_JS; return visitNodeArray(nodes, typescript.isStatement(nodes[0]), false); } function visitNodeArray(nodes, isStatementLike, isFunctionBody) { const previousParentStatement = parentStatement; const previousSeenJS = seenJS; if (isFunctionBody) seenJS = false; for(let i = 0; i < nodes.length; i++){ const n = nodes[i]; if (isStatementLike) parentStatement = n; if (visitStatementLike(n) === VISITED_JS) seenJS = true; } parentStatement = previousParentStatement; if (isFunctionBody) seenJS = previousSeenJS; return seenJS ? VISITED_JS : VISIT_BLANKED; } function visitStatementLike(node) { const kind = node.kind; switch(kind){ case SK.ImportDeclaration: return visitImportDeclaration(node); case SK.ExportDeclaration: return visitExportDeclaration(node); case SK.ExportAssignment: return visitExportAssignment(node); case SK.ImportEqualsDeclaration: onError && onError(node); return VISITED_JS; } return innerVisitor(node, kind); } function visitor(node) { const r = innerVisitor(node, node.kind); if (r === VISITED_JS) seenJS = true; return r; } function innerVisitor(node, kind) { switch(kind){ case SK.Identifier: return VISITED_JS; case SK.VariableDeclaration: return visitVariableDeclaration(node); case SK.VariableStatement: return visitVariableStatement(node); case SK.CallExpression: case SK.NewExpression: return visitCallOrNewExpression(node); case SK.TypeAliasDeclaration: case SK.InterfaceDeclaration: blankStatement(node); return VISIT_BLANKED; case SK.ClassDeclaration: case SK.ClassExpression: return visitClassLike(node); case SK.ExpressionWithTypeArguments: return visitExpressionWithTypeArguments(node); case SK.PropertyDeclaration: return visitPropertyDeclaration(node); case SK.NonNullExpression: return visitNonNullExpression(node); case SK.SatisfiesExpression: case SK.AsExpression: return visitTypeAssertion(node); case SK.ArrowFunction: case SK.FunctionDeclaration: case SK.MethodDeclaration: case SK.Constructor: case SK.FunctionExpression: case SK.GetAccessor: case SK.SetAccessor: return visitFunctionLikeDeclaration(node, kind); case SK.EnumDeclaration: return visitEnum(node); case SK.ModuleDeclaration: return visitModule(node); case SK.IndexSignature: blankExact(node); return VISIT_BLANKED; case SK.TaggedTemplateExpression: return visitTaggedTemplate(node); case SK.TypeAssertionExpression: return visitLegacyTypeAssertion(node); } return node.forEachChild(visitor, visitUnknownNodeArray) || VISITED_JS; } function visitVariableStatement(node) { if (node.modifiers && modifiersContainsDeclare(node.modifiers)) { blankStatement(node); return VISIT_BLANKED; } node.forEachChild(visitor, visitUnknownNodeArray); return VISITED_JS; } function visitCallOrNewExpression(node) { visitor(node.expression); if (node.typeArguments) blankGenerics(node, node.typeArguments, false); if (node.arguments) { const args = node.arguments; for(let i = 0; i < args.length; i++)visitor(args[i]); } return VISITED_JS; } function visitTaggedTemplate(node) { visitor(node.tag); if (node.typeArguments) blankGenerics(node, node.typeArguments, false); visitor(node.template); return VISITED_JS; } function visitVariableDeclaration(node) { visitor(node.name); node.exclamationToken && blankExact(node.exclamationToken); node.type && blankTypeNode(node.type); if (node.initializer) visitor(node.initializer); return VISITED_JS; } function visitClassLike(node) { if (node.modifiers) { if (modifiersContainsDeclare(node.modifiers)) { blankStatement(node); return VISIT_BLANKED; } visitModifiers(node.modifiers, false); } if (node.typeParameters && node.typeParameters.length) blankGenerics(node, node.typeParameters, false); const { heritageClauses } = node; if (heritageClauses) for(let i = 0; i < heritageClauses.length; i++){ const hc = heritageClauses[i]; if (hc.token === SK.ImplementsKeyword) blankExact(hc); else if (hc.token === SK.ExtendsKeyword) hc.forEachChild(visitor); } visitNodeArray(node.members, true, false); return VISITED_JS; } function visitExpressionWithTypeArguments(node) { visitor(node.expression); if (node.typeArguments) blankGenerics(node, node.typeArguments, false); return VISITED_JS; } const classElementModifiersToRemoveArray = [ SK.AbstractKeyword, SK.DeclareKeyword, SK.OverrideKeyword, SK.PrivateKeyword, SK.ProtectedKeyword, SK.PublicKeyword, SK.ReadonlyKeyword ]; const classElementModifiersToRemove = new Set(classElementModifiersToRemoveArray); function isRemovedModifier(kind) { return classElementModifiersToRemove.has(kind); } function visitModifiers(modifiers, addSemi) { for(let i = 0; i < modifiers.length; i++){ const modifier = modifiers[i]; const kind = modifier.kind; if (isRemovedModifier(kind)) { if (addSemi && 0 === i) { str.blankButStartWithSemi(modifier.getStart(ast), modifier.end); addSemi = false; } else blankExact(modifier); continue; } if (kind === SK.Decorator) { visitor(modifier); continue; } } } function visitPropertyDeclaration(node) { if (node.modifiers) { if (modifiersContainsAbstractOrDeclare(node.modifiers)) { blankStatement(node); return VISIT_BLANKED; } visitModifiers(node.modifiers, node.name.kind === SK.ComputedPropertyName); } node.exclamationToken && blankExact(node.exclamationToken); node.questionToken && blankExact(node.questionToken); node.type && blankTypeNode(node.type); visitor(node.name); if (node.initializer) visitor(node.initializer); return VISITED_JS; } function visitNonNullExpression(node) { visitor(node.expression); str.blank(node.end - 1, node.end); return VISITED_JS; } function visitTypeAssertion(node) { const r = visitor(node.expression); const nodeEnd = node.end; if (parentStatement && nodeEnd === parentStatement.end && 59 !== src.charCodeAt(nodeEnd)) str.blankButStartWithSemi(node.expression.end, nodeEnd); else str.blank(node.expression.end, nodeEnd); return r; } function visitLegacyTypeAssertion(node) { onError && onError(node); return visitor(node.expression); } const unsupportedParameterModifiers = new Set([ SK.PublicKeyword, SK.ProtectedKeyword, SK.PrivateKeyword, SK.ReadonlyKeyword ]); function visitFunctionLikeDeclaration(node, kind) { if (!node.body) { if (node.modifiers && modifiersContainsDeclare(node.modifiers)) { blankStatement(node); return VISIT_BLANKED; } blankExact(node); return VISIT_BLANKED; } const nodeName = node.name; if (node.modifiers) visitModifiers(node.modifiers, !!nodeName && nodeName.kind === SK.ComputedPropertyName); if (nodeName) visitor(nodeName); let moveOpenParen = false; const params = node.parameters; if (node.typeParameters && node.typeParameters.length) { moveOpenParen = spansLines(node.typeParameters.pos, params.pos); blankGenerics(node, node.typeParameters, moveOpenParen); } node.questionToken && blankExact(node.questionToken); if (moveOpenParen) str.blank(params.pos - 1, params.pos); for(let i = 0; i < params.length; i++){ const p = params[i]; if (0 === i && "this" === p.name.getText(ast)) { blankExactAndOptionalTrailingComma(p); continue; } if (onError && p.modifiers) for(let i = 0; i < p.modifiers.length; i++){ const modifier = p.modifiers[i]; if (unsupportedParameterModifiers.has(modifier.kind)) onError(modifier); } visitor(p.name); p.questionToken && blankExact(p.questionToken); p.type && blankTypeNode(p.type); p.initializer && visitor(p.initializer); } const returnType = node.type; const isArrow = kind === SK.ArrowFunction; if (returnType) if (isArrow && spansLines(node.parameters.end, node.equalsGreaterThanToken.pos)) { const paramEnd = getClosingParenthesisPos(node.parameters); str.blankButEndWithCloseParen(paramEnd - 1, returnType.getEnd()); } else blankTypeNode(returnType); const body = node.body; if (body.kind === SK.Block) visitNodeArray(body.statements, true, true); else visitor(node.body); return VISITED_JS; } function spansLines(a, b) { for(let i = a; i < b; i++)if (10 === src.charCodeAt(i)) return true; return false; } function visitImportDeclaration(node) { if (node.importClause) { if (node.importClause.isTypeOnly) { blankStatement(node); return VISIT_BLANKED; } const { namedBindings } = node.importClause; if (namedBindings && typescript.isNamedImports(namedBindings)) { const elements = namedBindings.elements; for(let i = 0; i < elements.length; i++){ const e = elements[i]; e.isTypeOnly && blankExactAndOptionalTrailingComma(e); } } } return VISITED_JS; } function visitExportDeclaration(node) { if (node.isTypeOnly) { blankStatement(node); return VISIT_BLANKED; } const { exportClause } = node; if (exportClause && typescript.isNamedExports(exportClause)) { const elements = exportClause.elements; for(let i = 0; i < elements.length; i++){ const e = elements[i]; e.isTypeOnly && blankExactAndOptionalTrailingComma(e); } } return VISITED_JS; } function visitExportAssignment(node) { if (node.isExportEquals) { onError && onError(node); return VISITED_JS; } visitor(node.expression); return VISITED_JS; } function visitModule(node) { if (node.flags & typescript.NodeFlags.GlobalAugmentation || node.flags & typescript.NodeFlags.Namespace && (node.modifiers && modifiersContainsDeclare(node.modifiers) || !valueNamespaceWorker(node)) || node.name.kind === SK.StringLiteral) { blankStatement(node); return VISIT_BLANKED; } onError && onError(node); return VISITED_JS; } function visitEnum(node) { if (node.modifiers && modifiersContainsDeclare(node.modifiers)) { blankStatement(node); return VISIT_BLANKED; } onError && onError(node); return VISITED_JS; } function valueNamespaceWorker(node) { switch(node.kind){ case SK.TypeAliasDeclaration: case SK.InterfaceDeclaration: return false; case SK.ImportEqualsDeclaration: { const { modifiers } = node; return modifiers?.some((m)=>m.kind === SK.ExportKeyword) || false; } case SK.ModuleDeclaration: { if (!(node.flags & typescript.NodeFlags.Namespace)) return true; const { body } = node; if (!body) return false; if (body.kind === SK.ModuleDeclaration) return valueNamespaceWorker(body); return body.forEachChild(valueNamespaceWorker) || false; } } return true; } function modifiersContainsDeclare(modifiers) { for(let i = 0; i < modifiers.length; i++){ const modifier = modifiers[i]; if (modifier.kind === SK.DeclareKeyword) return true; } return false; } function modifiersContainsAbstractOrDeclare(modifiers) { for(let i = 0; i < modifiers.length; i++){ const modifierKind = modifiers[i].kind; if (modifierKind === SK.AbstractKeyword || modifierKind === SK.DeclareKeyword) return true; } return false; } function scanRange(start, end, callback) { return scanner.scanRange(start, end - start, callback); } function endPosOfToken(token) { let first = true; let start = 0; while(true){ const next = scanner.scan(); if (first) { start = scanner.getTokenStart(); first = false; } if (next === token) break; if (next === SK.EndOfFileToken) return start; } return scanner.getTokenEnd(); } function getGreaterThanToken() { return endPosOfToken(SK.GreaterThanToken); } function getClosingParen() { return endPosOfToken(SK.CloseParenToken); } function blankTypeNode(n) { str.blank(n.getFullStart() - 1, n.end); } function blankExact(n) { str.blank(n.getStart(ast), n.end); } function blankStatement(n) { if (seenJS) str.blankButStartWithSemi(n.getStart(ast), n.end); else str.blank(n.getStart(ast), n.end); } function blankExactAndOptionalTrailingComma(n) { scanner.resetTokenState(n.end); const trailingComma = scanner.scan() === SK.CommaToken; str.blank(n.getStart(ast), trailingComma ? scanner.getTokenEnd() : n.end); } function blankGenerics(node, arr, startWithParen) { const start = arr.pos - 1; const end = scanRange(arr.end, node.end, getGreaterThanToken); startWithParen ? str.blankButStartWithOpenParen(start, end) : str.blank(start, end); } function getClosingParenthesisPos(node) { return scanRange(0 === node.length ? node.pos : node[node.length - 1].end, ast.end, getClosingParen); } const state = { active: true, port: null, options: null }; const hooks_initialize = function(data) { state.options = data.options ?? { resolve: true, load: true }; state.port = data.port; data.port.on('message', onMessage); }; const hooks_resolve = async function(specifier, context, nextResolve) { if (!state.options?.resolve) return nextResolve(specifier, context); try { return await nextResolve(specifier, context); } catch (err) { const url = parseURL(err?.url); if (state.active && url?.pathname.endsWith('.js')) try { return await nextResolve(url.pathname.slice(0, -3) + '.ts', context); } catch {} throw err; } }; const hooks_load = async function(url, context, nextLoad) { if (!state.options?.load) return nextLoad(url, context); const fullURL = parseURL(url); if (!fullURL) return nextLoad(url, context); const { pathname } = fullURL; const isCts = pathname.endsWith('.cts'); if (!(pathname.endsWith('.ts') || pathname.endsWith('.mts') || isCts) || !state.active) return nextLoad(url, context); const result = await nextLoad(url, { ...context, format: 'module' }); const transformedSource = tsBlankSpace(result.source.toString()); return { format: isCts ? 'commonjs' : 'module', shortCircuit: true, source: transformedSource + '\n//# sourceURL=' + url }; }; function onMessage(message) { if ('deactivate' === message) state.active = false; state.port?.off('message', onMessage); } function parseURL(url) { if (null == url) return null; try { return new URL(url, 'file://'); } catch { return null; } } export { hooks_initialize as initialize, hooks_load as load, hooks_resolve as resolve };