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 +4 -4
- package/src/index.js +23 -112
- package/src/rewrite-specifiers.d.ts +1 -1
- package/src/rewrite-specifiers.js +1 -1
- package/src/validation.d.ts +21 -0
- package/src/validation.js +142 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-node-pack",
|
|
3
|
-
"version": "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
|
-
"
|
|
31
|
-
"
|
|
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@
|
|
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
|
|
18
|
-
import {
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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 =
|
|
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" &&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
+
}
|