@lynx-js/rspeedy
Version:
A webpack/rspack-based frontend toolchain for Lynx
560 lines (559 loc) • 19.4 kB
JavaScript
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 };