@ttsc/banner 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/banner.go +286 -35
- package/package.json +2 -2
package/driver/banner.go
CHANGED
|
@@ -92,16 +92,26 @@ func parseBanner(config map[string]any, cwd, tsconfigPath string) (string, error
|
|
|
92
92
|
return b.String(), nil
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// sanitizeJSDocLine escapes any JSDoc-closing sequence in a banner text line
|
|
96
|
+
// by replacing "*/" with "* /" so the generated block comment stays valid.
|
|
97
|
+
func sanitizeJSDocLine(line string) string {
|
|
98
|
+
return strings.ReplaceAll(line, "*/", "* /")
|
|
99
|
+
}
|
|
100
|
+
|
|
95
101
|
// resolveBannerText extracts the banner text from the plugin config.
|
|
96
102
|
// The config entry is validated first: only the "configFile" key (plus
|
|
97
103
|
// framework keys) is accepted. When "configFile" is present its value is
|
|
98
104
|
// resolved to an absolute path and loaded. When absent the upward-walk
|
|
99
105
|
// discovery is used. Returns an error when the config is invalid or when
|
|
100
106
|
// no banner text can be found.
|
|
107
|
+
//
|
|
108
|
+
// The discovery base directory doubles as the resolution root the config
|
|
109
|
+
// loader anchors its toolchain lookup on; see configToolAnchors.
|
|
101
110
|
func resolveBannerText(config map[string]any, cwd, tsconfigPath string) (string, error) {
|
|
102
111
|
if err := validateBannerConfig(config); err != nil {
|
|
103
112
|
return "", err
|
|
104
113
|
}
|
|
114
|
+
resolutionRoot := tsconfigBaseDir(cwd, tsconfigPath)
|
|
105
115
|
|
|
106
116
|
if rawConfigFile, ok := config["configFile"]; ok {
|
|
107
117
|
configFile, ok := rawConfigFile.(string)
|
|
@@ -109,7 +119,7 @@ func resolveBannerText(config map[string]any, cwd, tsconfigPath string) (string,
|
|
|
109
119
|
return "", fmt.Errorf("@ttsc/banner: \"configFile\" must be a non-empty string path")
|
|
110
120
|
}
|
|
111
121
|
location := resolveBannerConfigPath(configFile, cwd, tsconfigPath)
|
|
112
|
-
raw, err := loadBannerConfigFile(location)
|
|
122
|
+
raw, err := loadBannerConfigFile(location, resolutionRoot)
|
|
113
123
|
if err != nil {
|
|
114
124
|
return "", err
|
|
115
125
|
}
|
|
@@ -130,7 +140,7 @@ func resolveBannerText(config map[string]any, cwd, tsconfigPath string) (string,
|
|
|
130
140
|
if location == "" {
|
|
131
141
|
return "", fmt.Errorf("@ttsc/banner: no banner.config.{ts,cts,mts,js,cjs,mjs,json} file found; create one or set \"configFile\" in the tsconfig plugin entry")
|
|
132
142
|
}
|
|
133
|
-
raw, err := loadBannerConfigFile(location)
|
|
143
|
+
raw, err := loadBannerConfigFile(location, resolutionRoot)
|
|
134
144
|
if err != nil {
|
|
135
145
|
return "", err
|
|
136
146
|
}
|
|
@@ -238,7 +248,11 @@ func tsconfigBaseDir(cwd, tsconfigPath string) string {
|
|
|
238
248
|
// must be named banner.config.{ts,cts,mts,js,cjs,mjs,json}; JS/CJS/MJS variants
|
|
239
249
|
// run under Node, TypeScript variants compile and run via ttsx in a temp
|
|
240
250
|
// directory, and JSON files are parsed natively.
|
|
241
|
-
|
|
251
|
+
//
|
|
252
|
+
// resolutionRoot is the project directory the TypeScript branch anchors its
|
|
253
|
+
// toolchain resolution on when the config file's own ancestry answers nothing;
|
|
254
|
+
// see configToolAnchors. The JSON and JS branches spawn no ttsx and ignore it.
|
|
255
|
+
func loadBannerConfigFile(location, resolutionRoot string) (any, error) {
|
|
242
256
|
if !isBannerConfigFileName(filepath.Base(location)) {
|
|
243
257
|
return nil, fmt.Errorf("@ttsc/banner: config file must be named banner.config.{ts,cts,mts,js,cjs,mjs,json}: %s", location)
|
|
244
258
|
}
|
|
@@ -249,7 +263,7 @@ func loadBannerConfigFile(location string) (any, error) {
|
|
|
249
263
|
case ".js", ".cjs", ".mjs":
|
|
250
264
|
return loadBannerScriptConfigFile(location)
|
|
251
265
|
}
|
|
252
|
-
return loadBannerTypeScriptConfigFile(location)
|
|
266
|
+
return loadBannerTypeScriptConfigFile(location, resolutionRoot)
|
|
253
267
|
}
|
|
254
268
|
|
|
255
269
|
// isBannerConfigFileName reports whether name is an allowed banner config file name.
|
|
@@ -362,7 +376,11 @@ function toSerializableBanner(value) {
|
|
|
362
376
|
// is created so the config file can import its own dependencies. The ttsx
|
|
363
377
|
// build runs with `--no-plugins` so evaluating the config never triggers the
|
|
364
378
|
// host project's transform/check plugins against the loader tsconfig.
|
|
365
|
-
|
|
379
|
+
//
|
|
380
|
+
// Both tools this spawns — the launcher and the compiler handed to it — are
|
|
381
|
+
// resolved from the project rather than from the process environment alone;
|
|
382
|
+
// see configToolAnchors.
|
|
383
|
+
func loadBannerTypeScriptConfigFile(location, resolutionRoot string) (any, error) {
|
|
366
384
|
tempDir, err := os.MkdirTemp(loaderTempBase(location, os.TempDir()), "ttsc-banner-config-")
|
|
367
385
|
if err != nil {
|
|
368
386
|
return nil, fmt.Errorf("@ttsc/banner: create config loader tempdir: %w", err)
|
|
@@ -393,14 +411,15 @@ func loadBannerTypeScriptConfigFile(location string) (any, error) {
|
|
|
393
411
|
"--cache-dir", filepath.Join(tempDir, "cache"),
|
|
394
412
|
"--no-plugins",
|
|
395
413
|
}
|
|
396
|
-
|
|
414
|
+
anchors := configToolAnchors(location, resolutionRoot)
|
|
415
|
+
if tsgo := resolveConfigTsgo(anchors); tsgo != "" {
|
|
397
416
|
args = append(args, "--binary", tsgo)
|
|
398
417
|
}
|
|
399
418
|
args = append(args, loader)
|
|
400
419
|
|
|
401
420
|
ctx, cancel := context.WithCancel(context.Background())
|
|
402
421
|
defer cancel()
|
|
403
|
-
cmd := ttsxCommandContext(ctx, args...)
|
|
422
|
+
cmd := ttsxCommandContext(ctx, anchors, args...)
|
|
404
423
|
cmd.Env = nodeConfigLoaderEnv(location)
|
|
405
424
|
// The child's stderr is human output and goes straight to this process's
|
|
406
425
|
// stderr as it is written. Collecting it only to replay it afterwards is what
|
|
@@ -424,20 +443,6 @@ func loadBannerTypeScriptConfigFile(location string) (any, error) {
|
|
|
424
443
|
return out, nil
|
|
425
444
|
}
|
|
426
445
|
|
|
427
|
-
// relativeImportSpecifier returns a "./" or "../"-prefixed slash-separated
|
|
428
|
-
// import specifier for location relative to fromDir.
|
|
429
|
-
func relativeImportSpecifier(fromDir, location string) (string, error) {
|
|
430
|
-
relative, err := filepath.Rel(fromDir, location)
|
|
431
|
-
if err != nil {
|
|
432
|
-
return "", fmt.Errorf("@ttsc/banner: resolve relative config import %s: %w", location, err)
|
|
433
|
-
}
|
|
434
|
-
relative = filepath.ToSlash(relative)
|
|
435
|
-
if strings.HasPrefix(relative, "../") || strings.HasPrefix(relative, "./") {
|
|
436
|
-
return relative, nil
|
|
437
|
-
}
|
|
438
|
-
return "./" + relative, nil
|
|
439
|
-
}
|
|
440
|
-
|
|
441
446
|
// bannerTypeScriptConfigLoaderSource returns the source of a TypeScript loader
|
|
442
447
|
// module that imports the banner config file specified by importLiteral (a
|
|
443
448
|
// JSON-encoded import specifier) and writes the serialized banner value to stdout.
|
|
@@ -551,6 +556,23 @@ func typeScriptConfigLoaderTsconfig(loader, location, outDir string) string {
|
|
|
551
556
|
return string(body)
|
|
552
557
|
}
|
|
553
558
|
|
|
559
|
+
// ttsc:config-loader-shared begin
|
|
560
|
+
//
|
|
561
|
+
// One policy in three Go copies: everything between these markers is
|
|
562
|
+
// duplicated verbatim in packages/lint/linthost/config.go,
|
|
563
|
+
// packages/banner/driver/banner.go and packages/strip/driver/config.go. #1169
|
|
564
|
+
// decided against extracting it — the only home the three modules could share
|
|
565
|
+
// is the public `packages/ttsc/driver` seam, and packages/lint's go.mod
|
|
566
|
+
// deliberately requires no in-tree ttsc module — and replaced the checklist
|
|
567
|
+
// with a gate: `scripts/ci/config-loader-copies.cjs` compares every function
|
|
568
|
+
// between these markers across all three copies on every pull request, so
|
|
569
|
+
// editing one and not the others fails by name. That file's header carries the
|
|
570
|
+
// full decision and the rules for changing this block.
|
|
571
|
+
//
|
|
572
|
+
// The code between the markers must stay identical. Comments may differ, the
|
|
573
|
+
// `@ttsc/<pkg>:` error prefix may differ, and @ttsc/strip spells each name with
|
|
574
|
+
// a `strip` prefix. Anything package-specific belongs outside the markers.
|
|
575
|
+
|
|
554
576
|
// configModuleOption returns the loader tsconfig's "module" for a config file:
|
|
555
577
|
// the module kind Node itself would give that file.
|
|
556
578
|
//
|
|
@@ -673,21 +695,240 @@ func resolveDirLink(dir string) string {
|
|
|
673
695
|
return dir
|
|
674
696
|
}
|
|
675
697
|
|
|
698
|
+
// realpathIfPossible resolves location through its symlinks, and returns it
|
|
699
|
+
// unchanged when it cannot be evaluated (a path that does not exist, or an
|
|
700
|
+
// NTFS junction filepath.EvalSymlinks refuses to traverse).
|
|
701
|
+
func realpathIfPossible(location string) string {
|
|
702
|
+
real, err := filepath.EvalSymlinks(location)
|
|
703
|
+
if err != nil {
|
|
704
|
+
return location
|
|
705
|
+
}
|
|
706
|
+
return real
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Both tools the TypeScript config evaluator needs — the `ttsx` launcher it
|
|
710
|
+
// spawns and the native compiler it hands that launcher — are resolved from
|
|
711
|
+
// the project being compiled, with an explicit environment variable winning
|
|
712
|
+
// and a last resort that invents no path.
|
|
713
|
+
//
|
|
714
|
+
// The three Go copies are held identical by the gate named at the top of this
|
|
715
|
+
// block. The JS original — `resolveConfigTsgo` / `resolveTtsxLauncher` in
|
|
716
|
+
// packages/lint/src/index.ts — is a fourth copy in another language that no Go
|
|
717
|
+
// gate can reach; what it owes is that both policies stay describable in one
|
|
718
|
+
// sentence.
|
|
719
|
+
//
|
|
720
|
+
// The environment alone is the wrong place to ask. `ttsx` exports
|
|
721
|
+
// TTSC_TSGO_BINARY and TTSC_TTSX_BINARY to its own descendants, so a host
|
|
722
|
+
// launched under `ttsx` inherited both and a host launched any other way
|
|
723
|
+
// inherited neither. The shipped `ttscserver` binary invoked with its
|
|
724
|
+
// documented `--tsgo <path>` flag keeps that path in a local and exports
|
|
725
|
+
// nothing, and an embedder of the driver package exports nothing either. For
|
|
726
|
+
// those the evaluator spawned a bare `ttsx` that only a global install puts on
|
|
727
|
+
// PATH, and, past that, a compiler-less child that aborted with
|
|
728
|
+
// `ttsc: typescript is required` before a line of the config was read.
|
|
729
|
+
//
|
|
730
|
+
// configToolAnchors lists the file paths those resolutions walk upward from,
|
|
731
|
+
// in order: the config file being evaluated, then the resolution root's
|
|
732
|
+
// manifest. The config comes first because it is the file whose own
|
|
733
|
+
// installation decides which toolchain the config's imports were written
|
|
734
|
+
// against; the resolution root answers for a config that lives outside the
|
|
735
|
+
// project tree (a `configFile` pointed at a shared package), and for one
|
|
736
|
+
// discovered above a workspace that installs its own toolchain.
|
|
737
|
+
func configToolAnchors(configPath, resolutionRoot string) []string {
|
|
738
|
+
anchors := make([]string, 0, 2)
|
|
739
|
+
if strings.TrimSpace(configPath) != "" {
|
|
740
|
+
anchors = append(anchors, configPath)
|
|
741
|
+
}
|
|
742
|
+
if strings.TrimSpace(resolutionRoot) != "" {
|
|
743
|
+
anchors = append(anchors, filepath.Join(resolutionRoot, "package.json"))
|
|
744
|
+
}
|
|
745
|
+
return anchors
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// resolveConfigTsgo returns the native TypeScript compiler the evaluator hands
|
|
749
|
+
// its ttsx child through `--binary`, or "" to leave the child resolving for
|
|
750
|
+
// itself.
|
|
751
|
+
//
|
|
752
|
+
// The child runs with `--cwd <ephemeral loader dir>`, so it cannot discover
|
|
753
|
+
// `typescript` the way an ordinary invocation does: linkNearestNodeModules is
|
|
754
|
+
// the only thing that puts the project's modules within its reach, and it links
|
|
755
|
+
// nothing when the config's ancestry carries no node_modules. An explicit
|
|
756
|
+
// TTSC_TSGO_BINARY still wins, so an embedder that pins a compiler keeps
|
|
757
|
+
// pinning it. "" is the unchanged last resort: a project that cannot answer
|
|
758
|
+
// here could not answer inside the child either, and the child's own diagnostic
|
|
759
|
+
// is the one that names the missing package.
|
|
760
|
+
func resolveConfigTsgo(anchors []string) string {
|
|
761
|
+
if explicit := strings.TrimSpace(os.Getenv("TTSC_TSGO_BINARY")); explicit != "" {
|
|
762
|
+
return explicit
|
|
763
|
+
}
|
|
764
|
+
for _, anchor := range anchors {
|
|
765
|
+
if binary := tsgoBinaryFrom(anchor); binary != "" {
|
|
766
|
+
return binary
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return ""
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// tsgoBinaryFrom returns the platform compiler executable of the `typescript`
|
|
773
|
+
// install `anchor` can see, or "" when this anchor reaches neither the package
|
|
774
|
+
// nor its platform dependency.
|
|
775
|
+
//
|
|
776
|
+
// Mirrors resolveTsgo.ts so the Go plugin and the JS launcher name one file:
|
|
777
|
+
// the `typescript` manifest, then `@typescript/typescript-<platform>-<arch>`
|
|
778
|
+
// resolved from that manifest, then `lib/tsc` inside it.
|
|
779
|
+
//
|
|
780
|
+
// The install is chased to its real directory before the second hop, because
|
|
781
|
+
// Node resolves a module's own dependencies from its real location. pnpm keeps
|
|
782
|
+
// the real `typescript` directory in its content-addressed store with the
|
|
783
|
+
// platform package beside it and leaves a link in the project's node_modules,
|
|
784
|
+
// so a walk that started at the link would climb straight past the platform
|
|
785
|
+
// package. NTFS junctions defeat filepath.EvalSymlinks, so the link component
|
|
786
|
+
// is chased by hand first, the same order loaderTempBase uses.
|
|
787
|
+
func tsgoBinaryFrom(anchor string) string {
|
|
788
|
+
manifest := nodePackageManifestFrom(anchor, "typescript")
|
|
789
|
+
if manifest == "" {
|
|
790
|
+
return ""
|
|
791
|
+
}
|
|
792
|
+
packageDir := realpathIfPossible(resolveDirLink(filepath.Dir(manifest)))
|
|
793
|
+
platform, arch := nodePlatformPair()
|
|
794
|
+
platformManifest := nodePackageManifestFrom(
|
|
795
|
+
filepath.Join(packageDir, "package.json"),
|
|
796
|
+
"@typescript/typescript-"+platform+"-"+arch,
|
|
797
|
+
)
|
|
798
|
+
if platformManifest == "" {
|
|
799
|
+
return ""
|
|
800
|
+
}
|
|
801
|
+
name := "tsc"
|
|
802
|
+
if runtime.GOOS == "windows" {
|
|
803
|
+
name = "tsc.exe"
|
|
804
|
+
}
|
|
805
|
+
binary := filepath.Join(filepath.Dir(platformManifest), "lib", name)
|
|
806
|
+
if stat, err := os.Stat(binary); err != nil || stat.IsDir() {
|
|
807
|
+
return ""
|
|
808
|
+
}
|
|
809
|
+
return binary
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// resolveTtsxLauncher returns the launcher ttsxCommandContext spawns.
|
|
813
|
+
//
|
|
814
|
+
// An explicit TTSC_TTSX_BINARY wins. Otherwise the launcher is derived from the
|
|
815
|
+
// `ttsc` installation one of the anchors can see, because a bare command name
|
|
816
|
+
// only works when a bin link happens to be on PATH — which it is for a global
|
|
817
|
+
// install and is not for the ordinary project-local one. The bare `"ttsx"` name
|
|
818
|
+
// remains the unchanged last resort for an installation no anchor reaches.
|
|
819
|
+
func resolveTtsxLauncher(anchors []string) string {
|
|
820
|
+
if explicit := strings.TrimSpace(os.Getenv("TTSC_TTSX_BINARY")); explicit != "" {
|
|
821
|
+
return explicit
|
|
822
|
+
}
|
|
823
|
+
for _, anchor := range anchors {
|
|
824
|
+
if launcher := ttsxLauncherFrom(anchor); launcher != "" {
|
|
825
|
+
return launcher
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return "ttsx"
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// ttsxLauncherFrom returns `lib/launcher/ttsx.js` of the `ttsc` install
|
|
832
|
+
// `anchor` can see, or "" when this anchor reaches no such install. Only the
|
|
833
|
+
// manifest is an exported subpath, so the launcher is derived from where the
|
|
834
|
+
// manifest resolved rather than requested as a subpath of its own.
|
|
835
|
+
func ttsxLauncherFrom(anchor string) string {
|
|
836
|
+
manifest := nodePackageManifestFrom(anchor, "ttsc")
|
|
837
|
+
if manifest == "" {
|
|
838
|
+
return ""
|
|
839
|
+
}
|
|
840
|
+
launcher := filepath.Join(filepath.Dir(manifest), "lib", "launcher", "ttsx.js")
|
|
841
|
+
if stat, err := os.Stat(launcher); err != nil || stat.IsDir() {
|
|
842
|
+
return ""
|
|
843
|
+
}
|
|
844
|
+
return launcher
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// nodePackageManifestFrom resolves `<pkg>/package.json` the way Node's
|
|
848
|
+
// require.resolve does from the FILE `anchor`: walk upward from the anchor's
|
|
849
|
+
// directory and return the first `<dir>/node_modules/<pkg>/package.json` that
|
|
850
|
+
// exists. The anchor is treated as a file path, so its own directory is the
|
|
851
|
+
// first candidate's parent, and it need not exist — Node derives the search
|
|
852
|
+
// paths from the string alone.
|
|
853
|
+
//
|
|
854
|
+
// A directory already named `node_modules` contributes no candidate of its own,
|
|
855
|
+
// matching Module._nodeModulePaths, so nothing ever resolves through
|
|
856
|
+
// `node_modules/node_modules`.
|
|
857
|
+
//
|
|
858
|
+
// A relative anchor is resolved against the process directory before the walk,
|
|
859
|
+
// again matching Node. Walking a relative path instead would terminate at "."
|
|
860
|
+
// after one step and silently answer nothing for a config named relatively.
|
|
861
|
+
func nodePackageManifestFrom(anchor, pkg string) string {
|
|
862
|
+
if strings.TrimSpace(anchor) == "" || pkg == "" {
|
|
863
|
+
return ""
|
|
864
|
+
}
|
|
865
|
+
if absolute, err := filepath.Abs(anchor); err == nil {
|
|
866
|
+
anchor = absolute
|
|
867
|
+
}
|
|
868
|
+
dir := filepath.Dir(filepath.Clean(anchor))
|
|
869
|
+
for {
|
|
870
|
+
if filepath.Base(dir) != "node_modules" {
|
|
871
|
+
candidate := filepath.Join(dir, "node_modules", filepath.FromSlash(pkg), "package.json")
|
|
872
|
+
if stat, err := os.Stat(candidate); err == nil && !stat.IsDir() {
|
|
873
|
+
return candidate
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
parent := filepath.Dir(dir)
|
|
877
|
+
if parent == dir {
|
|
878
|
+
return ""
|
|
879
|
+
}
|
|
880
|
+
dir = parent
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// nodePlatformPair is nodePlatformPairFor applied to this build's own target.
|
|
885
|
+
func nodePlatformPair() (string, string) {
|
|
886
|
+
return nodePlatformPairFor(runtime.GOOS, runtime.GOARCH)
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// nodePlatformPairFor maps a Go build target onto the `process.platform` and
|
|
890
|
+
// `process.arch` pair npm spells a platform package with, so the package name
|
|
891
|
+
// this plugin resolves is the same one the JS launcher resolves.
|
|
892
|
+
//
|
|
893
|
+
// Only the members whose two vocabularies disagree are mapped. Every other
|
|
894
|
+
// value is identical on both sides and passes through, which keeps a target
|
|
895
|
+
// neither side publishes yet resolvable rather than silently wrong, and keeps
|
|
896
|
+
// this from becoming a list that has to grow with every new port.
|
|
897
|
+
func nodePlatformPairFor(goos, goarch string) (string, string) {
|
|
898
|
+
platform := goos
|
|
899
|
+
switch platform {
|
|
900
|
+
case "windows":
|
|
901
|
+
platform = "win32"
|
|
902
|
+
case "solaris":
|
|
903
|
+
platform = "sunos"
|
|
904
|
+
}
|
|
905
|
+
arch := goarch
|
|
906
|
+
switch arch {
|
|
907
|
+
case "amd64":
|
|
908
|
+
arch = "x64"
|
|
909
|
+
case "386":
|
|
910
|
+
arch = "ia32"
|
|
911
|
+
case "ppc64le":
|
|
912
|
+
arch = "ppc64"
|
|
913
|
+
}
|
|
914
|
+
return platform, arch
|
|
915
|
+
}
|
|
916
|
+
|
|
676
917
|
// ttsxCommand builds an exec.Cmd that runs ttsx with the given args.
|
|
677
|
-
// When
|
|
918
|
+
// When the resolved launcher has a script extension (.js, .ts, …) the binary is
|
|
678
919
|
// invoked via the Node runtime so it is executed correctly on all platforms.
|
|
679
|
-
func ttsxCommand(args ...string) *exec.Cmd {
|
|
680
|
-
return ttsxCommandContext(context.Background(), args...)
|
|
920
|
+
func ttsxCommand(anchors []string, args ...string) *exec.Cmd {
|
|
921
|
+
return ttsxCommandContext(context.Background(), anchors, args...)
|
|
681
922
|
}
|
|
682
923
|
|
|
683
924
|
// ttsxCommandContext is the context-bound variant used by config loaders. It
|
|
684
925
|
// carries no deadline: evaluating a user config is the user's own code running,
|
|
685
926
|
// and how long that is allowed to take is not this binary's decision.
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
927
|
+
//
|
|
928
|
+
// `anchors` are the file paths the launcher is resolved from; see
|
|
929
|
+
// resolveTtsxLauncher.
|
|
930
|
+
func ttsxCommandContext(ctx context.Context, anchors []string, args ...string) *exec.Cmd {
|
|
931
|
+
ttsx := resolveTtsxLauncher(anchors)
|
|
691
932
|
if shouldRunTtsxThroughNode(ttsx) {
|
|
692
933
|
node := os.Getenv("TTSC_NODE_BINARY")
|
|
693
934
|
if node == "" {
|
|
@@ -771,6 +1012,20 @@ func findNearestNodeModules(start string) string {
|
|
|
771
1012
|
}
|
|
772
1013
|
}
|
|
773
1014
|
|
|
1015
|
+
// relativeImportSpecifier returns a "./" or "../"-prefixed slash-separated
|
|
1016
|
+
// import specifier for location relative to fromDir.
|
|
1017
|
+
func relativeImportSpecifier(fromDir, location string) (string, error) {
|
|
1018
|
+
relative, err := filepath.Rel(fromDir, location)
|
|
1019
|
+
if err != nil {
|
|
1020
|
+
return "", fmt.Errorf("@ttsc/banner: resolve relative config import %s: %w", location, err)
|
|
1021
|
+
}
|
|
1022
|
+
relative = filepath.ToSlash(relative)
|
|
1023
|
+
if strings.HasPrefix(relative, "../") || strings.HasPrefix(relative, "./") {
|
|
1024
|
+
return relative, nil
|
|
1025
|
+
}
|
|
1026
|
+
return "./" + relative, nil
|
|
1027
|
+
}
|
|
1028
|
+
|
|
774
1029
|
// setEnv returns a copy of env with key=value. If key already exists in env,
|
|
775
1030
|
// its value is updated in-place; otherwise the entry is appended.
|
|
776
1031
|
func setEnv(env []string, key, value string) []string {
|
|
@@ -784,12 +1039,6 @@ func setEnv(env []string, key, value string) []string {
|
|
|
784
1039
|
return append(env, prefix+value)
|
|
785
1040
|
}
|
|
786
1041
|
|
|
787
|
-
// sanitizeJSDocLine escapes any JSDoc-closing sequence in a banner text line
|
|
788
|
-
// by replacing "*/" with "* /" so the generated block comment stays valid.
|
|
789
|
-
func sanitizeJSDocLine(line string) string {
|
|
790
|
-
return strings.ReplaceAll(line, "*/", "* /")
|
|
791
|
-
}
|
|
792
|
-
|
|
793
1042
|
// loaderFailureReason reads the failure envelope a config loader writes to its
|
|
794
1043
|
// payload channel when it stops on an error it can name.
|
|
795
1044
|
//
|
|
@@ -809,3 +1058,5 @@ func loaderFailureReason(output []byte) string {
|
|
|
809
1058
|
}
|
|
810
1059
|
return strings.TrimSpace(envelope.Message)
|
|
811
1060
|
}
|
|
1061
|
+
|
|
1062
|
+
// ttsc:config-loader-shared end
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttsc/banner",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.2",
|
|
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.26.
|
|
38
|
+
"ttsc": "0.26.2"
|
|
39
39
|
},
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|