@ttsc/strip 0.24.0 → 0.26.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/driver/config.go +155 -53
- package/package.json +1 -1
package/driver/config.go
CHANGED
|
@@ -10,18 +10,11 @@ import (
|
|
|
10
10
|
"path/filepath"
|
|
11
11
|
"runtime"
|
|
12
12
|
"strings"
|
|
13
|
-
"time"
|
|
14
13
|
|
|
15
14
|
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
16
15
|
"github.com/samchon/ttsc/packages/ttsc/driver/windowsjunction"
|
|
17
16
|
)
|
|
18
17
|
|
|
19
|
-
// configLoaderTimeout caps every `ttsx`/`node -e` subprocess that evaluates a
|
|
20
|
-
// user-supplied strip config. Mirrors the lint package budget: 60 s is generous
|
|
21
|
-
// for cold ttsx starts on CI runners and tight enough to keep user-visible
|
|
22
|
-
// feedback under a minute.
|
|
23
|
-
const configLoaderTimeout = 60 * time.Second
|
|
24
|
-
|
|
25
18
|
// stripConfigFilenames is the ordered list of candidate filenames that
|
|
26
19
|
// findStripConfigFile checks in each directory during upward discovery.
|
|
27
20
|
var stripConfigFilenames = []string{
|
|
@@ -207,7 +200,14 @@ const { pathToFileURL } = require("node:url");
|
|
|
207
200
|
process.stdout.write(JSON.stringify(value));
|
|
208
201
|
})().catch((error) => {
|
|
209
202
|
process.stderr.write(error && error.stack ? error.stack : String(error));
|
|
210
|
-
|
|
203
|
+
// The stack above is for the reader. This is for the caller: the parent reads
|
|
204
|
+
// stdout as the payload channel either way, so a failure reason travels as
|
|
205
|
+
// data rather than as text scraped back out of a captured stream. The exit
|
|
206
|
+
// code is set before the write so a callback that never fires still fails the
|
|
207
|
+
// load, and the write's completion is what triggers the exit, because
|
|
208
|
+
// process.exit abandons a pending pipe write.
|
|
209
|
+
process.exitCode = 1;
|
|
210
|
+
process.stdout.write(JSON.stringify({ __ttscLoaderError: error && error.message ? String(error.message) : String(error) }), () => process.exit(1));
|
|
211
211
|
});
|
|
212
212
|
`
|
|
213
213
|
|
|
@@ -219,21 +219,22 @@ func loadStripScriptConfigFile(location string) (any, error) {
|
|
|
219
219
|
if node == "" {
|
|
220
220
|
node = "node"
|
|
221
221
|
}
|
|
222
|
-
ctx, cancel := context.
|
|
222
|
+
ctx, cancel := context.WithCancel(context.Background())
|
|
223
223
|
defer cancel()
|
|
224
224
|
cmd := exec.CommandContext(ctx, node, "-e", stripScriptLoaderSource, location)
|
|
225
225
|
cmd.Env = stripNodeConfigLoaderEnv(location)
|
|
226
|
+
// The child's stderr is human output and goes straight to this process's
|
|
227
|
+
// stderr as it is written. Collecting it only to replay it afterwards is what
|
|
228
|
+
// made a long evaluation print nothing at all, and what would make a loud one
|
|
229
|
+
// grow this process's memory without bound.
|
|
230
|
+
cmd.Stderr = os.Stderr
|
|
226
231
|
output, err := cmd.Output()
|
|
227
232
|
if err != nil {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
stderr = strings.TrimSpace(string(exit.Stderr))
|
|
234
|
-
}
|
|
235
|
-
if stderr != "" {
|
|
236
|
-
return nil, fmt.Errorf("@ttsc/strip: load config file %s: %s", location, stderr)
|
|
233
|
+
// The loader's stack already reached this process's stderr as it ran.
|
|
234
|
+
// What it could not put there is a reason a caller can act on, so that
|
|
235
|
+
// arrives through the payload channel instead.
|
|
236
|
+
if reason := loaderFailureReason(output); reason != "" {
|
|
237
|
+
return nil, fmt.Errorf("@ttsc/strip: load config file %s: %s", location, reason)
|
|
237
238
|
}
|
|
238
239
|
return nil, fmt.Errorf("@ttsc/strip: load config file %s: %w", location, err)
|
|
239
240
|
}
|
|
@@ -252,37 +253,55 @@ func stripTypeScriptLoaderSource(importLiteral string) string {
|
|
|
252
253
|
return fmt.Sprintf(`import * as importedConfig from %s;
|
|
253
254
|
|
|
254
255
|
declare const process: {
|
|
255
|
-
|
|
256
|
+
exitCode?: number;
|
|
257
|
+
stdout: { write(value: string, callback?: () => void): void };
|
|
256
258
|
stderr: { write(value: string): void };
|
|
257
259
|
exit(code?: number): never;
|
|
258
260
|
};
|
|
259
261
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
262
|
+
// Wrapped rather than written as a top-level await: the loader tsconfig's
|
|
263
|
+
// "module" follows the config's own package, and TS1378 rejects top-level await
|
|
264
|
+
// under a CommonJS module option however this .mts file emits. The body's own
|
|
265
|
+
// catch is the only failure path — it ends the process — so there is nothing
|
|
266
|
+
// left for a trailing handler to settle.
|
|
267
|
+
(async () => {
|
|
268
|
+
try {
|
|
269
|
+
let current: unknown = importedConfig;
|
|
270
|
+
for (let i = 0; i < 8; i++) {
|
|
271
|
+
if (current !== null && typeof current === "object" && Object.prototype.hasOwnProperty.call(current as Record<string, unknown>, "default")) {
|
|
272
|
+
current = (current as Record<string, unknown>).default;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
break;
|
|
266
276
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
current
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
277
|
+
if (typeof current === "function") {
|
|
278
|
+
current = await (current as () => unknown | Promise<unknown>)();
|
|
279
|
+
}
|
|
280
|
+
if (current === null || typeof current !== "object" || Array.isArray(current)) {
|
|
281
|
+
throw new Error("strip config file must export an object");
|
|
282
|
+
}
|
|
283
|
+
process.stdout.write(JSON.stringify(current));
|
|
284
|
+
} catch (error) {
|
|
285
|
+
process.stderr.write(error instanceof Error && error.stack ? error.stack : String(error));
|
|
286
|
+
// The stack above is for the reader. This is for the caller: the parent
|
|
287
|
+
// reads stdout as the payload channel either way, so a failure reason
|
|
288
|
+
// travels as data rather than as text scraped back out of a captured
|
|
289
|
+
// stream. The exit code is set before the write so a callback that never
|
|
290
|
+
// fires still fails the load, and the write's completion is what triggers
|
|
291
|
+
// the exit, because process.exit abandons a pending pipe write.
|
|
292
|
+
process.exitCode = 1;
|
|
293
|
+
process.stdout.write(
|
|
294
|
+
JSON.stringify({ __ttscLoaderError: error instanceof Error ? error.message : String(error) }),
|
|
295
|
+
() => process.exit(1),
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
})();
|
|
280
299
|
`, importLiteral)
|
|
281
300
|
}
|
|
282
301
|
|
|
283
302
|
// loadStripTypeScriptConfigFile evaluates a .ts/.cts/.mts config file by writing
|
|
284
303
|
// an ephemeral loader script and tsconfig into a temp directory, symlinking the
|
|
285
|
-
// nearest node_modules, then running ttsx
|
|
304
|
+
// nearest node_modules, then running ttsx.
|
|
286
305
|
//
|
|
287
306
|
// The ttsx build runs with `--no-plugins`: the loader only needs to
|
|
288
307
|
// type-check and execute the strip config file, so loading the host
|
|
@@ -327,21 +346,22 @@ func loadStripTypeScriptConfigFile(location string) (any, error) {
|
|
|
327
346
|
}
|
|
328
347
|
args = append(args, loader)
|
|
329
348
|
|
|
330
|
-
ctx, cancel := context.
|
|
349
|
+
ctx, cancel := context.WithCancel(context.Background())
|
|
331
350
|
defer cancel()
|
|
332
351
|
cmd := stripTtsxCommandContext(ctx, args...)
|
|
333
352
|
cmd.Env = stripNodeConfigLoaderEnv(location)
|
|
353
|
+
// The child's stderr is human output and goes straight to this process's
|
|
354
|
+
// stderr as it is written. Collecting it only to replay it afterwards is what
|
|
355
|
+
// made a long evaluation print nothing at all, and what would make a loud one
|
|
356
|
+
// grow this process's memory without bound.
|
|
357
|
+
cmd.Stderr = os.Stderr
|
|
334
358
|
output, err := cmd.Output()
|
|
335
359
|
if err != nil {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
stderr = strings.TrimSpace(string(exit.Stderr))
|
|
342
|
-
}
|
|
343
|
-
if stderr != "" {
|
|
344
|
-
return nil, fmt.Errorf("@ttsc/strip: load TypeScript config file %s: %s", location, stderr)
|
|
360
|
+
// The loader's stack already reached this process's stderr as it ran.
|
|
361
|
+
// What it could not put there is a reason a caller can act on, so that
|
|
362
|
+
// arrives through the payload channel instead.
|
|
363
|
+
if reason := loaderFailureReason(output); reason != "" {
|
|
364
|
+
return nil, fmt.Errorf("@ttsc/strip: load TypeScript config file %s: %s", location, reason)
|
|
345
365
|
}
|
|
346
366
|
return nil, fmt.Errorf("@ttsc/strip: load TypeScript config file %s: %w", location, err)
|
|
347
367
|
}
|
|
@@ -357,10 +377,16 @@ func loadStripTypeScriptConfigFile(location string) (any, error) {
|
|
|
357
377
|
func stripTypeScriptLoaderTsconfig(loader, location, outDir string) string {
|
|
358
378
|
content := map[string]any{
|
|
359
379
|
"compilerOptions": map[string]any{
|
|
360
|
-
"allowImportingTsExtensions":
|
|
361
|
-
"allowJs":
|
|
362
|
-
"checkJs":
|
|
363
|
-
|
|
380
|
+
"allowImportingTsExtensions": true,
|
|
381
|
+
"allowJs": true,
|
|
382
|
+
"checkJs": false,
|
|
383
|
+
// The config is a Node module, so Node's rule decides its format: the
|
|
384
|
+
// nearest package.json "type" above it. Hardcoding one answer ran every
|
|
385
|
+
// ambiguous `.ts` config as ESM and broke __dirname in an ordinary
|
|
386
|
+
// CommonJS package (#1069). moduleResolution stays "bundler", which tsgo
|
|
387
|
+
// accepts for both kinds, so extensionless relative imports keep
|
|
388
|
+
// resolving either way.
|
|
389
|
+
"module": stripConfigModuleOption(location),
|
|
364
390
|
"moduleResolution": "bundler",
|
|
365
391
|
"noImplicitAny": false,
|
|
366
392
|
"outDir": filepath.ToSlash(filepath.Join(outDir, "out")),
|
|
@@ -369,6 +395,12 @@ func stripTypeScriptLoaderTsconfig(loader, location, outDir string) string {
|
|
|
369
395
|
"skipLibCheck": true,
|
|
370
396
|
"strict": false,
|
|
371
397
|
"target": "ES2022",
|
|
398
|
+
// TypeScript 7 includes no ambient type package unless "types" asks for
|
|
399
|
+
// it, and this Program extends nothing, so without the wildcard a config
|
|
400
|
+
// could not name a single Node global (#1069). The loader directory links
|
|
401
|
+
// the config's nearest node_modules, so the default typeRoots walk finds
|
|
402
|
+
// exactly what the project installed.
|
|
403
|
+
"types": []string{"*"},
|
|
372
404
|
},
|
|
373
405
|
"files": []string{
|
|
374
406
|
filepath.ToSlash(loader),
|
|
@@ -382,6 +414,56 @@ func stripTypeScriptLoaderTsconfig(loader, location, outDir string) string {
|
|
|
382
414
|
return string(body)
|
|
383
415
|
}
|
|
384
416
|
|
|
417
|
+
// stripConfigModuleOption returns the loader tsconfig's "module" for a config
|
|
418
|
+
// file: the module kind Node itself would give that file.
|
|
419
|
+
//
|
|
420
|
+
// An explicit .cts/.cjs or .mts/.mjs extension already decides the emit format
|
|
421
|
+
// on its own, so those keep the ES-module setting and let the extension win —
|
|
422
|
+
// the same precedence tsgo applies. Everything ambiguous walks up for the
|
|
423
|
+
// nearest package.json "type", exactly as Node does when it loads the file.
|
|
424
|
+
func stripConfigModuleOption(location string) string {
|
|
425
|
+
switch strings.ToLower(filepath.Ext(location)) {
|
|
426
|
+
case ".ts", ".tsx", ".js":
|
|
427
|
+
if stripNearestPackageType(location) == "commonjs" {
|
|
428
|
+
return "CommonJS"
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return "ESNext"
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// stripNearestPackageType mirrors Node's package-scope lookup for the nearest
|
|
435
|
+
// package.json above location: the walk stops at the FIRST manifest it finds,
|
|
436
|
+
// and a manifest declaring no "type" means CommonJS rather than a reason to
|
|
437
|
+
// keep climbing. Reaching the filesystem root without any manifest also means
|
|
438
|
+
// CommonJS. The location is made absolute first, so a relative config path
|
|
439
|
+
// cannot end the walk at "." after a single step.
|
|
440
|
+
func stripNearestPackageType(location string) string {
|
|
441
|
+
absolute, err := filepath.Abs(location)
|
|
442
|
+
if err != nil {
|
|
443
|
+
absolute = location
|
|
444
|
+
}
|
|
445
|
+
dir := filepath.Dir(absolute)
|
|
446
|
+
for {
|
|
447
|
+
raw, err := os.ReadFile(filepath.Join(dir, "package.json"))
|
|
448
|
+
if err == nil {
|
|
449
|
+
var manifest struct {
|
|
450
|
+
Type string `json:"type"`
|
|
451
|
+
}
|
|
452
|
+
// A manifest that does not parse still bounds the package scope; Node
|
|
453
|
+
// refuses to look past it, and CommonJS is the format it defaults to.
|
|
454
|
+
if json.Unmarshal(raw, &manifest) == nil && manifest.Type == "module" {
|
|
455
|
+
return "module"
|
|
456
|
+
}
|
|
457
|
+
return "commonjs"
|
|
458
|
+
}
|
|
459
|
+
parent := filepath.Dir(dir)
|
|
460
|
+
if parent == dir {
|
|
461
|
+
return "commonjs"
|
|
462
|
+
}
|
|
463
|
+
dir = parent
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
385
467
|
// stripLoaderRootDir returns the widest rootDir that still contains the
|
|
386
468
|
// loader tsconfig's inputs: the volume root of the loader temp dir (`C:/` on
|
|
387
469
|
// Windows, `/` elsewhere). A literal "/" is not an ancestor of drive-letter
|
|
@@ -571,3 +653,23 @@ func stripSetEnv(env []string, key, value string) []string {
|
|
|
571
653
|
}
|
|
572
654
|
return append(env, prefix+value)
|
|
573
655
|
}
|
|
656
|
+
|
|
657
|
+
// loaderFailureReason reads the failure envelope a config loader writes to its
|
|
658
|
+
// payload channel when it stops on an error it can name.
|
|
659
|
+
//
|
|
660
|
+
// The loader's stack goes to this process's stderr as it runs, which is where a
|
|
661
|
+
// reader wants it. But the *reason* — "config file must export an object with a
|
|
662
|
+
// non-empty text string" — is a fact about the user's config, and a caller
|
|
663
|
+
// deserves it in the error rather than having to go find it in the log. So it
|
|
664
|
+
// travels as data through the same stdout the payload uses, and only a
|
|
665
|
+
// well-formed envelope is honoured: anything else leaves the process status to
|
|
666
|
+
// speak for itself.
|
|
667
|
+
func loaderFailureReason(output []byte) string {
|
|
668
|
+
var envelope struct {
|
|
669
|
+
Message string `json:"__ttscLoaderError"`
|
|
670
|
+
}
|
|
671
|
+
if json.Unmarshal(output, &envelope) != nil {
|
|
672
|
+
return ""
|
|
673
|
+
}
|
|
674
|
+
return strings.TrimSpace(envelope.Message)
|
|
675
|
+
}
|
package/package.json
CHANGED