refactor: consume Commonlib as a package

This commit is contained in:
vorotamoroz
2026-07-17 11:13:16 +00:00
parent 6475d32769
commit a721d3b602
665 changed files with 3438 additions and 31592 deletions
+3 -17
View File
@@ -32,7 +32,7 @@ Converts standard global variable usages to compatibility wrappers to ensure saf
* **Targets**: `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval`, `requestAnimationFrame`, `cancelAnimationFrame`, `localStorage`, `navigator`, `location`, `window`, `globalThis`, and `document`.
* **Actions**:
* Replaces global namespace references (like `window` and `globalThis`) with `compatGlobal`.
* Replaces `document` with `_activeDocument` (from `@lib/common/coreEnvFunctions.ts`).
* Replaces `document` with `_activeDocument` from the Commonlib compatibility entry.
* Injects or updates the necessary imports in modified files.
* **Command**:
```bash
@@ -86,29 +86,15 @@ Scans the codebase and logs all occurrences of explicit `any` types.
```
### 6. Import Normalisation (`normalise-imports.ts`)
Ensures that all import statements are standardised across the codebase, resolving paths to aliases such as `@lib/` and `@/` where applicable.
Ensures that internal plug-in import statements are standardised to the `@/` alias where applicable. Commonlib imports remain explicit package subpaths and are not rewritten.
* **Command**:
```bash
deno run --allow-read --allow-write --allow-env normalise-imports.ts
```
### 7. CLI Node.js Import Redirection (`refactor-cli-node-imports.ts`)
Redirects direct Node.js built-in module imports (like `fs` and `path`) within the CLI codebase to use a single barrel file (`src/apps/cli/node-compat.ts`).
* **Actions**:
* Finds imports of Node.js built-in APIs (`fs`, `fs/promises`, `path`, and `readline/promises`) in CLI source files.
* Replaces them with imports from the local `node-compat.ts` barrel file.
* This eliminates duplicate browser-targeted linter warnings on Node.js built-ins in the CLI workspace, keeping linter ignores consolidated.
* **Command**:
```bash
deno run --allow-read --allow-write --allow-env refactor-cli-node-imports.ts
```
---
## Safety and Exclusions
* **Tests Excluded**: All scripts automatically skip files located in `_test/` or `testdeno/` folders, as well as files ending with `.spec.ts` or `.test.ts`.
* **Submodule Caution**: Some tools will run against the `src/lib/` submodule. Ensure you verify changes inside the submodule prior to committing.
* **Package Boundary**: These tools operate on this repository only. Changes to Commonlib belong in its own repository and must be validated with its package checks.
* **Verification**: Always run `npm run check` and `npm run test:unit` after performing refactoring tasks to verify that type safety and tests remain intact.
+6 -27
View File
@@ -1,4 +1,4 @@
// Normalise import and export paths in the codebase to use @lib/ and @/ aliases correctly.
// Normalise import and export paths in the codebase to use the @/ alias correctly.
// Use this script by running `deno run --allow-read --allow-write normalise-imports.ts` from the utilsdeno directory.
// Set the --run flag to apply changes: `deno run --allow-read --allow-write normalise-imports.ts --run`
// Set the --all-alias flag to also normalise sibling/child imports (starting with ./): `deno run --allow-read --allow-write normalise-imports.ts --all-alias`
@@ -39,12 +39,9 @@ function toPosixPath(filePath: string): string {
const posixProjectRoot = toPosixPath(projectRoot);
const posixSrc = `${posixProjectRoot}/src`;
const posixLibSrc = `${posixProjectRoot}/src/lib/src`;
const posixSubrepo = `${posixProjectRoot}/src/lib`;
console.log(`Project Root: ${posixProjectRoot}`);
console.log(`Source Directory: ${posixSrc}`);
console.log(`Library Source Directory: ${posixLibSrc}`);
console.log("");
let modifiedFilesCount = 0;
@@ -87,7 +84,7 @@ for (const sourceFile of project.getSourceFiles()) {
// Determine if it is an internal import.
const isRelative = moduleSpecifier.startsWith(".");
const isAlias = moduleSpecifier.startsWith("@/") || moduleSpecifier.startsWith("@lib/");
const isAlias = moduleSpecifier.startsWith("@/");
if (!isRelative && !isAlias) {
// Skip external packages/modules.
@@ -96,9 +93,7 @@ for (const sourceFile of project.getSourceFiles()) {
// Resolve path to an absolute POSIX path.
let resolvedPath = "";
if (moduleSpecifier.startsWith("@lib/")) {
resolvedPath = `${posixLibSrc}/${moduleSpecifier.slice(5)}`;
} else if (moduleSpecifier.startsWith("@/")) {
if (moduleSpecifier.startsWith("@/")) {
resolvedPath = `${posixSrc}/${moduleSpecifier.slice(2)}`;
} else {
// Relative path.
@@ -107,14 +102,9 @@ for (const sourceFile of project.getSourceFiles()) {
resolvedPath = toPosixPath(path.normalize(resolvedPath));
// Keep relative sibling/child imports unchanged (e.g. ./utils) unless:
// 1. --all-alias is set, OR
// 2. the import crosses the subrepository boundary (src/lib/)
// Keep relative sibling/child imports unchanged (e.g. ./utils) unless --all-alias is set.
const isSibling = isRelative && !moduleSpecifier.startsWith("..");
const importerInsideSubrepo = posixFilePath.startsWith(posixSubrepo + "/");
const targetInsideSubrepo = resolvedPath.startsWith(posixSubrepo + "/");
const crossesSubrepo = importerInsideSubrepo !== targetInsideSubrepo;
if (isSibling && !allAlias && !crossesSubrepo) {
if (isSibling && !allAlias) {
continue;
}
@@ -126,18 +116,7 @@ for (const sourceFile of project.getSourceFiles()) {
moduleSpecifier.endsWith(".svelte") ||
moduleSpecifier.endsWith(".d.ts");
if (resolvedPath.startsWith(posixLibSrc + "/")) {
let rel = resolvedPath.slice(posixLibSrc.length + 1);
if (!hasExtension && (rel.endsWith(".ts") || rel.endsWith(".js"))) {
// Strip extension if the original import did not have one.
if (rel.endsWith(".ts") && !rel.endsWith(".d.ts")) {
rel = rel.slice(0, -3);
} else if (rel.endsWith(".js")) {
rel = rel.slice(0, -3);
}
}
newSpecifier = `@lib/${rel}`;
} else if (resolvedPath.startsWith(posixSrc + "/")) {
if (resolvedPath.startsWith(posixSrc + "/")) {
let rel = resolvedPath.slice(posixSrc.length + 1);
if (!hasExtension && (rel.endsWith(".ts") || rel.endsWith(".js"))) {
// Strip extension if the original import did not have one.
-132
View File
@@ -1,132 +0,0 @@
// Refactor Node.js imports in the CLI application to use the barrel compatibility file.
// Use this script by running `deno run --allow-read --allow-write --allow-env refactor-cli-node-imports.ts` from the utilsdeno directory.
// Run with --run flag to apply changes.
import { Project, SyntaxKind, Node } from "npm:ts-morph";
import path from "node:path";
import { fileURLToPath } from "node:url";
const isDryRun = !Deno.args.includes("--run");
if (isDryRun) {
console.log("=== DRY RUN MODE ===");
console.log(
"To apply changes, run with: deno run --allow-read --allow-write --allow-env refactor-cli-node-imports.ts --run\n"
);
} else {
console.log("=== RUN MODE: WILL MODIFY FILES ===");
}
const project = new Project({ tsConfigFilePath: "../tsconfig.json" });
project.addSourceFilesAtPaths("../src/apps/cli/**/*.ts");
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, "..");
const nodeCompatPath = path.resolve(projectRoot, "src", "apps", "cli", "node-compat.ts");
function toPosixPath(filePath: string): string {
return filePath.replace(/\\/g, "/");
}
const posixProjectRoot = toPosixPath(projectRoot);
const posixSrc = `${posixProjectRoot}/src`;
function getRelativeImportPath(fromFile: string, toFile: string): string {
let rel = path.relative(path.dirname(fromFile), toFile);
rel = rel.replace(/\\/g, "/");
if (!rel.startsWith(".") && !rel.startsWith("/")) {
rel = "./" + rel;
}
if (rel.endsWith(".ts")) {
rel = rel.slice(0, -3);
}
return rel;
}
let modifiedFilesCount = 0;
for (const sourceFile of project.getSourceFiles()) {
const filePath = sourceFile.getFilePath();
const posixFilePath = toPosixPath(filePath);
// Only process CLI source files under src/apps/cli/
if (!posixFilePath.includes("/src/apps/cli/")) continue;
if (
posixFilePath.endsWith("node-compat.ts") ||
posixFilePath.endsWith("vite.config.ts") ||
posixFilePath.endsWith(".spec.ts") ||
posixFilePath.endsWith(".test.ts") ||
posixFilePath.includes("/_test/") ||
posixFilePath.includes("/testdeno/") ||
posixFilePath.includes("/test/")
) {
continue;
}
const importDeclarations = sourceFile.getImportDeclarations();
const targetImports: any[] = [];
const namedImportsToAdd: string[] = [];
for (const impDecl of importDeclarations) {
const specifier = impDecl.getModuleSpecifierValue();
// Check if it's a Node.js built-in module we want to redirect
let exportedName = "";
if (specifier === "fs/promises" || specifier === "node:fs/promises") {
exportedName = "fsPromises";
} else if (specifier === "fs" || specifier === "node:fs") {
exportedName = "fs";
} else if (specifier === "path" || specifier === "node:path") {
exportedName = "path";
} else if (specifier === "node:readline/promises") {
exportedName = "readline";
}
if (exportedName) {
const localName = impDecl.getNamespaceImport()?.getText() || impDecl.getDefaultImport()?.getText();
if (localName) {
targetImports.push({ impDecl, exportedName, localName });
}
}
}
if (targetImports.length > 0) {
console.log(`File: ${posixFilePath.slice(posixProjectRoot.length + 1)}`);
for (const { impDecl, exportedName, localName } of targetImports) {
const { line } = sourceFile.getLineAndColumnAtPos(impDecl.getStart());
console.log(` Line ${line}: Redirecting "${impDecl.getText()}"`);
if (exportedName === localName) {
namedImportsToAdd.push(exportedName);
} else {
namedImportsToAdd.push(`${exportedName} as ${localName}`);
}
if (!isDryRun) {
impDecl.remove();
}
}
const relImportPath = getRelativeImportPath(filePath, nodeCompatPath);
console.log(` Adding: import { ${namedImportsToAdd.join(", ")} } from "${relImportPath}"`);
if (!isDryRun) {
sourceFile.addImportDeclaration({
namedImports: namedImportsToAdd,
moduleSpecifier: relImportPath,
});
}
modifiedFilesCount++;
}
}
console.log(`\nTotal files to modify: ${modifiedFilesCount}`);
if (!isDryRun) {
project.saveSync();
console.log("All changes successfully saved.");
} else {
console.log("Dry run complete. No changes were written to files.");
}
+2 -3
View File
@@ -32,7 +32,6 @@ function toPosixPath(filePath: string): string {
const posixProjectRoot = toPosixPath(projectRoot);
const posixSrc = `${posixProjectRoot}/src`;
const posixLibSrc = `${posixProjectRoot}/src/lib`;
const TARGET_GLOBALS = new Set([
"setTimeout",
@@ -191,7 +190,7 @@ for (const sourceFile of project.getSourceFiles()) {
if (requiredImports.length > 0) {
const existingImport = sourceFile.getImportDeclarations().find((imp) => {
const spec = imp.getModuleSpecifierValue();
return spec === "@lib/common/coreEnvFunctions" || spec === "@lib/common/coreEnvFunctions.ts";
return spec === "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions" || spec === "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
});
if (existingImport) {
@@ -206,7 +205,7 @@ for (const sourceFile of project.getSourceFiles()) {
} else {
sourceFile.addImportDeclaration({
namedImports: requiredImports,
moduleSpecifier: "@lib/common/coreEnvFunctions.ts",
moduleSpecifier: "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions",
});
}
}
-187
View File
@@ -1,187 +0,0 @@
// Delete references to utils.ts and replace them with new imports based on the importMap.
// Use this script by running `deno run --allow-read --allow-write --allow-run refactor-import-utils.ts` from the utilsdeno directory.
import { Project } from "npm:ts-morph";
const isDryRun = !Deno.args.includes("--run");
if (isDryRun) {
console.log("=== DRY RUN MODE ===");
console.log(
"To apply changes, run with: deno run --allow-read --allow-write --allow-run refactor-import-utils.ts --run\n"
);
}
// const project = new Project({ tsConfigFilePath: "../src/apps/cli/tsconfig.json" });
const project = new Project({ tsConfigFilePath: "../tsconfig.json" });
const importMap = new Map<string, string>();
const targetFiles = [
"utils.concurrency.ts",
"utils.timer.ts",
"utils.notations.ts",
"utils.database.ts",
"utils.regexp.ts",
"utils.settings.ts",
"utils.patch.ts",
"utils.misc.ts",
];
// 1. Map exports from our newly created subfiles
for (const sourceFile of project.getSourceFiles()) {
const filePath = sourceFile.getFilePath();
const fileName = sourceFile.getBaseName();
if (filePath.includes("src/lib/src/common/") && targetFiles.includes(fileName)) {
const exports = sourceFile.getExportedDeclarations();
for (const [name] of exports) {
const relativePath = filePath.split("src/lib/src/")[1].replace(/\.ts$/, "");
importMap.set(name, `@lib/${relativePath}`);
}
}
}
// 2. Map exports/imports of octagonal-wheels in utils.ts
const utilsFile = project.getSourceFile("src/lib/src/common/utils.ts");
if (utilsFile) {
// Parse imports from octagonal-wheels
for (const imp of utilsFile.getImportDeclarations()) {
const moduleSpec = imp.getModuleSpecifierValue();
if (moduleSpec.startsWith("octagonal-wheels")) {
for (const namedImport of imp.getNamedImports()) {
importMap.set(namedImport.getName(), moduleSpec);
}
}
}
// Parse export declarations from octagonal-wheels
for (const exp of utilsFile.getExportDeclarations()) {
const moduleSpec = exp.getModuleSpecifierValue();
if (moduleSpec && moduleSpec.startsWith("octagonal-wheels")) {
for (const namedExport of exp.getNamedExports()) {
importMap.set(namedExport.getName(), moduleSpec);
}
}
}
}
console.log(`Built importMap with ${importMap.size} mappings.\n`);
let modifiedFilesCount = 0;
// 3. Loop through all source files and replace imports
for (const sourceFile of project.getSourceFiles()) {
let fileModified = false;
const imports = sourceFile.getImportDeclarations();
for (const imp of imports) {
const moduleSpec = imp.getModuleSpecifierValue();
const isUtilsImport =
moduleSpec === "@lib/common/utils" ||
moduleSpec === "@lib/common/utils.ts" ||
moduleSpec.endsWith("/common/utils") ||
moduleSpec.endsWith("/common/utils.ts");
if (isUtilsImport) {
const namedImports = imp.getNamedImports();
const defaultImport = imp.getDefaultImport();
const importsToReplace: Record<string, { name: string; newPath: string; isTypeOnly: boolean }[]> = {};
for (const namedImport of namedImports) {
const name = namedImport.getName();
let newPath = importMap.get(name);
if (newPath) {
// If original ended with .ts and the new path starts with @lib, keep .ts
if (moduleSpec.endsWith(".ts") && newPath.startsWith("@lib/")) {
newPath = newPath + ".ts";
}
if (!importsToReplace[newPath]) {
importsToReplace[newPath] = [];
}
importsToReplace[newPath].push({
name,
newPath,
isTypeOnly: namedImport.isTypeOnly() || imp.isTypeOnly(),
});
}
}
if (Object.keys(importsToReplace).length > 0 || (defaultImport && importMap.has(defaultImport.getText()))) {
fileModified = true;
console.log(`File: ${sourceFile.getFilePath().split("obsidian-livesync/")[1]}`);
console.log(` Old: ${imp.getText()}`);
}
if (!isDryRun) {
// Apply replacements
for (const newPath in importsToReplace) {
const isTypeOnly = importsToReplace[newPath].filter((i) => i.isTypeOnly);
if (isTypeOnly.length > 0) {
sourceFile.insertImportDeclaration(imp.getChildIndex(), {
namedImports: isTypeOnly.map((i) => i.name),
moduleSpecifier: newPath,
isTypeOnly: true,
});
}
const isValueImport = importsToReplace[newPath].filter((i) => !i.isTypeOnly);
if (isValueImport.length > 0) {
sourceFile.insertImportDeclaration(imp.getChildIndex(), {
namedImports: isValueImport.map((i) => i.name),
moduleSpecifier: newPath,
isTypeOnly: false,
});
}
for (const { name } of importsToReplace[newPath]) {
const namedImport = imp.getNamedImports().find((ni) => ni.getName() === name);
if (namedImport) {
namedImport.remove();
}
}
}
} else {
// In dry run, just print what it would do
for (const newPath in importsToReplace) {
const names = importsToReplace[newPath].map((i) => i.name).join(", ");
console.log(` -> Would import { ${names} } from "${newPath}"`);
}
}
if (defaultImport) {
const name = defaultImport.getText();
let newPath = importMap.get(name);
if (newPath) {
if (moduleSpec.endsWith(".ts") && newPath.startsWith("@lib/")) {
newPath = newPath + ".ts";
}
if (!isDryRun) {
sourceFile.insertImportDeclaration(imp.getChildIndex(), {
defaultImport: name,
moduleSpecifier: newPath,
isTypeOnly: imp.isTypeOnly(),
});
imp.removeDefaultImport();
} else {
console.log(` -> Would import default ${name} from "${newPath}"`);
}
}
}
if (!isDryRun) {
if (imp.getNamedImports().length === 0 && !imp.getDefaultImport()) {
imp.remove();
}
}
}
}
if (fileModified) {
modifiedFilesCount++;
}
}
console.log(`\nTotal files to modify: ${modifiedFilesCount}`);
if (!isDryRun) {
project.saveSync();
console.log("All changes successfully saved.");
} else {
console.log("Dry run complete. No changes were written to files.");
}
-155
View File
@@ -1,155 +0,0 @@
// Delete references to types.ts and replace them with new imports based on the importMap. It will also split imports if some are type-only and some are value imports.
// Use this script by running `deno run --allow-read --allow-write --allow-run refactor-imports.ts` from the utilsdeno directory. It will read all source files, find imports from types.ts, and replace them with the new paths based on the importMap. Make sure to review the changes before saving, as it will modify your source files.
import { Project } from "npm:ts-morph";
const isDryRun = !Deno.args.includes("--run");
if (isDryRun) {
console.log("=== DRY RUN MODE ===");
console.log(
"To apply changes, run with: deno run --allow-read --allow-write --allow-run refactor-import-utils.ts --run\n"
);
}
// const project = new Project({ tsConfigFilePath: "../src/apps/cli/tsconfig.json" });
const project = new Project({ tsConfigFilePath: "../tsconfig.json" });
const importMap = new Map<string, string>();
// Build a map of types moved out of Models.
// Under src/lib/src/common/models.
for (const sourceFile of project.getSourceFiles()) {
if (sourceFile.getFilePath().includes("src/lib/src/common/models")) {
const exports = sourceFile.getExportedDeclarations();
for (const [name, declarations] of exports) {
for (const declaration of declarations) {
if (
// declaration.getKindName() === "TypeAliasDeclaration" ||
// declaration.getKindName() === "InterfaceDeclaration" ||
// declaration.getKindName() === "EnumDeclaration" ||
true
) {
// console.log(`Found type export in ${sourceFile.getFilePath()}:`, name);
const relativePath = sourceFile.getFilePath().split("src/lib/src/")[1].replace(/\.ts$/, "");
importMap.set(name, `@lib/${relativePath}`);
}
}
}
}
}
// Extras
importMap.set("LOG_LEVEL_NOTICE", "@lib/common/logger");
importMap.set("LOG_LEVEL_VERBOSE", "@lib/common/logger");
importMap.set("LOG_LEVEL_INFO", "@lib/common/logger");
importMap.set("LOG_LEVEL_DEBUG", "@lib/common/logger");
importMap.set("LOG_LEVEL_URGENT", "@lib/common/logger");
importMap.set("LOG_LEVEL", "@lib/common/logger");
importMap.set("Logger", "@lib/common/logger");
// console.log("Import map:", importMap);
// Loop through all files that import from types.ts.
for (const sourceFile of project.getSourceFiles()) {
const imports = sourceFile.getImportDeclarations();
// if import from types.ts and the file is pointing `/lib/src/common/types.ts` (resolved), then we will check if the imported names exist in the importMap, if yes, we will replace the import path with the new path from importMap.
for (const imp of imports) {
const moduleSpecifier = imp.getModuleSpecifierValue();
if (moduleSpecifier.endsWith("types") || moduleSpecifier.endsWith("types.ts")) {
const filePath = sourceFile.getFilePath();
const lineNumber = imp.getStartLineNumber();
const resolvedModule = imp.getModuleSpecifierSourceFile();
if (!resolvedModule || !resolvedModule.getFilePath().includes("/lib/src/common/types.ts")) {
continue;
}
// Collect imports from types.ts.
const namedImports = imp.getNamedImports();
const defaultImport = imp.getDefaultImport();
console.log(`Found import in ${filePath} at line ${lineNumber}:`, {
namedImports: namedImports.map((ni) => ni.getText()),
defaultImport: defaultImport ? defaultImport.getText() : null,
});
// Group imports by their names and generate new import paths based on the importMap
const importsToReplace: Record<string, { name: string; newPath: string; isTypeOnly: boolean }[]> = {};
for (const namedImport of namedImports) {
const name = namedImport.getName();
const newPath = importMap.get(name);
if (newPath) {
console.log(
`Will replace import of ${name} in ${filePath} at line ${lineNumber} with new path:`,
newPath
);
if (!importsToReplace[newPath]) {
importsToReplace[newPath] = [];
}
importsToReplace[newPath].push({
name,
newPath,
isTypeOnly: namedImport.isTypeOnly() || imp.isTypeOnly(),
});
}
}
// For each import, generate a new path from importMap and replace it.
// Split the import when it needs to become multiple imports.
for (const newPath in importsToReplace) {
// First, handle type-only imports.
const isTypeOnly = importsToReplace[newPath].filter((i) => i.isTypeOnly);
if (isTypeOnly.length > 0) {
sourceFile.insertImportDeclaration(imp.getChildIndex(), {
namedImports: isTypeOnly.map((i) => i.name),
moduleSpecifier: newPath,
isTypeOnly: true,
});
}
// Then, handle non-type-only imports.
const isValueImport = importsToReplace[newPath].filter((i) => !i.isTypeOnly);
if (isValueImport.length > 0) {
sourceFile.insertImportDeclaration(imp.getChildIndex(), {
namedImports: isValueImport.map((i) => i.name),
moduleSpecifier: newPath,
isTypeOnly: false,
});
}
// Remove the replaced named imports from the old import.
for (const { name } of importsToReplace[newPath]) {
const namedImport = imp.getNamedImports().find((ni) => ni.getName() === name);
if (namedImport) {
namedImport.remove();
}
}
}
// If there is also a default import and it exists in importMap, replace it too.
if (defaultImport) {
const name = defaultImport.getText();
const newPath = importMap.get(name);
if (newPath) {
console.log(
`Replacing default import of ${name} in ${filePath} at line ${lineNumber} with new path:`,
newPath
);
// Add the new import statement.
sourceFile.insertImportDeclaration(imp.getChildIndex(), {
defaultImport: name,
moduleSpecifier: newPath,
isTypeOnly: imp.isTypeOnly(),
});
// Remove the default import from the old import.
imp.removeDefaultImport();
}
}
if (imp.getNamedImports().length === 0 && !imp.getDefaultImport()) {
// Delete the entire import statement if nothing remains.
imp.remove();
}
}
}
}
// Save everything at the end.
if (!isDryRun) {
project.saveSync();
}
-1
View File
@@ -32,7 +32,6 @@ function toPosixPath(filePath: string): string {
const posixProjectRoot = toPosixPath(projectRoot);
const posixSrc = `${posixProjectRoot}/src`;
const posixLibSrc = `${posixProjectRoot}/src/lib`;
function matchStyleAccess(node: Node): { element: Node; propertyName: string; isComputed: boolean } | undefined {
if (Node.isPropertyAccessExpression(node)) {
-223
View File
@@ -1,223 +0,0 @@
import { Project, SyntaxKind } from "npm:ts-morph";
function processFile(filePath: string, origin: string, repoHash: string): string {
const project = new Project();
const sourceFile = project.addSourceFileAtPath(filePath);
let updated = false;
// 0. insert a commit hash comment at the top of the file
sourceFile.insertText(0, `// @ts-nocheck\n// REPO: ${origin} Commit hash: ${repoHash}\n`);
updated = true;
// 1. Replacements for Uint8Array<ArrayBuffer> and DataView<ArrayBuffer>
let sourceText = sourceFile.getFullText();
if (sourceText.includes("Uint8Array<ArrayBuffer>") || sourceText.includes("DataView<ArrayBuffer>")) {
sourceText = sourceText.replace(/Uint8Array<ArrayBuffer>/g, "Uint8Array");
sourceText = sourceText.replace(/DataView<ArrayBuffer>/g, "DataView");
sourceFile.replaceWithText(sourceText);
updated = true;
}
// 2. Remove EventEmitter import from "events" and declare class EventEmitter inline
const imports = sourceFile.getImportDeclarations();
imports.forEach((importDecl) => {
if (importDecl.getModuleSpecifierValue() === "events") {
const defaultImport = importDecl.getDefaultImport();
if (defaultImport && defaultImport.getText() === "EventEmitter") {
importDecl.remove();
sourceFile.addClass({
name: "EventEmitter",
isExported: false,
methods: [
{
name: "on",
parameters: [
{ name: "event", type: "string | symbol" },
{ name: "listener", type: "(...args: any[]) => void" },
],
returnType: "this",
},
{
name: "once",
parameters: [
{ name: "event", type: "string | symbol" },
{ name: "listener", type: "(...args: any[]) => void" },
],
returnType: "this",
},
{
name: "off",
parameters: [
{ name: "event", type: "string | symbol" },
{ name: "listener", type: "(...args: any[]) => void" },
],
returnType: "this",
},
{
name: "emit",
parameters: [
{ name: "event", type: "string | symbol" },
{ name: "args", isRestParameter: true, type: "any[]" },
],
returnType: "boolean",
},
{
name: "addListener",
parameters: [
{ name: "event", type: "string | symbol" },
{ name: "listener", type: "(...args: any[]) => void" },
],
returnType: "this",
},
{
name: "removeListener",
parameters: [
{ name: "event", type: "string | symbol" },
{ name: "listener", type: "(...args: any[]) => void" },
],
returnType: "this",
},
{
name: "removeAllListeners",
parameters: [{ name: "event", isOptional: true, type: "string | symbol" }],
returnType: "this",
},
],
});
updated = true;
}
}
});
// 3. Collect targets for inline disable comments
const targetAnyLines = new Set<number>();
const targetEmptyObjectLines = new Set<number>();
const targetEmptyInterfaceLines = new Set<number>();
const targetDuplicateEnumLines = new Set<number>();
// 3.1. 'any' type nodes
const anyTypeNodes = sourceFile.getDescendantsOfKind(SyntaxKind.AnyKeyword);
anyTypeNodes.forEach((anyNode: any) => {
const { line } = sourceFile.getLineAndColumnAtPos(anyNode.getStart());
targetAnyLines.add(line - 1);
});
// 3.2. Empty object type literals {}
const typeLiterals = sourceFile.getDescendantsOfKind(SyntaxKind.TypeLiteral);
typeLiterals.forEach((node) => {
if (node.getMembers().length === 0) {
const { line } = sourceFile.getLineAndColumnAtPos(node.getStart());
targetEmptyObjectLines.add(line - 1);
}
});
// 3.3. Empty interfaces
const interfaces = sourceFile.getInterfaces();
interfaces.forEach((node) => {
if (node.getMembers().length === 0) {
const { line } = sourceFile.getLineAndColumnAtPos(node.getStart());
targetEmptyInterfaceLines.add(line - 1);
}
});
// 3.4. Duplicate enum member values
const enums = sourceFile.getEnums();
enums.forEach((enumDecl) => {
const values = new Set<string>();
enumDecl.getMembers().forEach((member) => {
const initValue = member.getInitializer()?.getText();
if (initValue) {
if (values.has(initValue)) {
const { line } = sourceFile.getLineAndColumnAtPos(member.getStart());
targetDuplicateEnumLines.add(line - 1);
} else {
values.add(initValue);
}
}
});
});
// 4. Inject ignore comments line by line
const finalSourceText = sourceFile.getFullText();
const lineBreak = finalSourceText.includes("\r\n") ? "\r\n" : "\n";
const lines = finalSourceText.split(/\r?\n/);
// 4.1. Add inline disable to lines that contain 'any'
for (const lineIndex of targetAnyLines) {
const line = lines[lineIndex];
if (!line) continue;
if (line.includes("eslint-disable-line @typescript-eslint/no-explicit-any")) continue;
lines[lineIndex] = `${line} // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration`;
updated = true;
}
// 4.2. Add inline disable to lines that contain empty object {}
for (const lineIndex of targetEmptyObjectLines) {
const line = lines[lineIndex];
if (!line) continue;
if (line.includes("eslint-disable-line") || line.includes("eslint-disable-next-line")) continue;
lines[lineIndex] =
`${line} // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/ban-types -- Empty object type`;
updated = true;
}
// 4.3. Add inline disable to lines that contain empty interface
for (const lineIndex of targetEmptyInterfaceLines) {
const line = lines[lineIndex];
if (!line) continue;
if (line.includes("eslint-disable-line") || line.includes("eslint-disable-next-line")) continue;
lines[lineIndex] =
`${line} // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-empty-interface -- Empty interface`;
updated = true;
}
// 4.4. Add inline disable to lines with duplicate enums
for (const lineIndex of targetDuplicateEnumLines) {
const line = lines[lineIndex];
if (!line) continue;
if (line.includes("eslint-disable-line") || line.includes("eslint-disable-next-line")) continue;
lines[lineIndex] =
`${line} // eslint-disable-line @typescript-eslint/no-duplicate-enum-values -- Duplicate enum value`;
updated = true;
}
const updatedSourceText = lines.join(lineBreak);
if (updated) {
console.log(`Processed file: ${filePath}`);
}
return updatedSourceText;
}
const targetDir = `./_types`;
async function processDir(dirPath: string) {
for await (const entry of Deno.readDir(dirPath)) {
if (entry.isDirectory) {
await processDir(`${dirPath}/${entry.name}`);
}
if (entry.isFile && entry.name.endsWith(".d.ts")) {
const filePath = `${dirPath}/${entry.name}`;
console.log(`Processing: ${filePath}`);
const updatedContent = processFile(filePath, repoRemoteOriginStr, gitCommitHashStr);
// Write the file. To revert, regenerate it with npm run lib:build:types.
await Deno.writeTextFile(filePath, updatedContent);
}
}
}
const subDir = "./src/lib/";
const repoRemoteOrigins = new Deno.Command("git", {
args: ["remote", "get-url", "origin"],
cwd: subDir,
stdout: "piped",
}).outputSync().stdout;
const repoRemoteOriginStr = new TextDecoder().decode(repoRemoteOrigins).trim();
console.log(`STAMP: Git remote origin: ${repoRemoteOriginStr}`);
const gitCommitHashSub = new Deno.Command("git", {
args: ["rev-parse", "--short", "HEAD"],
cwd: subDir,
stdout: "piped",
}).outputSync().stdout;
const gitCommitHashStr = new TextDecoder().decode(gitCommitHashSub).trim();
console.log(`STAMP: Git commit hash: ${gitCommitHashStr}`);
await processDir(targetDir);