@emeryld/rrroutes-export 1.0.7 → 1.0.9

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.
package/README.md CHANGED
@@ -66,6 +66,8 @@ Settings:
66
66
  - `--html <path>`: optional. Baked HTML viewer for changelog payload.
67
67
  - `--tsconfig <path>`: optional. TS config used for source extraction in snapshots.
68
68
  - `--with-source`: accepted for parity; changelog mode always enables source extraction.
69
+ - `--cache-dir <path>`: optional. Directory for per-commit compile cache artifacts.
70
+ - `--no-cache`: optional flag. Disable compile cache and force recompilation.
69
71
 
70
72
  Behavior summary:
71
73
 
@@ -73,6 +75,8 @@ Behavior summary:
73
75
  - Filters commits to those touching module tree/relevant config files.
74
76
  - Ensures tip commit is always included.
75
77
  - Creates a temp git worktree per selected commit.
78
+ - For TypeScript module entries, compiles the commit snapshot to JS and imports emitted output.
79
+ - For JavaScript module entries, imports directly from worktree.
76
80
  - Exports snapshots, then computes adjacent diffs:
77
81
  - route events (`route_added`, `route_removed`, `schema_changed`, `cfg_changed`)
78
82
  - source-object events (`source_object_added`, `source_object_removed`, `source_schema_changed`)
@@ -80,8 +84,15 @@ Behavior summary:
80
84
  Runtime progress logs:
81
85
 
82
86
  - `rrroutes-export-changelog` now prints step-by-step progress (window resolution, commit filtering, per-commit snapshot progress, diff totals, output writing, cleanup).
87
+ - Changelog logs include compile lifecycle details: selected tsconfig, cache hit/miss, emitted entry path.
83
88
  - Bundle script prints stage logs for snapshot export + changelog export.
84
89
 
90
+ Troubleshooting historical TS imports:
91
+
92
+ - Changelog execution no longer relies on direct Node runtime loading of TS sources.
93
+ - Historical imports like `./x.js` pointing to `x.ts` are handled by snapshot compilation.
94
+ - If tsconfig auto-discovery fails, pass `--tsconfig`.
95
+
85
96
  ## Recommended end-to-end script
86
97
 
87
98
  Use this when you want one command to produce everything (snapshot + changelog + HTML + summary + timestamped storage).
@@ -95,6 +106,7 @@ pnpm --filter @emeryld/rrroutes-export run export:bundle -- \
95
106
  --tsconfig ./tsconfig.json \
96
107
  --from v1.4.0 \
97
108
  --to HEAD \
109
+ --cache-dir ./.rroutes-exports/.cache \
98
110
  --dir ./.rrroutes-exports
