ushman-equiv 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/AGENTS.md +81 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +201 -0
  4. package/bin/ushman-equiv +19 -0
  5. package/dist/analysis-context.d.ts +102 -0
  6. package/dist/analysis-context.d.ts.map +1 -0
  7. package/dist/analysis-context.js +708 -0
  8. package/dist/ast-guards.d.ts +24 -0
  9. package/dist/ast-guards.d.ts.map +1 -0
  10. package/dist/ast-guards.js +83 -0
  11. package/dist/candidate-boot.d.ts +30 -0
  12. package/dist/candidate-boot.d.ts.map +1 -0
  13. package/dist/candidate-boot.js +262 -0
  14. package/dist/canonicalize.d.ts +19 -0
  15. package/dist/canonicalize.d.ts.map +1 -0
  16. package/dist/canonicalize.js +525 -0
  17. package/dist/cli.d.ts +4 -0
  18. package/dist/cli.d.ts.map +1 -0
  19. package/dist/cli.js +312 -0
  20. package/dist/equiv-execution-context.d.ts +25 -0
  21. package/dist/equiv-execution-context.d.ts.map +1 -0
  22. package/dist/equiv-execution-context.js +82 -0
  23. package/dist/index.d.ts +13 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +11 -0
  26. package/dist/run.d.ts +8 -0
  27. package/dist/run.d.ts.map +1 -0
  28. package/dist/run.js +129 -0
  29. package/dist/shared.d.ts +9 -0
  30. package/dist/shared.d.ts.map +1 -0
  31. package/dist/shared.js +47 -0
  32. package/dist/tier-i-import-graph.d.ts +7 -0
  33. package/dist/tier-i-import-graph.d.ts.map +1 -0
  34. package/dist/tier-i-import-graph.js +34 -0
  35. package/dist/tier-l-child-runtime.d.ts +2 -0
  36. package/dist/tier-l-child-runtime.d.ts.map +1 -0
  37. package/dist/tier-l-child-runtime.js +62 -0
  38. package/dist/tier-l-module-load.d.ts +6 -0
  39. package/dist/tier-l-module-load.d.ts.map +1 -0
  40. package/dist/tier-l-module-load.js +139 -0
  41. package/dist/tier-l-stub-source.d.ts +11 -0
  42. package/dist/tier-l-stub-source.d.ts.map +1 -0
  43. package/dist/tier-l-stub-source.js +246 -0
  44. package/dist/tier-r-replay.d.ts +6 -0
  45. package/dist/tier-r-replay.d.ts.map +1 -0
  46. package/dist/tier-r-replay.js +382 -0
  47. package/dist/tier-s-symbol-diff.d.ts +19 -0
  48. package/dist/tier-s-symbol-diff.d.ts.map +1 -0
  49. package/dist/tier-s-symbol-diff.js +156 -0
  50. package/dist/types.d.ts +91 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +19 -0
  53. package/dist/workspace.d.ts +63 -0
  54. package/dist/workspace.d.ts.map +1 -0
  55. package/dist/workspace.js +459 -0
  56. package/package.json +64 -0
