ts-node-pack 0.1.0 → 0.1.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-node-pack",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Pack a TypeScript package into a Node-compatible npm tarball without modifying the source tree",
5
5
  "keywords": [
6
6
  "cli",
@@ -26,13 +26,13 @@
26
26
  "exports": {
27
27
  ".": "./src/index.js"
28
28
  },
29
- "engines": {
30
- "node": ">=20"
31
- },
32
- "packageManager": "pnpm@10.33.0",
33
29
  "dependencies": {
34
30
  "npm-packlist": "^10.0.4",
35
31
  "ts-blank-space": "^0.8.0"
36
32
  },
33
+ "engines": {
34
+ "node": ">=20"
35
+ },
36
+ "packageManager": "pnpm@10.33.0",
37
37
  "types": "./src/index.d.ts"
38
38
  }
package/src/index.d.ts CHANGED
@@ -9,46 +9,6 @@ export interface TsNodePackOptions {
9
9
  * Returns the path to the .tgz file (or the staging dir if --emit-only).
10
10
  */
11
11
  export declare function tsNodePack(packageDir: string, options?: TsNodePackOptions): Promise<string>;
12
- /**
13
- * Rewrite .ts/.tsx extensions to .js in relative import/export specifiers.
14
- *
15
- * Why regex is sound here:
16
- *
17
- * The input is exclusively tsc-generated .d.ts files, not arbitrary source.
18
- * tsc emits a highly constrained subset of syntax in declarations:
19
- *
20
- * - Import/export specifiers are always single-line string literals.
21
- * - Specifiers are always wrapped in matching quotes (' or ").
22
- * - Relative paths always start with ./ or ../ (tsc never normalizes these).
23
- * - No template literals, concatenation, or computed specifiers appear.
24
- * - The only keywords introducing module specifiers are `from`, `import`,
25
- * and `import()`.
26
- *
27
- * Because the grammar of specifiers in .d.ts output is regular (a finite
28
- * set of keyword prefixes + a quoted string literal), regex matches it
29
- * exactly — no ambiguity, no context-sensitivity, no false positives.
30
- *
31
- * If tsc ever adds native .d.ts rewriting (TypeScript#56556), this
32
- * function becomes a no-op: the `if (rewritten !== original)` guard
33
- * in rewriteDtsFiles means no file is touched.
34
- *
35
- * Handles:
36
- * - Named/default/type imports: import { x } from './foo.js'
37
- * - Re-exports: export { x } from './foo.js'
38
- * - Star re-exports: export * from './foo.js'
39
- * - Side-effect imports: import './foo.js'
40
- * - Dynamic imports: import('./foo.js')
41
- * - .tsx extensions: import './component.js'
42
- *
43
- * Does NOT rewrite (by design):
44
- * - Non-relative specifiers: import 'lodash'
45
- * - Already-.js specifiers: import './foo.js'
46
- * - import.meta.url: not a module specifier
47
- * - require(): not valid in ESM .d.ts
48
- * - Bare string literals: const x = './foo.ts'
49
- */
50
- export declare const TS_SPECIFIER_PATTERNS: RegExp[];
51
- export declare function rewriteTsSpecifiers(content: any): any;
52
12
  /**
53
13
  * Resolve `workspace:` protocol specifiers in the package's dependency
54
14
  * fields to concrete versions, matching yarn-berry and pnpm behavior.
package/src/index.js CHANGED
@@ -15,6 +15,7 @@ import { basename, dirname, join, relative, resolve } from "node:path";
15
15
  import { promisify } from "node:util";
16
16
  import packlist from "npm-packlist";
17
17
  import tsBlankSpace from "ts-blank-space";
18
+ import { TS_SPECIFIER_PATTERNS, rewriteTsSpecifiers } from "./rewrite-specifiers.js";
18
19
 
19
20
  const execFileAsync = promisify(execFile);
20
21
 
@@ -45,7 +46,7 @@ export async function tsNodePack(
45
46
  log(`Package: ${pkgJson.name}@${pkgJson.version}`);
46
47
 
47
48
  const tsconfigPath = resolveTsconfig(packageDir, tsconfig);
48
- log(`Using tsconfig: ${tsconfigPath}`);
49
+ if (tsconfigPath) log(`Found tsconfig: ${tsconfigPath}`);
49
50
 
50
51
  // ── Phase 2: Create staging directory ─────────────────────────────────
51
52
  log("Phase 2: Creating staging directory...");
@@ -55,99 +56,94 @@ export async function tsNodePack(
55
56
  log(`Staging: ${stagingDir}`);
56
57
 
57
58
  try {
58
- // ── Phase 3: Generate derived tsconfig ────────────────────────────────
59
- // Written inside the temp dir — never touches the source tree.
60
- // `extends` takes an absolute path; paths defined by the base tsconfig
61
- // (include/files/exclude/rootDir/paths) resolve relative to the base
62
- // config's own location, which is still inside packageDir, so globs
63
- // continue to work.
64
- log("Phase 3: Generating derived tsconfig...");
65
- const emitConfigPath = join(tmpDir, "tsconfig.emit.json");
66
-
67
- // Because the emit config lives outside packageDir, tsc's typeRoots
68
- // walk-up (which starts at the config's directory) can't reach
69
- // packageDir/node_modules/@types. Pin typeRoots explicitly when that
70
- // directory exists so entries named in the base config's `types`
71
- // field — e.g. `types: ["node"]` — can be resolved. TS 6.0 made this
72
- // explicit opt-in (no more auto-inclusion of every @types package),
73
- // which surfaces the missing resolution as a clear TS2688 error
74
- // rather than silent "Cannot find module 'node:util'".
75
- const atTypesDir = join(packageDir, "node_modules", "@types");
76
- const hasAtTypes = existsSync(atTypesDir);
77
-
78
- const emitConfig = {
79
- extends: tsconfigPath,
80
- compilerOptions: {
81
- // Force rootDir so emit preserves the source layout — otherwise
82
- // tsc infers the common ancestor and strips the `src/` prefix,
83
- // breaking `main`/`exports` that reference `./src/...`.
84
- rootDir: packageDir,
85
- outDir: stagingDir,
86
- declaration: true,
87
- // ts-blank-space handles .js emit (Phase 6); tsc only emits .d.ts.
88
- emitDeclarationOnly: true,
89
- // Extract types from JS+JSDoc sources in mixed packages.
90
- allowJs: true,
91
- noEmit: false,
92
- // Incremental/composite would try to write a .tsbuildinfo whose
93
- // path tsc computes relative to the base config, producing
94
- // garbled paths when our outDir crosses directory trees.
95
- incremental: false,
96
- composite: false,
97
- tsBuildInfoFile: null,
98
- ...(hasAtTypes ? { typeRoots: [atTypesDir] } : {}),
99
- },
100
- };
101
- await writeFile(emitConfigPath, JSON.stringify(emitConfig, null, 2) + "\n");
102
-
103
- // ── Phase 4: Copy the npm-packlist into staging ──────────────────────
59
+ // ── Phase 3: Copy the npm-packlist into staging ──────────────────────
104
60
  // `npm-packlist` is the same file-enumeration engine `npm pack` uses
105
61
  // internally. It applies `files`, `.npmignore`, the npm default
106
62
  // includes (package.json, README*, LICENSE*) and default excludes
107
63
  // (.git, node_modules, ...) — so the staging directory ends up as a
108
64
  // faithful mirror of whatever `npm pack` would have produced from
109
- // the source tree, minus our downstream transforms (ts-blank-space,
110
- // declaration emit, specifier rewrite).
111
- log("Phase 4: Enumerating package files via npm-packlist...");
112
- // npm-packlist's tree-based API (v7+) expects a node with `path` +
113
- // parsed `package.json`. We fabricate a minimal tree rather than
114
- // pulling in @npmcli/arborist — we don't need bundled-dependency
115
- // resolution, workspace walking, or the rest of arborist's
116
- // machinery, just the pack-file list.
65
+ // the source tree, minus our downstream transforms.
117
66
  //
118
- // `isProjectRoot: true` sends npm-packlist down the
119
- // "root package" branch of `gatherBundles`, which looks only at
120
- // bundleDependencies (missing here, so no-op). Without this, it
121
- // takes the "bundled dep of some other project" branch and tries
122
- // to walk `edgesOut` — which doesn't exist on our hand-built tree.
67
+ // `isProjectRoot: true` sends npm-packlist down its "root package"
68
+ // branch without it, it tries to walk `edgesOut` which doesn't
69
+ // exist on our hand-built tree (no @npmcli/arborist).
70
+ log("Phase 3: Enumerating package files via npm-packlist...");
123
71
  const packFiles = await packlist({
124
72
  path: packageDir,
125
73
  package: pkgJson,
126
74
  isProjectRoot: true,
127
75
  });
128
- // Pre-create every parent directory serially (mkdir on the same
129
- // path from multiple concurrent calls can race), then copy files
130
- // in parallel.
131
- const dirs = new Set (
132
- packFiles.map((rel) => dirname(join(stagingDir, rel))),
133
- );
76
+ // Pre-create unique parent dirs serially (concurrent mkdir on the
77
+ // same path can race on some filesystems), then copy in parallel.
78
+ const dirs = new Set (packFiles.map((rel) => dirname(join(stagingDir, rel))));
134
79
  for (const d of dirs) await mkdir(d, { recursive: true });
135
80
  await Promise.all(
136
- packFiles.map((rel) =>
137
- copyFile(join(packageDir, rel), join(stagingDir, rel)),
138
- ),
81
+ packFiles.map((rel) => copyFile(join(packageDir, rel), join(stagingDir, rel))),
139
82
  );
140
83
  log(`Copied ${packFiles.length} file(s) into staging`);
141
84
 
142
- // ── Phase 5: Emit declarations via tsc ──────────────────────────────
143
- // tsc reads sources from packageDir (the base config's include
144
- // resolves there) and writes .d.ts files into stagingDir via outDir.
145
- // The copies from Phase 4 are left alone — tsc's outputs land
146
- // alongside them. tsc may emit declarations for files outside the
147
- // `files` array (e.g. test/); those won't make it into the final
148
- // tarball because npm pack only ships what `files` references.
149
- log("Phase 5: Emitting .d.ts files via tsc...");
150
- await runTsc(emitConfigPath, packageDir, log);
85
+ // ── Phase 4: Decide whether to run tsc, generate config if so ────────
86
+ // Three conditions trigger declaration emit:
87
+ // 1. user passed --tsconfig (explicit opt-in)
88
+ // 2. tsconfig.build.json exists (agoric convention for opt-in)
89
+ // 3. any source contains .ts/.tsx/.mts (we'd be stripping them
90
+ // anyway, and probably want their declarations)
91
+ // For pure JS+JSDoc packages with only `tsconfig.json` and no .ts
92
+ // sources, this skips tsc entirely — matching what `npm pack` would
93
+ // have done before ts-node-pack: ship .js files, no .d.ts.
94
+ const hasTsSources = packFiles.some(
95
+ (f) => /\.(ts|tsx|mts)$/.test(f) && !/\.d\.(ts|mts)$/.test(f),
96
+ );
97
+ const isExplicitOptIn =
98
+ tsconfigPath !== null &&
99
+ (tsconfig !== undefined || basename(tsconfigPath) === "tsconfig.build.json");
100
+ const shouldRunTsc = tsconfigPath !== null && (isExplicitOptIn || hasTsSources);
101
+
102
+ if (shouldRunTsc) {
103
+ log("Phase 4: Generating derived tsconfig...");
104
+ const emitConfigPath = join(tmpDir, "tsconfig.emit.json");
105
+ // tsc's typeRoots walk-up starts at the config's directory, so it
106
+ // can't reach packageDir/node_modules/@types from our temp-dir
107
+ // location. Pin typeRoots when that directory exists. (TS 6.0
108
+ // surfaces the missing resolution as TS2688 instead of the silent
109
+ // "Cannot find module 'node:util'" cascade of earlier versions.)
110
+ const atTypesDir = join(packageDir, "node_modules", "@types");
111
+ const hasAtTypes = existsSync(atTypesDir);
112
+ const emitConfig = {
113
+ extends: tsconfigPath,
114
+ compilerOptions: {
115
+ // Force rootDir so emit preserves the source layout —
116
+ // otherwise tsc infers the common ancestor and strips the
117
+ // `src/` prefix, breaking `main`/`exports` that reference
118
+ // `./src/...`.
119
+ rootDir: packageDir,
120
+ outDir: stagingDir,
121
+ declaration: true,
122
+ // ts-blank-space handles .js emit (Phase 6); tsc only emits .d.ts.
123
+ emitDeclarationOnly: true,
124
+ // Extract types from JS+JSDoc sources in mixed packages.
125
+ allowJs: true,
126
+ noEmit: false,
127
+ // Incremental/composite write a .tsbuildinfo whose path tsc
128
+ // computes relative to the base config, producing garbled
129
+ // paths when our outDir crosses directory trees.
130
+ incremental: false,
131
+ composite: false,
132
+ tsBuildInfoFile: null,
133
+ ...(hasAtTypes ? { typeRoots: [atTypesDir] } : {}),
134
+ },
135
+ };
136
+ await writeFile(emitConfigPath, JSON.stringify(emitConfig, null, 2) + "\n");
137
+
138
+ log("Phase 5: Emitting .d.ts files via tsc...");
139
+ await runTsc(emitConfigPath, packageDir, log);
140
+ } else {
141
+ log(
142
+ tsconfigPath === null
143
+ ? "Phase 4-5: Skipping tsc (no tsconfig found; pure-JS package)"
144
+ : "Phase 4-5: Skipping tsc (no .ts sources and no tsconfig.build.json opt-in)",
145
+ );
146
+ }
151
147
 
152
148
  // ── Phase 6: Strip types and rewrite specifiers ─────────────────────
153
149
  // Single-pass transform of every staging file that could contain a
@@ -157,10 +153,7 @@ export async function tsNodePack(
157
153
  // ts-blank-space preserves line and column positions, so no
158
154
  // sourcemaps are needed for debugging.
159
155
  log("Phase 6: Stripping types and rewriting specifiers...");
160
- const { strippedCount, rewrittenCount } = await processStagingFiles(
161
- stagingDir,
162
- log,
163
- );
156
+ const { strippedCount, rewrittenCount } = await processStagingFiles(stagingDir, log);
164
157
  log(
165
158
  `Stripped ${strippedCount} type-annotated file(s); rewrote ${rewrittenCount} other file(s)`,
166
159
  );
@@ -173,16 +166,9 @@ export async function tsNodePack(
173
166
  // packages to publish), then flip entry paths and strip dev-only
174
167
  // fields.
175
168
  log("Phase 7: Rewriting package.json...");
176
- const resolvedPkg = await resolveWorkspaceDependencies(
177
- pkgJson,
178
- packageDir,
179
- log,
180
- );
169
+ const resolvedPkg = await resolveWorkspaceDependencies(pkgJson, packageDir, log);
181
170
  const rewrittenPkg = rewritePackageJson(resolvedPkg);
182
- await writeFile(
183
- join(stagingDir, "package.json"),
184
- JSON.stringify(rewrittenPkg, null, 2) + "\n",
185
- );
171
+ await writeFile(join(stagingDir, "package.json"), JSON.stringify(rewrittenPkg, null, 2) + "\n");
186
172
 
187
173
  // ── Phase 8: Validate ─────────────────────────────────────────────────
188
174
  log("Phase 8: Validating...");
@@ -191,7 +177,7 @@ export async function tsNodePack(
191
177
  // ── Phase 9: Pack ─────────────────────────────────────────────────────
192
178
  if (!emitOnly) {
193
179
  log("Phase 9: Packing...");
194
- const tgzPath = await pack(stagingDir, log);
180
+ const tgzPath = await pack(stagingDir);
195
181
  const tgzName = basename(tgzPath);
196
182
  const dest = join(process.cwd(), tgzName);
197
183
  await copyFile(tgzPath, dest);
@@ -214,7 +200,12 @@ export async function tsNodePack(
214
200
 
215
201
  // ── Phase helpers ──────────────────────────────────────────────────────────
216
202
 
217
- function resolveTsconfig(packageDir, tsconfigOption) {
203
+ /**
204
+ * Find the tsconfig to extend, or null if the package has no TypeScript
205
+ * config at all (pure JS package). The `--tsconfig` flag is the one
206
+ * explicit case where missing-config is fatal.
207
+ */
208
+ function resolveTsconfig(packageDir, tsconfigOption) {
218
209
  if (tsconfigOption) {
219
210
  const p = resolve(packageDir, tsconfigOption);
220
211
  if (!existsSync(p)) {
@@ -222,14 +213,11 @@ function resolveTsconfig(packageDir, tsconfigOption) {
222
213
  }
223
214
  return p;
224
215
  }
225
-
226
216
  const buildConfig = join(packageDir, "tsconfig.build.json");
227
217
  if (existsSync(buildConfig)) return buildConfig;
228
-
229
218
  const defaultConfig = join(packageDir, "tsconfig.json");
230
219
  if (existsSync(defaultConfig)) return defaultConfig;
231
-
232
- throw new Error(`No tsconfig found in ${packageDir}. Provide one with --tsconfig.`);
220
+ return null;
233
221
  }
234
222
 
235
223
  /**
@@ -314,18 +302,14 @@ async function processStagingFiles(stagingDir, log) {
314
302
  });
315
303
  if (errors.length > 0) {
316
304
  throw new Error(
317
- "ts-blank-space rejected non-erasable TypeScript:\n " +
318
- errors.join("\n "),
305
+ "ts-blank-space rejected non-erasable TypeScript:\n " + errors.join("\n "),
319
306
  );
320
307
  }
321
308
  }
322
309
  content = rewriteTsSpecifiers(content);
323
310
 
324
311
  if (isTs || isMts) {
325
- const destPath = srcPath.replace(
326
- isMts ? /\.mts$/ : /\.ts$/,
327
- isMts ? ".mjs" : ".js",
328
- );
312
+ const destPath = srcPath.replace(isMts ? /\.mts$/ : /\.ts$/, isMts ? ".mjs" : ".js");
329
313
  await writeFile(destPath, content);
330
314
  await unlink(srcPath);
331
315
  strippedCount++;
@@ -339,70 +323,6 @@ async function processStagingFiles(stagingDir, log) {
339
323
  return { strippedCount, rewrittenCount };
340
324
  }
341
325
 
342
- /**
343
- * Rewrite .ts/.tsx extensions to .js in relative import/export specifiers.
344
- *
345
- * Why regex is sound here:
346
- *
347
- * The input is exclusively tsc-generated .d.ts files, not arbitrary source.
348
- * tsc emits a highly constrained subset of syntax in declarations:
349
- *
350
- * - Import/export specifiers are always single-line string literals.
351
- * - Specifiers are always wrapped in matching quotes (' or ").
352
- * - Relative paths always start with ./ or ../ (tsc never normalizes these).
353
- * - No template literals, concatenation, or computed specifiers appear.
354
- * - The only keywords introducing module specifiers are `from`, `import`,
355
- * and `import()`.
356
- *
357
- * Because the grammar of specifiers in .d.ts output is regular (a finite
358
- * set of keyword prefixes + a quoted string literal), regex matches it
359
- * exactly — no ambiguity, no context-sensitivity, no false positives.
360
- *
361
- * If tsc ever adds native .d.ts rewriting (TypeScript#56556), this
362
- * function becomes a no-op: the `if (rewritten !== original)` guard
363
- * in rewriteDtsFiles means no file is touched.
364
- *
365
- * Handles:
366
- * - Named/default/type imports: import { x } from './foo.js'
367
- * - Re-exports: export { x } from './foo.js'
368
- * - Star re-exports: export * from './foo.js'
369
- * - Side-effect imports: import './foo.js'
370
- * - Dynamic imports: import('./foo.js')
371
- * - .tsx extensions: import './component.js'
372
- *
373
- * Does NOT rewrite (by design):
374
- * - Non-relative specifiers: import 'lodash'
375
- * - Already-.js specifiers: import './foo.js'
376
- * - import.meta.url: not a module specifier
377
- * - require(): not valid in ESM .d.ts
378
- * - Bare string literals: const x = './foo.ts'
379
- */
380
- // Three shapes for a relative TypeScript specifier in a string literal:
381
- // 1. `from './foo.js'` — named/default/type imports, re-exports
382
- // 2. `import './foo.js'` — bare side-effect imports
383
- // 3. `import('./foo.js')` — dynamic imports
384
- //
385
- // The extension capture group matches `ts`, `tsx`, or `mts`. Rewriting
386
- // turns `.ts`/`.tsx` into `.js` and `.mts` into `.mjs`, matching the
387
- // output extensions ts-blank-space produces in Phase 6.
388
- //
389
- // Used both to rewrite specifiers and (in the validator) to detect any
390
- // specifier that slipped through. Must stay in sync with each other.
391
- export const TS_SPECIFIER_PATTERNS = [
392
- /(from\s*['"])(\.\.?\/[^'"]*?)\.(tsx?|mts)(['"])/g,
393
- /(import\s+['"])(\.\.?\/[^'"]*?)\.(tsx?|mts)(['"])/g,
394
- /(import\s*\(\s*['"])(\.\.?\/[^'"]*?)\.(tsx?|mts)(['"]\s*\))/g,
395
- ];
396
-
397
- export function rewriteTsSpecifiers(content) {
398
- for (const pattern of TS_SPECIFIER_PATTERNS) {
399
- content = content.replace(pattern, (_m, pre, path, ext, post) =>
400
- pre + path + (ext === "mts" ? ".mjs" : ".js") + post,
401
- );
402
- }
403
- return content;
404
- }
405
-
406
326
  const DEP_FIELDS_WITH_WORKSPACE = [
407
327
  "dependencies",
408
328
  "peerDependencies",
@@ -481,9 +401,7 @@ async function findWorkspaceRoot(startDir) {
481
401
  let dir = startDir;
482
402
  while (true) {
483
403
  try {
484
- const pkg = JSON.parse(
485
- await readFile(join(dir, "package.json"), "utf8"),
486
- );
404
+ const pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf8"));
487
405
  if (pkg.workspaces) return dir;
488
406
  } catch {
489
407
  // Missing or unreadable package.json — not the root, keep going.
@@ -495,9 +413,7 @@ async function findWorkspaceRoot(startDir) {
495
413
  }
496
414
 
497
415
  async function buildWorkspaceVersionMap(workspaceRoot) {
498
- const rootPkg = JSON.parse(
499
- await readFile(join(workspaceRoot, "package.json"), "utf8"),
500
- );
416
+ const rootPkg = JSON.parse(await readFile(join(workspaceRoot, "package.json"), "utf8"));
501
417
  const patterns = Array.isArray(rootPkg.workspaces)
502
418
  ? rootPkg.workspaces
503
419
  : rootPkg.workspaces?.packages || [];
@@ -507,9 +423,7 @@ async function buildWorkspaceVersionMap(workspaceRoot) {
507
423
  const dirs = await expandWorkspacePattern(workspaceRoot, pattern);
508
424
  for (const d of dirs) {
509
425
  try {
510
- const pkg = JSON.parse(
511
- await readFile(join(d, "package.json"), "utf8"),
512
- );
426
+ const pkg = JSON.parse(await readFile(join(d, "package.json"), "utf8"));
513
427
  if (pkg.name && pkg.version) map.set(pkg.name, pkg.version);
514
428
  } catch {
515
429
  // Missing or malformed sibling package.json — skip.
@@ -546,9 +460,7 @@ async function expandWorkspacePattern(workspaceRoot, pattern) {
546
460
  }
547
461
  } else if (part.includes("*")) {
548
462
  const regex = new RegExp(
549
- "^" +
550
- part.replace(/[.+^$()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") +
551
- "$",
463
+ "^" + part.replace(/[.+^$()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$",
552
464
  );
553
465
  try {
554
466
  const entries = await readdir(base, { withFileTypes: true });
@@ -673,9 +585,7 @@ async function validate(stagingDir, pkg, log) {
673
585
  // false positives. Comment stripping here is intentionally coarse
674
586
  // (doesn't understand strings-containing-comment-markers) — good enough
675
587
  // for tsc-emitted output, where comments are well-behaved.
676
- const content = raw
677
- .replace(/\/\*[\s\S]*?\*\//g, "")
678
- .replace(/(^|[^:])\/\/.*$/gm, "$1");
588
+ const content = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
679
589
  const relPath = relative(stagingDir, filePath);
680
590
 
681
591
  for (const pattern of TS_SPECIFIER_PATTERNS) {
@@ -751,7 +661,7 @@ function collectExportsRefs(value, refs) {
751
661
  }
752
662
  }
753
663
 
754
- async function pack(stagingDir, log) {
664
+ async function pack(stagingDir) {
755
665
  const { stdout } = await execFileAsync("npm", ["pack"], {
756
666
  cwd: stagingDir,
757
667
  });
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @file Rewrite relative TypeScript specifiers to JavaScript ones.
3
+ *
4
+ * The input grammar is intentionally narrow: a relative path (`./` or
5
+ * `../`) ending in `.ts`, `.tsx`, or `.mts`, embedded in one of six
6
+ * specific syntactic contexts. The output replaces the extension with
7
+ * `.js` (for `.ts`/`.tsx`) or `.mjs` (for `.mts`), matching what
8
+ * `ts-blank-space` produces in the strip phase.
9
+ *
10
+ * Why regex is sound here:
11
+ *
12
+ * The inputs are tsc-emitted `.d.ts` declaration files and JS sources
13
+ * stripped of types — both highly constrained subsets of JavaScript:
14
+ *
15
+ * - Import/export specifiers are always single-line string literals.
16
+ * - Specifiers are always wrapped in matching quotes (' or ").
17
+ * - Relative paths always start with `./` or `../`.
18
+ * - No template literals, concatenation, or computed specifiers appear.
19
+ *
20
+ * Because the grammar of relative specifiers in this input is regular
21
+ * (a finite set of keyword prefixes + a quoted string literal), regex
22
+ * matches it exactly with no false positives — provided the patterns
23
+ * below stay in sync with each other and the validator that uses them.
24
+ */
25
+ export declare const TS_SPECIFIER_PATTERNS: RegExp[];
26
+ /**
27
+ * Apply every pattern in TS_SPECIFIER_PATTERNS to `content`, rewriting
28
+ * each matched relative TypeScript specifier to its corresponding
29
+ * JavaScript form. Returns the transformed string; the input is not
30
+ * mutated.
31
+ */
32
+ export declare function rewriteTsSpecifiers(content: string): string;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @file Rewrite relative TypeScript specifiers to JavaScript ones.
3
+ *
4
+ * The input grammar is intentionally narrow: a relative path (`./` or
5
+ * `../`) ending in `.ts`, `.tsx`, or `.mts`, embedded in one of six
6
+ * specific syntactic contexts. The output replaces the extension with
7
+ * `.js` (for `.ts`/`.tsx`) or `.mjs` (for `.mts`), matching what
8
+ * `ts-blank-space` produces in the strip phase.
9
+ *
10
+ * Why regex is sound here:
11
+ *
12
+ * The inputs are tsc-emitted `.d.ts` declaration files and JS sources
13
+ * stripped of types — both highly constrained subsets of JavaScript:
14
+ *
15
+ * - Import/export specifiers are always single-line string literals.
16
+ * - Specifiers are always wrapped in matching quotes (' or ").
17
+ * - Relative paths always start with `./` or `../`.
18
+ * - No template literals, concatenation, or computed specifiers appear.
19
+ *
20
+ * Because the grammar of relative specifiers in this input is regular
21
+ * (a finite set of keyword prefixes + a quoted string literal), regex
22
+ * matches it exactly with no false positives — provided the patterns
23
+ * below stay in sync with each other and the validator that uses them.
24
+ */
25
+
26
+ // Six shapes for a relative TypeScript specifier in a string literal:
27
+ // 1. `from './foo.js'` — named/default/type imports, re-exports
28
+ // 2. `import './foo.js'` — bare side-effect imports
29
+ // 3. `import('./foo.js')` — dynamic imports
30
+ // 4. `require('./foo.js')` — CJS requires (in .cjs / .d.cts)
31
+ // 5. `declare module './foo.js' {…}` — TypeScript ambient declarations
32
+ // 6. `/// <reference path="./foo.js"/>` — triple-slash directives
33
+ //
34
+ // The extension capture group matches `ts`, `tsx`, or `mts`. The
35
+ // negative lookbehind `(?<!\.d)` prevents the regex from matching
36
+ // `./foo.d.ts` (and `.d.mts`/`.d.cts`/`.d.tsx`), which are pure
37
+ // declaration files that must keep their original specifiers.
38
+ //
39
+ // Word-boundary anchors (`\b`) on the keyword-leading patterns prevent
40
+ // false positives on identifiers like `myfrom` or `customRequire`.
41
+ //
42
+ // Used both to rewrite specifiers and (in the validator) to detect any
43
+ // specifier that slipped through. Patterns must stay in sync.
44
+ export const TS_SPECIFIER_PATTERNS = [
45
+ /(\bfrom\s*['"])(\.\.?\/[^'"]*?)(?<!\.d)\.(tsx?|mts)(['"])/g,
46
+ /(\bimport\s+['"])(\.\.?\/[^'"]*?)(?<!\.d)\.(tsx?|mts)(['"])/g,
47
+ /(\bimport\s*\(\s*['"])(\.\.?\/[^'"]*?)(?<!\.d)\.(tsx?|mts)(['"]\s*\))/g,
48
+ /(\brequire\s*\(\s*['"])(\.\.?\/[^'"]*?)(?<!\.d)\.(tsx?|mts)(['"]\s*\))/g,
49
+ /(\bdeclare\s+module\s*['"])(\.\.?\/[^'"]*?)(?<!\.d)\.(tsx?|mts)(['"])/g,
50
+ /(<reference\s+path\s*=\s*['"])(\.\.?\/[^'"]*?)(?<!\.d)\.(tsx?|mts)(['"])/g,
51
+ ];
52
+
53
+ /**
54
+ * Apply every pattern in TS_SPECIFIER_PATTERNS to `content`, rewriting
55
+ * each matched relative TypeScript specifier to its corresponding
56
+ * JavaScript form. Returns the transformed string; the input is not
57
+ * mutated.
58
+ */
59
+ export function rewriteTsSpecifiers(content ) {
60
+ for (const pattern of TS_SPECIFIER_PATTERNS) {
61
+ content = content.replace(
62
+ pattern,
63
+ (_m, pre, path, ext, post) => pre + path + (ext === "mts" ? ".mjs" : ".js") + post,
64
+ );
65
+ }
66
+ return content;
67
+ }