@ttsc/banner 0.12.2 → 0.12.4
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 +392 -331
- package/lib/index.js +10 -0
- package/lib/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +38 -0
package/driver/banner.go
CHANGED
|
@@ -1,215 +1,250 @@
|
|
|
1
1
|
package banner
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"os/exec"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"strings"
|
|
10
|
+
|
|
11
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
12
12
|
)
|
|
13
13
|
|
|
14
14
|
func init() {
|
|
15
|
-
|
|
15
|
+
driver.RegisterPlugin(plugin{})
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// plugin implements driver.SourcePreamblePlugin for @ttsc/banner.
|
|
18
19
|
type plugin struct{}
|
|
19
20
|
|
|
20
21
|
var (
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
// linkConfigNodeModules is overridable in tests to avoid real symlink creation.
|
|
23
|
+
linkConfigNodeModules = linkNearestNodeModules
|
|
24
|
+
// writeConfigLoaderFile is overridable in tests to avoid real file I/O.
|
|
25
|
+
writeConfigLoaderFile = os.WriteFile
|
|
23
26
|
)
|
|
24
27
|
|
|
28
|
+
// SourcePreamble resolves the banner text from the plugin config and returns it
|
|
29
|
+
// formatted as a JSDoc block comment suitable for prepending to each emitted file.
|
|
25
30
|
func (plugin) SourcePreamble(ctx driver.PluginContext) (string, error) {
|
|
26
|
-
|
|
31
|
+
return parseBanner(ctx.Entry.Config, ctx.Cwd, ctx.Tsconfig)
|
|
27
32
|
}
|
|
28
33
|
|
|
34
|
+
// parseBanner resolves and formats banner text into a JSDoc block comment.
|
|
35
|
+
// Trailing blank lines are stripped from the resolved text before formatting.
|
|
29
36
|
func parseBanner(config map[string]any, cwd, tsconfigPath string) (string, error) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
37
|
+
text, err := resolveBannerText(config, cwd, tsconfigPath)
|
|
38
|
+
if err != nil {
|
|
39
|
+
return "", err
|
|
40
|
+
}
|
|
41
|
+
lines := strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n")
|
|
42
|
+
for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" {
|
|
43
|
+
lines = lines[:len(lines)-1]
|
|
44
|
+
}
|
|
45
|
+
var b strings.Builder
|
|
46
|
+
sep := strings.Repeat("-", 64)
|
|
47
|
+
b.WriteString("/**\n")
|
|
48
|
+
b.WriteString(" * ")
|
|
49
|
+
b.WriteString(sep)
|
|
50
|
+
b.WriteByte('\n')
|
|
51
|
+
for _, line := range lines {
|
|
52
|
+
b.WriteString(" * ")
|
|
53
|
+
b.WriteString(sanitizeJSDocLine(line))
|
|
54
|
+
b.WriteByte('\n')
|
|
55
|
+
}
|
|
56
|
+
b.WriteString(" *\n")
|
|
57
|
+
b.WriteString(" * @packageDocumentation\n ")
|
|
58
|
+
b.WriteString("*/\n")
|
|
59
|
+
return b.String(), nil
|
|
53
60
|
}
|
|
54
61
|
|
|
62
|
+
// resolveBannerText extracts the banner text from the plugin config.
|
|
63
|
+
// It tries, in order: inline "text" key, explicit "config" path, auto-discovered
|
|
64
|
+
// banner.config.* file. Returns an error when none of these sources provides text.
|
|
55
65
|
func resolveBannerText(config map[string]any, cwd, tsconfigPath string) (string, error) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
66
|
+
if text, ok, err := bannerTextFromConfigValue(config["text"], `"text"`); ok || err != nil {
|
|
67
|
+
return text, err
|
|
68
|
+
}
|
|
69
|
+
if rawConfigPath, ok := config["config"]; ok {
|
|
70
|
+
configPath, ok := rawConfigPath.(string)
|
|
71
|
+
if !ok || strings.TrimSpace(configPath) == "" {
|
|
72
|
+
return "", fmt.Errorf("@ttsc/banner: \"config\" must be a non-empty string path")
|
|
73
|
+
}
|
|
74
|
+
location := resolveBannerConfigPath(configPath, cwd, tsconfigPath)
|
|
75
|
+
raw, err := loadBannerConfigFile(location)
|
|
76
|
+
if err != nil {
|
|
77
|
+
return "", err
|
|
78
|
+
}
|
|
79
|
+
text, ok, err := bannerTextFromConfigValue(raw, filepath.Base(location))
|
|
80
|
+
if err != nil {
|
|
81
|
+
return "", err
|
|
82
|
+
}
|
|
83
|
+
if !ok {
|
|
84
|
+
return "", fmt.Errorf("@ttsc/banner: %s must export a non-empty string or an object with a non-empty \"text\" string", location)
|
|
85
|
+
}
|
|
86
|
+
return text, nil
|
|
87
|
+
}
|
|
88
|
+
location, err := findBannerConfigFile(cwd, tsconfigPath)
|
|
89
|
+
if err != nil {
|
|
90
|
+
return "", err
|
|
91
|
+
}
|
|
92
|
+
if location == "" {
|
|
93
|
+
return "", fmt.Errorf("@ttsc/banner: \"text\" must be a non-empty string or a banner.config.{js,cjs,mjs,ts,mts,cts} file must exist")
|
|
94
|
+
}
|
|
95
|
+
raw, err := loadBannerConfigFile(location)
|
|
96
|
+
if err != nil {
|
|
97
|
+
return "", err
|
|
98
|
+
}
|
|
99
|
+
text, ok, err := bannerTextFromConfigValue(raw, filepath.Base(location))
|
|
100
|
+
if err != nil {
|
|
101
|
+
return "", err
|
|
102
|
+
}
|
|
103
|
+
if !ok {
|
|
104
|
+
return "", fmt.Errorf("@ttsc/banner: %s must export a non-empty string or an object with a non-empty \"text\" string", location)
|
|
105
|
+
}
|
|
106
|
+
return text, nil
|
|
97
107
|
}
|
|
98
108
|
|
|
109
|
+
// bannerTextFromConfigValue extracts a banner text string from a config value.
|
|
110
|
+
// raw may be a string, a map with a "text" key, or nil (not present).
|
|
111
|
+
// Returns (text, true, nil) on success, ("", false, nil) when absent, or
|
|
112
|
+
// ("", true, err) / ("", false, err) on a type mismatch. label is used in
|
|
113
|
+
// error messages and should describe the config source (e.g. "\"text\"").
|
|
99
114
|
func bannerTextFromConfigValue(raw any, label string) (string, bool, error) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
115
|
+
if raw == nil {
|
|
116
|
+
return "", false, nil
|
|
117
|
+
}
|
|
118
|
+
text, ok := raw.(string)
|
|
119
|
+
if ok {
|
|
120
|
+
if strings.TrimSpace(text) == "" {
|
|
121
|
+
return "", true, fmt.Errorf("@ttsc/banner: %s must be a non-empty string", label)
|
|
122
|
+
}
|
|
123
|
+
return text, true, nil
|
|
124
|
+
}
|
|
125
|
+
object, ok := raw.(map[string]any)
|
|
126
|
+
if !ok {
|
|
127
|
+
return "", true, fmt.Errorf("@ttsc/banner: %s must be a string or an object with a non-empty \"text\" string", label)
|
|
128
|
+
}
|
|
129
|
+
rawText, ok := object["text"]
|
|
130
|
+
if !ok {
|
|
131
|
+
return "", false, nil
|
|
132
|
+
}
|
|
133
|
+
text, ok = rawText.(string)
|
|
134
|
+
if !ok || strings.TrimSpace(text) == "" {
|
|
135
|
+
return "", true, fmt.Errorf("@ttsc/banner: %s.text must be a non-empty string", label)
|
|
136
|
+
}
|
|
137
|
+
return text, true, nil
|
|
123
138
|
}
|
|
124
139
|
|
|
140
|
+
// findBannerConfigFile walks up from the tsconfig (or cwd) directory looking for
|
|
141
|
+
// a banner.config.{js,cjs,mjs,ts,cts,mts} file. Returns the path when exactly
|
|
142
|
+
// one match is found per directory, "" when none exists at any level, or an
|
|
143
|
+
// error when multiple candidates exist in the same directory.
|
|
125
144
|
func findBannerConfigFile(cwd, tsconfigPath string) (string, error) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
145
|
+
dir := discoveryConfigBaseDir(cwd, tsconfigPath)
|
|
146
|
+
for {
|
|
147
|
+
matches := make([]string, 0, 1)
|
|
148
|
+
for _, name := range []string{
|
|
149
|
+
"banner.config.js",
|
|
150
|
+
"banner.config.cjs",
|
|
151
|
+
"banner.config.mjs",
|
|
152
|
+
"banner.config.ts",
|
|
153
|
+
"banner.config.cts",
|
|
154
|
+
"banner.config.mts",
|
|
155
|
+
} {
|
|
156
|
+
candidate := filepath.Join(dir, name)
|
|
157
|
+
if stat, err := os.Stat(candidate); err == nil && !stat.IsDir() {
|
|
158
|
+
matches = append(matches, candidate)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if len(matches) > 1 {
|
|
162
|
+
return "", fmt.Errorf("@ttsc/banner: multiple banner.config.* files found in %s", dir)
|
|
163
|
+
}
|
|
164
|
+
if len(matches) == 1 {
|
|
165
|
+
return matches[0], nil
|
|
166
|
+
}
|
|
167
|
+
parent := filepath.Dir(dir)
|
|
168
|
+
if parent == dir {
|
|
169
|
+
return "", nil
|
|
170
|
+
}
|
|
171
|
+
dir = parent
|
|
172
|
+
}
|
|
154
173
|
}
|
|
155
174
|
|
|
175
|
+
// resolveBannerConfigPath resolves a config path from the plugin entry.
|
|
176
|
+
// Absolute paths are returned as-is; relative paths are resolved against the
|
|
177
|
+
// tsconfig directory (or cwd when no tsconfig is set).
|
|
156
178
|
func resolveBannerConfigPath(configPath, cwd, tsconfigPath string) string {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
179
|
+
if filepath.IsAbs(configPath) {
|
|
180
|
+
return configPath
|
|
181
|
+
}
|
|
182
|
+
return filepath.Join(tsconfigBaseDir(cwd, tsconfigPath), configPath)
|
|
161
183
|
}
|
|
162
184
|
|
|
185
|
+
// tsconfigBaseDir returns the directory that contains the tsconfig file, or cwd
|
|
186
|
+
// when tsconfigPath is empty. Used as the base for resolving explicit config paths.
|
|
163
187
|
func tsconfigBaseDir(cwd, tsconfigPath string) string {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
188
|
+
if tsconfigPath == "" {
|
|
189
|
+
return cwd
|
|
190
|
+
}
|
|
191
|
+
resolvedTsconfig := tsconfigPath
|
|
192
|
+
if !filepath.IsAbs(resolvedTsconfig) {
|
|
193
|
+
resolvedTsconfig = filepath.Join(cwd, resolvedTsconfig)
|
|
194
|
+
}
|
|
195
|
+
return filepath.Dir(resolvedTsconfig)
|
|
172
196
|
}
|
|
173
197
|
|
|
198
|
+
// discoveryConfigBaseDir returns the starting directory for the upward banner
|
|
199
|
+
// config file search. Semantically identical to tsconfigBaseDir; kept separate
|
|
200
|
+
// to make the call sites self-documenting.
|
|
174
201
|
func discoveryConfigBaseDir(cwd, tsconfigPath string) string {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
202
|
+
if tsconfigPath != "" {
|
|
203
|
+
resolvedTsconfig := tsconfigPath
|
|
204
|
+
if !filepath.IsAbs(resolvedTsconfig) {
|
|
205
|
+
resolvedTsconfig = filepath.Join(cwd, resolvedTsconfig)
|
|
206
|
+
}
|
|
207
|
+
return filepath.Dir(resolvedTsconfig)
|
|
208
|
+
}
|
|
209
|
+
return cwd
|
|
183
210
|
}
|
|
184
211
|
|
|
212
|
+
// loadBannerConfigFile loads and evaluates a banner config file, returning its
|
|
213
|
+
// exported value as a Go any (string or map[string]any). The file must be named
|
|
214
|
+
// banner.config.{js,cjs,mjs,ts,cts,mts}; JS/CJS/MJS variants run under Node,
|
|
215
|
+
// TypeScript variants compile and run via ttsx in a temp directory.
|
|
185
216
|
func loadBannerConfigFile(location string) (any, error) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
217
|
+
if !isBannerConfigFileName(filepath.Base(location)) {
|
|
218
|
+
return nil, fmt.Errorf("@ttsc/banner: config file must be named banner.config.{js,cjs,mjs,ts,mts,cts}: %s", location)
|
|
219
|
+
}
|
|
220
|
+
ext := strings.ToLower(filepath.Ext(location))
|
|
221
|
+
switch ext {
|
|
222
|
+
case ".js", ".cjs", ".mjs":
|
|
223
|
+
return loadBannerScriptConfigFile(location)
|
|
224
|
+
}
|
|
225
|
+
return loadBannerTypeScriptConfigFile(location)
|
|
195
226
|
}
|
|
196
227
|
|
|
228
|
+
// isBannerConfigFileName reports whether name is an allowed banner config file name.
|
|
197
229
|
func isBannerConfigFileName(name string) bool {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
230
|
+
switch name {
|
|
231
|
+
case "banner.config.js",
|
|
232
|
+
"banner.config.cjs",
|
|
233
|
+
"banner.config.mjs",
|
|
234
|
+
"banner.config.ts",
|
|
235
|
+
"banner.config.cts",
|
|
236
|
+
"banner.config.mts":
|
|
237
|
+
return true
|
|
238
|
+
default:
|
|
239
|
+
return false
|
|
240
|
+
}
|
|
209
241
|
}
|
|
210
242
|
|
|
243
|
+
// loadBannerScriptConfigFile evaluates a JS/CJS/MJS banner config file by
|
|
244
|
+
// running a small Node.js loader script that dynamic-imports the file and
|
|
245
|
+
// serializes its exported value to stdout as JSON.
|
|
211
246
|
func loadBannerScriptConfigFile(location string) (any, error) {
|
|
212
|
-
|
|
247
|
+
const script = `
|
|
213
248
|
const { pathToFileURL } = require("node:url");
|
|
214
249
|
|
|
215
250
|
(async () => {
|
|
@@ -232,99 +267,107 @@ function toSerializableBanner(value) {
|
|
|
232
267
|
throw new Error("config file must export a string or an object with a text string");
|
|
233
268
|
}
|
|
234
269
|
`
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
270
|
+
node := os.Getenv("TTSC_NODE_BINARY")
|
|
271
|
+
if node == "" {
|
|
272
|
+
node = "node"
|
|
273
|
+
}
|
|
274
|
+
cmd := exec.Command(node, "-e", script, location)
|
|
275
|
+
cmd.Env = nodeConfigLoaderEnv(location)
|
|
276
|
+
output, err := cmd.Output()
|
|
277
|
+
if err != nil {
|
|
278
|
+
stderr := ""
|
|
279
|
+
if exit, ok := err.(*exec.ExitError); ok {
|
|
280
|
+
stderr = strings.TrimSpace(string(exit.Stderr))
|
|
281
|
+
}
|
|
282
|
+
if stderr != "" {
|
|
283
|
+
return nil, fmt.Errorf("@ttsc/banner: load config file %s: %s", location, stderr)
|
|
284
|
+
}
|
|
285
|
+
return nil, fmt.Errorf("@ttsc/banner: load config file %s: %w", location, err)
|
|
286
|
+
}
|
|
287
|
+
var out any
|
|
288
|
+
if err := json.Unmarshal(output, &out); err != nil {
|
|
289
|
+
return nil, fmt.Errorf("@ttsc/banner: parse config file %s output: %w", location, err)
|
|
290
|
+
}
|
|
291
|
+
return out, nil
|
|
257
292
|
}
|
|
258
293
|
|
|
294
|
+
// loadBannerTypeScriptConfigFile compiles and runs a TypeScript banner config
|
|
295
|
+
// file using ttsx in a temp directory. A symlink to the nearest node_modules
|
|
296
|
+
// is created so the config file can import its own dependencies.
|
|
259
297
|
func loadBannerTypeScriptConfigFile(location string) (any, error) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
298
|
+
tempDir, err := os.MkdirTemp("", "ttsc-banner-config-")
|
|
299
|
+
if err != nil {
|
|
300
|
+
return nil, fmt.Errorf("@ttsc/banner: create config loader tempdir: %w", err)
|
|
301
|
+
}
|
|
302
|
+
defer os.RemoveAll(tempDir)
|
|
303
|
+
|
|
304
|
+
if err := linkConfigNodeModules(tempDir, filepath.Dir(location)); err != nil {
|
|
305
|
+
return nil, err
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
loader := filepath.Join(tempDir, "loader.mts")
|
|
309
|
+
tsconfig := filepath.Join(tempDir, "tsconfig.json")
|
|
310
|
+
importSpecifier, err := relativeImportSpecifier(tempDir, location)
|
|
311
|
+
if err != nil {
|
|
312
|
+
return nil, err
|
|
313
|
+
}
|
|
314
|
+
importLiteral, _ := json.Marshal(importSpecifier)
|
|
315
|
+
if err := writeConfigLoaderFile(loader, []byte(bannerTypeScriptConfigLoaderSource(string(importLiteral))), 0o644); err != nil {
|
|
316
|
+
return nil, fmt.Errorf("@ttsc/banner: write config loader: %w", err)
|
|
317
|
+
}
|
|
318
|
+
if err := writeConfigLoaderFile(tsconfig, []byte(typeScriptConfigLoaderTsconfig(loader, location, tempDir)), 0o644); err != nil {
|
|
319
|
+
return nil, fmt.Errorf("@ttsc/banner: write config loader tsconfig: %w", err)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
args := []string{
|
|
323
|
+
"--project", tsconfig,
|
|
324
|
+
"--cwd", tempDir,
|
|
325
|
+
"--cache-dir", filepath.Join(tempDir, "cache"),
|
|
326
|
+
}
|
|
327
|
+
if tsgo := os.Getenv("TTSC_TSGO_BINARY"); tsgo != "" {
|
|
328
|
+
args = append(args, "--binary", tsgo)
|
|
329
|
+
}
|
|
330
|
+
args = append(args, loader)
|
|
331
|
+
|
|
332
|
+
cmd := ttsxCommand(args...)
|
|
333
|
+
cmd.Env = nodeConfigLoaderEnv(location)
|
|
334
|
+
output, err := cmd.Output()
|
|
335
|
+
if err != nil {
|
|
336
|
+
stderr := ""
|
|
337
|
+
if exit, ok := err.(*exec.ExitError); ok {
|
|
338
|
+
stderr = strings.TrimSpace(string(exit.Stderr))
|
|
339
|
+
}
|
|
340
|
+
if stderr != "" {
|
|
341
|
+
return nil, fmt.Errorf("@ttsc/banner: load TypeScript config file %s: %s", location, stderr)
|
|
342
|
+
}
|
|
343
|
+
return nil, fmt.Errorf("@ttsc/banner: load TypeScript config file %s: %w", location, err)
|
|
344
|
+
}
|
|
345
|
+
var out any
|
|
346
|
+
if err := json.Unmarshal(output, &out); err != nil {
|
|
347
|
+
return nil, fmt.Errorf("@ttsc/banner: parse TypeScript config file %s output: %w", location, err)
|
|
348
|
+
}
|
|
349
|
+
return out, nil
|
|
312
350
|
}
|
|
313
351
|
|
|
352
|
+
// relativeImportSpecifier returns a "./" or "../"-prefixed slash-separated
|
|
353
|
+
// import specifier for location relative to fromDir.
|
|
314
354
|
func relativeImportSpecifier(fromDir, location string) (string, error) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
355
|
+
relative, err := filepath.Rel(fromDir, location)
|
|
356
|
+
if err != nil {
|
|
357
|
+
return "", fmt.Errorf("@ttsc/banner: resolve relative config import %s: %w", location, err)
|
|
358
|
+
}
|
|
359
|
+
relative = filepath.ToSlash(relative)
|
|
360
|
+
if strings.HasPrefix(relative, "../") || strings.HasPrefix(relative, "./") {
|
|
361
|
+
return relative, nil
|
|
362
|
+
}
|
|
363
|
+
return "./" + relative, nil
|
|
324
364
|
}
|
|
325
365
|
|
|
366
|
+
// bannerTypeScriptConfigLoaderSource returns the source of a TypeScript loader
|
|
367
|
+
// module that imports the banner config file specified by importLiteral (a
|
|
368
|
+
// JSON-encoded import specifier) and writes the serialized banner value to stdout.
|
|
326
369
|
func bannerTypeScriptConfigLoaderSource(importLiteral string) string {
|
|
327
|
-
|
|
370
|
+
return fmt.Sprintf(`import * as importedConfig from %s;
|
|
328
371
|
|
|
329
372
|
declare const process: {
|
|
330
373
|
stdout: { write(value: string): void };
|
|
@@ -375,105 +418,123 @@ function toSerializableBanner(value: unknown): unknown {
|
|
|
375
418
|
`, importLiteral)
|
|
376
419
|
}
|
|
377
420
|
|
|
421
|
+
// typeScriptConfigLoaderTsconfig returns the JSON content of a tsconfig that
|
|
422
|
+
// compiles loader and location together so ttsx can execute the loader.
|
|
378
423
|
func typeScriptConfigLoaderTsconfig(loader, location, outDir string) string {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
424
|
+
content := map[string]any{
|
|
425
|
+
"compilerOptions": map[string]any{
|
|
426
|
+
"allowImportingTsExtensions": true,
|
|
427
|
+
"module": "ESNext",
|
|
428
|
+
"moduleResolution": "bundler",
|
|
429
|
+
"outDir": filepath.ToSlash(filepath.Join(outDir, "out")),
|
|
430
|
+
"rewriteRelativeImportExtensions": true,
|
|
431
|
+
"rootDir": "/",
|
|
432
|
+
"skipLibCheck": true,
|
|
433
|
+
"strict": true,
|
|
434
|
+
"target": "ES2022",
|
|
435
|
+
},
|
|
436
|
+
"files": []string{
|
|
437
|
+
filepath.ToSlash(loader),
|
|
438
|
+
filepath.ToSlash(location),
|
|
439
|
+
},
|
|
440
|
+
}
|
|
441
|
+
body, _ := json.MarshalIndent(content, "", " ")
|
|
442
|
+
return string(body)
|
|
398
443
|
}
|
|
399
444
|
|
|
445
|
+
// ttsxCommand builds an exec.Cmd that runs ttsx with the given args.
|
|
446
|
+
// When TTSC_TTSX_BINARY has a script extension (.js, .ts, …) the binary is
|
|
447
|
+
// invoked via the Node runtime so it is executed correctly on all platforms.
|
|
400
448
|
func ttsxCommand(args ...string) *exec.Cmd {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
449
|
+
ttsx := os.Getenv("TTSC_TTSX_BINARY")
|
|
450
|
+
if ttsx == "" {
|
|
451
|
+
ttsx = "ttsx"
|
|
452
|
+
}
|
|
453
|
+
if shouldRunTtsxThroughNode(ttsx) {
|
|
454
|
+
node := os.Getenv("TTSC_NODE_BINARY")
|
|
455
|
+
if node == "" {
|
|
456
|
+
node = "node"
|
|
457
|
+
}
|
|
458
|
+
return exec.Command(node, append([]string{ttsx}, args...)...)
|
|
459
|
+
}
|
|
460
|
+
return exec.Command(ttsx, args...)
|
|
413
461
|
}
|
|
414
462
|
|
|
463
|
+
// shouldRunTtsxThroughNode reports whether binary has a script file extension
|
|
464
|
+
// and therefore must be launched via node rather than executed directly.
|
|
415
465
|
func shouldRunTtsxThroughNode(binary string) bool {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
466
|
+
switch strings.ToLower(filepath.Ext(binary)) {
|
|
467
|
+
case ".js", ".cjs", ".mjs", ".ts", ".cts", ".mts":
|
|
468
|
+
return true
|
|
469
|
+
default:
|
|
470
|
+
return false
|
|
471
|
+
}
|
|
422
472
|
}
|
|
423
473
|
|
|
474
|
+
// nodeConfigLoaderEnv builds an environment slice for the config-loader Node
|
|
475
|
+
// process. It prepends the nearest node_modules directory to NODE_PATH so
|
|
476
|
+
// the config file can resolve its own package dependencies.
|
|
424
477
|
func nodeConfigLoaderEnv(location string) []string {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
478
|
+
env := os.Environ()
|
|
479
|
+
parts := make([]string, 0, 2)
|
|
480
|
+
if nodeModules := findNearestNodeModules(filepath.Dir(location)); nodeModules != "" {
|
|
481
|
+
parts = append(parts, nodeModules)
|
|
482
|
+
}
|
|
483
|
+
if existing := os.Getenv("NODE_PATH"); existing != "" {
|
|
484
|
+
parts = append(parts, existing)
|
|
485
|
+
}
|
|
486
|
+
if len(parts) == 0 {
|
|
487
|
+
return env
|
|
488
|
+
}
|
|
489
|
+
return setEnv(env, "NODE_PATH", strings.Join(parts, string(os.PathListSeparator)))
|
|
437
490
|
}
|
|
438
491
|
|
|
492
|
+
// linkNearestNodeModules creates a node_modules symlink inside tempDir pointing
|
|
493
|
+
// to the nearest node_modules ancestor of sourceDir. Does nothing when none is found.
|
|
439
494
|
func linkNearestNodeModules(tempDir, sourceDir string) error {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
495
|
+
nodeModules := findNearestNodeModules(sourceDir)
|
|
496
|
+
if nodeModules == "" {
|
|
497
|
+
return nil
|
|
498
|
+
}
|
|
499
|
+
link := filepath.Join(tempDir, "node_modules")
|
|
500
|
+
if err := os.Symlink(nodeModules, link); err != nil {
|
|
501
|
+
return fmt.Errorf("@ttsc/banner: link config node_modules %s: %w", nodeModules, err)
|
|
502
|
+
}
|
|
503
|
+
return nil
|
|
449
504
|
}
|
|
450
505
|
|
|
506
|
+
// findNearestNodeModules walks up from start looking for a node_modules directory.
|
|
507
|
+
// Returns the absolute path of the first match, or "" when none is found.
|
|
451
508
|
func findNearestNodeModules(start string) string {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
509
|
+
dir := filepath.Clean(start)
|
|
510
|
+
for {
|
|
511
|
+
candidate := filepath.Join(dir, "node_modules")
|
|
512
|
+
if stat, err := os.Stat(candidate); err == nil && stat.IsDir() {
|
|
513
|
+
return candidate
|
|
514
|
+
}
|
|
515
|
+
parent := filepath.Dir(dir)
|
|
516
|
+
if parent == dir {
|
|
517
|
+
return ""
|
|
518
|
+
}
|
|
519
|
+
dir = parent
|
|
520
|
+
}
|
|
464
521
|
}
|
|
465
522
|
|
|
523
|
+
// setEnv returns a copy of env with key=value. If key already exists in env,
|
|
524
|
+
// its value is updated in-place; otherwise the entry is appended.
|
|
466
525
|
func setEnv(env []string, key, value string) []string {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
526
|
+
prefix := key + "="
|
|
527
|
+
for i, entry := range env {
|
|
528
|
+
if strings.HasPrefix(entry, prefix) {
|
|
529
|
+
env[i] = prefix + value
|
|
530
|
+
return env
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return append(env, prefix+value)
|
|
475
534
|
}
|
|
476
535
|
|
|
536
|
+
// sanitizeJSDocLine escapes any JSDoc-closing sequence in a banner text line
|
|
537
|
+
// by replacing "*/" with "* /" so the generated block comment stays valid.
|
|
477
538
|
func sanitizeJSDocLine(line string) string {
|
|
478
|
-
|
|
539
|
+
return strings.ReplaceAll(line, "*/", "* /")
|
|
479
540
|
}
|
package/lib/index.js
CHANGED
|
@@ -21,11 +21,21 @@ exports.default = createTtscBanner;
|
|
|
21
21
|
const node_path_1 = __importDefault(require("node:path"));
|
|
22
22
|
__exportStar(require("./structures/index"), exports);
|
|
23
23
|
/**
|
|
24
|
+
* Plugin factory for `@ttsc/banner` — called by the ttsc host to obtain the
|
|
25
|
+
* plugin descriptor.
|
|
26
|
+
*
|
|
27
|
+
* The factory is intentionally minimal: the banner plugin has no factory-level
|
|
28
|
+
* configuration that would alter which Go source to compile. All banner options
|
|
29
|
+
* (text, config path, enabled flag) are read at transform time by the Go
|
|
30
|
+
* driver.
|
|
31
|
+
*
|
|
24
32
|
* @internal
|
|
25
33
|
*/
|
|
26
34
|
function createTtscBanner(_context) {
|
|
27
35
|
return {
|
|
28
36
|
name: "@ttsc/banner",
|
|
37
|
+
// Point at the `driver/` directory one level above `lib/` in the
|
|
38
|
+
// installed package tree (where the Go sources live).
|
|
29
39
|
source: node_path_1.default.resolve(__dirname, "..", "driver"),
|
|
30
40
|
stage: "transform",
|
|
31
41
|
};
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA,0DAA6B;AAI7B,qDAAmC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA,0DAA6B;AAI7B,qDAAmC;AA4CnC;;;;;;;;;;GAUG;AACH,0BACE,QAA2D;IAE3D,OAAO;QACL,IAAI,EAAE,cAAc;QACpB,iEAAiE;QACjE,sDAAsD;QACtD,MAAM,EAAE,mBAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,QAAQ,CAAC;QAC/C,KAAK,EAAE,WAAW;KACnB,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttsc/banner",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.4",
|
|
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",
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
],
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "^25.3.0",
|
|
36
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
36
|
+
"@typescript/native-preview": "7.0.0-dev.20260519.1",
|
|
37
37
|
"rimraf": "^6.1.2",
|
|
38
|
-
"ttsc": "0.12.
|
|
38
|
+
"ttsc": "0.12.4"
|
|
39
39
|
},
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|
package/src/index.ts
CHANGED
|
@@ -4,21 +4,57 @@ import type { ITtscBannerPluginConfig } from "./structures";
|
|
|
4
4
|
|
|
5
5
|
export * from "./structures/index";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* The shape returned by a ttsc plugin factory function.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the `driver.PluginDescriptor` contract in the Go host. The `source`
|
|
11
|
+
* field points to the Go source directory that the host will compile into a
|
|
12
|
+
* plugin binary (cached under `.ttsc/`).
|
|
13
|
+
*/
|
|
7
14
|
type TtscPluginDescriptor = {
|
|
15
|
+
/** Human-readable plugin name used in logs and error messages. */
|
|
8
16
|
name: string;
|
|
17
|
+
/** Absolute path to the Go source directory for this plugin. */
|
|
9
18
|
source: string;
|
|
19
|
+
/**
|
|
20
|
+
* Pipeline stage. `"transform"` plugins may rewrite source files; `"check"`
|
|
21
|
+
* plugins only produce diagnostics. Defaults to `"check"`.
|
|
22
|
+
*/
|
|
10
23
|
stage?: "check" | "transform";
|
|
11
24
|
};
|
|
12
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Context object passed by the ttsc host to every plugin factory function.
|
|
28
|
+
*
|
|
29
|
+
* The factory may inspect the context to customise the descriptor — for example
|
|
30
|
+
* selecting a different Go source directory based on `plugin` config — but most
|
|
31
|
+
* factories ignore it.
|
|
32
|
+
*/
|
|
13
33
|
type TtscPluginFactoryContext<TConfig> = {
|
|
34
|
+
/**
|
|
35
|
+
* Absolute path to the compiled plugin binary (not yet built at factory
|
|
36
|
+
* time).
|
|
37
|
+
*/
|
|
14
38
|
binary: string;
|
|
39
|
+
/** Working directory of the ttsc invocation. */
|
|
15
40
|
cwd: string;
|
|
41
|
+
/** The raw plugin entry from `compilerOptions.plugins[]`. */
|
|
16
42
|
plugin: TConfig;
|
|
43
|
+
/** Absolute path to the project root (directory containing tsconfig). */
|
|
17
44
|
projectRoot: string;
|
|
45
|
+
/** Absolute path to the resolved tsconfig. */
|
|
18
46
|
tsconfig: string;
|
|
19
47
|
};
|
|
20
48
|
|
|
21
49
|
/**
|
|
50
|
+
* Plugin factory for `@ttsc/banner` — called by the ttsc host to obtain the
|
|
51
|
+
* plugin descriptor.
|
|
52
|
+
*
|
|
53
|
+
* The factory is intentionally minimal: the banner plugin has no factory-level
|
|
54
|
+
* configuration that would alter which Go source to compile. All banner options
|
|
55
|
+
* (text, config path, enabled flag) are read at transform time by the Go
|
|
56
|
+
* driver.
|
|
57
|
+
*
|
|
22
58
|
* @internal
|
|
23
59
|
*/
|
|
24
60
|
export default function createTtscBanner(
|
|
@@ -26,6 +62,8 @@ export default function createTtscBanner(
|
|
|
26
62
|
): TtscPluginDescriptor {
|
|
27
63
|
return {
|
|
28
64
|
name: "@ttsc/banner",
|
|
65
|
+
// Point at the `driver/` directory one level above `lib/` in the
|
|
66
|
+
// installed package tree (where the Go sources live).
|
|
29
67
|
source: path.resolve(__dirname, "..", "driver"),
|
|
30
68
|
stage: "transform",
|
|
31
69
|
};
|