@ttsc/banner 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/banner.go +143 -38
- package/package.json +2 -2
package/driver/banner.go
CHANGED
|
@@ -10,17 +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 subprocesses that evaluate user-supplied banner
|
|
20
|
-
// config files. This matches the strip/lint loaders so a hanging config does
|
|
21
|
-
// not block the compiler indefinitely.
|
|
22
|
-
const configLoaderTimeout = 60 * time.Second
|
|
23
|
-
|
|
24
18
|
func init() {
|
|
25
19
|
driver.RegisterPlugin(plugin{})
|
|
26
20
|
}
|
|
@@ -316,7 +310,14 @@ const { pathToFileURL } = require("node:url");
|
|
|
316
310
|
process.stdout.write(JSON.stringify(toSerializableBanner(value)));
|
|
317
311
|
})().catch((error) => {
|
|
318
312
|
process.stderr.write(error && error.stack ? error.stack : String(error));
|
|
319
|
-
|
|
313
|
+
// The stack above is for the reader. This is for the caller: the parent reads
|
|
314
|
+
// stdout as the payload channel either way, so a failure reason travels as
|
|
315
|
+
// data rather than as text scraped back out of a captured stream. The exit
|
|
316
|
+
// code is set before the write so a callback that never fires still fails the
|
|
317
|
+
// load, and the write's completion is what triggers the exit, because
|
|
318
|
+
// process.exit abandons a pending pipe write.
|
|
319
|
+
process.exitCode = 1;
|
|
320
|
+
process.stdout.write(JSON.stringify({ __ttscLoaderError: error && error.message ? String(error.message) : String(error) }), () => process.exit(1));
|
|
320
321
|
});
|
|
321
322
|
|
|
322
323
|
function toSerializableBanner(value) {
|
|
@@ -330,21 +331,22 @@ function toSerializableBanner(value) {
|
|
|
330
331
|
if node == "" {
|
|
331
332
|
node = "node"
|
|
332
333
|
}
|
|
333
|
-
ctx, cancel := context.
|
|
334
|
+
ctx, cancel := context.WithCancel(context.Background())
|
|
334
335
|
defer cancel()
|
|
335
336
|
cmd := exec.CommandContext(ctx, node, "-e", script, location)
|
|
336
337
|
cmd.Env = nodeConfigLoaderEnv(location)
|
|
338
|
+
// The child's stderr is human output and goes straight to this process's
|
|
339
|
+
// stderr as it is written. Collecting it only to replay it afterwards is what
|
|
340
|
+
// made a long evaluation print nothing at all, and what would make a loud one
|
|
341
|
+
// grow this process's memory without bound.
|
|
342
|
+
cmd.Stderr = os.Stderr
|
|
337
343
|
output, err := cmd.Output()
|
|
338
344
|
if err != nil {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
stderr = strings.TrimSpace(string(exit.Stderr))
|
|
345
|
-
}
|
|
346
|
-
if stderr != "" {
|
|
347
|
-
return nil, fmt.Errorf("@ttsc/banner: load config file %s: %s", location, stderr)
|
|
345
|
+
// The loader's stack already reached this process's stderr as it ran.
|
|
346
|
+
// What it could not put there is a reason a caller can act on, so that
|
|
347
|
+
// arrives through the payload channel instead.
|
|
348
|
+
if reason := loaderFailureReason(output); reason != "" {
|
|
349
|
+
return nil, fmt.Errorf("@ttsc/banner: load config file %s: %s", location, reason)
|
|
348
350
|
}
|
|
349
351
|
return nil, fmt.Errorf("@ttsc/banner: load config file %s: %w", location, err)
|
|
350
352
|
}
|
|
@@ -396,21 +398,22 @@ func loadBannerTypeScriptConfigFile(location string) (any, error) {
|
|
|
396
398
|
}
|
|
397
399
|
args = append(args, loader)
|
|
398
400
|
|
|
399
|
-
ctx, cancel := context.
|
|
401
|
+
ctx, cancel := context.WithCancel(context.Background())
|
|
400
402
|
defer cancel()
|
|
401
403
|
cmd := ttsxCommandContext(ctx, args...)
|
|
402
404
|
cmd.Env = nodeConfigLoaderEnv(location)
|
|
405
|
+
// The child's stderr is human output and goes straight to this process's
|
|
406
|
+
// stderr as it is written. Collecting it only to replay it afterwards is what
|
|
407
|
+
// made a long evaluation print nothing at all, and what would make a loud one
|
|
408
|
+
// grow this process's memory without bound.
|
|
409
|
+
cmd.Stderr = os.Stderr
|
|
403
410
|
output, err := cmd.Output()
|
|
404
411
|
if err != nil {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
stderr = strings.TrimSpace(string(exit.Stderr))
|
|
411
|
-
}
|
|
412
|
-
if stderr != "" {
|
|
413
|
-
return nil, fmt.Errorf("@ttsc/banner: load TypeScript config file %s: %s", location, stderr)
|
|
412
|
+
// The loader's stack already reached this process's stderr as it ran.
|
|
413
|
+
// What it could not put there is a reason a caller can act on, so that
|
|
414
|
+
// arrives through the payload channel instead.
|
|
415
|
+
if reason := loaderFailureReason(output); reason != "" {
|
|
416
|
+
return nil, fmt.Errorf("@ttsc/banner: load TypeScript config file %s: %s", location, reason)
|
|
414
417
|
}
|
|
415
418
|
return nil, fmt.Errorf("@ttsc/banner: load TypeScript config file %s: %w", location, err)
|
|
416
419
|
}
|
|
@@ -442,18 +445,36 @@ func bannerTypeScriptConfigLoaderSource(importLiteral string) string {
|
|
|
442
445
|
return fmt.Sprintf(`import * as importedConfig from %s;
|
|
443
446
|
|
|
444
447
|
declare const process: {
|
|
445
|
-
|
|
448
|
+
exitCode?: number;
|
|
449
|
+
stdout: { write(value: string, callback?: () => void): void };
|
|
446
450
|
stderr: { write(value: string): void };
|
|
447
451
|
exit(code?: number): never;
|
|
448
452
|
};
|
|
449
453
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
454
|
+
// Wrapped rather than written as a top-level await: the loader tsconfig's
|
|
455
|
+
// "module" follows the config's own package, and TS1378 rejects top-level await
|
|
456
|
+
// under a CommonJS module option however this .mts file emits. The body's own
|
|
457
|
+
// catch is the only failure path — it ends the process — so there is nothing
|
|
458
|
+
// left for a trailing handler to settle.
|
|
459
|
+
(async () => {
|
|
460
|
+
try {
|
|
461
|
+
const value = await resolveConfig(importedConfig);
|
|
462
|
+
process.stdout.write(JSON.stringify(toSerializableBanner(value)));
|
|
463
|
+
} catch (error) {
|
|
464
|
+
process.stderr.write(error instanceof Error && error.stack ? error.stack : String(error));
|
|
465
|
+
// The stack above is for the reader. This is for the caller: the parent
|
|
466
|
+
// reads stdout as the payload channel either way, so a failure reason
|
|
467
|
+
// travels as data rather than as text scraped back out of a captured
|
|
468
|
+
// stream. The exit code is set before the write so a callback that never
|
|
469
|
+
// fires still fails the load, and the write's completion is what triggers
|
|
470
|
+
// the exit, because process.exit abandons a pending pipe write.
|
|
471
|
+
process.exitCode = 1;
|
|
472
|
+
process.stdout.write(
|
|
473
|
+
JSON.stringify({ __ttscLoaderError: error instanceof Error ? error.message : String(error) }),
|
|
474
|
+
() => process.exit(1),
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
})();
|
|
457
478
|
|
|
458
479
|
async function resolveConfig(value: unknown): Promise<unknown> {
|
|
459
480
|
let current = isObject(value) && hasOwn(value, "default") ? value.default : value;
|
|
@@ -499,8 +520,14 @@ function toSerializableBanner(value: unknown): unknown {
|
|
|
499
520
|
func typeScriptConfigLoaderTsconfig(loader, location, outDir string) string {
|
|
500
521
|
content := map[string]any{
|
|
501
522
|
"compilerOptions": map[string]any{
|
|
502
|
-
"allowImportingTsExtensions":
|
|
503
|
-
|
|
523
|
+
"allowImportingTsExtensions": true,
|
|
524
|
+
// The config is a Node module, so Node's rule decides its format: the
|
|
525
|
+
// nearest package.json "type" above it. Hardcoding one answer ran every
|
|
526
|
+
// ambiguous `.ts` config as ESM and broke __dirname in an ordinary
|
|
527
|
+
// CommonJS package (#1069). moduleResolution stays "bundler", which tsgo
|
|
528
|
+
// accepts for both kinds, so extensionless relative imports keep
|
|
529
|
+
// resolving either way.
|
|
530
|
+
"module": configModuleOption(location),
|
|
504
531
|
"moduleResolution": "bundler",
|
|
505
532
|
"outDir": filepath.ToSlash(filepath.Join(outDir, "out")),
|
|
506
533
|
"rewriteRelativeImportExtensions": true,
|
|
@@ -508,6 +535,12 @@ func typeScriptConfigLoaderTsconfig(loader, location, outDir string) string {
|
|
|
508
535
|
"skipLibCheck": true,
|
|
509
536
|
"strict": true,
|
|
510
537
|
"target": "ES2022",
|
|
538
|
+
// TypeScript 7 includes no ambient type package unless "types" asks for
|
|
539
|
+
// it, and this Program extends nothing, so without the wildcard a config
|
|
540
|
+
// could not name a single Node global (#1069). The loader directory links
|
|
541
|
+
// the config's nearest node_modules, so the default typeRoots walk finds
|
|
542
|
+
// exactly what the project installed.
|
|
543
|
+
"types": []string{"*"},
|
|
511
544
|
},
|
|
512
545
|
"files": []string{
|
|
513
546
|
filepath.ToSlash(loader),
|
|
@@ -518,6 +551,56 @@ func typeScriptConfigLoaderTsconfig(loader, location, outDir string) string {
|
|
|
518
551
|
return string(body)
|
|
519
552
|
}
|
|
520
553
|
|
|
554
|
+
// configModuleOption returns the loader tsconfig's "module" for a config file:
|
|
555
|
+
// the module kind Node itself would give that file.
|
|
556
|
+
//
|
|
557
|
+
// An explicit .cts/.cjs or .mts/.mjs extension already decides the emit format
|
|
558
|
+
// on its own, so those keep the ES-module setting and let the extension win —
|
|
559
|
+
// the same precedence tsgo applies. Everything ambiguous walks up for the
|
|
560
|
+
// nearest package.json "type", exactly as Node does when it loads the file.
|
|
561
|
+
func configModuleOption(location string) string {
|
|
562
|
+
switch strings.ToLower(filepath.Ext(location)) {
|
|
563
|
+
case ".ts", ".tsx", ".js":
|
|
564
|
+
if nearestPackageType(location) == "commonjs" {
|
|
565
|
+
return "CommonJS"
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return "ESNext"
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// nearestPackageType mirrors Node's package-scope lookup for the nearest
|
|
572
|
+
// package.json above location: the walk stops at the FIRST manifest it finds,
|
|
573
|
+
// and a manifest declaring no "type" means CommonJS rather than a reason to
|
|
574
|
+
// keep climbing. Reaching the filesystem root without any manifest also means
|
|
575
|
+
// CommonJS. The location is made absolute first, so a relative config path
|
|
576
|
+
// cannot end the walk at "." after a single step.
|
|
577
|
+
func nearestPackageType(location string) string {
|
|
578
|
+
absolute, err := filepath.Abs(location)
|
|
579
|
+
if err != nil {
|
|
580
|
+
absolute = location
|
|
581
|
+
}
|
|
582
|
+
dir := filepath.Dir(absolute)
|
|
583
|
+
for {
|
|
584
|
+
raw, err := os.ReadFile(filepath.Join(dir, "package.json"))
|
|
585
|
+
if err == nil {
|
|
586
|
+
var manifest struct {
|
|
587
|
+
Type string `json:"type"`
|
|
588
|
+
}
|
|
589
|
+
// A manifest that does not parse still bounds the package scope; Node
|
|
590
|
+
// refuses to look past it, and CommonJS is the format it defaults to.
|
|
591
|
+
if json.Unmarshal(raw, &manifest) == nil && manifest.Type == "module" {
|
|
592
|
+
return "module"
|
|
593
|
+
}
|
|
594
|
+
return "commonjs"
|
|
595
|
+
}
|
|
596
|
+
parent := filepath.Dir(dir)
|
|
597
|
+
if parent == dir {
|
|
598
|
+
return "commonjs"
|
|
599
|
+
}
|
|
600
|
+
dir = parent
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
521
604
|
// loaderRootDir returns the widest rootDir that still contains the loader
|
|
522
605
|
// tsconfig's inputs: the volume root of the loader temp dir (`C:/` on
|
|
523
606
|
// Windows, `/` elsewhere). A literal "/" is not an ancestor of drive-letter
|
|
@@ -597,7 +680,9 @@ func ttsxCommand(args ...string) *exec.Cmd {
|
|
|
597
680
|
return ttsxCommandContext(context.Background(), args...)
|
|
598
681
|
}
|
|
599
682
|
|
|
600
|
-
// ttsxCommandContext is the
|
|
683
|
+
// ttsxCommandContext is the context-bound variant used by config loaders. It
|
|
684
|
+
// carries no deadline: evaluating a user config is the user's own code running,
|
|
685
|
+
// and how long that is allowed to take is not this binary's decision.
|
|
601
686
|
func ttsxCommandContext(ctx context.Context, args ...string) *exec.Cmd {
|
|
602
687
|
ttsx := os.Getenv("TTSC_TTSX_BINARY")
|
|
603
688
|
if ttsx == "" {
|
|
@@ -704,3 +789,23 @@ func setEnv(env []string, key, value string) []string {
|
|
|
704
789
|
func sanitizeJSDocLine(line string) string {
|
|
705
790
|
return strings.ReplaceAll(line, "*/", "* /")
|
|
706
791
|
}
|
|
792
|
+
|
|
793
|
+
// loaderFailureReason reads the failure envelope a config loader writes to its
|
|
794
|
+
// payload channel when it stops on an error it can name.
|
|
795
|
+
//
|
|
796
|
+
// The loader's stack goes to this process's stderr as it runs, which is where a
|
|
797
|
+
// reader wants it. But the *reason* — "config file must export an object with a
|
|
798
|
+
// non-empty text string" — is a fact about the user's config, and a caller
|
|
799
|
+
// deserves it in the error rather than having to go find it in the log. So it
|
|
800
|
+
// travels as data through the same stdout the payload uses, and only a
|
|
801
|
+
// well-formed envelope is honoured: anything else leaves the process status to
|
|
802
|
+
// speak for itself.
|
|
803
|
+
func loaderFailureReason(output []byte) string {
|
|
804
|
+
var envelope struct {
|
|
805
|
+
Message string `json:"__ttscLoaderError"`
|
|
806
|
+
}
|
|
807
|
+
if json.Unmarshal(output, &envelope) != nil {
|
|
808
|
+
return ""
|
|
809
|
+
}
|
|
810
|
+
return strings.TrimSpace(envelope.Message)
|
|
811
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttsc/banner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "First-party ttsc plugin that adds package-documentation JSDoc banners during emit.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@types/node": "^25.3.0",
|
|
36
36
|
"rimraf": "^6.1.2",
|
|
37
37
|
"typescript": "^7.0.2",
|
|
38
|
-
"ttsc": "0.
|
|
38
|
+
"ttsc": "0.26.0"
|
|
39
39
|
},
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|