ts-node-pack 0.1.0 → 0.1.2
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 +5 -5
- package/src/index.d.ts +0 -40
- package/src/index.js +126 -185
- package/src/rewrite-specifiers.d.ts +32 -0
- package/src/rewrite-specifiers.js +67 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-node-pack",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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(`
|
|
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,106 @@ export async function tsNodePack(
|
|
|
55
56
|
log(`Staging: ${stagingDir}`);
|
|
56
57
|
|
|
57
58
|
try {
|
|
58
|
-
// ── Phase 3:
|
|
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
|
|
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
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
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
|
|
129
|
-
// path
|
|
130
|
-
|
|
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
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
85
|
+
// ── Phase 4: Decide whether to run tsc, generate config if so ────────
|
|
86
|
+
// Trigger declaration emit only when there's something to derive:
|
|
87
|
+
// 1. user passed --tsconfig (explicit opt-in), OR
|
|
88
|
+
// 2. any source contains .ts/.tsx/.mts (we'd be stripping them
|
|
89
|
+
// anyway, and probably want their declarations).
|
|
90
|
+
// The mere presence of tsconfig.build.json is NOT enough — monorepos
|
|
91
|
+
// commonly keep one per package for a root project-references build
|
|
92
|
+
// (`tsc --build`) without intending each package to be independently
|
|
93
|
+
// emit-able. Pure JS+JSDoc packages with no .ts sources skip tsc and
|
|
94
|
+
// ship whatever .js files are already in the packlist, matching plain
|
|
95
|
+
// `npm pack` semantics.
|
|
96
|
+
const hasTsSources = packFiles.some(
|
|
97
|
+
(f) => /\.(ts|tsx|mts)$/.test(f) && !/\.d\.(ts|mts)$/.test(f),
|
|
98
|
+
);
|
|
99
|
+
const shouldRunTsc = tsconfigPath !== null && (tsconfig !== undefined || hasTsSources);
|
|
100
|
+
|
|
101
|
+
if (shouldRunTsc) {
|
|
102
|
+
log("Phase 4: Generating derived tsconfig...");
|
|
103
|
+
const emitConfigPath = join(tmpDir, "tsconfig.emit.json");
|
|
104
|
+
// tsc's typeRoots walk-up starts at the config's directory, so it
|
|
105
|
+
// can't reach packageDir/node_modules/@types from our temp-dir
|
|
106
|
+
// location. Pin typeRoots when that directory exists. (TS 6.0
|
|
107
|
+
// surfaces the missing resolution as TS2688 instead of the silent
|
|
108
|
+
// "Cannot find module 'node:util'" cascade of earlier versions.)
|
|
109
|
+
// Walk upward to find the nearest ancestor that actually has
|
|
110
|
+
// node_modules/@types. Yarn 4's pnpm linker keeps `@types` only at
|
|
111
|
+
// the workspace root, so the package's own node_modules is empty.
|
|
112
|
+
const findAtTypes = (start ) => {
|
|
113
|
+
let dir = start;
|
|
114
|
+
while (true) {
|
|
115
|
+
const candidate = join(dir, "node_modules", "@types");
|
|
116
|
+
if (existsSync(candidate)) return candidate;
|
|
117
|
+
const parent = dirname(dir);
|
|
118
|
+
if (parent === dir) return null;
|
|
119
|
+
dir = parent;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const atTypesDir = findAtTypes(packageDir);
|
|
123
|
+
const hasAtTypes = atTypesDir !== null;
|
|
124
|
+
const emitConfig = {
|
|
125
|
+
extends: tsconfigPath,
|
|
126
|
+
compilerOptions: {
|
|
127
|
+
// Force rootDir so emit preserves the source layout —
|
|
128
|
+
// otherwise tsc infers the common ancestor and strips the
|
|
129
|
+
// `src/` prefix, breaking `main`/`exports` that reference
|
|
130
|
+
// `./src/...`.
|
|
131
|
+
rootDir: packageDir,
|
|
132
|
+
outDir: stagingDir,
|
|
133
|
+
declaration: true,
|
|
134
|
+
// ts-blank-space handles .js emit (Phase 6); tsc only emits .d.ts.
|
|
135
|
+
emitDeclarationOnly: true,
|
|
136
|
+
// Extract types from JS+JSDoc sources in mixed packages.
|
|
137
|
+
allowJs: true,
|
|
138
|
+
noEmit: false,
|
|
139
|
+
// Incremental/composite write a .tsbuildinfo whose path tsc
|
|
140
|
+
// computes relative to the base config, producing garbled
|
|
141
|
+
// paths when our outDir crosses directory trees.
|
|
142
|
+
incremental: false,
|
|
143
|
+
composite: false,
|
|
144
|
+
tsBuildInfoFile: null,
|
|
145
|
+
...(hasAtTypes ? { typeRoots: [atTypesDir] } : {}),
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
await writeFile(emitConfigPath, JSON.stringify(emitConfig, null, 2) + "\n");
|
|
149
|
+
|
|
150
|
+
log("Phase 5: Emitting .d.ts files via tsc...");
|
|
151
|
+
await runTsc(emitConfigPath, packageDir, log);
|
|
152
|
+
} else {
|
|
153
|
+
log(
|
|
154
|
+
tsconfigPath === null
|
|
155
|
+
? "Phase 4-5: Skipping tsc (no tsconfig found; pure-JS package)"
|
|
156
|
+
: "Phase 4-5: Skipping tsc (no .ts sources and no tsconfig.build.json opt-in)",
|
|
157
|
+
);
|
|
158
|
+
}
|
|
151
159
|
|
|
152
160
|
// ── Phase 6: Strip types and rewrite specifiers ─────────────────────
|
|
153
161
|
// Single-pass transform of every staging file that could contain a
|
|
@@ -157,10 +165,7 @@ export async function tsNodePack(
|
|
|
157
165
|
// ts-blank-space preserves line and column positions, so no
|
|
158
166
|
// sourcemaps are needed for debugging.
|
|
159
167
|
log("Phase 6: Stripping types and rewriting specifiers...");
|
|
160
|
-
const { strippedCount, rewrittenCount } = await processStagingFiles(
|
|
161
|
-
stagingDir,
|
|
162
|
-
log,
|
|
163
|
-
);
|
|
168
|
+
const { strippedCount, rewrittenCount } = await processStagingFiles(stagingDir, log);
|
|
164
169
|
log(
|
|
165
170
|
`Stripped ${strippedCount} type-annotated file(s); rewrote ${rewrittenCount} other file(s)`,
|
|
166
171
|
);
|
|
@@ -173,16 +178,9 @@ export async function tsNodePack(
|
|
|
173
178
|
// packages to publish), then flip entry paths and strip dev-only
|
|
174
179
|
// fields.
|
|
175
180
|
log("Phase 7: Rewriting package.json...");
|
|
176
|
-
const resolvedPkg = await resolveWorkspaceDependencies(
|
|
177
|
-
pkgJson,
|
|
178
|
-
packageDir,
|
|
179
|
-
log,
|
|
180
|
-
);
|
|
181
|
+
const resolvedPkg = await resolveWorkspaceDependencies(pkgJson, packageDir, log);
|
|
181
182
|
const rewrittenPkg = rewritePackageJson(resolvedPkg);
|
|
182
|
-
await writeFile(
|
|
183
|
-
join(stagingDir, "package.json"),
|
|
184
|
-
JSON.stringify(rewrittenPkg, null, 2) + "\n",
|
|
185
|
-
);
|
|
183
|
+
await writeFile(join(stagingDir, "package.json"), JSON.stringify(rewrittenPkg, null, 2) + "\n");
|
|
186
184
|
|
|
187
185
|
// ── Phase 8: Validate ─────────────────────────────────────────────────
|
|
188
186
|
log("Phase 8: Validating...");
|
|
@@ -191,7 +189,7 @@ export async function tsNodePack(
|
|
|
191
189
|
// ── Phase 9: Pack ─────────────────────────────────────────────────────
|
|
192
190
|
if (!emitOnly) {
|
|
193
191
|
log("Phase 9: Packing...");
|
|
194
|
-
const tgzPath = await pack(stagingDir
|
|
192
|
+
const tgzPath = await pack(stagingDir);
|
|
195
193
|
const tgzName = basename(tgzPath);
|
|
196
194
|
const dest = join(process.cwd(), tgzName);
|
|
197
195
|
await copyFile(tgzPath, dest);
|
|
@@ -214,7 +212,12 @@ export async function tsNodePack(
|
|
|
214
212
|
|
|
215
213
|
// ── Phase helpers ──────────────────────────────────────────────────────────
|
|
216
214
|
|
|
217
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Find the tsconfig to extend, or null if the package has no TypeScript
|
|
217
|
+
* config at all (pure JS package). The `--tsconfig` flag is the one
|
|
218
|
+
* explicit case where missing-config is fatal.
|
|
219
|
+
*/
|
|
220
|
+
function resolveTsconfig(packageDir, tsconfigOption) {
|
|
218
221
|
if (tsconfigOption) {
|
|
219
222
|
const p = resolve(packageDir, tsconfigOption);
|
|
220
223
|
if (!existsSync(p)) {
|
|
@@ -222,14 +225,11 @@ function resolveTsconfig(packageDir, tsconfigOption) {
|
|
|
222
225
|
}
|
|
223
226
|
return p;
|
|
224
227
|
}
|
|
225
|
-
|
|
226
228
|
const buildConfig = join(packageDir, "tsconfig.build.json");
|
|
227
229
|
if (existsSync(buildConfig)) return buildConfig;
|
|
228
|
-
|
|
229
230
|
const defaultConfig = join(packageDir, "tsconfig.json");
|
|
230
231
|
if (existsSync(defaultConfig)) return defaultConfig;
|
|
231
|
-
|
|
232
|
-
throw new Error(`No tsconfig found in ${packageDir}. Provide one with --tsconfig.`);
|
|
232
|
+
return null;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
/**
|
|
@@ -248,14 +248,33 @@ function findLocalBin(startDir, name) {
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
async function
|
|
251
|
+
async function findLocalTsc(cwd) {
|
|
252
252
|
// Prefer a local tsc so users control the compiler version (and to avoid
|
|
253
253
|
// npx resolving to macOS's /usr/bin/tsc — the TeX/Smalltalk compiler —
|
|
254
254
|
// when no local install exists). In a monorepo, the package's own
|
|
255
255
|
// node_modules/.bin may be empty while the workspace root has the
|
|
256
256
|
// binary, so walk upward the same way npm's `$PATH` composition does.
|
|
257
257
|
const binName = process.platform === "win32" ? "tsc.cmd" : "tsc";
|
|
258
|
-
const
|
|
258
|
+
const fromBin = findLocalBin(cwd, binName);
|
|
259
|
+
if (fromBin !== null) return fromBin;
|
|
260
|
+
// Yarn 4's pnpm/PnP linkers do not populate node_modules/.bin/. Fall back
|
|
261
|
+
// to `yarn bin tsc`, which resolves through the active linker and prints
|
|
262
|
+
// the absolute path of the tsc binary when the workspace depends on it.
|
|
263
|
+
try {
|
|
264
|
+
const { stdout } = await execFileAsync("yarn", ["bin", "tsc"], {
|
|
265
|
+
cwd,
|
|
266
|
+
maxBuffer: 1024 * 1024,
|
|
267
|
+
});
|
|
268
|
+
const candidate = stdout.trim().split("\n").pop();
|
|
269
|
+
if (candidate && existsSync(candidate)) return candidate;
|
|
270
|
+
} catch {
|
|
271
|
+
// `yarn` not on PATH or not a yarn project — fall through to npx.
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function runTsc(emitConfigPath, cwd, log) {
|
|
277
|
+
const localTsc = await findLocalTsc(cwd);
|
|
259
278
|
const useLocal = localTsc !== null;
|
|
260
279
|
const [cmd, argv] = useLocal
|
|
261
280
|
? [localTsc, ["-p", emitConfigPath]]
|
|
@@ -314,18 +333,14 @@ async function processStagingFiles(stagingDir, log) {
|
|
|
314
333
|
});
|
|
315
334
|
if (errors.length > 0) {
|
|
316
335
|
throw new Error(
|
|
317
|
-
"ts-blank-space rejected non-erasable TypeScript:\n " +
|
|
318
|
-
errors.join("\n "),
|
|
336
|
+
"ts-blank-space rejected non-erasable TypeScript:\n " + errors.join("\n "),
|
|
319
337
|
);
|
|
320
338
|
}
|
|
321
339
|
}
|
|
322
340
|
content = rewriteTsSpecifiers(content);
|
|
323
341
|
|
|
324
342
|
if (isTs || isMts) {
|
|
325
|
-
const destPath = srcPath.replace(
|
|
326
|
-
isMts ? /\.mts$/ : /\.ts$/,
|
|
327
|
-
isMts ? ".mjs" : ".js",
|
|
328
|
-
);
|
|
343
|
+
const destPath = srcPath.replace(isMts ? /\.mts$/ : /\.ts$/, isMts ? ".mjs" : ".js");
|
|
329
344
|
await writeFile(destPath, content);
|
|
330
345
|
await unlink(srcPath);
|
|
331
346
|
strippedCount++;
|
|
@@ -339,70 +354,6 @@ async function processStagingFiles(stagingDir, log) {
|
|
|
339
354
|
return { strippedCount, rewrittenCount };
|
|
340
355
|
}
|
|
341
356
|
|
|
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
357
|
const DEP_FIELDS_WITH_WORKSPACE = [
|
|
407
358
|
"dependencies",
|
|
408
359
|
"peerDependencies",
|
|
@@ -481,9 +432,7 @@ async function findWorkspaceRoot(startDir) {
|
|
|
481
432
|
let dir = startDir;
|
|
482
433
|
while (true) {
|
|
483
434
|
try {
|
|
484
|
-
const pkg = JSON.parse(
|
|
485
|
-
await readFile(join(dir, "package.json"), "utf8"),
|
|
486
|
-
);
|
|
435
|
+
const pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf8"));
|
|
487
436
|
if (pkg.workspaces) return dir;
|
|
488
437
|
} catch {
|
|
489
438
|
// Missing or unreadable package.json — not the root, keep going.
|
|
@@ -495,9 +444,7 @@ async function findWorkspaceRoot(startDir) {
|
|
|
495
444
|
}
|
|
496
445
|
|
|
497
446
|
async function buildWorkspaceVersionMap(workspaceRoot) {
|
|
498
|
-
const rootPkg = JSON.parse(
|
|
499
|
-
await readFile(join(workspaceRoot, "package.json"), "utf8"),
|
|
500
|
-
);
|
|
447
|
+
const rootPkg = JSON.parse(await readFile(join(workspaceRoot, "package.json"), "utf8"));
|
|
501
448
|
const patterns = Array.isArray(rootPkg.workspaces)
|
|
502
449
|
? rootPkg.workspaces
|
|
503
450
|
: rootPkg.workspaces?.packages || [];
|
|
@@ -507,9 +454,7 @@ async function buildWorkspaceVersionMap(workspaceRoot) {
|
|
|
507
454
|
const dirs = await expandWorkspacePattern(workspaceRoot, pattern);
|
|
508
455
|
for (const d of dirs) {
|
|
509
456
|
try {
|
|
510
|
-
const pkg = JSON.parse(
|
|
511
|
-
await readFile(join(d, "package.json"), "utf8"),
|
|
512
|
-
);
|
|
457
|
+
const pkg = JSON.parse(await readFile(join(d, "package.json"), "utf8"));
|
|
513
458
|
if (pkg.name && pkg.version) map.set(pkg.name, pkg.version);
|
|
514
459
|
} catch {
|
|
515
460
|
// Missing or malformed sibling package.json — skip.
|
|
@@ -546,9 +491,7 @@ async function expandWorkspacePattern(workspaceRoot, pattern) {
|
|
|
546
491
|
}
|
|
547
492
|
} else if (part.includes("*")) {
|
|
548
493
|
const regex = new RegExp(
|
|
549
|
-
"^" +
|
|
550
|
-
part.replace(/[.+^$()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") +
|
|
551
|
-
"$",
|
|
494
|
+
"^" + part.replace(/[.+^$()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$",
|
|
552
495
|
);
|
|
553
496
|
try {
|
|
554
497
|
const entries = await readdir(base, { withFileTypes: true });
|
|
@@ -673,9 +616,7 @@ async function validate(stagingDir, pkg, log) {
|
|
|
673
616
|
// false positives. Comment stripping here is intentionally coarse
|
|
674
617
|
// (doesn't understand strings-containing-comment-markers) — good enough
|
|
675
618
|
// for tsc-emitted output, where comments are well-behaved.
|
|
676
|
-
const content = raw
|
|
677
|
-
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
678
|
-
.replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
619
|
+
const content = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
679
620
|
const relPath = relative(stagingDir, filePath);
|
|
680
621
|
|
|
681
622
|
for (const pattern of TS_SPECIFIER_PATTERNS) {
|
|
@@ -751,7 +692,7 @@ function collectExportsRefs(value, refs) {
|
|
|
751
692
|
}
|
|
752
693
|
}
|
|
753
694
|
|
|
754
|
-
async function pack(stagingDir
|
|
695
|
+
async function pack(stagingDir) {
|
|
755
696
|
const { stdout } = await execFileAsync("npm", ["pack"], {
|
|
756
697
|
cwd: stagingDir,
|
|
757
698
|
});
|
|
@@ -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
|
+
}
|