ts-node-pack 0.2.0 → 0.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-node-pack",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Pack a TypeScript package into a Node-compatible npm tarball without modifying the source tree",
5
5
  "keywords": [
6
6
  "cli",
@@ -27,12 +27,12 @@
27
27
  ".": "./src/index.js"
28
28
  },
29
29
  "dependencies": {
30
- "npm-packlist": "^10.0.4",
31
- "ts-blank-space": "^0.8.0"
30
+ "amaro": "^0.3.2",
31
+ "npm-packlist": "^10.0.4"
32
32
  },
33
33
  "engines": {
34
34
  "node": ">=20"
35
35
  },
36
- "packageManager": "pnpm@10.33.0",
36
+ "packageManager": "pnpm@11.1.2",
37
37
  "types": "./src/index.d.ts"
38
38
  }
package/src/index.js CHANGED
@@ -14,8 +14,9 @@ import { tmpdir } from "node:os";
14
14
  import { basename, dirname, join, relative, resolve } from "node:path";
15
15
  import { promisify } from "node:util";
16
16
  import packlist from "npm-packlist";
17
- import tsBlankSpace from "ts-blank-space";
18
- import { TS_SPECIFIER_PATTERNS, rewriteTsSpecifiers } from "./rewrite-specifiers.js";
17
+ import { transformSync } from "amaro";
18
+ import { rewriteTsSpecifiers } from "./rewrite-specifiers.js";
19
+ import { validate } from "./validation.js";
19
20
 
20
21
  const execFileAsync = promisify(execFile);
21
22
 