99
111
  ```
100
112
 
@@ -105,6 +117,8 @@ Bundle script settings (`scripts/export-release-bundle.ts`):
105
117
  - `--tsconfig <path>`: optional.
106
118
  - `--from <rev>`: optional.
107
119
  - `--to <rev>`: optional.
120
+ - `--cache-dir <path>`: optional.
121
+ - `--no-cache`: optional flag.
108
122
  - `--dir <path>`: optional base directory for stored bundles. Default: `.rrroutes-exports`.
109
123
 
110
124
  Per run, it writes a timestamped directory:
@@ -138,6 +152,8 @@ const changelog = await exportFinalizedLeavesChangelog({
138
152
  exportName: 'leaves',
139
153
  from: 'v1.4.0',
140
154
  to: 'HEAD',
155
+ cacheDir: './.cache/rrroutes-export',
156
+ noCache: false,
141
157
  outFile: './finalized-leaves.changelog.json',
142
158
  htmlFile: './finalized-leaves.changelog.viewer.html',
143
159
  log: (message) => console.log(message),
@@ -6,6 +6,8 @@ export type FinalizedLeavesChangelogCliArgs = {
6
6
  tsconfigPath?: string;
7
7
  from?: string;
8
8
  to?: string;
9
+ cacheDir?: string;
10
+ noCache: boolean;
9
11
  };
10
12
  export declare function parseFinalizedLeavesChangelogCliArgs(argv: string[]): FinalizedLeavesChangelogCliArgs;
11
13
  export declare function runExportFinalizedLeavesChangelogCli(argv: string[]): Promise<{
@@ -115,6 +115,8 @@ export type ExportFinalizedLeavesChangelogOptions = {
115
115
  includeSource?: boolean;
116
116
  handlers?: ExportFinalizedLeavesOptions['handlers'];
117
117
  log?: (message: string) => void;
118
+ cacheDir?: string;
119
+ noCache?: boolean;
118
120
  };
119
121
  declare function resolveCommitWindow(cwd: string, from?: string, to?: string): Promise<{
120
122
  from: string;
@@ -125,6 +127,26 @@ declare function filterCommits(cwd: string, commits: string[], moduleRel: string
125
127
  declare function buildSourceIdentity(section: ChangelogSchemaSection, value: Record<string, unknown>): SourceIdentity;
126
128
  declare function toRouteSnapshots(payload: FinalizedLeavesExport): Record<string, RouteSnapshot>;
127
129
  declare function toSourceObjectSnapshots(routesByKey: Record<string, RouteSnapshot>): Record<string, SourceObjectSnapshot>;
130
+ declare function resolveSnapshotTsconfig(worktreeDir: string, modulePath: string, tsconfigOverride?: string): Promise<string>;
131
+ declare function toEmittedModulePath(moduleRel: string): string;
132
+ declare function buildCompileCacheKey(commitSha: string, moduleRel: string, tsconfigPath: string, tsconfigStat: {
133
+ mtimeMs: number;
134
+ size: number;
135
+ }): string;
136
+ declare function compileSnapshotEntry(options: {
137
+ worktreeDir: string;
138
+ moduleRel: string;
139
+ modulePath: string;
140
+ tsconfigPath: string;
141
+ commitSha: string;
142
+ cacheRoot: string;
143
+ noCache?: boolean;
144
+ log?: (message: string) => void;
145
+ }): Promise<string>;
146
+ declare function loadSnapshotExport(options: {
147
+ entryPath: string;
148
+ exportName: string;
149
+ }): Promise<{}>;
128
150
  declare function diffSnapshots(previous: CommitSnapshot, current: CommitSnapshot): {
129
151
  routeEvents: RouteChangeEvent[];
130
152
  sourceEvents: SourceObjectChangeEvent[];
@@ -138,6 +160,11 @@ export declare const __private: {
138
160
  resolveCommitWindow: typeof resolveCommitWindow;
139
161
  resolveCommitPath: typeof resolveCommitPath;
140
162
  filterCommits: typeof filterCommits;
163
+ resolveSnapshotTsconfig: typeof resolveSnapshotTsconfig;
164
+ toEmittedModulePath: typeof toEmittedModulePath;
165
+ buildCompileCacheKey: typeof buildCompileCacheKey;
166
+ compileSnapshotEntry: typeof compileSnapshotEntry;
167
+ loadSnapshotExport: typeof loadSnapshotExport;
141
168
  toRouteSnapshots: typeof toRouteSnapshots;
142
169
  toSourceObjectSnapshots: typeof toSourceObjectSnapshots;
143
170
  diffSnapshots: typeof diffSnapshots;
package/dist/index.cjs CHANGED
@@ -1394,9 +1394,11 @@ async function runExportFinalizedLeavesCli(argv) {
1394
1394
  var import_promises2 = __toESM(require("fs/promises"), 1);
1395
1395
  var import_node_os = __toESM(require("os"), 1);
1396
1396
  var import_node_path4 = __toESM(require("path"), 1);
1397
+ var import_node_crypto = __toESM(require("crypto"), 1);
1397
1398
  var import_node_child_process2 = require("child_process");
1398
1399
  var import_node_util = require("util");
1399
1400
  var import_node_url2 = require("url");
1401
+ var import_typescript2 = __toESM(require("typescript"), 1);
1400
1402
  var execFile = (0, import_node_util.promisify)(import_node_child_process2.execFile);
1401
1403
  var CHANGESET_CFG_FIELDS = [
1402
1404
  "summary",
@@ -1871,20 +1873,221 @@ async function ensureWorktreeDependencies(repoRoot, worktreeDir) {
1871
1873
  }
1872
1874
  await import_promises2.default.symlink(rootNodeModules, worktreeNodeModules, "junction");
1873
1875
  }
1874
- async function createSnapshot(repoRoot, moduleRel, exportName, commit, tempRoot, tsconfigRel, exportOptions) {
1876
+ async function pathExists(filePath) {
1877
+ try {
1878
+ await import_promises2.default.access(filePath);
1879
+ return true;
1880
+ } catch {
1881
+ return false;
1882
+ }
1883
+ }
1884
+ function isTypeScriptModule(modulePath) {
1885
+ const ext = import_node_path4.default.extname(modulePath).toLowerCase();
1886
+ return ext === ".ts" || ext === ".tsx" || ext === ".mts" || ext === ".cts";
1887
+ }
1888
+ function isJavaScriptModule(modulePath) {
1889
+ const ext = import_node_path4.default.extname(modulePath).toLowerCase();
1890
+ return ext === ".js" || ext === ".mjs" || ext === ".cjs";
1891
+ }
1892
+ async function resolveSnapshotTsconfig(worktreeDir, modulePath, tsconfigOverride) {
1893
+ if (tsconfigOverride) {
1894
+ const resolved = import_node_path4.default.resolve(worktreeDir, tsconfigOverride);
1895
+ if (!await pathExists(resolved)) {
1896
+ throw new Error(`tsconfig not found: ${resolved}`);
1897
+ }
1898
+ return resolved;
1899
+ }
1900
+ let current = import_node_path4.default.dirname(modulePath);
1901
+ const normalizedRoot = normalizePath(import_node_path4.default.resolve(worktreeDir));
1902
+ while (true) {
1903
+ const candidate = import_node_path4.default.join(current, "tsconfig.json");
1904
+ if (await pathExists(candidate)) {
1905
+ return candidate;
1906
+ }
1907
+ const parent = import_node_path4.default.dirname(current);
1908
+ if (parent === current) break;
1909
+ if (!normalizePath(parent).startsWith(normalizedRoot)) break;
1910
+ current = parent;
1911
+ }
1912
+ throw new Error(
1913
+ `Unable to resolve tsconfig for TypeScript module: ${modulePath}. Pass --tsconfig to override.`
1914
+ );
1915
+ }
1916
+ function toEmittedModulePath(moduleRel) {
1917
+ const normalized = normalizePath(moduleRel);
1918
+ if (normalized.endsWith(".mts")) return normalized.slice(0, -4) + ".mjs";
1919
+ if (normalized.endsWith(".cts")) return normalized.slice(0, -4) + ".cjs";
1920
+ if (normalized.endsWith(".tsx")) return normalized.slice(0, -4) + ".js";
1921
+ if (normalized.endsWith(".ts")) return normalized.slice(0, -3) + ".js";
1922
+ return normalized;
1923
+ }
1924
+ function buildCompileCacheKey(commitSha, moduleRel, tsconfigPath, tsconfigStat) {
1925
+ return import_node_crypto.default.createHash("sha1").update(commitSha).update("\n").update(normalizePath(moduleRel)).update("\n").update(normalizePath(tsconfigPath)).update("\n").update(String(tsconfigStat.mtimeMs)).update("\n").update(String(tsconfigStat.size)).digest("hex");
1926
+ }
1927
+ async function collectFilesRecursive(rootDir) {
1928
+ const files = [];
1929
+ const walk = async (dir) => {
1930
+ const entries = await import_promises2.default.readdir(dir, { withFileTypes: true });
1931
+ for (const entry of entries) {
1932
+ const target = import_node_path4.default.join(dir, entry.name);
1933
+ if (entry.isDirectory()) {
1934
+ await walk(target);
1935
+ continue;
1936
+ }
1937
+ if (entry.isFile()) {
1938
+ files.push(target);
1939
+ }
1940
+ }
1941
+ };
1942
+ if (await pathExists(rootDir)) {
1943
+ await walk(rootDir);
1944
+ }
1945
+ return files;
1946
+ }
1947
+ function suffixMatchScore(candidate, wanted) {
1948
+ const left = normalizePath(candidate).split("/");
1949
+ const right = normalizePath(wanted).split("/");
1950
+ let score = 0;
1951
+ while (score < left.length && score < right.length) {
1952
+ const a = left[left.length - 1 - score];
1953
+ const b = right[right.length - 1 - score];
1954
+ if (a !== b) break;
1955
+ score += 1;
1956
+ }
1957
+ return score;
1958
+ }
1959
+ async function resolveEmittedEntryPath(emitRoot, moduleRel) {
1960
+ const expected = import_node_path4.default.resolve(emitRoot, toEmittedModulePath(moduleRel));
1961
+ if (await pathExists(expected)) {
1962
+ return expected;
1963
+ }
1964
+ const wantedRel = toEmittedModulePath(moduleRel);
1965
+ const wantedBase = import_node_path4.default.posix.basename(normalizePath(wantedRel));
1966
+ const files = await collectFilesRecursive(emitRoot);
1967
+ const candidates = files.filter(
1968
+ (file) => import_node_path4.default.posix.basename(normalizePath(file)) === wantedBase
1969
+ );
1970
+ if (candidates.length === 0) {
1971
+ return void 0;
1972
+ }
1973
+ candidates.sort(
1974
+ (a, b) => suffixMatchScore(b, wantedRel) - suffixMatchScore(a, wantedRel)
1975
+ );
1976
+ return candidates[0];
1977
+ }
1978
+ async function compileSnapshotEntry(options) {
1979
+ const log = options.log ?? (() => void 0);
1980
+ const tsconfigStat = await import_promises2.default.stat(options.tsconfigPath);
1981
+ const cacheKey = buildCompileCacheKey(
1982
+ options.commitSha,
1983
+ options.moduleRel,
1984
+ options.tsconfigPath,
1985
+ { mtimeMs: tsconfigStat.mtimeMs, size: tsconfigStat.size }
1986
+ );
1987
+ const emitRoot = import_node_path4.default.join(options.cacheRoot, cacheKey, "dist");
1988
+ let emittedEntry = import_node_path4.default.resolve(emitRoot, toEmittedModulePath(options.moduleRel));
1989
+ if (!options.noCache && await pathExists(emittedEntry)) {
1990
+ log(`[changelog] compile cache hit: ${options.commitSha.slice(0, 12)}`);
1991
+ return emittedEntry;
1992
+ }
1993
+ log(
1994
+ `[changelog] compile cache ${options.noCache ? "disabled" : "miss"}: ${options.commitSha.slice(0, 12)}`
1995
+ );
1996
+ await import_promises2.default.rm(import_node_path4.default.join(options.cacheRoot, cacheKey), { recursive: true, force: true }).catch(
1997
+ () => void 0
1998
+ );
1999
+ await import_promises2.default.mkdir(emitRoot, { recursive: true });
2000
+ const readResult = import_typescript2.default.readConfigFile(options.tsconfigPath, import_typescript2.default.sys.readFile);
2001
+ if (readResult.error) {
2002
+ throw new Error(import_typescript2.default.flattenDiagnosticMessageText(readResult.error.messageText, "\n"));
2003
+ }
2004
+ const parsed = import_typescript2.default.parseJsonConfigFileContent(
2005
+ readResult.config,
2006
+ import_typescript2.default.sys,
2007
+ import_node_path4.default.dirname(options.tsconfigPath)
2008
+ );
2009
+ const compilerOptions = {
2010
+ ...parsed.options,
2011
+ noEmit: false,
2012
+ declaration: false,
2013
+ declarationMap: false,
2014
+ sourceMap: false,
2015
+ inlineSourceMap: false,
2016
+ composite: false,
2017
+ incremental: false,
2018
+ tsBuildInfoFile: void 0,
2019
+ outDir: emitRoot
2020
+ };
2021
+ const program = import_typescript2.default.createProgram([options.modulePath], compilerOptions);
2022
+ const emitResult = program.emit();
2023
+ const diagnostics = import_typescript2.default.getPreEmitDiagnostics(program).concat(emitResult.diagnostics).filter((diagnostic) => diagnostic.category === import_typescript2.default.DiagnosticCategory.Error);
2024
+ if (diagnostics.length > 0) {
2025
+ const host = {
2026
+ getCanonicalFileName: (fileName) => fileName,
2027
+ getCurrentDirectory: () => options.worktreeDir,
2028
+ getNewLine: () => "\n"
2029
+ };
2030
+ const message = import_typescript2.default.formatDiagnosticsWithColorAndContext(diagnostics.slice(0, 20), host);
2031
+ throw new Error(
2032
+ `TypeScript snapshot compile failed for ${options.moduleRel} at ${options.commitSha.slice(0, 12)}:
2033
+ ${message}`
2034
+ );
2035
+ }
2036
+ emittedEntry = await resolveEmittedEntryPath(emitRoot, options.moduleRel) ?? emittedEntry;
2037
+ if (!await pathExists(emittedEntry)) {
2038
+ throw new Error(
2039
+ `Compiled snapshot entry was not emitted: ${emittedEntry}. Check module/tsconfig compatibility.`
2040
+ );
2041
+ }
2042
+ log(`[changelog] compiled snapshot entry: ${emittedEntry}`);
2043
+ return emittedEntry;
2044
+ }
2045
+ async function loadSnapshotExport(options) {
2046
+ return loadChangelogInput(options.entryPath, options.exportName);
2047
+ }
2048
+ async function createSnapshot(repoRoot, moduleRel, exportName, commit, tempRoot, tsconfigRel, exportOptions, log) {
1875
2049
  const worktreeDir = import_node_path4.default.join(tempRoot, `wt-${commit.sha.slice(0, 12)}`);
1876
2050
  await runGit(repoRoot, ["worktree", "add", "--detach", worktreeDir, commit.sha]);
1877
2051
  try {
1878
2052
  await ensureWorktreeDependencies(repoRoot, worktreeDir);
1879
2053
  const modulePath = import_node_path4.default.resolve(worktreeDir, moduleRel);
1880
- const tsconfigPath = tsconfigRel ? import_node_path4.default.resolve(worktreeDir, tsconfigRel) : void 0;
1881
- const input = await loadChangelogInput(modulePath, exportName);
2054
+ let snapshotRuntimePath = modulePath;
2055
+ let resolvedTsconfigPath;
2056
+ if (isTypeScriptModule(modulePath)) {
2057
+ resolvedTsconfigPath = await resolveSnapshotTsconfig(
2058
+ worktreeDir,
2059
+ modulePath,
2060
+ tsconfigRel
2061
+ );
2062
+ log?.(`[changelog] using tsconfig: ${resolvedTsconfigPath}`);
2063
+ const compileCacheRoot = import_node_path4.default.resolve(
2064
+ exportOptions?.cacheDir ?? import_node_path4.default.join(import_node_os.default.tmpdir(), "rrroutes-export-changelog-cache")
2065
+ );
2066
+ snapshotRuntimePath = await compileSnapshotEntry({
2067
+ worktreeDir,
2068
+ moduleRel,
2069
+ modulePath,
2070
+ tsconfigPath: resolvedTsconfigPath,
2071
+ commitSha: commit.sha,
2072
+ cacheRoot: compileCacheRoot,
2073
+ noCache: exportOptions?.noCache,
2074
+ log
2075
+ });
2076
+ } else if (!isJavaScriptModule(modulePath)) {
2077
+ throw new Error(
2078
+ `Unsupported module extension for changelog snapshots: ${modulePath}`
2079
+ );
2080
+ }
2081
+ const input = await loadSnapshotExport({
2082
+ entryPath: snapshotRuntimePath,
2083
+ exportName
2084
+ });
1882
2085
  const payload = await exportFinalizedLeaves(input, {
1883
2086
  includeSource: true,
1884
2087
  sourceModulePath: modulePath,
1885
2088
  sourceExportName: exportName,
1886
- tsconfigPath,
1887
- ...exportOptions
2089
+ tsconfigPath: resolvedTsconfigPath ?? (tsconfigRel ? import_node_path4.default.resolve(worktreeDir, tsconfigRel) : void 0),
2090
+ handlers: exportOptions?.handlers
1888
2091
  });
1889
2092
  const routesByKey = toRouteSnapshots(payload);
1890
2093
  const sourceObjectsById = toSourceObjectSnapshots(routesByKey);
@@ -2154,7 +2357,8 @@ async function exportFinalizedLeavesChangelog(options) {
2154
2357
  commit,
2155
2358
  tempRoot,
2156
2359
  tsconfigRelative,
2157
- options
2360
+ options,
2361
+ log
2158
2362
  )
2159
2363
  );
2160
2364
  }
@@ -2208,6 +2412,11 @@ var __private = {
2208
2412
  resolveCommitWindow,
2209
2413
  resolveCommitPath,
2210
2414
  filterCommits,
2415
+ resolveSnapshotTsconfig,
2416
+ toEmittedModulePath,
2417
+ buildCompileCacheKey,
2418
+ compileSnapshotEntry,
2419
+ loadSnapshotExport,
2211
2420
  toRouteSnapshots,
2212
2421
  toSourceObjectSnapshots,
2213
2422
  diffSnapshots,
@@ -2218,6 +2427,7 @@ var __private = {
2218
2427
  var import_node_path5 = __toESM(require("path"), 1);
2219
2428
  function parseFinalizedLeavesChangelogCliArgs(argv) {
2220
2429
  const args = /* @__PURE__ */ new Map();
2430
+ let noCache = false;
2221
2431
  for (let i = 0; i < argv.length; i += 1) {
2222
2432
  const key = argv[i];
2223
2433
  if (key === "--") continue;
@@ -2225,6 +2435,10 @@ function parseFinalizedLeavesChangelogCliArgs(argv) {
2225
2435
  if (key === "--with-source") {
2226
2436
  continue;
2227
2437
  }
2438
+ if (key === "--no-cache") {
2439
+ noCache = true;
2440
+ continue;
2441
+ }
2228
2442
  const value = argv[i + 1];
2229
2443
  if (!value || value.startsWith("--")) {
2230
2444
  throw new Error(`Missing value for ${key}`);
@@ -2243,7 +2457,9 @@ function parseFinalizedLeavesChangelogCliArgs(argv) {
2243
2457
  htmlFile: args.get("--html") ?? void 0,
2244
2458
  tsconfigPath: args.get("--tsconfig") ?? void 0,
2245
2459
  from: args.get("--from") ?? void 0,
2246
- to: args.get("--to") ?? void 0
2460
+ to: args.get("--to") ?? void 0,
2461
+ cacheDir: args.get("--cache-dir") ?? void 0,
2462
+ noCache
2247
2463
  };
2248
2464
  }
2249
2465
  async function runExportFinalizedLeavesChangelogCli(argv) {
@@ -2261,6 +2477,8 @@ async function runExportFinalizedLeavesChangelogCli(argv) {
2261
2477
  from: args.from,
2262
2478
  to: args.to,
2263
2479
  includeSource: true,
2480
+ cacheDir: args.cacheDir,
2481
+ noCache: args.noCache,
2264
2482
  log
2265
2483
  };
2266
2484
  log("[changelog-cli] starting changelog export");