@ttsc/strip 0.26.1 → 0.26.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/driver/config.go +264 -11
- package/package.json +1 -1
package/driver/config.go
CHANGED
|
@@ -55,6 +55,10 @@ func loadStripConfigMap(pluginConfig map[string]any, cwd, tsconfigPath string) (
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// The discovery base directory doubles as the resolution root the config
|
|
59
|
+
// loader anchors its toolchain lookup on; see stripConfigToolAnchors.
|
|
60
|
+
resolutionRoot := stripDiscoveryBaseDir(cwd, tsconfigPath)
|
|
61
|
+
|
|
58
62
|
// Resolve the config file: explicit configFile wins over discovery.
|
|
59
63
|
configFilePath := ""
|
|
60
64
|
if rawCF, ok := pluginConfig["configFile"]; ok {
|
|
@@ -77,7 +81,7 @@ func loadStripConfigMap(pluginConfig map[string]any, cwd, tsconfigPath string) (
|
|
|
77
81
|
return map[string]any{}, nil
|
|
78
82
|
}
|
|
79
83
|
|
|
80
|
-
raw, err := loadStripConfigFile(configFilePath)
|
|
84
|
+
raw, err := loadStripConfigFile(configFilePath, resolutionRoot)
|
|
81
85
|
if err != nil {
|
|
82
86
|
return nil, err
|
|
83
87
|
}
|
|
@@ -147,7 +151,12 @@ func resolveStripConfigFilePath(configPath, cwd, tsconfigPath string) string {
|
|
|
147
151
|
// loadStripConfigFile loads and deserializes a strip config file at location.
|
|
148
152
|
// The format is determined by extension: .json is parsed natively; .js/.cjs/.mjs
|
|
149
153
|
// run through a Node subprocess; .ts/.cts/.mts run through ttsx.
|
|
150
|
-
|
|
154
|
+
//
|
|
155
|
+
// resolutionRoot is the project directory the TypeScript branch anchors its
|
|
156
|
+
// toolchain resolution on when the config file's own ancestry answers nothing;
|
|
157
|
+
// see stripConfigToolAnchors. The JSON and JS branches spawn no ttsx and
|
|
158
|
+
// ignore it.
|
|
159
|
+
func loadStripConfigFile(location, resolutionRoot string) (any, error) {
|
|
151
160
|
ext := strings.ToLower(filepath.Ext(location))
|
|
152
161
|
switch ext {
|
|
153
162
|
case ".json":
|
|
@@ -155,7 +164,7 @@ func loadStripConfigFile(location string) (any, error) {
|
|
|
155
164
|
case ".js", ".cjs", ".mjs":
|
|
156
165
|
return loadStripScriptConfigFile(location)
|
|
157
166
|
case ".ts", ".cts", ".mts":
|
|
158
|
-
return loadStripTypeScriptConfigFile(location)
|
|
167
|
+
return loadStripTypeScriptConfigFile(location, resolutionRoot)
|
|
159
168
|
default:
|
|
160
169
|
return nil, fmt.Errorf("@ttsc/strip: unsupported config file extension %q for %s", ext, location)
|
|
161
170
|
}
|
|
@@ -307,7 +316,11 @@ declare const process: {
|
|
|
307
316
|
// type-check and execute the strip config file, so loading the host
|
|
308
317
|
// project's transform/check plugins would be wasteful and could fail the
|
|
309
318
|
// build against this deliberately lenient loader tsconfig.
|
|
310
|
-
|
|
319
|
+
//
|
|
320
|
+
// Both tools this spawns — the launcher and the compiler handed to it — are
|
|
321
|
+
// resolved from the project rather than from the process environment alone;
|
|
322
|
+
// see stripConfigToolAnchors.
|
|
323
|
+
func loadStripTypeScriptConfigFile(location, resolutionRoot string) (any, error) {
|
|
311
324
|
tempDir, err := os.MkdirTemp(stripLoaderTempBase(location, os.TempDir()), "ttsc-strip-config-")
|
|
312
325
|
if err != nil {
|
|
313
326
|
return nil, fmt.Errorf("@ttsc/strip: create config loader tempdir: %w", err)
|
|
@@ -341,14 +354,15 @@ func loadStripTypeScriptConfigFile(location string) (any, error) {
|
|
|
341
354
|
"--cache-dir", filepath.Join(tempDir, "cache"),
|
|
342
355
|
"--no-plugins",
|
|
343
356
|
}
|
|
344
|
-
|
|
357
|
+
anchors := stripConfigToolAnchors(location, resolutionRoot)
|
|
358
|
+
if tsgo := stripResolveConfigTsgo(anchors); tsgo != "" {
|
|
345
359
|
args = append(args, "--binary", tsgo)
|
|
346
360
|
}
|
|
347
361
|
args = append(args, loader)
|
|
348
362
|
|
|
349
363
|
ctx, cancel := context.WithCancel(context.Background())
|
|
350
364
|
defer cancel()
|
|
351
|
-
cmd := stripTtsxCommandContext(ctx, args...)
|
|
365
|
+
cmd := stripTtsxCommandContext(ctx, anchors, args...)
|
|
352
366
|
cmd.Env = stripNodeConfigLoaderEnv(location)
|
|
353
367
|
// The child's stderr is human output and goes straight to this process's
|
|
354
368
|
// stderr as it is written. Collecting it only to replay it afterwards is what
|
|
@@ -414,6 +428,23 @@ func stripTypeScriptLoaderTsconfig(loader, location, outDir string) string {
|
|
|
414
428
|
return string(body)
|
|
415
429
|
}
|
|
416
430
|
|
|
431
|
+
// ttsc:config-loader-shared begin
|
|
432
|
+
//
|
|
433
|
+
// One policy in three Go copies: everything between these markers is
|
|
434
|
+
// duplicated verbatim in packages/lint/linthost/config.go,
|
|
435
|
+
// packages/banner/driver/banner.go and packages/strip/driver/config.go. #1169
|
|
436
|
+
// decided against extracting it — the only home the three modules could share
|
|
437
|
+
// is the public `packages/ttsc/driver` seam, and packages/lint's go.mod
|
|
438
|
+
// deliberately requires no in-tree ttsc module — and replaced the checklist
|
|
439
|
+
// with a gate: `scripts/ci/config-loader-copies.cjs` compares every function
|
|
440
|
+
// between these markers across all three copies on every pull request, so
|
|
441
|
+
// editing one and not the others fails by name. That file's header carries the
|
|
442
|
+
// full decision and the rules for changing this block.
|
|
443
|
+
//
|
|
444
|
+
// The code between the markers must stay identical. Comments may differ, the
|
|
445
|
+
// `@ttsc/<pkg>:` error prefix may differ, and @ttsc/strip spells each name with
|
|
446
|
+
// a `strip` prefix. Anything package-specific belongs outside the markers.
|
|
447
|
+
|
|
417
448
|
// stripConfigModuleOption returns the loader tsconfig's "module" for a config
|
|
418
449
|
// file: the module kind Node itself would give that file.
|
|
419
450
|
//
|
|
@@ -536,13 +567,233 @@ func stripResolveDirLink(dir string) string {
|
|
|
536
567
|
return dir
|
|
537
568
|
}
|
|
538
569
|
|
|
570
|
+
// stripRealpathIfPossible resolves location through its symlinks, and returns
|
|
571
|
+
// it unchanged when it cannot be evaluated (a path that does not exist, or an
|
|
572
|
+
// NTFS junction filepath.EvalSymlinks refuses to traverse).
|
|
573
|
+
func stripRealpathIfPossible(location string) string {
|
|
574
|
+
real, err := filepath.EvalSymlinks(location)
|
|
575
|
+
if err != nil {
|
|
576
|
+
return location
|
|
577
|
+
}
|
|
578
|
+
return real
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Both tools the TypeScript config evaluator needs — the `ttsx` launcher it
|
|
582
|
+
// spawns and the native compiler it hands that launcher — are resolved from
|
|
583
|
+
// the project being compiled, with an explicit environment variable winning
|
|
584
|
+
// and a last resort that invents no path.
|
|
585
|
+
//
|
|
586
|
+
// The three Go copies are held identical by the gate named at the top of this
|
|
587
|
+
// block. The JS original — `resolveConfigTsgo` / `resolveTtsxLauncher` in
|
|
588
|
+
// packages/lint/src/index.ts — is a fourth copy in another language that no Go
|
|
589
|
+
// gate can reach; what it owes is that both policies stay describable in one
|
|
590
|
+
// sentence.
|
|
591
|
+
//
|
|
592
|
+
// The environment alone is the wrong place to ask. `ttsx` exports
|
|
593
|
+
// TTSC_TSGO_BINARY and TTSC_TTSX_BINARY to its own descendants, so a host
|
|
594
|
+
// launched under `ttsx` inherited both and a host launched any other way
|
|
595
|
+
// inherited neither. The shipped `ttscserver` binary invoked with its
|
|
596
|
+
// documented `--tsgo <path>` flag keeps that path in a local and exports
|
|
597
|
+
// nothing, and an embedder of the driver package exports nothing either. For
|
|
598
|
+
// those the evaluator spawned a bare `ttsx` that only a global install puts on
|
|
599
|
+
// PATH, and, past that, a compiler-less child that aborted with
|
|
600
|
+
// `ttsc: typescript is required` before a line of the config was read.
|
|
601
|
+
//
|
|
602
|
+
// stripConfigToolAnchors lists the file paths those resolutions walk upward
|
|
603
|
+
// from, in order: the config file being evaluated, then the resolution root's
|
|
604
|
+
// manifest. The config comes first because it is the file whose own
|
|
605
|
+
// installation decides which toolchain the config's imports were written
|
|
606
|
+
// against; the resolution root answers for a config that lives outside the
|
|
607
|
+
// project tree (a `configFile` pointed at a shared package), and for one
|
|
608
|
+
// discovered above a workspace that installs its own toolchain.
|
|
609
|
+
func stripConfigToolAnchors(configPath, resolutionRoot string) []string {
|
|
610
|
+
anchors := make([]string, 0, 2)
|
|
611
|
+
if strings.TrimSpace(configPath) != "" {
|
|
612
|
+
anchors = append(anchors, configPath)
|
|
613
|
+
}
|
|
614
|
+
if strings.TrimSpace(resolutionRoot) != "" {
|
|
615
|
+
anchors = append(anchors, filepath.Join(resolutionRoot, "package.json"))
|
|
616
|
+
}
|
|
617
|
+
return anchors
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// stripResolveConfigTsgo returns the native TypeScript compiler the evaluator
|
|
621
|
+
// hands its ttsx child through `--binary`, or "" to leave the child resolving
|
|
622
|
+
// for itself.
|
|
623
|
+
//
|
|
624
|
+
// The child runs with `--cwd <ephemeral loader dir>`, so it cannot discover
|
|
625
|
+
// `typescript` the way an ordinary invocation does: stripLinkNearestNodeModules
|
|
626
|
+
// is the only thing that puts the project's modules within its reach, and it
|
|
627
|
+
// links nothing when the config's ancestry carries no node_modules. An explicit
|
|
628
|
+
// TTSC_TSGO_BINARY still wins, so an embedder that pins a compiler keeps
|
|
629
|
+
// pinning it. "" is the unchanged last resort: a project that cannot answer
|
|
630
|
+
// here could not answer inside the child either, and the child's own diagnostic
|
|
631
|
+
// is the one that names the missing package.
|
|
632
|
+
func stripResolveConfigTsgo(anchors []string) string {
|
|
633
|
+
if explicit := strings.TrimSpace(os.Getenv("TTSC_TSGO_BINARY")); explicit != "" {
|
|
634
|
+
return explicit
|
|
635
|
+
}
|
|
636
|
+
for _, anchor := range anchors {
|
|
637
|
+
if binary := stripTsgoBinaryFrom(anchor); binary != "" {
|
|
638
|
+
return binary
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return ""
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// stripTsgoBinaryFrom returns the platform compiler executable of the
|
|
645
|
+
// `typescript` install `anchor` can see, or "" when this anchor reaches neither
|
|
646
|
+
// the package nor its platform dependency.
|
|
647
|
+
//
|
|
648
|
+
// Mirrors resolveTsgo.ts so the Go plugin and the JS launcher name one file:
|
|
649
|
+
// the `typescript` manifest, then `@typescript/typescript-<platform>-<arch>`
|
|
650
|
+
// resolved from that manifest, then `lib/tsc` inside it.
|
|
651
|
+
//
|
|
652
|
+
// The install is chased to its real directory before the second hop, because
|
|
653
|
+
// Node resolves a module's own dependencies from its real location. pnpm keeps
|
|
654
|
+
// the real `typescript` directory in its content-addressed store with the
|
|
655
|
+
// platform package beside it and leaves a link in the project's node_modules,
|
|
656
|
+
// so a walk that started at the link would climb straight past the platform
|
|
657
|
+
// package. NTFS junctions defeat filepath.EvalSymlinks, so the link component
|
|
658
|
+
// is chased by hand first, the same order stripLoaderTempBase uses.
|
|
659
|
+
func stripTsgoBinaryFrom(anchor string) string {
|
|
660
|
+
manifest := stripNodePackageManifestFrom(anchor, "typescript")
|
|
661
|
+
if manifest == "" {
|
|
662
|
+
return ""
|
|
663
|
+
}
|
|
664
|
+
packageDir := stripRealpathIfPossible(stripResolveDirLink(filepath.Dir(manifest)))
|
|
665
|
+
platform, arch := stripNodePlatformPair()
|
|
666
|
+
platformManifest := stripNodePackageManifestFrom(
|
|
667
|
+
filepath.Join(packageDir, "package.json"),
|
|
668
|
+
"@typescript/typescript-"+platform+"-"+arch,
|
|
669
|
+
)
|
|
670
|
+
if platformManifest == "" {
|
|
671
|
+
return ""
|
|
672
|
+
}
|
|
673
|
+
name := "tsc"
|
|
674
|
+
if runtime.GOOS == "windows" {
|
|
675
|
+
name = "tsc.exe"
|
|
676
|
+
}
|
|
677
|
+
binary := filepath.Join(filepath.Dir(platformManifest), "lib", name)
|
|
678
|
+
if stat, err := os.Stat(binary); err != nil || stat.IsDir() {
|
|
679
|
+
return ""
|
|
680
|
+
}
|
|
681
|
+
return binary
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// stripResolveTtsxLauncher returns the launcher stripTtsxCommandContext spawns.
|
|
685
|
+
//
|
|
686
|
+
// An explicit TTSC_TTSX_BINARY wins. Otherwise the launcher is derived from the
|
|
687
|
+
// `ttsc` installation one of the anchors can see, because a bare command name
|
|
688
|
+
// only works when a bin link happens to be on PATH — which it is for a global
|
|
689
|
+
// install and is not for the ordinary project-local one. The bare `"ttsx"` name
|
|
690
|
+
// remains the unchanged last resort for an installation no anchor reaches.
|
|
691
|
+
func stripResolveTtsxLauncher(anchors []string) string {
|
|
692
|
+
if explicit := strings.TrimSpace(os.Getenv("TTSC_TTSX_BINARY")); explicit != "" {
|
|
693
|
+
return explicit
|
|
694
|
+
}
|
|
695
|
+
for _, anchor := range anchors {
|
|
696
|
+
if launcher := stripTtsxLauncherFrom(anchor); launcher != "" {
|
|
697
|
+
return launcher
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return "ttsx"
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// stripTtsxLauncherFrom returns `lib/launcher/ttsx.js` of the `ttsc` install
|
|
704
|
+
// `anchor` can see, or "" when this anchor reaches no such install. Only the
|
|
705
|
+
// manifest is an exported subpath, so the launcher is derived from where the
|
|
706
|
+
// manifest resolved rather than requested as a subpath of its own.
|
|
707
|
+
func stripTtsxLauncherFrom(anchor string) string {
|
|
708
|
+
manifest := stripNodePackageManifestFrom(anchor, "ttsc")
|
|
709
|
+
if manifest == "" {
|
|
710
|
+
return ""
|
|
711
|
+
}
|
|
712
|
+
launcher := filepath.Join(filepath.Dir(manifest), "lib", "launcher", "ttsx.js")
|
|
713
|
+
if stat, err := os.Stat(launcher); err != nil || stat.IsDir() {
|
|
714
|
+
return ""
|
|
715
|
+
}
|
|
716
|
+
return launcher
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// stripNodePackageManifestFrom resolves `<pkg>/package.json` the way Node's
|
|
720
|
+
// require.resolve does from the FILE `anchor`: walk upward from the anchor's
|
|
721
|
+
// directory and return the first `<dir>/node_modules/<pkg>/package.json` that
|
|
722
|
+
// exists. The anchor is treated as a file path, so its own directory is the
|
|
723
|
+
// first candidate's parent, and it need not exist — Node derives the search
|
|
724
|
+
// paths from the string alone.
|
|
725
|
+
//
|
|
726
|
+
// A directory already named `node_modules` contributes no candidate of its own,
|
|
727
|
+
// matching Module._nodeModulePaths, so nothing ever resolves through
|
|
728
|
+
// `node_modules/node_modules`.
|
|
729
|
+
//
|
|
730
|
+
// A relative anchor is resolved against the process directory before the walk,
|
|
731
|
+
// again matching Node. Walking a relative path instead would terminate at "."
|
|
732
|
+
// after one step and silently answer nothing for a config named relatively.
|
|
733
|
+
func stripNodePackageManifestFrom(anchor, pkg string) string {
|
|
734
|
+
if strings.TrimSpace(anchor) == "" || pkg == "" {
|
|
735
|
+
return ""
|
|
736
|
+
}
|
|
737
|
+
if absolute, err := filepath.Abs(anchor); err == nil {
|
|
738
|
+
anchor = absolute
|
|
739
|
+
}
|
|
740
|
+
dir := filepath.Dir(filepath.Clean(anchor))
|
|
741
|
+
for {
|
|
742
|
+
if filepath.Base(dir) != "node_modules" {
|
|
743
|
+
candidate := filepath.Join(dir, "node_modules", filepath.FromSlash(pkg), "package.json")
|
|
744
|
+
if stat, err := os.Stat(candidate); err == nil && !stat.IsDir() {
|
|
745
|
+
return candidate
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
parent := filepath.Dir(dir)
|
|
749
|
+
if parent == dir {
|
|
750
|
+
return ""
|
|
751
|
+
}
|
|
752
|
+
dir = parent
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// stripNodePlatformPair is stripNodePlatformPairFor applied to this build's own
|
|
757
|
+
// target.
|
|
758
|
+
func stripNodePlatformPair() (string, string) {
|
|
759
|
+
return stripNodePlatformPairFor(runtime.GOOS, runtime.GOARCH)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// stripNodePlatformPairFor maps a Go build target onto the `process.platform`
|
|
763
|
+
// and `process.arch` pair npm spells a platform package with, so the package
|
|
764
|
+
// name this plugin resolves is the same one the JS launcher resolves.
|
|
765
|
+
//
|
|
766
|
+
// Only the members whose two vocabularies disagree are mapped. Every other
|
|
767
|
+
// value is identical on both sides and passes through, which keeps a target
|
|
768
|
+
// neither side publishes yet resolvable rather than silently wrong, and keeps
|
|
769
|
+
// this from becoming a list that has to grow with every new port.
|
|
770
|
+
func stripNodePlatformPairFor(goos, goarch string) (string, string) {
|
|
771
|
+
platform := goos
|
|
772
|
+
switch platform {
|
|
773
|
+
case "windows":
|
|
774
|
+
platform = "win32"
|
|
775
|
+
case "solaris":
|
|
776
|
+
platform = "sunos"
|
|
777
|
+
}
|
|
778
|
+
arch := goarch
|
|
779
|
+
switch arch {
|
|
780
|
+
case "amd64":
|
|
781
|
+
arch = "x64"
|
|
782
|
+
case "386":
|
|
783
|
+
arch = "ia32"
|
|
784
|
+
case "ppc64le":
|
|
785
|
+
arch = "ppc64"
|
|
786
|
+
}
|
|
787
|
+
return platform, arch
|
|
788
|
+
}
|
|
789
|
+
|
|
539
790
|
// stripTtsxCommandContext returns an exec.Cmd that runs ttsx with the given
|
|
540
791
|
// arguments, routing through node when the resolved binary is a script file.
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
792
|
+
//
|
|
793
|
+
// `anchors` are the file paths the launcher is resolved from; see
|
|
794
|
+
// stripResolveTtsxLauncher.
|
|
795
|
+
func stripTtsxCommandContext(ctx context.Context, anchors []string, args ...string) *exec.Cmd {
|
|
796
|
+
ttsx := stripResolveTtsxLauncher(anchors)
|
|
546
797
|
if stripShouldRunThroughNode(ttsx) {
|
|
547
798
|
node := os.Getenv("TTSC_NODE_BINARY")
|
|
548
799
|
if node == "" {
|
|
@@ -673,3 +924,5 @@ func loaderFailureReason(output []byte) string {
|
|
|
673
924
|
}
|
|
674
925
|
return strings.TrimSpace(envelope.Message)
|
|
675
926
|
}
|
|
927
|
+
|
|
928
|
+
// ttsc:config-loader-shared end
|
package/package.json
CHANGED