mirror of
https://github.com/earendil-works/pi.git
synced 2026-09-28 05:54:43 +08:00
feat: build with TypeScript 7 and run sources with plain node
Replace the tsgo native preview with typescript@7.0.2 and port the check scripts to the TS 7 API. Target ES2024, drop useDefineForClassFields: false and unused decorator options, and enable verbatimModuleSyntax. Replace tsx with node plus the source resolver hook, passed to --import as a file URL. closes #9965
This commit is contained in:
@@ -3,9 +3,24 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { isBuiltin } from "node:module";
|
||||
import { isAbsolute, join, relative, resolve } from "node:path";
|
||||
import ts from "typescript";
|
||||
import { SyntaxKind } from "typescript/unstable/ast";
|
||||
import {
|
||||
isCallExpression,
|
||||
isExportDeclaration,
|
||||
isIdentifier,
|
||||
isImportDeclaration,
|
||||
isNamedExports,
|
||||
isNamedImports,
|
||||
isNoSubstitutionTemplateLiteral,
|
||||
isPropertyAccessExpression,
|
||||
isStringLiteral,
|
||||
} from "typescript/unstable/ast/is";
|
||||
import { API } from "typescript/unstable/sync";
|
||||
import { getPublicWorkspacePackages } from "./release-packages.mjs";
|
||||
|
||||
// Packages without tsconfig.build.json are checked against a synthetic config.
|
||||
const fallbackConfigName = "tsconfig.runtime-deps-fallback.json";
|
||||
const fallbackConfig = JSON.stringify({ include: ["src/**/*"] });
|
||||
const failures = [];
|
||||
|
||||
function checkSource(source, manifest) {
|
||||
@@ -18,7 +33,7 @@ function checkSource(source, manifest) {
|
||||
]);
|
||||
|
||||
function checkSpecifier(node) {
|
||||
if (!node || !ts.isStringLiteralLike(node)) return;
|
||||
if (!node || !(isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node))) return;
|
||||
const specifier = node.text;
|
||||
if (specifier.startsWith(".") || specifier.startsWith("/") || isBuiltin(specifier)) return;
|
||||
const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
|
||||
@@ -28,61 +43,86 @@ function checkSource(source, manifest) {
|
||||
}
|
||||
|
||||
function visit(node) {
|
||||
if (ts.isImportDeclaration(node)) {
|
||||
if (isImportDeclaration(node)) {
|
||||
const clause = node.importClause;
|
||||
const bindings = clause?.namedBindings;
|
||||
if (
|
||||
!clause ||
|
||||
(!clause.isTypeOnly &&
|
||||
(clause.name || !bindings || !ts.isNamedImports(bindings) ||
|
||||
bindings.elements.length === 0 || bindings.elements.some((element) => !element.isTypeOnly)))
|
||||
(clause.phaseModifier !== SyntaxKind.TypeKeyword &&
|
||||
(clause.name ||
|
||||
!bindings ||
|
||||
!isNamedImports(bindings) ||
|
||||
bindings.elements.length === 0 ||
|
||||
bindings.elements.some((element) => !element.isTypeOnly)))
|
||||
) {
|
||||
checkSpecifier(node.moduleSpecifier);
|
||||
}
|
||||
} else if (ts.isExportDeclaration(node) && !node.isTypeOnly) {
|
||||
} else if (isExportDeclaration(node) && !node.isTypeOnly) {
|
||||
const clause = node.exportClause;
|
||||
if (!clause || !ts.isNamedExports(clause) || clause.elements.length === 0 || clause.elements.some((element) => !element.isTypeOnly)) {
|
||||
if (
|
||||
!clause ||
|
||||
!isNamedExports(clause) ||
|
||||
clause.elements.length === 0 ||
|
||||
clause.elements.some((element) => !element.isTypeOnly)
|
||||
) {
|
||||
checkSpecifier(node.moduleSpecifier);
|
||||
}
|
||||
} else if (
|
||||
ts.isCallExpression(node) &&
|
||||
(node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
||||
(ts.isIdentifier(node.expression) && node.expression.text === "require") ||
|
||||
(ts.isPropertyAccessExpression(node.expression) && node.expression.getText(source) === "require.resolve"))
|
||||
isCallExpression(node) &&
|
||||
(node.expression.kind === SyntaxKind.ImportKeyword ||
|
||||
(isIdentifier(node.expression) && node.expression.text === "require") ||
|
||||
(isPropertyAccessExpression(node.expression) && node.expression.getText(source) === "require.resolve"))
|
||||
) {
|
||||
checkSpecifier(node.arguments[0]);
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
node.forEachChild(visit);
|
||||
}
|
||||
visit(source);
|
||||
}
|
||||
|
||||
for (const { directory } of getPublicWorkspacePackages()) {
|
||||
const sourceDirectory = resolve(directory, "src");
|
||||
if (!existsSync(sourceDirectory)) continue;
|
||||
const manifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
||||
const configPath = join(directory, "tsconfig.build.json");
|
||||
const config = existsSync(configPath)
|
||||
? ts.readConfigFile(configPath, ts.sys.readFile)
|
||||
: { config: { include: ["src/**/*"] } };
|
||||
if (config.error) throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, "\n"));
|
||||
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, resolve(directory));
|
||||
if (parsed.errors.length > 0) {
|
||||
throw new Error(parsed.errors.map((error) => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"));
|
||||
}
|
||||
const roots = new Set(parsed.fileNames.map((file) => resolve(file)));
|
||||
const program = ts.createProgram(parsed.fileNames, parsed.options);
|
||||
for (const source of program.getSourceFiles()) {
|
||||
if (source.isDeclarationFile || source.fileName.endsWith(".json")) continue;
|
||||
const path = relative(sourceDirectory, resolve(source.fileName));
|
||||
if (path.startsWith("..") || isAbsolute(path)) continue;
|
||||
// TypeScript's exclude only filters roots: imports can pull excluded files
|
||||
// back into the build. Reject that too, including type-only imports.
|
||||
if (!roots.has(resolve(source.fileName))) {
|
||||
failures.push(`${source.fileName} is excluded from ${manifest.name}'s build but imported by it`);
|
||||
const packages = getPublicWorkspacePackages()
|
||||
.map(({ directory }) => ({ directory, sourceDirectory: resolve(directory, "src") }))
|
||||
.filter(({ sourceDirectory }) => existsSync(sourceDirectory))
|
||||
.map(({ directory, sourceDirectory }) => {
|
||||
const configPath = resolve(directory, "tsconfig.build.json");
|
||||
return {
|
||||
sourceDirectory,
|
||||
manifest: JSON.parse(readFileSync(join(directory, "package.json"), "utf8")),
|
||||
configPath: existsSync(configPath) ? configPath : resolve(directory, fallbackConfigName),
|
||||
};
|
||||
});
|
||||
|
||||
const fallbackConfigs = new Set(
|
||||
packages.map(({ configPath }) => configPath).filter((path) => path.endsWith(fallbackConfigName)),
|
||||
);
|
||||
const api = new API({
|
||||
cwd: process.cwd(),
|
||||
fs: {
|
||||
fileExists: (fileName) => (fallbackConfigs.has(resolve(fileName)) ? true : undefined),
|
||||
readFile: (fileName) => (fallbackConfigs.has(resolve(fileName)) ? fallbackConfig : undefined),
|
||||
},
|
||||
});
|
||||
try {
|
||||
const snapshot = api.updateSnapshot({ openProjects: packages.map(({ configPath }) => configPath) });
|
||||
for (const { sourceDirectory, manifest, configPath } of packages) {
|
||||
const project = snapshot.getProject(configPath);
|
||||
const diagnostics = project.program.getConfigFileParsingDiagnostics();
|
||||
if (diagnostics.length > 0) throw new Error(diagnostics.map((diagnostic) => diagnostic.text).join("\n"));
|
||||
const roots = new Set(project.rootFiles.map((file) => resolve(file)));
|
||||
for (const fileName of project.program.getSourceFileNames()) {
|
||||
if (fileName.endsWith(".d.ts") || fileName.endsWith(".json")) continue;
|
||||
const path = relative(sourceDirectory, resolve(fileName));
|
||||
if (path.startsWith("..") || isAbsolute(path)) continue;
|
||||
// TypeScript's exclude only filters roots: imports can pull excluded files
|
||||
// back into the build. Reject that too, including type-only imports.
|
||||
if (!roots.has(resolve(fileName))) {
|
||||
failures.push(`${fileName} is excluded from ${manifest.name}'s build but imported by it`);
|
||||
}
|
||||
checkSource(project.program.getSourceFile(fileName), manifest);
|
||||
}
|
||||
checkSource(source, manifest);
|
||||
}
|
||||
} finally {
|
||||
api.close();
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import ts from "typescript";
|
||||
import { readdirSync } from "node:fs";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
import { SyntaxKind } from "typescript/unstable/ast";
|
||||
import {
|
||||
isCallExpression,
|
||||
isExportDeclaration,
|
||||
isImportDeclaration,
|
||||
isImportTypeNode,
|
||||
isLiteralTypeNode,
|
||||
isNoSubstitutionTemplateLiteral,
|
||||
isStringLiteral,
|
||||
} from "typescript/unstable/ast/is";
|
||||
import { API } from "typescript/unstable/sync";
|
||||
|
||||
const ignoredDirectories = new Set([".git", "coverage", "dist", "node_modules"]);
|
||||
const files = [];
|
||||
@@ -20,13 +30,17 @@ function collectTypescriptFiles(directory) {
|
||||
}
|
||||
}
|
||||
|
||||
function isStringLiteralLike(node) {
|
||||
return node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node));
|
||||
}
|
||||
|
||||
function isRelativeJavaScriptSpecifier(specifier) {
|
||||
return /^\.\.?\//.test(specifier) && /\.js(?:[?#].*)?$/.test(specifier);
|
||||
}
|
||||
|
||||
function getImportTypeSpecifier(node) {
|
||||
if (!ts.isLiteralTypeNode(node.argument)) return undefined;
|
||||
if (!ts.isStringLiteralLike(node.argument.literal)) return undefined;
|
||||
if (!isLiteralTypeNode(node.argument)) return undefined;
|
||||
if (!isStringLiteralLike(node.argument.literal)) return undefined;
|
||||
return node.argument.literal;
|
||||
}
|
||||
|
||||
@@ -34,37 +48,54 @@ const failures = [];
|
||||
|
||||
collectTypescriptFiles(".");
|
||||
|
||||
for (const file of files.sort()) {
|
||||
const sourceText = readFileSync(file, "utf8");
|
||||
const sourceFile = ts.createSourceFile(file, sourceText, ts.ScriptTarget.Latest, true);
|
||||
// Parse every file through one synthetic project. noResolve keeps the program to exactly these files.
|
||||
const configPath = resolve("tsconfig.check-ts-relative-imports.json");
|
||||
const config = JSON.stringify({
|
||||
compilerOptions: { noResolve: true, noLib: true, types: [] },
|
||||
files: files.map((file) => resolve(file)),
|
||||
});
|
||||
const api = new API({
|
||||
cwd: process.cwd(),
|
||||
fs: {
|
||||
fileExists: (fileName) => (resolve(fileName) === configPath ? true : undefined),
|
||||
readFile: (fileName) => (resolve(fileName) === configPath ? config : undefined),
|
||||
},
|
||||
});
|
||||
|
||||
function checkSpecifier(node) {
|
||||
if (!isRelativeJavaScriptSpecifier(node.text)) return;
|
||||
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
||||
failures.push(`${file}:${line + 1}:${character + 1}: ${node.text}`);
|
||||
}
|
||||
try {
|
||||
const program = api.updateSnapshot({ openProjects: [configPath] }).getProject(configPath).program;
|
||||
for (const file of files.sort()) {
|
||||
const sourceFile = program.getSourceFile(resolve(file));
|
||||
|
||||
function visit(node) {
|
||||
if (ts.isImportDeclaration(node) && ts.isStringLiteralLike(node.moduleSpecifier)) {
|
||||
checkSpecifier(node.moduleSpecifier);
|
||||
} else if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) {
|
||||
checkSpecifier(node.moduleSpecifier);
|
||||
} else if (
|
||||
ts.isCallExpression(node) &&
|
||||
node.expression.kind === ts.SyntaxKind.ImportKeyword &&
|
||||
node.arguments[0] &&
|
||||
ts.isStringLiteralLike(node.arguments[0])
|
||||
) {
|
||||
checkSpecifier(node.arguments[0]);
|
||||
} else if (ts.isImportTypeNode(node)) {
|
||||
const specifier = getImportTypeSpecifier(node);
|
||||
if (specifier) checkSpecifier(specifier);
|
||||
function checkSpecifier(node) {
|
||||
if (!isRelativeJavaScriptSpecifier(node.text)) return;
|
||||
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
||||
failures.push(`${relative(".", file)}:${line + 1}:${character + 1}: ${node.text}`);
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
function visit(node) {
|
||||
if (isImportDeclaration(node) && isStringLiteralLike(node.moduleSpecifier)) {
|
||||
checkSpecifier(node.moduleSpecifier);
|
||||
} else if (isExportDeclaration(node) && isStringLiteralLike(node.moduleSpecifier)) {
|
||||
checkSpecifier(node.moduleSpecifier);
|
||||
} else if (
|
||||
isCallExpression(node) &&
|
||||
node.expression.kind === SyntaxKind.ImportKeyword &&
|
||||
isStringLiteralLike(node.arguments[0])
|
||||
) {
|
||||
checkSpecifier(node.arguments[0]);
|
||||
} else if (isImportTypeNode(node)) {
|
||||
const specifier = getImportTypeSpecifier(node);
|
||||
if (specifier) checkSpecifier(specifier);
|
||||
}
|
||||
|
||||
visit(sourceFile);
|
||||
node.forEachChild(visit);
|
||||
}
|
||||
|
||||
visit(sourceFile);
|
||||
}
|
||||
} finally {
|
||||
api.close();
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user