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 +5 -5
- package/src/index.d.ts +0 -40
- package/src/index.js +93 -183
- 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.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(`
|
|
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:
|
|
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
|
+
// 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
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|