@@ -172,7 +173,7 @@ export async function tsNodePack(
172
173
  rootDir: packageDir,
173
174
  outDir: stagingDir,
174
175
  declaration: true,
175
- // ts-blank-space handles .js emit (Phase 6); tsc only emits .d.ts.
176
+ // amaro handles .js emit (Phase 6); tsc only emits .d.ts.
176
177
  emitDeclarationOnly: true,
177
178
  // Extract types from JS+JSDoc sources in mixed packages.
178
179
  allowJs: true,
@@ -200,10 +201,10 @@ export async function tsNodePack(
200
201
 
201
202
  // ── Phase 6: Strip types and rewrite specifiers ─────────────────────
202
203
  // Single-pass transform of every staging file that could contain a
203
- // relative module specifier: .ts/.mts get ts-blank-space'd then
204
+ // relative module specifier: .ts/.mts get type-stripped then
204
205
  // specifier-rewritten (one read, one write, original unlinked);
205
206
  // .js/.mjs/.d.ts/.d.mts just get specifier-rewritten in place.
206
- // ts-blank-space preserves line and column positions, so no
207
+ // amaro preserves line and column positions, so no
207
208
  // sourcemaps are needed for debugging.
208
209
  log("Phase 6: Stripping types and rewriting specifiers...");
209
210
  const { strippedCount, rewrittenCount } = await processStagingFiles(stagingDir, log);
@@ -347,7 +348,7 @@ async function runTsc(emitConfigPath, cwd, log) {
347
348
  * changed, written once. Sourcemap files (.d.ts.map, .d.mts.map) are
348
349
  * skipped because they're binary-encoded JSON with no specifiers.
349
350
  *
350
- * Throws if ts-blank-space rejects a .ts/.mts file (non-erasable
351
+ * Throws if amaro rejects a .ts/.mts file (non-erasable
351
352
  * syntax like `enum`, `namespace`, or parameter properties).
352
353
  */
353
354
  async function processStagingFiles(stagingDir, log) {
@@ -371,16 +372,10 @@ async function processStagingFiles(stagingDir, log) {
371
372
 
372
373
  let content = source;
373
374
  if (isTs || isMts) {
374
- const errors = [];
375
- content = tsBlankSpace(source, (node) => {
376
- errors.push(
377
- `${rel}: unsupported non-erasable syntax: ${String(node.getText?.() ?? node).slice(0, 80)}`,
378
- );
379
- });
380
- if (errors.length > 0) {
381
- throw new Error(
382
- "ts-blank-space rejected non-erasable TypeScript:\n " + errors.join("\n "),
383
- );
375
+ try {
376
+ content = transformSync(source).code;
377
+ } catch (err ) {
378
+ throw new Error(`amaro rejected TypeScript in ${rel}:\n ${err.message}`);
384
379
  }
385
380
  }
386
381
  content = rewriteTsSpecifiers(content);
@@ -595,7 +590,7 @@ export function rewritePackageJson(pkg) {
595
590
  } else if (result.typings) {
596
591
  result.typings = rewriteTsToDts(result.typings);
597
592
  } else if (mainWasTs) {
598
- result.types = result.main.replace(/\.js$/, ".d.ts");
593
+ result.types = rewriteTsToDts(originalMain);
599
594
  }
600
595
 
601
596
  // Rewrite bin
@@ -617,7 +612,7 @@ export function rewritePackageJson(pkg) {
617
612
  // Rewrite files array
618
613
  if (Array.isArray(result.files)) {
619
614
  result.files = result.files.flatMap((f) => {
620
- if (typeof f === "string" && /\.tsx?$/.test(f)) {
615
+ if (typeof f === "string" && TS_SOURCE_EXT_RE.test(f)) {
621
616
  return [rewriteTsToJs(f), rewriteTsToDts(f)];
622
617
  }
623
618
  return [f];
@@ -627,14 +622,22 @@ export function rewritePackageJson(pkg) {
627
622
  return result;
628
623
  }
629
624
 
625
+ // `.ts`/`.tsx` → `.js`; `.mts` → `.mjs`. Matches what amaro
626
+ // writes in Phase 6 and the specifier-rewrite pass in rewrite-specifiers.ts.
630
627
  function rewriteTsToJs(p) {
631
- return typeof p === "string" ? p.replace(/\.tsx?$/, ".js") : p;
628
+ if (typeof p !== "string") return p;
629
+ return p.replace(/\.mts$/, ".mjs").replace(/\.tsx?$/, ".js");
632
630
  }
633
631
 
632
+ // `.ts`/`.tsx` → `.d.ts`; `.mts` → `.d.mts`. Matches the declaration files
633
+ // tsc emits from an .mts source.
634
634
  function rewriteTsToDts(p) {
635
- return typeof p === "string" ? p.replace(/\.tsx?$/, ".d.ts") : p;
635
+ if (typeof p !== "string") return p;
636
+ return p.replace(/\.mts$/, ".d.mts").replace(/\.tsx?$/, ".d.ts");
636
637
  }
637
638
 
639
+ const TS_SOURCE_EXT_RE = /\.(tsx?|mts)$/;
640
+
638
641
  function rewriteExportsValue(value, isTypesKey) {
639
642
  if (typeof value === "string") {
640
643
  return isTypesKey ? rewriteTsToDts(value) : rewriteTsToJs(value);
@@ -652,98 +655,6 @@ function rewriteExportsValue(value, isTypesKey) {
652
655
  return value;
653
656
  }
654
657
 
655
- async function validate(stagingDir, pkg, log) {
656
- const errors = [];
657
-
658
- // Check .js and .d.ts files for remaining .ts specifiers
659
- const allFiles = await findFiles(
660
- stagingDir,
661
- (name) => name.endsWith(".js") || name.endsWith(".d.ts"),
662
- );
663
-
664
- for (const filePath of allFiles) {
665
- const raw = await readFile(filePath, "utf8");
666
- // Strip block and line comments before scanning so JSDoc examples that
667
- // literally contain strings like `import './foo.js'` don't register as
668
- // false positives. Comment stripping here is intentionally coarse
669
- // (doesn't understand strings-containing-comment-markers) — good enough
670
- // for tsc-emitted output, where comments are well-behaved.
671
- const content = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
672
- const relPath = relative(stagingDir, filePath);
673
-
674
- for (const pattern of TS_SPECIFIER_PATTERNS) {
675
- for (const m of content.matchAll(pattern)) {
676
- errors.push(`${relPath}: remaining .ts specifier: ${m[0]}`);
677
- }
678
- }
679
- }
680
-
681
- // Check package.json for .ts references in entry points
682
- const entryFields = ["main", "module", "types", "typings"];
683
- for (const field of entryFields) {
684
- if (typeof pkg[field] === "string" && /(?<!\.d)\.tsx?$/.test(pkg[field])) {
685
- errors.push(`package.json "${field}" still references .ts: ${pkg[field]}`);
686
- }
687
- }
688
-
689
- // Non-fatal: yarn/npm tolerate `bin`/`main` pointing at missing
690
- // files. Real transformation bugs are caught by the specifier
691
- // checks above.
692
- const referencedFiles = collectReferencedFiles(pkg);
693
- for (const ref of referencedFiles) {
694
- if (!existsSync(join(stagingDir, ref))) {
695
- log(` Warning: referenced file missing from tarball: ${ref}`);
696
- }
697
- }
698
-
699
- if (errors.length > 0) {
700
- const msg = "Validation failed:\n " + errors.join("\n ");
701
- throw new Error(msg);
702
- }
703
-
704
- log(`Validated ${allFiles.length} file(s), no issues found`);
705
- }
706
-
707
- function collectReferencedFiles(pkg) {
708
- const refs = new Set ();
709
-
710
- for (const field of ["main", "module", "types", "typings"]) {
711
- if (typeof pkg[field] === "string") {
712
- refs.add(pkg[field].replace(/^\.\//, ""));
713
- }
714
- }
715
-
716
- if (typeof pkg.bin === "string") {
717
- refs.add(pkg.bin.replace(/^\.\//, ""));
718
- } else if (typeof pkg.bin === "object" && pkg.bin !== null) {
719
- for (const v of Object.values(pkg.bin)) {
720
- if (typeof v === "string") refs.add(v.replace(/^\.\//, ""));
721
- }
722
- }
723
-
724
- if (pkg.exports) {
725
- collectExportsRefs(pkg.exports, refs);
726
- }
727
-
728
- return refs;
729
- }
730
-
731
- function collectExportsRefs(value, refs) {
732
- if (typeof value === "string") {
733
- refs.add(value.replace(/^\.\//, ""));
734
- return;
735
- }
736
- if (Array.isArray(value)) {
737
- for (const v of value) collectExportsRefs(v, refs);
738
- return;
739
- }
740
- if (typeof value === "object" && value !== null) {
741
- for (const v of Object.values(value)) {
742
- collectExportsRefs(v, refs);
743
- }
744
- }
745
- }
746
-
747
658
  async function pack(stagingDir) {
748
659
  const { stdout } = await execFileAsync("npm", ["pack"], {
749
660
  cwd: stagingDir,
@@ -5,7 +5,7 @@
5
5
  * `../`) ending in `.ts`, `.tsx`, or `.mts`, embedded in one of six
6
6
  * specific syntactic contexts. The output replaces the extension with
7
7
  * `.js` (for `.ts`/`.tsx`) or `.mjs` (for `.mts`), matching what
8
- * `ts-blank-space` produces in the strip phase.
8
+ * amaro produces in the type stripping phase.
9
9
  *
10
10
  * Why regex is sound here:
11
11
  *
@@ -5,7 +5,7 @@
5
5
  * `../`) ending in `.ts`, `.tsx`, or `.mts`, embedded in one of six
6
6
  * specific syntactic contexts. The output replaces the extension with
7
7
  * `.js` (for `.ts`/`.tsx`) or `.mjs` (for `.mts`), matching what
8
- * `ts-blank-space` produces in the strip phase.
8
+ * amaro produces in the type stripping phase.
9
9
  *
10
10
  * Why regex is sound here:
11
11
  *
@@ -0,0 +1,21 @@
1
+ export declare function validate(stagingDir: any, pkg: any, log?: (...args: unknown[]) => void): Promise<void>;
2
+ /**
3
+ * Collect the relative file paths referenced by path-bearing fields of a
4
+ * package.json manifest, partitioned by how strictly a missing target
5
+ * should be treated.
6
+ *
7
+ * - `strict`: `main`, `module`, `types`, `typings`, and every leaf of
8
+ * `exports`. A missing target here is a bug in the tarball we produced —
9
+ * Node's module resolution will fail and `npm publish` warns on
10
+ * dangling `types`.
11
+ * - `lenient`: `bin` entries. yarn and npm both tolerate a `bin` pointing
12
+ * at a missing file (see the `bin-missing` fixture, which mirrors
13
+ * agoric/portfolio-api's intentional shape).
14
+ *
15
+ * Exported for direct unit-testing of the policy; the only runtime caller
16
+ * is `validate()` above.
17
+ */
18
+ export declare function collectReferencedFiles(pkg: any): {
19
+ strict: Set<string>;
20
+ lenient: Set<string>;
21
+ };
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Post-rewrite validation of a staged package. `validate()` is the one
3
+ * caller of `collectReferencedFiles`; both live here because the
4
+ * strict-vs-lenient partition is a policy decision owned by validation.
5
+ */
6
+ import { existsSync } from "node:fs";
7
+ import { readdir, readFile } from "node:fs/promises";
8
+ import { join, relative } from "node:path";
9
+ import { TS_SPECIFIER_PATTERNS } from "./rewrite-specifiers.js";
10
+
11
+ export async function validate(stagingDir, pkg, log = () => {}) {
12
+ const errors = [];
13
+
14
+ // Check .js/.mjs and .d.ts/.d.mts files for remaining .ts specifiers
15
+ const allFiles = await findFiles(
16
+ stagingDir,
17
+ (name) =>
18
+ name.endsWith(".js") ||
19
+ name.endsWith(".mjs") ||
20
+ name.endsWith(".d.ts") ||
21
+ name.endsWith(".d.mts"),
22
+ );
23
+
24
+ for (const filePath of allFiles) {
25
+ const raw = await readFile(filePath, "utf8");
26
+ // Strip block and line comments before scanning so JSDoc examples that
27
+ // literally contain strings like `import './foo.js'` don't register as
28
+ // false positives. Comment stripping here is intentionally coarse
29
+ // (doesn't understand strings-containing-comment-markers) — good enough
30
+ // for tsc-emitted output, where comments are well-behaved.
31
+ const content = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
32
+ const relPath = relative(stagingDir, filePath);
33
+
34
+ for (const pattern of TS_SPECIFIER_PATTERNS) {
35
+ for (const m of content.matchAll(pattern)) {
36
+ errors.push(`${relPath}: remaining .ts specifier: ${m[0]}`);
37
+ }
38
+ }
39
+ }
40
+
41
+ // Check package.json for .ts references in entry points
42
+ const entryFields = ["main", "module", "types", "typings"];
43
+ for (const field of entryFields) {
44
+ if (typeof pkg[field] === "string" && /(?<!\.d)\.(tsx?|mts)$/.test(pkg[field])) {
45
+ errors.push(`package.json "${field}" still references .ts: ${pkg[field]}`);
46
+ }
47
+ }
48
+
49
+ // main/module/types/typings/exports must resolve to a file that is
50
+ // actually in the staging dir. This catches rewrite bugs that specifier
51
+ // scanning can't — e.g. the @endo/ses-ava regression where a synthesized
52
+ // `types: ./index.d.ts` pointed at a .d.ts ts-node-pack never emitted.
53
+ // `bin` is deliberately excluded: yarn/npm both tolerate a missing bin
54
+ // target (see the bin-missing fixture — agoric/portfolio-api ships that
55
+ // way intentionally), so we only warn.
56
+ const { strict: strictRefs, lenient: lenientRefs } = collectReferencedFiles(pkg);
57
+ for (const ref of strictRefs) {
58
+ if (!existsSync(join(stagingDir, ref))) {
59
+ errors.push(`referenced file missing from tarball: ${ref}`);
60
+ }
61
+ }
62
+ for (const ref of lenientRefs) {
63
+ if (!existsSync(join(stagingDir, ref))) {
64
+ log(` Warning: referenced file missing from tarball: ${ref}`);
65
+ }
66
+ }
67
+
68
+ if (errors.length > 0) {
69
+ const msg = "Validation failed:\n " + errors.join("\n ");
70
+ throw new Error(msg);
71
+ }
72
+
73
+ log(`Validated ${allFiles.length} file(s), no issues found`);
74
+ }
75
+
76
+ /**
77
+ * Collect the relative file paths referenced by path-bearing fields of a
78
+ * package.json manifest, partitioned by how strictly a missing target
79
+ * should be treated.
80
+ *
81
+ * - `strict`: `main`, `module`, `types`, `typings`, and every leaf of
82
+ * `exports`. A missing target here is a bug in the tarball we produced —
83
+ * Node's module resolution will fail and `npm publish` warns on
84
+ * dangling `types`.
85
+ * - `lenient`: `bin` entries. yarn and npm both tolerate a `bin` pointing
86
+ * at a missing file (see the `bin-missing` fixture, which mirrors
87
+ * agoric/portfolio-api's intentional shape).
88
+ *
89
+ * Exported for direct unit-testing of the policy; the only runtime caller
90
+ * is `validate()` above.
91
+ */
92
+ export function collectReferencedFiles(pkg) {
93
+ const strict = new Set ();
94
+ const lenient = new Set ();
95
+ const addStrict = (p) => strict.add(stripDotSlash(p));
96
+ const addLenient = (p) => lenient.add(stripDotSlash(p));
97
+
98
+ for (const field of ["main", "module", "types", "typings"]) {
99
+ if (typeof pkg[field] === "string") addStrict(pkg[field]);
100
+ }
101
+
102
+ if (typeof pkg.bin === "string") {
103
+ addLenient(pkg.bin);
104
+ } else if (typeof pkg.bin === "object" && pkg.bin !== null) {
105
+ for (const v of Object.values(pkg.bin)) {
106
+ if (typeof v === "string") addLenient(v);
107
+ }
108
+ }
109
+
110
+ if (pkg.exports) collectExportsRefs(pkg.exports, strict);
111
+
112
+ return { strict, lenient };
113
+ }
114
+
115
+ function collectExportsRefs(value, refs) {
116
+ if (typeof value === "string") {
117
+ refs.add(stripDotSlash(value));
118
+ return;
119
+ }
120
+ if (Array.isArray(value)) {
121
+ for (const v of value) collectExportsRefs(v, refs);
122
+ return;
123
+ }
124
+ if (typeof value === "object" && value !== null) {
125
+ for (const v of Object.values(value)) collectExportsRefs(v, refs);
126
+ }
127
+ }
128
+
129
+ function stripDotSlash(p) {
130
+ return p.replace(/^\.\//, "");
131
+ }
132
+
133
+ async function findFiles(dir, test) {
134
+ const results = [];
135
+ const entries = await readdir(dir, { withFileTypes: true, recursive: true });
136
+ for (const entry of entries) {
137
+ if (entry.isFile() && test(entry.name)) {
138
+ results.push(join(entry.parentPath, entry.name));
139
+ }
140
+ }
141
+ return results;
142
+ }