@@ -0,0 +1,63 @@
1
+ import type { File } from '@babel/types';
2
+ import type { WorkspacePaths } from 'ushman-lab-types';
3
+ import { assertV4Workspace, buildV3MigrationMessage, createWorkspaceLayoutError, detectWorkspaceLayout, resolveWorkspacePaths, V4_MIGRATION_EXIT_CODE, WorkspaceLayoutError } from 'ushman-lab-types';
4
+ export declare const DEFAULT_MAX_CONCURRENCY = 4;
5
+ export declare const DEFAULT_EQUIV_MODE: "preview";
6
+ export declare const DEFAULT_SRC_ROOTS: readonly ["src"];
7
+ export declare const MAX_MAX_CONCURRENCY = 64;
8
+ type JsonLoadResult<T> = {
9
+ readonly status: 'ok';
10
+ readonly value: T;
11
+ } | {
12
+ readonly status: 'missing';
13
+ } | {
14
+ readonly message: string;
15
+ readonly status: 'invalid';
16
+ };
17
+ export type { WorkspaceLayout, WorkspacePaths } from 'ushman-lab-types';
18
+ export { assertV4Workspace, buildV3MigrationMessage, createWorkspaceLayoutError, detectWorkspaceLayout, resolveWorkspacePaths, V4_MIGRATION_EXIT_CODE, WorkspaceLayoutError, };
19
+ export declare const defaultBaselineSymbolsPath: (workspaceRoot: string, resolvedPaths?: WorkspacePaths) => string;
20
+ export declare const defaultFixturesDir: (workspaceRoot: string, resolvedPaths?: WorkspacePaths) => string;
21
+ export declare const defaultModulesDir: (workspaceRoot: string, resolvedPaths?: WorkspacePaths) => string;
22
+ export declare const defaultResultPath: (workspaceRoot: string, resolvedPaths?: WorkspacePaths) => string;
23
+ export declare const isBareSpecifier: (specifier: string) => boolean;
24
+ export declare const normalizeImportSpecifier: (specifier: string) => string;
25
+ export declare const resolveWithinWorkspace: (workspaceRoot: string, candidatePath: string) => string;
26
+ export declare const displayPath: (workspaceRoot: string, targetPath: string) => string;
27
+ export declare const fileExists: (filePath: string) => Promise<boolean>;
28
+ export declare const directoryExists: (filePath: string) => Promise<boolean>;
29
+ export declare const readJsonFile: <T>(filePath: string) => Promise<JsonLoadResult<T>>;
30
+ export declare const resolveWorkspaceInputPath: (workspaceRoot: string, candidatePath: string) => string;
31
+ export declare const parseSourceText: (filePath: string, source: string) => File;
32
+ export declare const parseSourceFile: (filePath: string) => Promise<{
33
+ readonly ast: File;
34
+ readonly source: string;
35
+ }>;
36
+ export type SourceFileCollection = {
37
+ readonly files: readonly string[];
38
+ readonly missingRoots: readonly string[];
39
+ };
40
+ export declare const collectSourceFiles: ({ includeEntrypoint, srcRoots, workspaceRoot, }: {
41
+ readonly includeEntrypoint?: string;
42
+ readonly srcRoots?: readonly string[];
43
+ readonly workspaceRoot: string;
44
+ }) => Promise<SourceFileCollection>;
45
+ export declare const discoverEntrypoint: ({ entrypoint, workspaceRoot, }: {
46
+ readonly entrypoint?: string;
47
+ readonly workspaceRoot: string;
48
+ }) => Promise<string>;
49
+ export declare const loadImportMap: (workspaceRoot: string) => Promise<Readonly<Record<string, string>>>;
50
+ export declare const importMapMatchesSpecifier: (importMap: Readonly<Record<string, string>>, specifier: string) => string | null;
51
+ export declare const resolveRelativeSpecifier: ({ importerPath, specifier, workspaceRoot, }: {
52
+ readonly importerPath: string;
53
+ readonly specifier: string;
54
+ readonly workspaceRoot: string;
55
+ }) => Promise<string | null>;
56
+ export declare const writeTextFileAtomically: (filePath: string, content: string) => Promise<void>;
57
+ export declare const sha256Text: (value: string) => string;
58
+ export declare const sha256File: (filePath: string) => Promise<string>;
59
+ export declare const createFilterMatcher: (filter: string | undefined) => ((value: string) => boolean);
60
+ export declare const chunk: <T>(values: readonly T[], size: number) => readonly (readonly T[])[];
61
+ export declare const toFileImportUrl: (filePath: string, salt?: string) => string;
62
+ export declare const sanitizeSymbolName: (value: string) => string;
63
+ //# sourceMappingURL=workspace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,459 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { realpathSync } from 'node:fs';
3
+ import { mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+ import { parse } from '@babel/parser';
7
+ import { assertV4Workspace, buildV3MigrationMessage, createWorkspaceLayoutError, detectWorkspaceLayout, resolveWorkspacePaths, V4_MIGRATION_EXIT_CODE, WorkspaceLayoutError, } from 'ushman-lab-types';
8
+ import { compareStrings } from "./shared.js";
9
+ export const DEFAULT_MAX_CONCURRENCY = 4;
10
+ export const DEFAULT_EQUIV_MODE = 'preview';
11
+ export const DEFAULT_SRC_ROOTS = ['src'];
12
+ export const MAX_MAX_CONCURRENCY = 64;
13
+ const DEFAULT_FIXTURES_SUBDIR = 'replay';
14
+ const DEFAULT_MODULES_SUBDIR = 'modules';
15
+ const DEFAULT_RESULT_FILE = 'result.json';
16
+ const DEFAULT_BASELINE_FILE = 'baseline.json';
17
+ const FALLBACK_HTML_CANDIDATES = ['index.html', 'public/index.html'];
18
+ const IGNORED_WALK_DIRECTORIES = new Set(['.git', '.lab', '.vite', 'asl', 'dist', 'node_modules']);
19
+ const SOURCE_EXTENSIONS = ['.cjs', '.js', '.jsx', '.mjs', '.ts', '.tsx'];
20
+ const RELATIVE_RESOLUTION_EXTENSIONS = ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx'];
21
+ const COMMON_BABEL_PLUGINS = ['decorators-legacy', 'dynamicImport', 'importAttributes', 'topLevelAwait'];
22
+ const REAL_WORKSPACE_ROOT_CACHE = new Map();
23
+ const isStringRecord = (value) => value !== null &&
24
+ typeof value === 'object' &&
25
+ !Array.isArray(value) &&
26
+ Object.values(value).every((entry) => typeof entry === 'string');
27
+ export { assertV4Workspace, buildV3MigrationMessage, createWorkspaceLayoutError, detectWorkspaceLayout, resolveWorkspacePaths, V4_MIGRATION_EXIT_CODE, WorkspaceLayoutError, };
28
+ const isSourceExtension = (filePath) => SOURCE_EXTENSIONS.some((extension) => filePath.endsWith(extension));
29
+ const resolveV4Paths = (workspaceRoot, resolvedPaths) => (resolvedPaths ?? resolveWorkspacePaths(workspaceRoot)).v4;
30
+ export const defaultBaselineSymbolsPath = (workspaceRoot, resolvedPaths) => path.join(resolveV4Paths(workspaceRoot, resolvedPaths).equiv, DEFAULT_BASELINE_FILE);
31
+ export const defaultFixturesDir = (workspaceRoot, resolvedPaths) => path.join(resolveV4Paths(workspaceRoot, resolvedPaths).characterize, DEFAULT_FIXTURES_SUBDIR);
32
+ export const defaultModulesDir = (workspaceRoot, resolvedPaths) => path.join(resolveV4Paths(workspaceRoot, resolvedPaths).characterize, DEFAULT_MODULES_SUBDIR);
33
+ export const defaultResultPath = (workspaceRoot, resolvedPaths) => path.join(resolveV4Paths(workspaceRoot, resolvedPaths).equiv, DEFAULT_RESULT_FILE);
34
+ export const isBareSpecifier = (specifier) => !specifier.startsWith('.') &&
35
+ !specifier.startsWith('/') &&
36
+ !specifier.startsWith('file:') &&
37
+ !specifier.startsWith('node:');
38
+ export const normalizeImportSpecifier = (specifier) => specifier.replace(/[?#].*$/u, '');
39
+ const isWithinWorkspace = (workspaceRoot, targetPath) => {
40
+ const relativePath = path.relative(workspaceRoot, targetPath);
41
+ return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
42
+ };
43
+ const resolveLexicalWorkspaceRoot = (workspaceRoot) => path.resolve(workspaceRoot);
44
+ const resolveRealWorkspaceRoot = (workspaceRoot) => {
45
+ const lexicalRoot = resolveLexicalWorkspaceRoot(workspaceRoot);
46
+ const cached = REAL_WORKSPACE_ROOT_CACHE.get(lexicalRoot);
47
+ if (cached) {
48
+ return cached;
49
+ }
50
+ const realRoot = realpathSync(lexicalRoot);
51
+ REAL_WORKSPACE_ROOT_CACHE.set(lexicalRoot, realRoot);
52
+ return realRoot;
53
+ };
54
+ const resolveWorkspaceCandidatePath = (workspaceRoot, candidatePath) => {
55
+ const lexicalRoot = resolveLexicalWorkspaceRoot(workspaceRoot);
56
+ if (path.isAbsolute(candidatePath) && isWithinWorkspace(lexicalRoot, candidatePath)) {
57
+ return path.resolve(candidatePath);
58
+ }
59
+ return candidatePath.startsWith('/')
60
+ ? path.resolve(lexicalRoot, `.${candidatePath}`)
61
+ : path.resolve(lexicalRoot, candidatePath);
62
+ };
63
+ const resolveNearestExistingRealPath = (rootPath, candidatePath) => {
64
+ let currentPath = candidatePath;
65
+ while (true) {
66
+ try {
67
+ return realpathSync(currentPath);
68
+ }
69
+ catch (error) {
70
+ const code = error.code;
71
+ if (code !== 'ENOENT') {
72
+ throw error;
73
+ }
74
+ if (currentPath === rootPath) {
75
+ throw error;
76
+ }
77
+ const parentPath = path.dirname(currentPath);
78
+ if (!isWithinWorkspace(rootPath, parentPath)) {
79
+ return realpathSync(rootPath);
80
+ }
81
+ currentPath = parentPath;
82
+ }
83
+ }
84
+ };
85
+ const assertWithinWorkspacePath = (workspaceRoot, candidatePath) => {
86
+ const lexicalRoot = resolveLexicalWorkspaceRoot(workspaceRoot);
87
+ const resolvedPath = path.resolve(candidatePath);
88
+ if (!isWithinWorkspace(lexicalRoot, resolvedPath)) {
89
+ throw new Error(`Path escapes workspace root: ${candidatePath}`);
90
+ }
91
+ const realRoot = resolveRealWorkspaceRoot(workspaceRoot);
92
+ const nearestExistingRealPath = resolveNearestExistingRealPath(lexicalRoot, resolvedPath);
93
+ if (!isWithinWorkspace(realRoot, nearestExistingRealPath)) {
94
+ throw new Error(`Path escapes workspace root via symlink: ${candidatePath}`);
95
+ }
96
+ return resolvedPath;
97
+ };
98
+ export const resolveWithinWorkspace = (workspaceRoot, candidatePath) => assertWithinWorkspacePath(workspaceRoot, resolveWorkspaceCandidatePath(workspaceRoot, candidatePath));
99
+ export const displayPath = (workspaceRoot, targetPath) => path.relative(workspaceRoot, targetPath) || '.';
100
+ export const fileExists = async (filePath) => {
101
+ try {
102
+ return (await stat(filePath)).isFile();
103
+ }
104
+ catch {
105
+ return false;
106
+ }
107
+ };
108
+ const getPathKind = async (filePath) => {
109
+ try {
110
+ const targetStat = await stat(filePath);
111
+ if (targetStat.isFile()) {
112
+ return 'file';
113
+ }
114
+ if (targetStat.isDirectory()) {
115
+ return 'directory';
116
+ }
117
+ return null;
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ };
123
+ export const directoryExists = async (filePath) => {
124
+ try {
125
+ return (await stat(filePath)).isDirectory();
126
+ }
127
+ catch {
128
+ return false;
129
+ }
130
+ };
131
+ export const readJsonFile = async (filePath) => {
132
+ try {
133
+ return { status: 'ok', value: JSON.parse(await readFile(filePath, 'utf8')) };
134
+ }
135
+ catch (error) {
136
+ if (error.code === 'ENOENT') {
137
+ return { status: 'missing' };
138
+ }
139
+ return {
140
+ message: error instanceof Error ? error.message : String(error),
141
+ status: 'invalid',
142
+ };
143
+ }
144
+ };
145
+ export const resolveWorkspaceInputPath = (workspaceRoot, candidatePath) => path.isAbsolute(candidatePath) ? path.resolve(candidatePath) : path.resolve(workspaceRoot, candidatePath);
146
+ const parserPluginsForPath = (filePath) => {
147
+ const plugins = [...COMMON_BABEL_PLUGINS];
148
+ if (filePath.endsWith('.ts') || filePath.endsWith('.tsx')) {
149
+ plugins.push('typescript');
150
+ }
151
+ if (filePath.endsWith('.jsx') || filePath.endsWith('.tsx')) {
152
+ plugins.push('jsx');
153
+ }
154
+ return plugins;
155
+ };
156
+ const parserSourceTypeForPath = (filePath) => {
157
+ if (filePath.endsWith('.cjs')) {
158
+ return 'script';
159
+ }
160
+ if (filePath.endsWith('.jsx') ||
161
+ filePath.endsWith('.mjs') ||
162
+ filePath.endsWith('.ts') ||
163
+ filePath.endsWith('.tsx')) {
164
+ return 'module';
165
+ }
166
+ return 'unambiguous';
167
+ };
168
+ export const parseSourceText = (filePath, source) => parse(source, {
169
+ plugins: parserPluginsForPath(filePath),
170
+ sourceFilename: filePath,
171
+ sourceType: parserSourceTypeForPath(filePath),
172
+ });
173
+ export const parseSourceFile = async (filePath) => {
174
+ const source = await readFile(filePath, 'utf8');
175
+ return {
176
+ ast: parseSourceText(filePath, source),
177
+ source,
178
+ };
179
+ };
180
+ const shouldIgnoreWalkDirectory = (entryName) => IGNORED_WALK_DIRECTORIES.has(entryName) || (entryName.startsWith('.') && entryName !== '.');
181
+ const walkDirectory = async (directoryPath) => {
182
+ const files = [];
183
+ const pendingDirectories = [directoryPath];
184
+ while (pendingDirectories.length > 0) {
185
+ const currentDirectory = pendingDirectories.pop();
186
+ if (!currentDirectory) {
187
+ continue;
188
+ }
189
+ const entries = await readdir(currentDirectory, { withFileTypes: true });
190
+ for (const entry of entries) {
191
+ const targetPath = path.join(currentDirectory, entry.name);
192
+ if (entry.isDirectory()) {
193
+ if (shouldIgnoreWalkDirectory(entry.name)) {
194
+ continue;
195
+ }
196
+ pendingDirectories.push(targetPath);
197
+ continue;
198
+ }
199
+ if (entry.isFile()) {
200
+ files.push(targetPath);
201
+ }
202
+ }
203
+ }
204
+ return files;
205
+ };
206
+ export const collectSourceFiles = async ({ includeEntrypoint, srcRoots, workspaceRoot, }) => {
207
+ const missingRoots = [];
208
+ const collectFromSourceRoot = async (files, srcRoot) => {
209
+ const absoluteRoot = resolveWithinWorkspace(workspaceRoot, srcRoot);
210
+ const pathKind = await getPathKind(absoluteRoot);
211
+ if (pathKind === 'file') {
212
+ if (isSourceExtension(absoluteRoot)) {
213
+ files.add(absoluteRoot);
214
+ }
215
+ return;
216
+ }
217
+ if (pathKind !== 'directory') {
218
+ missingRoots.push(srcRoot);
219
+ return;
220
+ }
221
+ for (const candidateFile of await walkDirectory(absoluteRoot)) {
222
+ if (isSourceExtension(candidateFile)) {
223
+ files.add(candidateFile);
224
+ }
225
+ }
226
+ };
227
+ const files = new Set();
228
+ for (const srcRoot of srcRoots ?? DEFAULT_SRC_ROOTS) {
229
+ await collectFromSourceRoot(files, srcRoot);
230
+ }
231
+ if (includeEntrypoint && (await fileExists(includeEntrypoint))) {
232
+ files.add(includeEntrypoint);
233
+ }
234
+ return {
235
+ files: [...files].sort(compareStrings),
236
+ missingRoots,
237
+ };
238
+ };
239
+ const readWorkspaceHtmlDocument = async (workspaceRoot) => {
240
+ for (const relativePath of FALLBACK_HTML_CANDIDATES) {
241
+ const absolutePath = resolveWithinWorkspace(workspaceRoot, relativePath);
242
+ if (!(await fileExists(absolutePath))) {
243
+ continue;
244
+ }
245
+ return {
246
+ htmlPath: absolutePath,
247
+ source: await readFile(absolutePath, 'utf8'),
248
+ };
249
+ }
250
+ return null;
251
+ };
252
+ const parseHtmlAttributes = (rawAttributes) => {
253
+ const attributes = {};
254
+ const attributePattern = /([^\s=/>]+)(?:\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/gu;
255
+ for (const match of rawAttributes.matchAll(attributePattern)) {
256
+ const [, attributeName, , doubleQuoted, singleQuoted, bareValue] = match;
257
+ attributes[attributeName.toLowerCase()] = doubleQuoted ?? singleQuoted ?? bareValue ?? '';
258
+ }
259
+ return attributes;
260
+ };
261
+ const parseScriptTags = (html) => [...html.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/giu)].map((match) => ({
262
+ attributes: parseHtmlAttributes(match[1] ?? ''),
263
+ content: (match[2] ?? '').trim(),
264
+ }));
265
+ const scoreModuleScriptSource = (candidatePath) => {
266
+ if (/^https?:\/\//u.test(candidatePath)) {
267
+ return 0;
268
+ }
269
+ if (/^(?:\/|\.\/)?src\/(?:main|index|app)\.[cm]?[jt]sx?$/u.test(candidatePath)) {
270
+ return 100;
271
+ }
272
+ if (/^(?:\/|\.\/)?src\//u.test(candidatePath)) {
273
+ return 80;
274
+ }
275
+ if (/^(?:\/|\.\/)?(?:main|index|app)\.[cm]?[jt]sx?$/u.test(candidatePath)) {
276
+ return 60;
277
+ }
278
+ return 40;
279
+ };
280
+ const extractScriptSource = (html) => {
281
+ const moduleScripts = parseScriptTags(html)
282
+ .map((scriptTag, index) => ({
283
+ index,
284
+ scriptTag,
285
+ }))
286
+ .filter((entry) => entry.scriptTag.attributes.type === 'module' && typeof entry.scriptTag.attributes.src === 'string')
287
+ .sort((left, right) => scoreModuleScriptSource(right.scriptTag.attributes.src) -
288
+ scoreModuleScriptSource(left.scriptTag.attributes.src) || left.index - right.index);
289
+ return moduleScripts[0]?.scriptTag.attributes.src ?? null;
290
+ };
291
+ const extractImportMapJson = (html) => {
292
+ for (const scriptTag of parseScriptTags(html)) {
293
+ if (scriptTag.attributes.type === 'importmap') {
294
+ return scriptTag.content;
295
+ }
296
+ }
297
+ return null;
298
+ };
299
+ const buildRegexMatcher = (pattern) => {
300
+ try {
301
+ return new RegExp(pattern);
302
+ }
303
+ catch (error) {
304
+ throw new Error(`Invalid filter regex: ${error instanceof Error ? error.message : String(error)}`);
305
+ }
306
+ };
307
+ const resolveHtmlRelativePath = (workspaceRoot, htmlPath, candidatePath) => {
308
+ if (candidatePath.startsWith('/')) {
309
+ return resolveWithinWorkspace(workspaceRoot, candidatePath);
310
+ }
311
+ return assertWithinWorkspacePath(workspaceRoot, path.resolve(path.dirname(htmlPath), candidatePath));
312
+ };
313
+ export const discoverEntrypoint = async ({ entrypoint, workspaceRoot, }) => {
314
+ if (entrypoint) {
315
+ const resolvedEntrypoint = resolveWithinWorkspace(workspaceRoot, entrypoint);
316
+ if (!(await fileExists(resolvedEntrypoint))) {
317
+ throw new Error(`Entrypoint file does not exist: ${entrypoint}`);
318
+ }
319
+ return resolvedEntrypoint;
320
+ }
321
+ const htmlDocument = await readWorkspaceHtmlDocument(workspaceRoot);
322
+ if (!htmlDocument) {
323
+ throw new Error(`Unable to locate index.html under ${workspaceRoot}`);
324
+ }
325
+ const scriptSource = extractScriptSource(htmlDocument.source);
326
+ if (!scriptSource) {
327
+ throw new Error(`Unable to locate <script type="module" src="..."> in ${displayPath(workspaceRoot, htmlDocument.htmlPath)}`);
328
+ }
329
+ return resolveHtmlRelativePath(workspaceRoot, htmlDocument.htmlPath, scriptSource);
330
+ };
331
+ export const loadImportMap = async (workspaceRoot) => {
332
+ const htmlDocument = await readWorkspaceHtmlDocument(workspaceRoot);
333
+ if (!htmlDocument) {
334
+ return {};
335
+ }
336
+ const importMapScript = parseScriptTags(htmlDocument.source).find((scriptTag) => scriptTag.attributes.type === 'importmap');
337
+ if (!importMapScript) {
338
+ return {};
339
+ }
340
+ const importMapJson = typeof importMapScript.attributes.src === 'string' && importMapScript.attributes.src.length > 0
341
+ ? await readFile(resolveHtmlRelativePath(workspaceRoot, htmlDocument.htmlPath, importMapScript.attributes.src), 'utf8')
342
+ : extractImportMapJson(htmlDocument.source);
343
+ if (!importMapJson) {
344
+ return {};
345
+ }
346
+ const parsed = JSON.parse(importMapJson);
347
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
348
+ throw new Error('Invalid importmap JSON: expected a top-level object.');
349
+ }
350
+ if (parsed.imports === undefined) {
351
+ return {};
352
+ }
353
+ if (!isStringRecord(parsed.imports)) {
354
+ throw new Error('Invalid importmap JSON: imports must be an object with string values.');
355
+ }
356
+ return parsed.imports;
357
+ };
358
+ export const importMapMatchesSpecifier = (importMap, specifier) => {
359
+ if (specifier in importMap) {
360
+ return importMap[specifier] ?? null;
361
+ }
362
+ const prefixMatch = Object.keys(importMap)
363
+ .filter((key) => key.endsWith('/'))
364
+ .sort((left, right) => right.length - left.length || compareStrings(left, right))
365
+ .find((key) => specifier.startsWith(key));
366
+ return prefixMatch ? (importMap[prefixMatch] ?? null) : null;
367
+ };
368
+ export const resolveRelativeSpecifier = async ({ importerPath, specifier, workspaceRoot, }) => {
369
+ const normalizedSpecifier = normalizeImportSpecifier(specifier);
370
+ const basePath = normalizedSpecifier.startsWith('/')
371
+ ? resolveWithinWorkspace(workspaceRoot, normalizedSpecifier)
372
+ : path.resolve(path.dirname(importerPath), normalizedSpecifier);
373
+ const candidates = new Set([basePath]);
374
+ if (!path.extname(basePath)) {
375
+ for (const extension of RELATIVE_RESOLUTION_EXTENSIONS) {
376
+ candidates.add(`${basePath}${extension}`);
377
+ }
378
+ for (const extension of RELATIVE_RESOLUTION_EXTENSIONS) {
379
+ candidates.add(path.join(basePath, `index${extension}`));
380
+ }
381
+ }
382
+ const existenceResults = await Promise.all([...candidates].map(async (candidatePath) => ({
383
+ candidatePath,
384
+ exists: await fileExists(candidatePath),
385
+ })));
386
+ for (const result of existenceResults) {
387
+ if (result.exists) {
388
+ return result.candidatePath;
389
+ }
390
+ }
391
+ return null;
392
+ };
393
+ export const writeTextFileAtomically = async (filePath, content) => {
394
+ await mkdir(path.dirname(filePath), { recursive: true });
395
+ const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
396
+ try {
397
+ await writeFile(tempPath, content, 'utf8');
398
+ await rename(tempPath, filePath);
399
+ }
400
+ catch (error) {
401
+ await rm(tempPath, { force: true }).catch(() => undefined);
402
+ throw error;
403
+ }
404
+ };
405
+ export const sha256Text = (value) => createHash('sha256').update(value).digest('hex');
406
+ export const sha256File = async (filePath) => sha256Text(await readFile(filePath, 'utf8'));
407
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
408
+ const buildGlobExpression = (filter) => {
409
+ const patternParts = ['^'];
410
+ for (let index = 0; index < filter.length; index += 1) {
411
+ const character = filter[index];
412
+ const nextCharacter = filter[index + 1];
413
+ if (character === '*' && nextCharacter === '*') {
414
+ patternParts.push('.*');
415
+ index += 1;
416
+ continue;
417
+ }
418
+ if (character === '*') {
419
+ patternParts.push('[^/]*');
420
+ continue;
421
+ }
422
+ if (character === '?') {
423
+ patternParts.push('[^/]');
424
+ continue;
425
+ }
426
+ patternParts.push(escapeRegExp(character));
427
+ }
428
+ patternParts.push('$');
429
+ return new RegExp(patternParts.join(''));
430
+ };
431
+ export const createFilterMatcher = (filter) => {
432
+ if (!filter) {
433
+ return () => true;
434
+ }
435
+ if (filter.startsWith('/') && filter.endsWith('/') && filter.length > 1) {
436
+ const expression = buildRegexMatcher(filter.slice(1, -1));
437
+ return (value) => expression.test(value);
438
+ }
439
+ const expression = buildGlobExpression(filter);
440
+ return (value) => expression.test(value);
441
+ };
442
+ export const chunk = (values, size) => {
443
+ if (!Number.isFinite(size) || size <= 0) {
444
+ return [values];
445
+ }
446
+ const chunks = [];
447
+ for (let index = 0; index < values.length; index += size) {
448
+ chunks.push(values.slice(index, index + size));
449
+ }
450
+ return chunks;
451
+ };
452
+ export const toFileImportUrl = (filePath, salt) => {
453
+ const fileUrl = pathToFileURL(filePath);
454
+ if (salt) {
455
+ fileUrl.searchParams.set('ushman-equiv', salt);
456
+ }
457
+ return fileUrl.href;
458
+ };
459
+ export const sanitizeSymbolName = (value) => value.replace(/[^\w.-]+/gu, '_');
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "author": {
3
+ "name": "Ragaeeb Haq",
4
+ "url": "https://github.com/ragaeeb"
5
+ },
6
+ "bin": {
7
+ "ushman-equiv": "dist/cli.js"
8
+ },
9
+ "bugs": {
10
+ "url": "https://github.com/ragaeeb/ushman-equiv/issues"
11
+ },
12
+ "dependencies": {
13
+ "@babel/parser": "^7.29.3",
14
+ "@babel/traverse": "^7.29.0",
15
+ "@babel/types": "^7.29.0",
16
+ "ushman-ledger": "^0.3.0"
17
+ },
18
+ "description": "Pure-Node equivalence checks for ushman refactors. Runs import-graph, module-load, symbol-diff, and replay tiers inside Node-only sandboxes.",
19
+ "devDependencies": {
20
+ "@biomejs/biome": "^2.4.15",
21
+ "@types/babel__traverse": "^7.28.0",
22
+ "@types/bun": "1.3.14",
23
+ "@types/node": "^25.8.0",
24
+ "typescript": "^6.0.3",
25
+ "ushman-lab-types": "^0.1.0"
26
+ },
27
+ "engines": {
28
+ "node": ">=24"
29
+ },
30
+ "files": [
31
+ "bin",
32
+ "dist",
33
+ "README.md",
34
+ "AGENTS.md",
35
+ "LICENSE.md"
36
+ ],
37
+ "homepage": "https://github.com/ragaeeb/ushman-equiv#readme",
38
+ "keywords": [
39
+ "ushman",
40
+ "equiv",
41
+ "equivalence",
42
+ "llm",
43
+ "reverse-engineering"
44
+ ],
45
+ "license": "MIT",
46
+ "main": "dist/index.js",
47
+ "name": "ushman-equiv",
48
+ "packageManager": "bun@1.3.14",
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/ragaeeb/ushman-equiv.git"
52
+ },
53
+ "scripts": {
54
+ "build": "tsc -p tsconfig.build.json",
55
+ "check": "bun run lint && bun run typecheck && bun test && bun run build",
56
+ "format": "biome check . --write && biome lint . --write",
57
+ "lint": "biome lint .",
58
+ "prepack": "tsc -p tsconfig.build.json",
59
+ "typecheck": "tsc --noEmit -p tsconfig.json"
60
+ },
61
+ "type": "module",
62
+ "types": "dist/index.d.ts",
63
+ "version": "0.4.0"
64
+ }