@ttsc/banner 0.30.3 → 0.31.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 +184 -609
- package/lib/createTtscBanner.d.ts +100 -0
- package/lib/createTtscBanner.js +167 -0
- package/lib/createTtscBanner.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +3 -128
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscBannerConfig.d.ts +40 -2
- package/lib/structures/ITtscBannerPluginConfig.d.ts +57 -7
- package/package.json +5 -3
- package/plugin/main.go +19 -19
- package/src/createTtscBanner.ts +256 -0
- package/src/index.ts +1 -209
- package/src/structures/ITtscBannerConfig.ts +40 -2
- package/src/structures/ITtscBannerPluginConfig.ts +57 -7
package/driver/banner.go
CHANGED
|
@@ -13,6 +13,7 @@ import (
|
|
|
13
13
|
"strings"
|
|
14
14
|
|
|
15
15
|
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
16
|
+
"github.com/samchon/ttsc/packages/ttsc/driver/resolutioninputs"
|
|
16
17
|
"github.com/samchon/ttsc/packages/ttsc/driver/windowsjunction"
|
|
17
18
|
)
|
|
18
19
|
|
|
@@ -23,13 +24,6 @@ func init() {
|
|
|
23
24
|
// plugin implements driver.SourcePreamblePlugin for @ttsc/banner.
|
|
24
25
|
type plugin struct{}
|
|
25
26
|
|
|
26
|
-
var (
|
|
27
|
-
// linkConfigNodeModules is overridable in tests to avoid real symlink creation.
|
|
28
|
-
linkConfigNodeModules = linkNearestNodeModules
|
|
29
|
-
// writeConfigLoaderFile is overridable in tests to avoid real file I/O.
|
|
30
|
-
writeConfigLoaderFile = os.WriteFile
|
|
31
|
-
)
|
|
32
|
-
|
|
33
27
|
// frameworkKeys lists the tsconfig plugin-entry keys that the ttsc host
|
|
34
28
|
// framework owns. They are accepted without error; all other keys are rejected.
|
|
35
29
|
var frameworkKeys = map[string]struct{}{
|
|
@@ -60,9 +54,31 @@ func validateBannerConfig(config map[string]any) error {
|
|
|
60
54
|
}
|
|
61
55
|
|
|
62
56
|
// SourcePreamble resolves the banner text from the plugin config and returns it
|
|
63
|
-
// formatted as
|
|
57
|
+
// formatted as JSDoc source for the host to prepend before parsing.
|
|
58
|
+
//
|
|
59
|
+
// Failure returns no preamble and does not report dependency completeness.
|
|
60
|
+
// Successful evaluation reports universal config inputs because the same banner
|
|
61
|
+
// text applies to every file. The host invokes this operation once per Program,
|
|
62
|
+
// then its filesystem wrapper injects the returned text and applies emit policy.
|
|
63
|
+
//
|
|
64
|
+
// CRLF normalization, trailing blank-line removal and closing-delimiter escaping
|
|
65
|
+
// make consumer text a closed comment. The host owns placement, authored-position
|
|
66
|
+
// remapping and removeComments. Discovery probes seven names per ancestor, then
|
|
67
|
+
// parsing and formatting process the config/banner bytes linearly.
|
|
68
|
+
//
|
|
69
|
+
// JSON loads need no child. Executable config uses a child process and TypeScript
|
|
70
|
+
// also owns a temporary directory, removed by defer. stderr streams to the
|
|
71
|
+
// parent; stdout has no byte cap and evaluation has no deadline. Cleanup errors
|
|
72
|
+
// are not reported. Successful loader output must carry dependency observations;
|
|
73
|
+
// unsupported public resolution observation is reported separately from input
|
|
74
|
+
// mutation, so fresh evaluation can succeed without claiming reusable inputs.
|
|
75
|
+
//
|
|
76
|
+
// Native paths use filepath; slash conversion is reserved for import/tsconfig
|
|
77
|
+
// spelling. Child arguments are separate from executable paths. A loader's temp
|
|
78
|
+
// directory stays on the config volume, with Windows junctions used when native
|
|
79
|
+
// symlink creation is unavailable.
|
|
64
80
|
func (plugin) SourcePreamble(ctx driver.PluginContext) (string, error) {
|
|
65
|
-
preamble, err := parseBannerWithReporters(ctx.Entry.Config, ctx.Cwd, ctx.Tsconfig, ctx.ReportHostInput, ctx.ReportHostInputHash, ctx.ReportHostInputRealpath)
|
|
81
|
+
preamble, err := parseBannerWithReporters(ctx.Entry.Config, ctx.Cwd, ctx.Tsconfig, ctx.ReportHostInput, ctx.ReportHostInputHash, ctx.ReportHostInputRealpath, ctx.ReportObservationIncomplete)
|
|
66
82
|
if err != nil {
|
|
67
83
|
return "", err
|
|
68
84
|
}
|
|
@@ -76,18 +92,13 @@ func (plugin) SourcePreamble(ctx driver.PluginContext) (string, error) {
|
|
|
76
92
|
return preamble, nil
|
|
77
93
|
}
|
|
78
94
|
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return parseBannerWithReporters(config, cwd, tsconfigPath, reporter, nil, nil)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
func parseBannerWithReporters(config map[string]any, cwd, tsconfigPath string, reporter func(string), hashReporter func(string, *string), realpathReporter func(string, *string)) (string, error) {
|
|
90
|
-
text, err := resolveBannerTextWithReporters(config, cwd, tsconfigPath, reporter, hashReporter, realpathReporter)
|
|
95
|
+
// parseBannerWithReporters resolves banner text and formats it as a JSDoc block
|
|
96
|
+
// comment, stripping trailing blank lines first, while forwarding exact config
|
|
97
|
+
// witnesses.
|
|
98
|
+
// Optional incomplete reporters disclose unavailable public resolution observation
|
|
99
|
+
// without deleting known witnesses or treating an evaluated value as a failure.
|
|
100
|
+
func parseBannerWithReporters(config map[string]any, cwd, tsconfigPath string, reporter func(string), hashReporter func(string, *string), realpathReporter func(string, *string), incompleteReporters ...func()) (string, error) {
|
|
101
|
+
text, err := resolveBannerTextWithReporters(config, cwd, tsconfigPath, reporter, hashReporter, realpathReporter, incompleteReporters...)
|
|
91
102
|
if err != nil {
|
|
92
103
|
return "", err
|
|
93
104
|
}
|
|
@@ -118,24 +129,18 @@ func sanitizeJSDocLine(line string) string {
|
|
|
118
129
|
return strings.ReplaceAll(line, "*/", "* /")
|
|
119
130
|
}
|
|
120
131
|
|
|
121
|
-
//
|
|
122
|
-
// The
|
|
132
|
+
// resolveBannerTextWithReporters extracts the banner text from the plugin
|
|
133
|
+
// config. The entry is validated first: only the "configFile" key (plus
|
|
123
134
|
// framework keys) is accepted. When "configFile" is present its value is
|
|
124
|
-
// resolved to an absolute path and loaded
|
|
125
|
-
// discovery is used. Returns an error when the config is invalid or
|
|
126
|
-
//
|
|
135
|
+
// resolved to an absolute path and loaded; when absent the upward-walk
|
|
136
|
+
// discovery is used. Returns an error when the config is invalid or no banner
|
|
137
|
+
// text can be found.
|
|
127
138
|
//
|
|
128
|
-
// The discovery base directory doubles as the resolution root the config
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
func resolveBannerTextWithReporter(config map[string]any, cwd, tsconfigPath string, reporter func(string)) (string, error) {
|
|
135
|
-
return resolveBannerTextWithReporters(config, cwd, tsconfigPath, reporter, nil, nil)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
func resolveBannerTextWithReporters(config map[string]any, cwd, tsconfigPath string, reporter func(string), hashReporter func(string, *string), realpathReporter func(string, *string)) (string, error) {
|
|
139
|
+
// The discovery base directory doubles as the resolution root the config loader
|
|
140
|
+
// anchors its toolchain lookup on; see configToolAnchors. Configuration value
|
|
141
|
+
// loading stays separate from observation capability: incomplete reports cannot
|
|
142
|
+
// restore a missing input proof.
|
|
143
|
+
func resolveBannerTextWithReporters(config map[string]any, cwd, tsconfigPath string, reporter func(string), hashReporter func(string, *string), realpathReporter func(string, *string), incompleteReporters ...func()) (string, error) {
|
|
139
144
|
if err := validateBannerConfig(config); err != nil {
|
|
140
145
|
return "", err
|
|
141
146
|
}
|
|
@@ -152,6 +157,13 @@ func resolveBannerTextWithReporters(config map[string]any, cwd, tsconfigPath str
|
|
|
152
157
|
return "", err
|
|
153
158
|
}
|
|
154
159
|
reportBannerConfigInputs(loaded.inputs, loaded.hashes, loaded.realpaths, reporter, hashReporter, realpathReporter)
|
|
160
|
+
if !loaded.complete {
|
|
161
|
+
for _, report := range incompleteReporters {
|
|
162
|
+
if report != nil {
|
|
163
|
+
report()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
155
167
|
text, ok, err := bannerTextFromConfigValue(loaded.value, filepath.Base(location))
|
|
156
168
|
if err != nil {
|
|
157
169
|
return "", err
|
|
@@ -179,6 +191,13 @@ func resolveBannerTextWithReporters(config map[string]any, cwd, tsconfigPath str
|
|
|
179
191
|
return "", err
|
|
180
192
|
}
|
|
181
193
|
reportBannerConfigInputs(loaded.inputs, loaded.hashes, loaded.realpaths, reporter, hashReporter, realpathReporter)
|
|
194
|
+
if !loaded.complete {
|
|
195
|
+
for _, report := range incompleteReporters {
|
|
196
|
+
if report != nil {
|
|
197
|
+
report()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
182
201
|
text, ok, err := bannerTextFromConfigValue(loaded.value, filepath.Base(location))
|
|
183
202
|
if err != nil {
|
|
184
203
|
return "", err
|
|
@@ -214,7 +233,8 @@ func bannerTextFromConfigValue(raw any, label string) (string, bool, error) {
|
|
|
214
233
|
return text, true, nil
|
|
215
234
|
}
|
|
216
235
|
|
|
217
|
-
// bannerConfigFilenames
|
|
236
|
+
// bannerConfigFilenames lists supported discovery names. Multiple files in one
|
|
237
|
+
// directory are ambiguous; this order does not select a preferred format.
|
|
218
238
|
var bannerConfigFilenames = []string{
|
|
219
239
|
"banner.config.json",
|
|
220
240
|
"banner.config.js",
|
|
@@ -275,28 +295,26 @@ func tsconfigBaseDir(cwd, tsconfigPath string) string {
|
|
|
275
295
|
return driver.PluginConfigBaseDir(cwd, tsconfigPath)
|
|
276
296
|
}
|
|
277
297
|
|
|
278
|
-
// loadBannerConfigFile loads and evaluates a banner config file, returning its
|
|
279
|
-
// exported value as a Go any. A valid banner config exports an object with a
|
|
280
|
-
// "text" string; the value is validated by bannerTextFromConfigValue. The file
|
|
281
|
-
// must be named banner.config.{ts,cts,mts,js,cjs,mjs,json}; JS/CJS/MJS variants
|
|
282
|
-
// run under Node, TypeScript variants compile and run via ttsx in a temp
|
|
283
|
-
// directory, and JSON files are parsed natively.
|
|
284
|
-
//
|
|
285
|
-
// resolutionRoot is the project directory the TypeScript branch anchors its
|
|
286
|
-
// toolchain resolution on when the config file's own ancestry answers nothing;
|
|
287
|
-
// see configToolAnchors. The JSON and JS branches spawn no ttsx and ignore it.
|
|
288
|
-
func loadBannerConfigFile(location, resolutionRoot string) (any, error) {
|
|
289
|
-
loaded, err := loadBannerConfigFileWithInputs(location, resolutionRoot)
|
|
290
|
-
return loaded.value, err
|
|
291
|
-
}
|
|
292
|
-
|
|
293
298
|
type bannerLoadedConfig struct {
|
|
299
|
+
// complete concerns resolution capability; missing per-input proofs still signal instability.
|
|
300
|
+
complete bool
|
|
301
|
+
|
|
294
302
|
hashes map[string]*string
|
|
295
303
|
inputs []string
|
|
296
304
|
realpaths map[string]*string
|
|
297
305
|
value any
|
|
298
306
|
}
|
|
299
307
|
|
|
308
|
+
// loadBannerConfigFileWithInputs loads and evaluates a banner config file and
|
|
309
|
+
// returns its exported value with the input observations of the load. The file
|
|
310
|
+
// must be named banner.config.{ts,cts,mts,js,cjs,mjs,json}; JS/CJS/MJS variants
|
|
311
|
+
// run under Node, TypeScript variants compile and run via ttsx in a temp
|
|
312
|
+
// directory, and JSON files are parsed natively. The value is validated by
|
|
313
|
+
// bannerTextFromConfigValue, not here.
|
|
314
|
+
//
|
|
315
|
+
// resolutionRoot is the project directory the TypeScript branch anchors its
|
|
316
|
+
// toolchain resolution on when the config file's own ancestry answers nothing;
|
|
317
|
+
// see configToolAnchors. The JSON and JS branches spawn no ttsx and ignore it.
|
|
300
318
|
func loadBannerConfigFileWithInputs(location, resolutionRoot string) (bannerLoadedConfig, error) {
|
|
301
319
|
if !isBannerConfigFileName(filepath.Base(location)) {
|
|
302
320
|
return bannerLoadedConfig{}, fmt.Errorf("@ttsc/banner: config file must be named banner.config.{ts,cts,mts,js,cjs,mjs,json}: %s", location)
|
|
@@ -310,7 +328,7 @@ func loadBannerConfigFileWithInputs(location, resolutionRoot string) (bannerLoad
|
|
|
310
328
|
}
|
|
311
329
|
value, err := parseBannerJSONConfigFile(location, body)
|
|
312
330
|
digest := fmt.Sprintf("%x", sha256.Sum256(body))
|
|
313
|
-
return bannerLoadedConfig{hashes: map[string]*string{location: &digest}, inputs: []string{location}, realpaths: map[string]*string{location: physicalHostInput(location)}, value: value}, err
|
|
331
|
+
return bannerLoadedConfig{complete: true, hashes: map[string]*string{location: &digest}, inputs: []string{location}, realpaths: map[string]*string{location: physicalHostInput(location)}, value: value}, err
|
|
314
332
|
case ".js", ".cjs", ".mjs":
|
|
315
333
|
return loadBannerScriptConfigFileWithInputs(location)
|
|
316
334
|
}
|
|
@@ -416,17 +434,10 @@ func isBannerConfigFileName(name string) bool {
|
|
|
416
434
|
}
|
|
417
435
|
}
|
|
418
436
|
|
|
419
|
-
//
|
|
420
|
-
// UTF-8 BOM is stripped
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
body, err := os.ReadFile(location)
|
|
424
|
-
if err != nil {
|
|
425
|
-
return nil, fmt.Errorf("@ttsc/banner: read config file %s: %w", location, err)
|
|
426
|
-
}
|
|
427
|
-
return parseBannerJSONConfigFile(location, body)
|
|
428
|
-
}
|
|
429
|
-
|
|
437
|
+
// parseBannerJSONConfigFile parses a banner config file already read from
|
|
438
|
+
// location. A leading UTF-8 BOM is stripped first so files saved by Windows
|
|
439
|
+
// editors are accepted; object and text validation belong to
|
|
440
|
+
// bannerTextFromConfigValue.
|
|
430
441
|
func parseBannerJSONConfigFile(location string, body []byte) (any, error) {
|
|
431
442
|
// Strip a leading UTF-8 BOM so files saved by Windows editors round
|
|
432
443
|
// trip through json.Unmarshal without an opaque "invalid character" failure.
|
|
@@ -438,239 +449,22 @@ func parseBannerJSONConfigFile(location string, body []byte) (any, error) {
|
|
|
438
449
|
return out, nil
|
|
439
450
|
}
|
|
440
451
|
|
|
441
|
-
//
|
|
442
|
-
// running a small Node.js loader script that dynamic-imports the file and
|
|
443
|
-
// serializes its exported value to stdout
|
|
444
|
-
func loadBannerScriptConfigFile(location string) (any, error) {
|
|
445
|
-
loaded, err := loadBannerScriptConfigFileWithInputs(location)
|
|
446
|
-
return loaded.value, err
|
|
447
|
-
}
|
|
448
|
-
|
|
452
|
+
// loadBannerScriptConfigFileWithInputs evaluates a JS/CJS/MJS banner config file
|
|
453
|
+
// by running a small Node.js loader script that dynamic-imports the file and
|
|
454
|
+
// serializes its exported value, with the loader's input observations, to stdout.
|
|
449
455
|
func loadBannerScriptConfigFileWithInputs(location string) (bannerLoadedConfig, error) {
|
|
450
456
|
const script = `
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
function existingFile(file) {
|
|
464
|
-
try { return fs.statSync(file).isFile(); }
|
|
465
|
-
catch { return false; }
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function missingPathError(error) {
|
|
469
|
-
return error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function inputMetadataSignature(file) {
|
|
473
|
-
const requested = path.resolve(file);
|
|
474
|
-
let current = requested;
|
|
475
|
-
for (;;) {
|
|
476
|
-
try {
|
|
477
|
-
const link = fs.lstatSync(current, { bigint: true });
|
|
478
|
-
let target = link;
|
|
479
|
-
if (link.isSymbolicLink()) {
|
|
480
|
-
try { target = fs.statSync(current, { bigint: true }); }
|
|
481
|
-
catch { return undefined; }
|
|
482
|
-
}
|
|
483
|
-
return [path.relative(current, requested), link.dev, link.ino, link.mode, link.size, link.mtimeNs, link.ctimeNs, target.dev, target.ino, target.mode, target.size, target.mtimeNs, target.ctimeNs].join(":");
|
|
484
|
-
} catch (error) {
|
|
485
|
-
if (!missingPathError(error)) return undefined;
|
|
486
|
-
const parent = path.dirname(current);
|
|
487
|
-
if (parent === current) return undefined;
|
|
488
|
-
current = parent;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function recordInput(file) {
|
|
494
|
-
file = path.resolve(file);
|
|
495
|
-
inputs.add(file);
|
|
496
|
-
if (unstableHashes.has(file)) return;
|
|
497
|
-
const beforeSignature = inputMetadataSignature(file);
|
|
498
|
-
let observed;
|
|
499
|
-
let observedRealpath;
|
|
500
|
-
try { observed = fs.statSync(file).isDirectory() ? crypto.createHash("sha256").update("ttsc:host-input:directory\0").digest("hex") : crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); }
|
|
501
|
-
catch { observed = null; }
|
|
502
|
-
try { observedRealpath = fs.realpathSync.native(file); }
|
|
503
|
-
catch { observedRealpath = null; }
|
|
504
|
-
const afterSignature = inputMetadataSignature(file);
|
|
505
|
-
if (beforeSignature === undefined || afterSignature === undefined || beforeSignature !== afterSignature || (signatures.has(file) && signatures.get(file) !== afterSignature) || (hashes.has(file) && hashes.get(file) !== observed) || (realpaths.has(file) && realpaths.get(file) !== observedRealpath)) {
|
|
506
|
-
hashes.delete(file);
|
|
507
|
-
realpaths.delete(file);
|
|
508
|
-
signatures.delete(file);
|
|
509
|
-
unstableHashes.add(file);
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
signatures.set(file, afterSignature);
|
|
513
|
-
hashes.set(file, observed);
|
|
514
|
-
realpaths.set(file, observedRealpath);
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function recordFile(file) {
|
|
518
|
-
const resolvedFile = path.resolve(file);
|
|
519
|
-
recordInput(resolvedFile);
|
|
520
|
-
for (let directory = path.dirname(resolvedFile);;) {
|
|
521
|
-
const manifest = path.join(directory, "package.json");
|
|
522
|
-
recordInput(manifest);
|
|
523
|
-
if (existingFile(manifest)) {
|
|
524
|
-
break;
|
|
525
|
-
}
|
|
526
|
-
const parent = path.dirname(directory);
|
|
527
|
-
if (parent === directory) {
|
|
528
|
-
break;
|
|
529
|
-
}
|
|
530
|
-
directory = parent;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
function recordPackageManifests(file) {
|
|
535
|
-
for (let directory = path.dirname(path.resolve(file));;) {
|
|
536
|
-
const manifest = path.join(directory, "package.json");
|
|
537
|
-
recordInput(manifest);
|
|
538
|
-
if (existingFile(manifest)) return;
|
|
539
|
-
const parent = path.dirname(directory);
|
|
540
|
-
if (parent === directory) return;
|
|
541
|
-
directory = parent;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const moduleProbeExtensions = [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json", ".node"];
|
|
546
|
-
function moduleCandidates(base) {
|
|
547
|
-
return [
|
|
548
|
-
base,
|
|
549
|
-
...moduleProbeExtensions.map((extension) => base + extension),
|
|
550
|
-
path.join(base, "package.json"),
|
|
551
|
-
...moduleProbeExtensions.map((extension) => path.join(base, "index" + extension)),
|
|
552
|
-
];
|
|
553
|
-
}
|
|
554
|
-
const recordedModuleBases = new Set();
|
|
555
|
-
function recordManifestTargets(value, directory, allowBare = false) {
|
|
556
|
-
if (typeof value === "string") {
|
|
557
|
-
if (value !== "" && (allowBare || value.startsWith("./") || value.startsWith("../"))) recordModuleCandidates(path.resolve(directory, value));
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
if (Array.isArray(value)) {
|
|
561
|
-
for (const item of value) recordManifestTargets(item, directory, allowBare);
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
if (value && typeof value === "object") {
|
|
565
|
-
for (const item of Object.values(value)) recordManifestTargets(item, directory, allowBare);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
function recordModuleCandidates(base) {
|
|
569
|
-
const resolvedBase = path.resolve(base);
|
|
570
|
-
if (recordedModuleBases.has(resolvedBase)) return;
|
|
571
|
-
recordedModuleBases.add(resolvedBase);
|
|
572
|
-
for (const candidate of moduleCandidates(resolvedBase)) recordInput(candidate);
|
|
573
|
-
try {
|
|
574
|
-
const manifest = JSON.parse(fs.readFileSync(path.join(resolvedBase, "package.json"), "utf8").replace(/^\uFEFF/, ""));
|
|
575
|
-
recordManifestTargets(manifest.exports, resolvedBase);
|
|
576
|
-
recordManifestTargets(manifest.module, resolvedBase, true);
|
|
577
|
-
recordManifestTargets(manifest.main, resolvedBase, true);
|
|
578
|
-
} catch {}
|
|
579
|
-
}
|
|
580
|
-
function candidateSelected(base, resolvedFile) {
|
|
581
|
-
for (const candidate of moduleCandidates(base)) {
|
|
582
|
-
try {
|
|
583
|
-
const canonical = fs.realpathSync.native(candidate);
|
|
584
|
-
const relative = path.relative(canonical, resolvedFile);
|
|
585
|
-
if (relative === "" || (fs.statSync(canonical).isDirectory() && relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative))) return true;
|
|
586
|
-
} catch {}
|
|
587
|
-
}
|
|
588
|
-
return false;
|
|
589
|
-
}
|
|
590
|
-
function localBases(specifier, parentDirectory) {
|
|
591
|
-
if (specifier.startsWith("file:")) return [fileURLToPath(specifier)];
|
|
592
|
-
const raw = path.resolve(parentDirectory, specifier);
|
|
593
|
-
const suffixStart = specifier.search(/[?#]/);
|
|
594
|
-
if (suffixStart === -1) return [raw];
|
|
595
|
-
const pathname = specifier.slice(0, suffixStart);
|
|
596
|
-
return pathname === "" ? [raw] : [...new Set([raw, path.resolve(parentDirectory, pathname)])];
|
|
597
|
-
}
|
|
598
|
-
function recordResolutionCandidates(specifier, parentURL, resolvedURL) {
|
|
599
|
-
if (typeof parentURL !== "string" || !parentURL.startsWith("file:")) return;
|
|
600
|
-
const parentDirectory = path.dirname(fileURLToPath(parentURL));
|
|
601
|
-
let resolvedFile;
|
|
602
|
-
try {
|
|
603
|
-
resolvedFile = typeof resolvedURL === "string" && resolvedURL.startsWith("file:")
|
|
604
|
-
? fs.realpathSync.native(fileURLToPath(resolvedURL))
|
|
605
|
-
: undefined;
|
|
606
|
-
} catch {}
|
|
607
|
-
if (specifier.startsWith(".") || path.isAbsolute(specifier) || specifier.startsWith("file:")) {
|
|
608
|
-
try {
|
|
609
|
-
for (const base of localBases(specifier, parentDirectory)) {
|
|
610
|
-
recordPackageManifests(base);
|
|
611
|
-
let exact = false;
|
|
612
|
-
try { exact = resolvedFile === undefined ? fs.statSync(base).isFile() : fs.realpathSync.native(base) === resolvedFile; } catch {}
|
|
613
|
-
if (exact) recordInput(base);
|
|
614
|
-
else recordModuleCandidates(base);
|
|
615
|
-
}
|
|
616
|
-
} catch {}
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
if (isBuiltin(specifier) || specifier.startsWith("#")) return;
|
|
620
|
-
const parts = specifier.split("/");
|
|
621
|
-
const packageParts = parts[0].startsWith("@") ? parts.slice(0, 2) : parts.slice(0, 1);
|
|
622
|
-
if (packageParts.some((part) => part === undefined || part === "")) return;
|
|
623
|
-
const packageName = packageParts.join("/");
|
|
624
|
-
const subpath = parts.slice(packageParts.length);
|
|
625
|
-
const searchPaths = createRequire(parentURL).resolve.paths(specifier) ?? [];
|
|
626
|
-
for (const searchPath of searchPaths) {
|
|
627
|
-
const packageDirectory = path.join(searchPath, packageName);
|
|
628
|
-
recordModuleCandidates(packageDirectory);
|
|
629
|
-
if (subpath.length !== 0) recordModuleCandidates(path.join(packageDirectory, ...subpath));
|
|
630
|
-
if (resolvedFile !== undefined && candidateSelected(packageDirectory, resolvedFile)) break;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
recordFile(process.argv[1]);
|
|
635
|
-
registerHooks({
|
|
636
|
-
resolve(specifier, context, nextResolve) {
|
|
637
|
-
recordResolutionCandidates(specifier, context.parentURL, undefined);
|
|
638
|
-
const resolved = nextResolve(specifier, context);
|
|
639
|
-
const url = typeof resolved === "string" ? resolved : resolved && resolved.url;
|
|
640
|
-
recordResolutionCandidates(specifier, context.parentURL, url);
|
|
641
|
-
if (typeof url === "string" && url.startsWith("file:")) {
|
|
642
|
-
recordFile(fileURLToPath(url));
|
|
643
|
-
}
|
|
644
|
-
return resolved;
|
|
645
|
-
},
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
// The hook above never sees a require() made from inside a CommonJS module the
|
|
649
|
-
// ESM loader evaluated, which on Node 22 is every require the config makes:
|
|
650
|
-
// module.registerHooks observes the import() of that module and nothing within
|
|
651
|
-
// it. A config's own dependencies would then be reported without the candidates
|
|
652
|
-
// that decide them, so a spelling appearing later could change what the config
|
|
653
|
-
// resolves to with nothing in the envelope to notice it (samchon/ttsc#1280).
|
|
654
|
-
// Wrapping the CommonJS resolver records the same two observations the hook
|
|
655
|
-
// does, on the graph the hook cannot reach.
|
|
656
|
-
const nextResolveFilename = nodeModule._resolveFilename;
|
|
657
|
-
nodeModule._resolveFilename = function resolveFilename(request, parent, isMain, options) {
|
|
658
|
-
// _resolveFilename is an internal entry point anything may call, so a
|
|
659
|
-
// non-string request arrives here as readily as a specifier does. Reading it
|
|
660
|
-
// would replace Node's own argument error with a TypeError from this loader.
|
|
661
|
-
if (typeof request !== "string") {
|
|
662
|
-
return nextResolveFilename.call(this, request, parent, isMain, options);
|
|
663
|
-
}
|
|
664
|
-
const parentFile = parent && typeof parent.filename === "string" ? parent.filename : undefined;
|
|
665
|
-
const parentURL = parentFile === undefined ? undefined : pathToFileURL(parentFile).href;
|
|
666
|
-
recordResolutionCandidates(request, parentURL, undefined);
|
|
667
|
-
const resolved = nextResolveFilename.call(this, request, parent, isMain, options);
|
|
668
|
-
if (path.isAbsolute(resolved)) {
|
|
669
|
-
recordResolutionCandidates(request, parentURL, pathToFileURL(resolved).href);
|
|
670
|
-
recordFile(resolved);
|
|
671
|
-
}
|
|
672
|
-
return resolved;
|
|
673
|
-
};
|
|
457
|
+
const { pathToFileURL } = require("node:url");
|
|
458
|
+
// Which inputs a resolution read is the one rule ttsc owns
|
|
459
|
+
// (resolutioninputs.Recorder), evaluated before any hook is installed.
|
|
460
|
+
const { createResolutionInputRecorder, observeResolutions } = RESOLUTION_INPUT_RECORDER;
|
|
461
|
+
const recorder = createResolutionInputRecorder({ extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json", ".node"] });
|
|
462
|
+
|
|
463
|
+
recorder.recordFile(process.argv[1]);
|
|
464
|
+
// Every resolution the config makes is recorded: through a resolve hook, and
|
|
465
|
+
// on a runtime whose require.resolve bypasses the hooks, through the one
|
|
466
|
+
// entry point it reaches (resolutioninputs.Recorder, samchon/ttsc#1523).
|
|
467
|
+
observeResolutions(recorder);
|
|
674
468
|
|
|
675
469
|
(async () => {
|
|
676
470
|
const mod = await import(pathToFileURL(process.argv[1]).href);
|
|
@@ -687,8 +481,8 @@ nodeModule._resolveFilename = function resolveFilename(request, parent, isMain,
|
|
|
687
481
|
}
|
|
688
482
|
const value = typeof current === "function" ? await current() : current;
|
|
689
483
|
const serializedValue = toSerializableBanner(value);
|
|
690
|
-
|
|
691
|
-
process.stdout.write(JSON.stringify({ value: serializedValue, hashes:
|
|
484
|
+
const recorded = recorder.finish();
|
|
485
|
+
process.stdout.write(JSON.stringify({ value: serializedValue, complete: recorded.complete, hashes: recorded.hashes, inputs: recorded.inputs, realpaths: recorded.realpaths }));
|
|
692
486
|
})().catch((error) => {
|
|
693
487
|
process.stderr.write(error && error.stack ? error.stack : String(error));
|
|
694
488
|
process.exitCode = 1;
|
|
@@ -714,7 +508,10 @@ function toSerializableBanner(value) {
|
|
|
714
508
|
// the loader runs. This preserves the historical process.argv layout seen by
|
|
715
509
|
// both the loader and the imported user config without using string eval.
|
|
716
510
|
cmd := exec.CommandContext(ctx, node, "--input-type=commonjs", "-", location)
|
|
717
|
-
cmd.Stdin = strings.NewReader(
|
|
511
|
+
cmd.Stdin = strings.NewReader(
|
|
512
|
+
"process.argv.splice(1, 1);\nconst RESOLUTION_INPUT_RECORDER = " +
|
|
513
|
+
resolutioninputs.CommonJSExpression() + ";\n" + script,
|
|
514
|
+
)
|
|
718
515
|
cmd.Env = nodeConfigLoaderEnv(location)
|
|
719
516
|
// The child's stderr is human output and goes straight to this process's
|
|
720
517
|
// stderr as it is written. Collecting it only to replay it afterwards is what
|
|
@@ -738,8 +535,14 @@ function toSerializableBanner(value) {
|
|
|
738
535
|
return loaded, nil
|
|
739
536
|
}
|
|
740
537
|
|
|
538
|
+
// decodeBannerConfigLoaderOutput requires the value and recorder snapshot from
|
|
539
|
+
// the executed loader. Accepting a bare value would report complete dependency
|
|
540
|
+
// observations even though no observations were returned. Missing probe values
|
|
541
|
+
// remain legitimate nullable entries inside the snapshot maps.
|
|
741
542
|
func decodeBannerConfigLoaderOutput(output []byte) (bannerLoadedConfig, error) {
|
|
742
543
|
var envelope struct {
|
|
544
|
+
Complete *bool `json:"complete"`
|
|
545
|
+
|
|
743
546
|
Error string `json:"__ttscLoaderError"`
|
|
744
547
|
Hashes map[string]*string `json:"hashes"`
|
|
745
548
|
Inputs []string `json:"inputs"`
|
|
@@ -752,37 +555,25 @@ func decodeBannerConfigLoaderOutput(output []byte) (bannerLoadedConfig, error) {
|
|
|
752
555
|
if envelope.Error != "" {
|
|
753
556
|
return bannerLoadedConfig{}, fmt.Errorf("%s", envelope.Error)
|
|
754
557
|
}
|
|
755
|
-
if len(envelope.Value) == 0 {
|
|
756
|
-
|
|
757
|
-
// the config value directly. Preserve that accepted contract while real
|
|
758
|
-
// loaders use the envelope to carry runtime inputs.
|
|
759
|
-
var value any
|
|
760
|
-
if err := json.Unmarshal(output, &value); err != nil {
|
|
761
|
-
return bannerLoadedConfig{}, err
|
|
762
|
-
}
|
|
763
|
-
return bannerLoadedConfig{value: value}, nil
|
|
558
|
+
if len(envelope.Value) == 0 || envelope.Complete == nil || envelope.Inputs == nil || envelope.Hashes == nil || envelope.Realpaths == nil {
|
|
559
|
+
return bannerLoadedConfig{}, fmt.Errorf("config loader must return a value with dependency observations")
|
|
764
560
|
}
|
|
765
561
|
var value any
|
|
766
562
|
if err := json.Unmarshal(envelope.Value, &value); err != nil {
|
|
767
563
|
return bannerLoadedConfig{}, err
|
|
768
564
|
}
|
|
769
|
-
return bannerLoadedConfig{hashes: envelope.Hashes, inputs: envelope.Inputs, realpaths: envelope.Realpaths, value: value}, nil
|
|
565
|
+
return bannerLoadedConfig{complete: *envelope.Complete, hashes: envelope.Hashes, inputs: envelope.Inputs, realpaths: envelope.Realpaths, value: value}, nil
|
|
770
566
|
}
|
|
771
567
|
|
|
772
|
-
//
|
|
773
|
-
// file using ttsx in a temp directory. A symlink to the nearest
|
|
774
|
-
// is created so the config file can import its own dependencies. The
|
|
775
|
-
// build runs with
|
|
568
|
+
// loadBannerTypeScriptConfigFileWithInputs compiles and runs a TypeScript banner
|
|
569
|
+
// config file using ttsx in a temp directory. A symlink to the nearest
|
|
570
|
+
// node_modules is created so the config file can import its own dependencies. The
|
|
571
|
+
// ttsx build runs with --no-plugins so evaluating the config never triggers the
|
|
776
572
|
// host project's transform/check plugins against the loader tsconfig.
|
|
777
573
|
//
|
|
778
|
-
// Both tools this spawns
|
|
779
|
-
// resolved from the project rather than from the process environment alone;
|
|
780
|
-
//
|
|
781
|
-
func loadBannerTypeScriptConfigFile(location, resolutionRoot string) (any, error) {
|
|
782
|
-
loaded, err := loadBannerTypeScriptConfigFileWithInputs(location, resolutionRoot)
|
|
783
|
-
return loaded.value, err
|
|
784
|
-
}
|
|
785
|
-
|
|
574
|
+
// Both tools this spawns, the launcher and the compiler handed to it, are
|
|
575
|
+
// resolved from the project rather than from the process environment alone; see
|
|
576
|
+
// configToolAnchors.
|
|
786
577
|
func loadBannerTypeScriptConfigFileWithInputs(location, resolutionRoot string) (bannerLoadedConfig, error) {
|
|
787
578
|
tempDir, err := os.MkdirTemp(loaderTempBase(location, os.TempDir()), "ttsc-banner-config-")
|
|
788
579
|
if err != nil {
|
|
@@ -790,23 +581,12 @@ func loadBannerTypeScriptConfigFileWithInputs(location, resolutionRoot string) (
|
|
|
790
581
|
}
|
|
791
582
|
defer os.RemoveAll(tempDir)
|
|
792
583
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
loader := filepath.Join(tempDir, "loader.mts")
|
|
798
|
-
tsconfig := filepath.Join(tempDir, "tsconfig.json")
|
|
799
|
-
importSpecifier, err := relativeImportSpecifier(tempDir, location)
|
|
584
|
+
loader, tsconfig, err := prepareBannerTypeScriptConfigLoader(
|
|
585
|
+
tempDir, location, linkNearestNodeModules, os.WriteFile,
|
|
586
|
+
)
|
|
800
587
|
if err != nil {
|
|
801
588
|
return bannerLoadedConfig{}, err
|
|
802
589
|
}
|
|
803
|
-
importLiteral, _ := json.Marshal(importSpecifier)
|
|
804
|
-
if err := writeConfigLoaderFile(loader, []byte(bannerTypeScriptConfigLoaderSource(string(importLiteral))), 0o644); err != nil {
|
|
805
|
-
return bannerLoadedConfig{}, fmt.Errorf("@ttsc/banner: write config loader: %w", err)
|
|
806
|
-
}
|
|
807
|
-
if err := writeConfigLoaderFile(tsconfig, []byte(typeScriptConfigLoaderTsconfig(loader, location, tempDir)), 0o644); err != nil {
|
|
808
|
-
return bannerLoadedConfig{}, fmt.Errorf("@ttsc/banner: write config loader tsconfig: %w", err)
|
|
809
|
-
}
|
|
810
590
|
|
|
811
591
|
args := []string{
|
|
812
592
|
"--project", tsconfig,
|
|
@@ -846,264 +626,68 @@ func loadBannerTypeScriptConfigFileWithInputs(location, resolutionRoot string) (
|
|
|
846
626
|
return loaded, nil
|
|
847
627
|
}
|
|
848
628
|
|
|
849
|
-
//
|
|
850
|
-
//
|
|
851
|
-
//
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
try { return fs.statSync(file).isFile(); }
|
|
868
|
-
catch { return false; }
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
function missingPathError(error: unknown): boolean {
|
|
872
|
-
const code = (error as { code?: unknown } | undefined)?.code;
|
|
873
|
-
return code === "ENOENT" || code === "ENOTDIR";
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
function inputMetadataSignature(file: string): string | undefined {
|
|
877
|
-
const requested = path.resolve(file);
|
|
878
|
-
let current = requested;
|
|
879
|
-
for (;;) {
|
|
880
|
-
try {
|
|
881
|
-
const link = fs.lstatSync(current, { bigint: true });
|
|
882
|
-
let target = link;
|
|
883
|
-
if (link.isSymbolicLink()) {
|
|
884
|
-
try { target = fs.statSync(current, { bigint: true }); }
|
|
885
|
-
catch { return undefined; }
|
|
886
|
-
}
|
|
887
|
-
return [path.relative(current, requested), link.dev, link.ino, link.mode, link.size, link.mtimeNs, link.ctimeNs, target.dev, target.ino, target.mode, target.size, target.mtimeNs, target.ctimeNs].join(":");
|
|
888
|
-
} catch (error) {
|
|
889
|
-
if (!missingPathError(error)) return undefined;
|
|
890
|
-
const parent = path.dirname(current);
|
|
891
|
-
if (parent === current) return undefined;
|
|
892
|
-
current = parent;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
function recordInput(file: string): void {
|
|
898
|
-
file = path.resolve(file);
|
|
899
|
-
inputs.add(file);
|
|
900
|
-
if (unstableHashes.has(file)) return;
|
|
901
|
-
const beforeSignature = inputMetadataSignature(file);
|
|
902
|
-
let observed: string | null;
|
|
903
|
-
let observedRealpath: string | null;
|
|
904
|
-
try { observed = fs.statSync(file).isDirectory() ? crypto.createHash("sha256").update("ttsc:host-input:directory\0").digest("hex") : crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); }
|
|
905
|
-
catch { observed = null; }
|
|
906
|
-
try { observedRealpath = fs.realpathSync.native(file); }
|
|
907
|
-
catch { observedRealpath = null; }
|
|
908
|
-
const afterSignature = inputMetadataSignature(file);
|
|
909
|
-
if (beforeSignature === undefined || afterSignature === undefined || beforeSignature !== afterSignature || (signatures.has(file) && signatures.get(file) !== afterSignature) || (hashes.has(file) && hashes.get(file) !== observed) || (realpaths.has(file) && realpaths.get(file) !== observedRealpath)) {
|
|
910
|
-
hashes.delete(file);
|
|
911
|
-
realpaths.delete(file);
|
|
912
|
-
signatures.delete(file);
|
|
913
|
-
unstableHashes.add(file);
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
signatures.set(file, afterSignature);
|
|
917
|
-
hashes.set(file, observed);
|
|
918
|
-
realpaths.set(file, observedRealpath);
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
function recordFile(file: string): void {
|
|
922
|
-
const resolvedFile = path.resolve(file);
|
|
923
|
-
recordInput(resolvedFile);
|
|
924
|
-
for (let directory = path.dirname(resolvedFile);;) {
|
|
925
|
-
const manifest = path.join(directory, "package.json");
|
|
926
|
-
recordInput(manifest);
|
|
927
|
-
if (existingFile(manifest)) {
|
|
928
|
-
break;
|
|
929
|
-
}
|
|
930
|
-
const parent = path.dirname(directory);
|
|
931
|
-
if (parent === directory) {
|
|
932
|
-
break;
|
|
933
|
-
}
|
|
934
|
-
directory = parent;
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
function recordPackageManifests(file: string): void {
|
|
939
|
-
for (let directory = path.dirname(path.resolve(file));;) {
|
|
940
|
-
const manifest = path.join(directory, "package.json");
|
|
941
|
-
recordInput(manifest);
|
|
942
|
-
if (existingFile(manifest)) return;
|
|
943
|
-
const parent = path.dirname(directory);
|
|
944
|
-
if (parent === directory) return;
|
|
945
|
-
directory = parent;
|
|
629
|
+
// prepareBannerTypeScriptConfigLoader owns the ordered preparation needed before
|
|
630
|
+
// the loader process can start: dependency links, resolution recorder, generated
|
|
631
|
+
// loader and tsconfig. Its caller supplies the invocation's filesystem operations
|
|
632
|
+
// explicitly and retains directory lifetime ownership. The native caller supplies
|
|
633
|
+
// linkNearestNodeModules and os.WriteFile; no mutable process-global hooks exist.
|
|
634
|
+
//
|
|
635
|
+
// A failed operation returns immediately with its original error context and
|
|
636
|
+
// never admits the process with partial preparation. Each fixed file is written
|
|
637
|
+
// once, native paths are joined through filepath, and helpers retain no state
|
|
638
|
+
// across invocations. I/O failure decisions can be verified at this owner without
|
|
639
|
+
// claiming that supplied callbacks establish actual kernel link capabilities.
|
|
640
|
+
func prepareBannerTypeScriptConfigLoader(
|
|
641
|
+
tempDir, location string,
|
|
642
|
+
linkNodeModules func(string, string) error,
|
|
643
|
+
writeFile func(string, []byte, os.FileMode) error,
|
|
644
|
+
) (string, string, error) {
|
|
645
|
+
if err := linkNodeModules(tempDir, filepath.Dir(location)); err != nil {
|
|
646
|
+
return "", "", err
|
|
946
647
|
}
|
|
947
|
-
}
|
|
948
648
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
[".cjs", [".cts"]],
|
|
955
|
-
]);
|
|
956
|
-
function sourceSubstitutionCandidates(base: string): string[] {
|
|
957
|
-
const extension = path.extname(base).toLowerCase();
|
|
958
|
-
const substitutions = jsToTsProbeExtensions.get(extension);
|
|
959
|
-
if (substitutions === undefined) return [];
|
|
960
|
-
const stem = base.slice(0, base.length - extension.length);
|
|
961
|
-
return substitutions.map((candidate) => stem + candidate);
|
|
962
|
-
}
|
|
963
|
-
function moduleCandidates(base: string): string[] {
|
|
964
|
-
return [
|
|
965
|
-
base,
|
|
966
|
-
...sourceSubstitutionCandidates(base),
|
|
967
|
-
...moduleProbeExtensions.map((extension) => base + extension),
|
|
968
|
-
path.join(base, "package.json"),
|
|
969
|
-
...moduleProbeExtensions.map((extension) => path.join(base, "index" + extension)),
|
|
970
|
-
];
|
|
971
|
-
}
|
|
972
|
-
const recordedModuleBases = new Set<string>();
|
|
973
|
-
function recordManifestTargets(value: unknown, directory: string, allowBare: boolean = false): void {
|
|
974
|
-
if (typeof value === "string") {
|
|
975
|
-
if (value !== "" && (allowBare || value.startsWith("./") || value.startsWith("../"))) recordModuleCandidates(path.resolve(directory, value));
|
|
976
|
-
return;
|
|
649
|
+
loader := filepath.Join(tempDir, "loader.mts")
|
|
650
|
+
tsconfig := filepath.Join(tempDir, "tsconfig.json")
|
|
651
|
+
importSpecifier, err := relativeImportSpecifier(tempDir, location)
|
|
652
|
+
if err != nil {
|
|
653
|
+
return "", "", err
|
|
977
654
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
655
|
+
importLiteral, _ := json.Marshal(importSpecifier)
|
|
656
|
+
recorder := filepath.Join(tempDir, "ttsc-resolution-inputs.cjs")
|
|
657
|
+
if err := writeFile(recorder, []byte(resolutioninputs.Recorder), 0o644); err != nil {
|
|
658
|
+
return "", "", fmt.Errorf("@ttsc/banner: write config loader: %w", err)
|
|
981
659
|
}
|
|
982
|
-
|
|
983
|
-
|
|
660
|
+
recorderLiteral, _ := json.Marshal(recorder)
|
|
661
|
+
if err := writeFile(loader, []byte(bannerTypeScriptConfigLoaderSource(string(importLiteral), string(recorderLiteral))), 0o644); err != nil {
|
|
662
|
+
return "", "", fmt.Errorf("@ttsc/banner: write config loader: %w", err)
|
|
984
663
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
const resolvedBase = path.resolve(base);
|
|
988
|
-
if (recordedModuleBases.has(resolvedBase)) return;
|
|
989
|
-
recordedModuleBases.add(resolvedBase);
|
|
990
|
-
for (const candidate of moduleCandidates(resolvedBase)) recordInput(candidate);
|
|
991
|
-
try {
|
|
992
|
-
const manifest = JSON.parse(fs.readFileSync(path.join(resolvedBase, "package.json"), "utf8").replace(/^\uFEFF/, ""));
|
|
993
|
-
recordManifestTargets(manifest.exports, resolvedBase);
|
|
994
|
-
recordManifestTargets(manifest.module, resolvedBase, true);
|
|
995
|
-
recordManifestTargets(manifest.main, resolvedBase, true);
|
|
996
|
-
} catch {}
|
|
997
|
-
}
|
|
998
|
-
function candidateSelected(base: string, resolvedFile: string): boolean {
|
|
999
|
-
for (const candidate of moduleCandidates(base)) {
|
|
1000
|
-
try {
|
|
1001
|
-
const canonical = fs.realpathSync.native(candidate);
|
|
1002
|
-
const relative = path.relative(canonical, resolvedFile);
|
|
1003
|
-
if (relative === "" || (fs.statSync(canonical).isDirectory() && relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative))) return true;
|
|
1004
|
-
} catch {}
|
|
1005
|
-
}
|
|
1006
|
-
return false;
|
|
1007
|
-
}
|
|
1008
|
-
function localBases(specifier: string, parentDirectory: string): string[] {
|
|
1009
|
-
if (specifier.startsWith("file:")) return [fileURLToPath(specifier)];
|
|
1010
|
-
const raw = path.resolve(parentDirectory, specifier);
|
|
1011
|
-
const suffixStart = specifier.search(/[?#]/);
|
|
1012
|
-
if (suffixStart === -1) return [raw];
|
|
1013
|
-
const pathname = specifier.slice(0, suffixStart);
|
|
1014
|
-
return pathname === "" ? [raw] : [...new Set([raw, path.resolve(parentDirectory, pathname)])];
|
|
1015
|
-
}
|
|
1016
|
-
function recordResolutionCandidates(specifier: string, parentURL: string | undefined, resolvedURL: string | undefined): void {
|
|
1017
|
-
if (typeof parentURL !== "string" || !parentURL.startsWith("file:")) return;
|
|
1018
|
-
const parentDirectory = path.dirname(fileURLToPath(parentURL));
|
|
1019
|
-
let resolvedFile: string | undefined;
|
|
1020
|
-
try {
|
|
1021
|
-
resolvedFile = typeof resolvedURL === "string" && resolvedURL.startsWith("file:")
|
|
1022
|
-
? fs.realpathSync.native(fileURLToPath(resolvedURL))
|
|
1023
|
-
: undefined;
|
|
1024
|
-
} catch {}
|
|
1025
|
-
if (specifier.startsWith(".") || path.isAbsolute(specifier) || specifier.startsWith("file:")) {
|
|
1026
|
-
try {
|
|
1027
|
-
for (const base of localBases(specifier, parentDirectory)) {
|
|
1028
|
-
recordPackageManifests(base);
|
|
1029
|
-
let exact = false;
|
|
1030
|
-
try { exact = resolvedFile === undefined ? fs.statSync(base).isFile() : fs.realpathSync.native(base) === resolvedFile; } catch {}
|
|
1031
|
-
if (exact) recordInput(base);
|
|
1032
|
-
else recordModuleCandidates(base);
|
|
1033
|
-
}
|
|
1034
|
-
} catch {}
|
|
1035
|
-
return;
|
|
1036
|
-
}
|
|
1037
|
-
if (isBuiltin(specifier) || specifier.startsWith("#")) return;
|
|
1038
|
-
const parts = specifier.split("/");
|
|
1039
|
-
const packageParts = parts[0]!.startsWith("@") ? parts.slice(0, 2) : parts.slice(0, 1);
|
|
1040
|
-
if (packageParts.some((part) => part === undefined || part === "")) return;
|
|
1041
|
-
const packageName = packageParts.join("/");
|
|
1042
|
-
const subpath = parts.slice(packageParts.length);
|
|
1043
|
-
const searchPaths = createRequire(parentURL).resolve.paths(specifier) ?? [];
|
|
1044
|
-
for (const searchPath of searchPaths) {
|
|
1045
|
-
const packageDirectory = path.join(searchPath, packageName);
|
|
1046
|
-
recordModuleCandidates(packageDirectory);
|
|
1047
|
-
if (subpath.length !== 0) recordModuleCandidates(path.join(packageDirectory, ...subpath));
|
|
1048
|
-
if (resolvedFile !== undefined && candidateSelected(packageDirectory, resolvedFile)) break;
|
|
664
|
+
if err := writeFile(tsconfig, []byte(typeScriptConfigLoaderTsconfig(loader, location, tempDir)), 0o644); err != nil {
|
|
665
|
+
return "", "", fmt.Errorf("@ttsc/banner: write config loader tsconfig: %w", err)
|
|
1049
666
|
}
|
|
667
|
+
|
|
668
|
+
return loader, tsconfig, nil
|
|
1050
669
|
}
|
|
1051
670
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
}
|
|
1061
|
-
return resolved;
|
|
1062
|
-
},
|
|
1063
|
-
});
|
|
671
|
+
// bannerTypeScriptConfigLoaderSource returns the source of a TypeScript loader
|
|
672
|
+
// module that imports the banner config file specified by importLiteral (a
|
|
673
|
+
// JSON-encoded import specifier) and writes the serialized banner value to stdout.
|
|
674
|
+
// recorderLiteral is the JSON-encoded path of the resolution input recorder
|
|
675
|
+
// written beside the loader (resolutioninputs.Recorder).
|
|
676
|
+
func bannerTypeScriptConfigLoaderSource(importLiteral, recorderLiteral string) string {
|
|
677
|
+
return fmt.Sprintf(`// @ts-nocheck
|
|
678
|
+
import { createRequire } from "node:module";
|
|
1064
679
|
|
|
1065
|
-
//
|
|
1066
|
-
//
|
|
1067
|
-
//
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
//
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
parent: { filename?: string | null } | null | undefined,
|
|
1077
|
-
isMain: boolean,
|
|
1078
|
-
options?: unknown,
|
|
1079
|
-
): string;
|
|
1080
|
-
};
|
|
1081
|
-
const nextResolveFilename = moduleInternals._resolveFilename;
|
|
1082
|
-
moduleInternals._resolveFilename = function resolveFilename(
|
|
1083
|
-
this: unknown,
|
|
1084
|
-
request: string,
|
|
1085
|
-
parent: { filename?: string | null } | null | undefined,
|
|
1086
|
-
isMain: boolean,
|
|
1087
|
-
options?: unknown,
|
|
1088
|
-
): string {
|
|
1089
|
-
// _resolveFilename is an internal entry point anything may call, so a
|
|
1090
|
-
// non-string request arrives here as readily as a specifier does. Reading it
|
|
1091
|
-
// would replace Node's own argument error with a TypeError from this loader.
|
|
1092
|
-
if (typeof request !== "string") {
|
|
1093
|
-
return nextResolveFilename.call(this, request, parent, isMain, options);
|
|
1094
|
-
}
|
|
1095
|
-
const parentURL =
|
|
1096
|
-
typeof parent?.filename === "string"
|
|
1097
|
-
? pathToFileURL(parent.filename).href
|
|
1098
|
-
: undefined;
|
|
1099
|
-
recordResolutionCandidates(request, parentURL, undefined);
|
|
1100
|
-
const resolved = nextResolveFilename.call(this, request, parent, isMain, options);
|
|
1101
|
-
if (path.isAbsolute(resolved)) {
|
|
1102
|
-
recordResolutionCandidates(request, parentURL, pathToFileURL(resolved).href);
|
|
1103
|
-
recordFile(resolved);
|
|
1104
|
-
}
|
|
1105
|
-
return resolved;
|
|
1106
|
-
};
|
|
680
|
+
// Which inputs a resolution read is the one rule ttsc owns
|
|
681
|
+
// (resolutioninputs.Recorder), written beside this loader and required before
|
|
682
|
+
// any hook is installed, so the recorder itself is no input.
|
|
683
|
+
const recorderFile: string = %[2]s;
|
|
684
|
+
const { createResolutionInputRecorder, observeResolutions } = createRequire(recorderFile)(recorderFile);
|
|
685
|
+
const recorder = createResolutionInputRecorder({ extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json", ".node"] });
|
|
686
|
+
|
|
687
|
+
// Every resolution the config makes is recorded: through a resolve hook, and
|
|
688
|
+
// on a runtime whose require.resolve bypasses the hooks, through the one
|
|
689
|
+
// entry point it reaches (resolutioninputs.Recorder, samchon/ttsc#1523).
|
|
690
|
+
observeResolutions(recorder);
|
|
1107
691
|
|
|
1108
692
|
declare const process: {
|
|
1109
693
|
exitCode?: number;
|
|
@@ -1119,15 +703,16 @@ declare const process: {
|
|
|
1119
703
|
// left for a trailing handler to settle.
|
|
1120
704
|
(async () => {
|
|
1121
705
|
try {
|
|
1122
|
-
const importedConfig = await import(%s);
|
|
706
|
+
const importedConfig = await import(%[1]s);
|
|
1123
707
|
const value = await resolveConfig(importedConfig);
|
|
1124
708
|
const serializedValue = toSerializableBanner(value);
|
|
1125
|
-
|
|
709
|
+
const recorded = recorder.finish();
|
|
1126
710
|
process.stdout.write(JSON.stringify({
|
|
1127
711
|
value: serializedValue,
|
|
1128
|
-
hashes:
|
|
1129
|
-
|
|
1130
|
-
|
|
712
|
+
hashes: recorded.hashes,
|
|
713
|
+
complete: recorded.complete,
|
|
714
|
+
inputs: recorded.inputs,
|
|
715
|
+
realpaths: recorded.realpaths,
|
|
1131
716
|
}));
|
|
1132
717
|
} catch (error) {
|
|
1133
718
|
process.stderr.write(error instanceof Error && error.stack ? error.stack : String(error));
|
|
@@ -1181,7 +766,7 @@ function toSerializableBanner(value: unknown): unknown {
|
|
|
1181
766
|
}
|
|
1182
767
|
throw new Error("config file must export an object with a non-empty \"text\" string");
|
|
1183
768
|
}
|
|
1184
|
-
`, importLiteral)
|
|
769
|
+
`, importLiteral, recorderLiteral)
|
|
1185
770
|
}
|
|
1186
771
|
|
|
1187
772
|
// typeScriptConfigLoaderTsconfig returns the JSON content of a tsconfig that
|
|
@@ -1228,11 +813,8 @@ func typeScriptConfigLoaderTsconfig(loader, location, outDir string) string {
|
|
|
1228
813
|
// packages/banner/driver/banner.go and packages/strip/driver/config.go. #1169
|
|
1229
814
|
// decided against extracting it — the only home the three modules could share
|
|
1230
815
|
// is the public `packages/ttsc/driver` seam, and packages/lint's go.mod
|
|
1231
|
-
// deliberately requires no in-tree ttsc module —
|
|
1232
|
-
//
|
|
1233
|
-
// between these markers across all three copies on every pull request, so
|
|
1234
|
-
// editing one and not the others fails by name. That file's header carries the
|
|
1235
|
-
// full decision and the rules for changing this block.
|
|
816
|
+
// deliberately requires no in-tree ttsc module — so a change to one copy must
|
|
817
|
+
// be made to all three.
|
|
1236
818
|
//
|
|
1237
819
|
// The code between the markers must stay identical. Comments may differ, the
|
|
1238
820
|
// `@ttsc/<pkg>:` error prefix may differ, and @ttsc/strip spells each name with
|
|
@@ -1376,11 +958,11 @@ func realpathIfPossible(location string) string {
|
|
|
1376
958
|
// the project being compiled, with an explicit environment variable winning
|
|
1377
959
|
// and a last resort that invents no path.
|
|
1378
960
|
//
|
|
1379
|
-
//
|
|
1380
|
-
//
|
|
1381
|
-
//
|
|
1382
|
-
//
|
|
1383
|
-
// sentence.
|
|
961
|
+
// No automated check holds the three Go copies identical: a change lands in
|
|
962
|
+
// all three by hand, and each package's own config-loader tests exercise it.
|
|
963
|
+
// The JS original — `resolveConfigTsgo` / `resolveTtsxLauncher` in
|
|
964
|
+
// packages/lint/src/index.ts — is a fourth copy in another language; what it
|
|
965
|
+
// owes is that both policies stay describable in one sentence.
|
|
1384
966
|
//
|
|
1385
967
|
// The environment alone is the wrong place to ask. `ttsx` exports
|
|
1386
968
|
// TTSC_TSGO_BINARY and TTSC_TTSX_BINARY to its own descendants, so a host
|
|
@@ -1579,13 +1161,6 @@ func nodePlatformPairFor(goos, goarch string) (string, string) {
|
|
|
1579
1161
|
return platform, arch
|
|
1580
1162
|
}
|
|
1581
1163
|
|
|
1582
|
-
// ttsxCommand builds an exec.Cmd that runs ttsx with the given args.
|
|
1583
|
-
// When the resolved launcher has a script extension (.js, .ts, …) the binary is
|
|
1584
|
-
// invoked via the Node runtime so it is executed correctly on all platforms.
|
|
1585
|
-
func ttsxCommand(anchors []string, args ...string) *exec.Cmd {
|
|
1586
|
-
return ttsxCommandContext(context.Background(), anchors, args...)
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
1164
|
// ttsxCommandContext is the context-bound variant used by config loaders. It
|
|
1590
1165
|
// carries no deadline: evaluating a user config is the user's own code running,
|
|
1591
1166
|
// and how long that is allowed to take is not this binary's decision.
|
|
@@ -1691,8 +1266,8 @@ func relativeImportSpecifier(fromDir, location string) (string, error) {
|
|
|
1691
1266
|
return "./" + relative, nil
|
|
1692
1267
|
}
|
|
1693
1268
|
|
|
1694
|
-
// setEnv
|
|
1695
|
-
//
|
|
1269
|
+
// setEnv updates env with key=value. If the key already exists its entry is
|
|
1270
|
+
// replaced in-place; otherwise the entry is appended to the returned slice.
|
|
1696
1271
|
func setEnv(env []string, key, value string) []string {
|
|
1697
1272
|
prefix := key + "="
|
|
1698
1273
|
for i, entry := range env {
|