@ttsc/banner 0.12.3 → 0.13.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/README.md +21 -10
- package/driver/banner.go +447 -342
- package/lib/index.js +30 -1
- package/lib/index.js.map +1 -1
- package/lib/structures/{TtscBannerConfig.d.ts → ITtscBannerConfig.d.ts} +2 -2
- package/lib/structures/{TtscBannerConfig.js → ITtscBannerConfig.js} +1 -1
- package/lib/structures/ITtscBannerConfig.js.map +1 -0
- package/lib/structures/ITtscBannerPluginConfig.d.ts +7 -6
- package/lib/structures/index.d.ts +1 -1
- package/lib/structures/index.js +1 -1
- package/lib/structures/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +62 -1
- package/src/structures/ITtscBannerConfig.ts +5 -0
- package/src/structures/ITtscBannerPluginConfig.ts +7 -8
- package/src/structures/index.ts +1 -1
- package/lib/structures/TtscBannerConfig.js.map +0 -1
- package/src/structures/TtscBannerConfig.ts +0 -7
package/README.md
CHANGED
|
@@ -20,45 +20,56 @@ npm install -D ttsc @typescript/native-preview
|
|
|
20
20
|
npm install -D @ttsc/banner
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Register the plugin in your `tsconfig.json`:
|
|
24
24
|
|
|
25
25
|
```jsonc
|
|
26
26
|
// tsconfig.json
|
|
27
27
|
{
|
|
28
28
|
"compilerOptions": {
|
|
29
29
|
"plugins": [
|
|
30
|
-
{ "transform": "@ttsc/banner"
|
|
30
|
+
{ "transform": "@ttsc/banner" }
|
|
31
31
|
]
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
Drop a `banner.config.ts` next to your `tsconfig.json`:
|
|
37
37
|
|
|
38
38
|
```ts
|
|
39
39
|
// banner.config.ts
|
|
40
|
-
import type {
|
|
40
|
+
import type { ITtscBannerConfig } from "@ttsc/banner";
|
|
41
41
|
|
|
42
42
|
export default {
|
|
43
43
|
text: "License MIT (c) 2026 Acme",
|
|
44
|
-
} satisfies
|
|
44
|
+
} satisfies ITtscBannerConfig;
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
A `banner.config.*` file always exports an object with a `text` string.
|
|
48
|
+
|
|
47
49
|
Run your normal `ttsc` command:
|
|
48
50
|
|
|
49
51
|
```bash
|
|
50
52
|
npx ttsc
|
|
51
53
|
```
|
|
52
54
|
|
|
53
|
-
If `@ttsc/banner` is installed and
|
|
55
|
+
If `@ttsc/banner` is installed and no `banner.config.*` file is found, the compile fails.
|
|
54
56
|
|
|
55
57
|
## Configuration
|
|
56
58
|
|
|
57
|
-
`@ttsc/banner`
|
|
59
|
+
`@ttsc/banner` discovers its config by walking upward from the tsconfig directory, looking for `banner.config.{ts,cts,mts,js,cjs,mjs,json}`.
|
|
60
|
+
|
|
61
|
+
To point at a specific file instead of using auto-discovery, set `configFile` on the tsconfig entry:
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
```jsonc
|
|
64
|
+
// tsconfig.json
|
|
65
|
+
{
|
|
66
|
+
"compilerOptions": {
|
|
67
|
+
"plugins": [
|
|
68
|
+
{ "transform": "@ttsc/banner", "configFile": "./config/banner.config.ts" }
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
62
73
|
|
|
63
74
|
The plugin formats every line of the resolved text inside a JSDoc block and appends `@packageDocumentation`.
|
|
64
75
|
|
package/driver/banner.go
CHANGED
|
@@ -1,215 +1,297 @@
|
|
|
1
1
|
package banner
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"os"
|
|
8
|
+
"os/exec"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"strings"
|
|
11
|
+
|
|
12
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
12
13
|
)
|
|
13
14
|
|
|
14
15
|
func init() {
|
|
15
|
-
|
|
16
|
+
driver.RegisterPlugin(plugin{})
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
// plugin implements driver.SourcePreamblePlugin for @ttsc/banner.
|
|
18
20
|
type plugin struct{}
|
|
19
21
|
|
|
20
22
|
var (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
// linkConfigNodeModules is overridable in tests to avoid real symlink creation.
|
|
24
|
+
linkConfigNodeModules = linkNearestNodeModules
|
|
25
|
+
// writeConfigLoaderFile is overridable in tests to avoid real file I/O.
|
|
26
|
+
writeConfigLoaderFile = os.WriteFile
|
|
23
27
|
)
|
|
24
28
|
|
|
29
|
+
// frameworkKeys lists the tsconfig plugin-entry keys that the ttsc host
|
|
30
|
+
// framework owns. They are accepted without error; all other keys are rejected.
|
|
31
|
+
var frameworkKeys = map[string]struct{}{
|
|
32
|
+
"enabled": {},
|
|
33
|
+
"name": {},
|
|
34
|
+
"stage": {},
|
|
35
|
+
"transform": {},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// validateBannerConfig rejects any tsconfig plugin entry key that is not a
|
|
39
|
+
// known framework key and is not the single banner-specific "configFile" key.
|
|
40
|
+
func validateBannerConfig(config map[string]any) error {
|
|
41
|
+
for key := range config {
|
|
42
|
+
if _, ok := frameworkKeys[key]; ok {
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
if key == "configFile" {
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
return fmt.Errorf(
|
|
49
|
+
"@ttsc/banner: tsconfig plugin entry contains unsupported key %q. "+
|
|
50
|
+
"Banner options must be placed in a banner.config.{ts,cts,mts,js,cjs,mjs,json} file. "+
|
|
51
|
+
"The only accepted key in the tsconfig entry is \"configFile\" (optional path to the config file).",
|
|
52
|
+
key,
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
return nil
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// SourcePreamble resolves the banner text from the plugin config and returns it
|
|
59
|
+
// formatted as a JSDoc block comment suitable for prepending to each emitted file.
|
|
25
60
|
func (plugin) SourcePreamble(ctx driver.PluginContext) (string, error) {
|
|
26
|
-
|
|
61
|
+
return parseBanner(ctx.Entry.Config, ctx.Cwd, ctx.Tsconfig)
|
|
27
62
|
}
|
|
28
63
|
|
|
64
|
+
// parseBanner resolves and formats banner text into a JSDoc block comment.
|
|
65
|
+
// Trailing blank lines are stripped from the resolved text before formatting.
|
|
29
66
|
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
|
-
|
|
67
|
+
text, err := resolveBannerText(config, cwd, tsconfigPath)
|
|
68
|
+
if err != nil {
|
|
69
|
+
return "", err
|
|
70
|
+
}
|
|
71
|
+
lines := strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n")
|
|
72
|
+
for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" {
|
|
73
|
+
lines = lines[:len(lines)-1]
|
|
74
|
+
}
|
|
75
|
+
var b strings.Builder
|
|
76
|
+
sep := strings.Repeat("-", 64)
|
|
77
|
+
b.WriteString("/**\n")
|
|
78
|
+
b.WriteString(" * ")
|
|
79
|
+
b.WriteString(sep)
|
|
80
|
+
b.WriteByte('\n')
|
|
81
|
+
for _, line := range lines {
|
|
82
|
+
b.WriteString(" * ")
|
|
83
|
+
b.WriteString(sanitizeJSDocLine(line))
|
|
84
|
+
b.WriteByte('\n')
|
|
85
|
+
}
|
|
86
|
+
b.WriteString(" *\n")
|
|
87
|
+
b.WriteString(" * @packageDocumentation\n ")
|
|
88
|
+
b.WriteString("*/\n")
|
|
89
|
+
return b.String(), nil
|
|
53
90
|
}
|
|
54
91
|
|
|
92
|
+
// resolveBannerText extracts the banner text from the plugin config.
|
|
93
|
+
// The config entry is validated first: only the "configFile" key (plus
|
|
94
|
+
// framework keys) is accepted. When "configFile" is present its value is
|
|
95
|
+
// resolved to an absolute path and loaded. When absent the upward-walk
|
|
96
|
+
// discovery is used. Returns an error when the config is invalid or when
|
|
97
|
+
// no banner text can be found.
|
|
55
98
|
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
|
-
|
|
99
|
+
if err := validateBannerConfig(config); err != nil {
|
|
100
|
+
return "", err
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if rawConfigFile, ok := config["configFile"]; ok {
|
|
104
|
+
configFile, ok := rawConfigFile.(string)
|
|
105
|
+
if !ok || strings.TrimSpace(configFile) == "" {
|
|
106
|
+
return "", fmt.Errorf("@ttsc/banner: \"configFile\" must be a non-empty string path")
|
|
107
|
+
}
|
|
108
|
+
location := resolveBannerConfigPath(configFile, cwd, tsconfigPath)
|
|
109
|
+
raw, err := loadBannerConfigFile(location)
|
|
110
|
+
if err != nil {
|
|
111
|
+
return "", err
|
|
112
|
+
}
|
|
113
|
+
text, ok, err := bannerTextFromConfigValue(raw, filepath.Base(location))
|
|
114
|
+
if err != nil {
|
|
115
|
+
return "", err
|
|
116
|
+
}
|
|
117
|
+
if !ok {
|
|
118
|
+
return "", fmt.Errorf("@ttsc/banner: %s must export an object with a non-empty \"text\" string", location)
|
|
119
|
+
}
|
|
120
|
+
return text, nil
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
location, err := findBannerConfigFile(cwd, tsconfigPath)
|
|
124
|
+
if err != nil {
|
|
125
|
+
return "", err
|
|
126
|
+
}
|
|
127
|
+
if location == "" {
|
|
128
|
+
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")
|
|
129
|
+
}
|
|
130
|
+
raw, err := loadBannerConfigFile(location)
|
|
131
|
+
if err != nil {
|
|
132
|
+
return "", err
|
|
133
|
+
}
|
|
134
|
+
text, ok, err := bannerTextFromConfigValue(raw, filepath.Base(location))
|
|
135
|
+
if err != nil {
|
|
136
|
+
return "", err
|
|
137
|
+
}
|
|
138
|
+
if !ok {
|
|
139
|
+
return "", fmt.Errorf("@ttsc/banner: %s must export an object with a non-empty \"text\" string", location)
|
|
140
|
+
}
|
|
141
|
+
return text, nil
|
|
97
142
|
}
|
|
98
143
|
|
|
144
|
+
// bannerTextFromConfigValue extracts a banner text string from a config value.
|
|
145
|
+
// A banner config value must be an object with a non-empty "text" string; raw
|
|
146
|
+
// may also be nil (not present). Returns (text, true, nil) on success,
|
|
147
|
+
// ("", false, nil) when absent, or ("", true, err) on a type mismatch. label is
|
|
148
|
+
// used in error messages and is the config file's base name (e.g.
|
|
149
|
+
// "banner.config.json").
|
|
99
150
|
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
|
-
return "", false, nil
|
|
117
|
-
}
|
|
118
|
-
text, ok = rawText.(string)
|
|
119
|
-
if !ok || strings.TrimSpace(text) == "" {
|
|
120
|
-
return "", true, fmt.Errorf("@ttsc/banner: %s.text must be a non-empty string", label)
|
|
121
|
-
}
|
|
122
|
-
return text, true, nil
|
|
151
|
+
if raw == nil {
|
|
152
|
+
return "", false, nil
|
|
153
|
+
}
|
|
154
|
+
object, ok := raw.(map[string]any)
|
|
155
|
+
if !ok {
|
|
156
|
+
return "", true, fmt.Errorf("@ttsc/banner: %s must be an object with a non-empty \"text\" string", label)
|
|
157
|
+
}
|
|
158
|
+
rawText, ok := object["text"]
|
|
159
|
+
if !ok {
|
|
160
|
+
return "", false, nil
|
|
161
|
+
}
|
|
162
|
+
text, ok := rawText.(string)
|
|
163
|
+
if !ok || strings.TrimSpace(text) == "" {
|
|
164
|
+
return "", true, fmt.Errorf("@ttsc/banner: %s.text must be a non-empty string", label)
|
|
165
|
+
}
|
|
166
|
+
return text, true, nil
|
|
123
167
|
}
|
|
124
168
|
|
|
169
|
+
// findBannerConfigFile walks up from the tsconfig (or cwd) directory looking for
|
|
170
|
+
// a banner.config.{ts,cts,mts,js,cjs,mjs,json} file. Returns the path when exactly
|
|
171
|
+
// one match is found per directory, "" when none exists at any level, or an
|
|
172
|
+
// error when multiple candidates exist in the same directory.
|
|
125
173
|
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
|
-
|
|
174
|
+
dir := tsconfigBaseDir(cwd, tsconfigPath)
|
|
175
|
+
for {
|
|
176
|
+
matches := make([]string, 0, 1)
|
|
177
|
+
for _, name := range []string{
|
|
178
|
+
"banner.config.json",
|
|
179
|
+
"banner.config.js",
|
|
180
|
+
"banner.config.cjs",
|
|
181
|
+
"banner.config.mjs",
|
|
182
|
+
"banner.config.ts",
|
|
183
|
+
"banner.config.cts",
|
|
184
|
+
"banner.config.mts",
|
|
185
|
+
} {
|
|
186
|
+
candidate := filepath.Join(dir, name)
|
|
187
|
+
if stat, err := os.Stat(candidate); err == nil && !stat.IsDir() {
|
|
188
|
+
matches = append(matches, candidate)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if len(matches) > 1 {
|
|
192
|
+
names := make([]string, len(matches))
|
|
193
|
+
for i, match := range matches {
|
|
194
|
+
names[i] = filepath.Base(match)
|
|
195
|
+
}
|
|
196
|
+
return "", fmt.Errorf(
|
|
197
|
+
"@ttsc/banner: multiple banner config files found in %s (%s); set \"configFile\" explicitly in the tsconfig plugin entry",
|
|
198
|
+
dir, strings.Join(names, ", "),
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
if len(matches) == 1 {
|
|
202
|
+
return matches[0], nil
|
|
203
|
+
}
|
|
204
|
+
parent := filepath.Dir(dir)
|
|
205
|
+
if parent == dir {
|
|
206
|
+
return "", nil
|
|
207
|
+
}
|
|
208
|
+
dir = parent
|
|
209
|
+
}
|
|
154
210
|
}
|
|
155
211
|
|
|
212
|
+
// resolveBannerConfigPath resolves a config path from the plugin entry.
|
|
213
|
+
// Absolute paths are returned as-is; relative paths are resolved against the
|
|
214
|
+
// tsconfig directory (or cwd when no tsconfig is set).
|
|
156
215
|
func resolveBannerConfigPath(configPath, cwd, tsconfigPath string) string {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
216
|
+
if filepath.IsAbs(configPath) {
|
|
217
|
+
return configPath
|
|
218
|
+
}
|
|
219
|
+
return filepath.Join(tsconfigBaseDir(cwd, tsconfigPath), configPath)
|
|
161
220
|
}
|
|
162
221
|
|
|
222
|
+
// tsconfigBaseDir returns the directory that contains the tsconfig file, or cwd
|
|
223
|
+
// when tsconfigPath is empty. It is the base both for resolving an explicit
|
|
224
|
+
// `configFile` path and for starting the upward banner-config-file search.
|
|
163
225
|
func tsconfigBaseDir(cwd, tsconfigPath string) string {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
func discoveryConfigBaseDir(cwd, tsconfigPath string) string {
|
|
175
|
-
if tsconfigPath != "" {
|
|
176
|
-
resolvedTsconfig := tsconfigPath
|
|
177
|
-
if !filepath.IsAbs(resolvedTsconfig) {
|
|
178
|
-
resolvedTsconfig = filepath.Join(cwd, resolvedTsconfig)
|
|
179
|
-
}
|
|
180
|
-
return filepath.Dir(resolvedTsconfig)
|
|
181
|
-
}
|
|
182
|
-
return cwd
|
|
226
|
+
if tsconfigPath == "" {
|
|
227
|
+
return cwd
|
|
228
|
+
}
|
|
229
|
+
resolvedTsconfig := tsconfigPath
|
|
230
|
+
if !filepath.IsAbs(resolvedTsconfig) {
|
|
231
|
+
resolvedTsconfig = filepath.Join(cwd, resolvedTsconfig)
|
|
232
|
+
}
|
|
233
|
+
return filepath.Dir(resolvedTsconfig)
|
|
183
234
|
}
|
|
184
235
|
|
|
236
|
+
// loadBannerConfigFile loads and evaluates a banner config file, returning its
|
|
237
|
+
// exported value as a Go any. A valid banner config exports an object with a
|
|
238
|
+
// "text" string; the value is validated by bannerTextFromConfigValue. The file
|
|
239
|
+
// must be named banner.config.{ts,cts,mts,js,cjs,mjs,json}; JS/CJS/MJS variants
|
|
240
|
+
// run under Node, TypeScript variants compile and run via ttsx in a temp
|
|
241
|
+
// directory, and JSON files are parsed natively.
|
|
185
242
|
func loadBannerConfigFile(location string) (any, error) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
243
|
+
if !isBannerConfigFileName(filepath.Base(location)) {
|
|
244
|
+
return nil, fmt.Errorf("@ttsc/banner: config file must be named banner.config.{ts,cts,mts,js,cjs,mjs,json}: %s", location)
|
|
245
|
+
}
|
|
246
|
+
ext := strings.ToLower(filepath.Ext(location))
|
|
247
|
+
switch ext {
|
|
248
|
+
case ".json":
|
|
249
|
+
return loadBannerJSONConfigFile(location)
|
|
250
|
+
case ".js", ".cjs", ".mjs":
|
|
251
|
+
return loadBannerScriptConfigFile(location)
|
|
252
|
+
}
|
|
253
|
+
return loadBannerTypeScriptConfigFile(location)
|
|
195
254
|
}
|
|
196
255
|
|
|
256
|
+
// isBannerConfigFileName reports whether name is an allowed banner config file name.
|
|
197
257
|
func isBannerConfigFileName(name string) bool {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
258
|
+
switch name {
|
|
259
|
+
case "banner.config.json",
|
|
260
|
+
"banner.config.js",
|
|
261
|
+
"banner.config.cjs",
|
|
262
|
+
"banner.config.mjs",
|
|
263
|
+
"banner.config.ts",
|
|
264
|
+
"banner.config.cts",
|
|
265
|
+
"banner.config.mts":
|
|
266
|
+
return true
|
|
267
|
+
default:
|
|
268
|
+
return false
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// loadBannerJSONConfigFile reads and JSON-parses a banner config file. A leading
|
|
273
|
+
// UTF-8 BOM is stripped before parsing so files saved by Windows editors are
|
|
274
|
+
// accepted. The parsed value must be an object with a non-empty "text" string.
|
|
275
|
+
func loadBannerJSONConfigFile(location string) (any, error) {
|
|
276
|
+
body, err := os.ReadFile(location)
|
|
277
|
+
if err != nil {
|
|
278
|
+
return nil, fmt.Errorf("@ttsc/banner: read config file %s: %w", location, err)
|
|
279
|
+
}
|
|
280
|
+
// Strip a leading UTF-8 BOM so files saved by Windows editors round
|
|
281
|
+
// trip through json.Unmarshal without an opaque "invalid character" failure.
|
|
282
|
+
body = bytes.TrimPrefix(body, []byte{0xEF, 0xBB, 0xBF})
|
|
283
|
+
var out any
|
|
284
|
+
if err := json.Unmarshal(body, &out); err != nil {
|
|
285
|
+
return nil, fmt.Errorf("@ttsc/banner: parse config file %s: %w", location, err)
|
|
286
|
+
}
|
|
287
|
+
return out, nil
|
|
209
288
|
}
|
|
210
289
|
|
|
290
|
+
// loadBannerScriptConfigFile evaluates a JS/CJS/MJS banner config file by
|
|
291
|
+
// running a small Node.js loader script that dynamic-imports the file and
|
|
292
|
+
// serializes its exported value to stdout as JSON.
|
|
211
293
|
func loadBannerScriptConfigFile(location string) (any, error) {
|
|
212
|
-
|
|
294
|
+
const script = `
|
|
213
295
|
const { pathToFileURL } = require("node:url");
|
|
214
296
|
|
|
215
297
|
(async () => {
|
|
@@ -223,108 +305,116 @@ const { pathToFileURL } = require("node:url");
|
|
|
223
305
|
});
|
|
224
306
|
|
|
225
307
|
function toSerializableBanner(value) {
|
|
226
|
-
if (typeof value === "string") {
|
|
227
|
-
return value;
|
|
228
|
-
}
|
|
229
308
|
if (value !== null && typeof value === "object" && typeof value.text === "string") {
|
|
230
309
|
return { text: value.text };
|
|
231
310
|
}
|
|
232
|
-
throw new Error("config file must export
|
|
311
|
+
throw new Error("config file must export an object with a non-empty \"text\" string");
|
|
233
312
|
}
|
|
234
313
|
`
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
314
|
+
node := os.Getenv("TTSC_NODE_BINARY")
|
|
315
|
+
if node == "" {
|
|
316
|
+
node = "node"
|
|
317
|
+
}
|
|
318
|
+
cmd := exec.Command(node, "-e", script, location)
|
|
319
|
+
cmd.Env = nodeConfigLoaderEnv(location)
|
|
320
|
+
output, err := cmd.Output()
|
|
321
|
+
if err != nil {
|
|
322
|
+
stderr := ""
|
|
323
|
+
if exit, ok := err.(*exec.ExitError); ok {
|
|
324
|
+
stderr = strings.TrimSpace(string(exit.Stderr))
|
|
325
|
+
}
|
|
326
|
+
if stderr != "" {
|
|
327
|
+
return nil, fmt.Errorf("@ttsc/banner: load config file %s: %s", location, stderr)
|
|
328
|
+
}
|
|
329
|
+
return nil, fmt.Errorf("@ttsc/banner: load config file %s: %w", location, err)
|
|
330
|
+
}
|
|
331
|
+
var out any
|
|
332
|
+
if err := json.Unmarshal(output, &out); err != nil {
|
|
333
|
+
return nil, fmt.Errorf("@ttsc/banner: parse config file %s output: %w", location, err)
|
|
334
|
+
}
|
|
335
|
+
return out, nil
|
|
257
336
|
}
|
|
258
337
|
|
|
338
|
+
// loadBannerTypeScriptConfigFile compiles and runs a TypeScript banner config
|
|
339
|
+
// file using ttsx in a temp directory. A symlink to the nearest node_modules
|
|
340
|
+
// is created so the config file can import its own dependencies. The ttsx
|
|
341
|
+
// build runs with `--no-plugins` so evaluating the config never triggers the
|
|
342
|
+
// host project's transform/check plugins against the loader tsconfig.
|
|
259
343
|
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
|
-
|
|
344
|
+
tempDir, err := os.MkdirTemp("", "ttsc-banner-config-")
|
|
345
|
+
if err != nil {
|
|
346
|
+
return nil, fmt.Errorf("@ttsc/banner: create config loader tempdir: %w", err)
|
|
347
|
+
}
|
|
348
|
+
defer os.RemoveAll(tempDir)
|
|
349
|
+
|
|
350
|
+
if err := linkConfigNodeModules(tempDir, filepath.Dir(location)); err != nil {
|
|
351
|
+
return nil, err
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
loader := filepath.Join(tempDir, "loader.mts")
|
|
355
|
+
tsconfig := filepath.Join(tempDir, "tsconfig.json")
|
|
356
|
+
importSpecifier, err := relativeImportSpecifier(tempDir, location)
|
|
357
|
+
if err != nil {
|
|
358
|
+
return nil, err
|
|
359
|
+
}
|
|
360
|
+
importLiteral, _ := json.Marshal(importSpecifier)
|
|
361
|
+
if err := writeConfigLoaderFile(loader, []byte(bannerTypeScriptConfigLoaderSource(string(importLiteral))), 0o644); err != nil {
|
|
362
|
+
return nil, fmt.Errorf("@ttsc/banner: write config loader: %w", err)
|
|
363
|
+
}
|
|
364
|
+
if err := writeConfigLoaderFile(tsconfig, []byte(typeScriptConfigLoaderTsconfig(loader, location, tempDir)), 0o644); err != nil {
|
|
365
|
+
return nil, fmt.Errorf("@ttsc/banner: write config loader tsconfig: %w", err)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
args := []string{
|
|
369
|
+
"--project", tsconfig,
|
|
370
|
+
"--cwd", tempDir,
|
|
371
|
+
"--cache-dir", filepath.Join(tempDir, "cache"),
|
|
372
|
+
"--no-plugins",
|
|
373
|
+
}
|
|
374
|
+
if tsgo := os.Getenv("TTSC_TSGO_BINARY"); tsgo != "" {
|
|
375
|
+
args = append(args, "--binary", tsgo)
|
|
376
|
+
}
|
|
377
|
+
args = append(args, loader)
|
|
378
|
+
|
|
379
|
+
cmd := ttsxCommand(args...)
|
|
380
|
+
cmd.Env = nodeConfigLoaderEnv(location)
|
|
381
|
+
output, err := cmd.Output()
|
|
382
|
+
if err != nil {
|
|
383
|
+
stderr := ""
|
|
384
|
+
if exit, ok := err.(*exec.ExitError); ok {
|
|
385
|
+
stderr = strings.TrimSpace(string(exit.Stderr))
|
|
386
|
+
}
|
|
387
|
+
if stderr != "" {
|
|
388
|
+
return nil, fmt.Errorf("@ttsc/banner: load TypeScript config file %s: %s", location, stderr)
|
|
389
|
+
}
|
|
390
|
+
return nil, fmt.Errorf("@ttsc/banner: load TypeScript config file %s: %w", location, err)
|
|
391
|
+
}
|
|
392
|
+
var out any
|
|
393
|
+
if err := json.Unmarshal(output, &out); err != nil {
|
|
394
|
+
return nil, fmt.Errorf("@ttsc/banner: parse TypeScript config file %s output: %w", location, err)
|
|
395
|
+
}
|
|
396
|
+
return out, nil
|
|
312
397
|
}
|
|
313
398
|
|
|
399
|
+
// relativeImportSpecifier returns a "./" or "../"-prefixed slash-separated
|
|
400
|
+
// import specifier for location relative to fromDir.
|
|
314
401
|
func relativeImportSpecifier(fromDir, location string) (string, error) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
402
|
+
relative, err := filepath.Rel(fromDir, location)
|
|
403
|
+
if err != nil {
|
|
404
|
+
return "", fmt.Errorf("@ttsc/banner: resolve relative config import %s: %w", location, err)
|
|
405
|
+
}
|
|
406
|
+
relative = filepath.ToSlash(relative)
|
|
407
|
+
if strings.HasPrefix(relative, "../") || strings.HasPrefix(relative, "./") {
|
|
408
|
+
return relative, nil
|
|
409
|
+
}
|
|
410
|
+
return "./" + relative, nil
|
|
324
411
|
}
|
|
325
412
|
|
|
413
|
+
// bannerTypeScriptConfigLoaderSource returns the source of a TypeScript loader
|
|
414
|
+
// module that imports the banner config file specified by importLiteral (a
|
|
415
|
+
// JSON-encoded import specifier) and writes the serialized banner value to stdout.
|
|
326
416
|
func bannerTypeScriptConfigLoaderSource(importLiteral string) string {
|
|
327
|
-
|
|
417
|
+
return fmt.Sprintf(`import * as importedConfig from %s;
|
|
328
418
|
|
|
329
419
|
declare const process: {
|
|
330
420
|
stdout: { write(value: string): void };
|
|
@@ -364,116 +454,131 @@ function hasOwn(value: Record<string, unknown>, key: string): boolean {
|
|
|
364
454
|
}
|
|
365
455
|
|
|
366
456
|
function toSerializableBanner(value: unknown): unknown {
|
|
367
|
-
if (typeof value === "string") {
|
|
368
|
-
return value;
|
|
369
|
-
}
|
|
370
457
|
if (isObject(value) && typeof value.text === "string") {
|
|
371
458
|
return { text: value.text };
|
|
372
459
|
}
|
|
373
|
-
throw new Error("config file must export
|
|
460
|
+
throw new Error("config file must export an object with a non-empty \"text\" string");
|
|
374
461
|
}
|
|
375
462
|
`, importLiteral)
|
|
376
463
|
}
|
|
377
464
|
|
|
465
|
+
// typeScriptConfigLoaderTsconfig returns the JSON content of a tsconfig that
|
|
466
|
+
// compiles loader and location together so ttsx can execute the loader.
|
|
378
467
|
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
|
-
|
|
468
|
+
content := map[string]any{
|
|
469
|
+
"compilerOptions": map[string]any{
|
|
470
|
+
"allowImportingTsExtensions": true,
|
|
471
|
+
"module": "ESNext",
|
|
472
|
+
"moduleResolution": "bundler",
|
|
473
|
+
"outDir": filepath.ToSlash(filepath.Join(outDir, "out")),
|
|
474
|
+
"rewriteRelativeImportExtensions": true,
|
|
475
|
+
"rootDir": "/",
|
|
476
|
+
"skipLibCheck": true,
|
|
477
|
+
"strict": true,
|
|
478
|
+
"target": "ES2022",
|
|
479
|
+
},
|
|
480
|
+
"files": []string{
|
|
481
|
+
filepath.ToSlash(loader),
|
|
482
|
+
filepath.ToSlash(location),
|
|
483
|
+
},
|
|
484
|
+
}
|
|
485
|
+
body, _ := json.MarshalIndent(content, "", " ")
|
|
486
|
+
return string(body)
|
|
398
487
|
}
|
|
399
488
|
|
|
489
|
+
// ttsxCommand builds an exec.Cmd that runs ttsx with the given args.
|
|
490
|
+
// When TTSC_TTSX_BINARY has a script extension (.js, .ts, …) the binary is
|
|
491
|
+
// invoked via the Node runtime so it is executed correctly on all platforms.
|
|
400
492
|
func ttsxCommand(args ...string) *exec.Cmd {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
493
|
+
ttsx := os.Getenv("TTSC_TTSX_BINARY")
|
|
494
|
+
if ttsx == "" {
|
|
495
|
+
ttsx = "ttsx"
|
|
496
|
+
}
|
|
497
|
+
if shouldRunTtsxThroughNode(ttsx) {
|
|
498
|
+
node := os.Getenv("TTSC_NODE_BINARY")
|
|
499
|
+
if node == "" {
|
|
500
|
+
node = "node"
|
|
501
|
+
}
|
|
502
|
+
return exec.Command(node, append([]string{ttsx}, args...)...)
|
|
503
|
+
}
|
|
504
|
+
return exec.Command(ttsx, args...)
|
|
413
505
|
}
|
|
414
506
|
|
|
507
|
+
// shouldRunTtsxThroughNode reports whether binary has a script file extension
|
|
508
|
+
// and therefore must be launched via node rather than executed directly.
|
|
415
509
|
func shouldRunTtsxThroughNode(binary string) bool {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
510
|
+
switch strings.ToLower(filepath.Ext(binary)) {
|
|
511
|
+
case ".js", ".cjs", ".mjs", ".ts", ".cts", ".mts":
|
|
512
|
+
return true
|
|
513
|
+
default:
|
|
514
|
+
return false
|
|
515
|
+
}
|
|
422
516
|
}
|
|
423
517
|
|
|
518
|
+
// nodeConfigLoaderEnv builds an environment slice for the config-loader Node
|
|
519
|
+
// process. It prepends the nearest node_modules directory to NODE_PATH so
|
|
520
|
+
// the config file can resolve its own package dependencies.
|
|
424
521
|
func nodeConfigLoaderEnv(location string) []string {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
522
|
+
env := os.Environ()
|
|
523
|
+
parts := make([]string, 0, 2)
|
|
524
|
+
if nodeModules := findNearestNodeModules(filepath.Dir(location)); nodeModules != "" {
|
|
525
|
+
parts = append(parts, nodeModules)
|
|
526
|
+
}
|
|
527
|
+
if existing := os.Getenv("NODE_PATH"); existing != "" {
|
|
528
|
+
parts = append(parts, existing)
|
|
529
|
+
}
|
|
530
|
+
if len(parts) == 0 {
|
|
531
|
+
return env
|
|
532
|
+
}
|
|
533
|
+
return setEnv(env, "NODE_PATH", strings.Join(parts, string(os.PathListSeparator)))
|
|
437
534
|
}
|
|
438
535
|
|
|
536
|
+
// linkNearestNodeModules creates a node_modules symlink inside tempDir pointing
|
|
537
|
+
// to the nearest node_modules ancestor of sourceDir. Does nothing when none is found.
|
|
439
538
|
func linkNearestNodeModules(tempDir, sourceDir string) error {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
539
|
+
nodeModules := findNearestNodeModules(sourceDir)
|
|
540
|
+
if nodeModules == "" {
|
|
541
|
+
return nil
|
|
542
|
+
}
|
|
543
|
+
link := filepath.Join(tempDir, "node_modules")
|
|
544
|
+
if err := os.Symlink(nodeModules, link); err != nil {
|
|
545
|
+
return fmt.Errorf("@ttsc/banner: link config node_modules %s: %w", nodeModules, err)
|
|
546
|
+
}
|
|
547
|
+
return nil
|
|
449
548
|
}
|
|
450
549
|
|
|
550
|
+
// findNearestNodeModules walks up from start looking for a node_modules directory.
|
|
551
|
+
// Returns the absolute path of the first match, or "" when none is found.
|
|
451
552
|
func findNearestNodeModules(start string) string {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
553
|
+
dir := filepath.Clean(start)
|
|
554
|
+
for {
|
|
555
|
+
candidate := filepath.Join(dir, "node_modules")
|
|
556
|
+
if stat, err := os.Stat(candidate); err == nil && stat.IsDir() {
|
|
557
|
+
return candidate
|
|
558
|
+
}
|
|
559
|
+
parent := filepath.Dir(dir)
|
|
560
|
+
if parent == dir {
|
|
561
|
+
return ""
|
|
562
|
+
}
|
|
563
|
+
dir = parent
|
|
564
|
+
}
|
|
464
565
|
}
|
|
465
566
|
|
|
567
|
+
// setEnv returns a copy of env with key=value. If key already exists in env,
|
|
568
|
+
// its value is updated in-place; otherwise the entry is appended.
|
|
466
569
|
func setEnv(env []string, key, value string) []string {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
570
|
+
prefix := key + "="
|
|
571
|
+
for i, entry := range env {
|
|
572
|
+
if strings.HasPrefix(entry, prefix) {
|
|
573
|
+
env[i] = prefix + value
|
|
574
|
+
return env
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return append(env, prefix+value)
|
|
475
578
|
}
|
|
476
579
|
|
|
580
|
+
// sanitizeJSDocLine escapes any JSDoc-closing sequence in a banner text line
|
|
581
|
+
// by replacing "*/" with "* /" so the generated block comment stays valid.
|
|
477
582
|
func sanitizeJSDocLine(line string) string {
|
|
478
|
-
|
|
583
|
+
return strings.ReplaceAll(line, "*/", "* /")
|
|
479
584
|
}
|
package/lib/index.js
CHANGED
|
@@ -21,11 +21,40 @@ exports.default = createTtscBanner;
|
|
|
21
21
|
const node_path_1 = __importDefault(require("node:path"));
|
|
22
22
|
__exportStar(require("./structures/index"), exports);
|
|
23
23
|
/**
|
|
24
|
+
* Keys that the ttsc plugin host injects into every plugin entry and are not
|
|
25
|
+
* owned by `@ttsc/banner`. These pass through the factory without validation so
|
|
26
|
+
* the host can freely add new framework keys in the future.
|
|
27
|
+
*/
|
|
28
|
+
const FRAMEWORK_KEYS = new Set([
|
|
29
|
+
"enabled",
|
|
30
|
+
"name",
|
|
31
|
+
"stage",
|
|
32
|
+
"transform",
|
|
33
|
+
]);
|
|
34
|
+
/**
|
|
35
|
+
* Plugin factory for `@ttsc/banner` — called by the ttsc host to obtain the
|
|
36
|
+
* plugin descriptor.
|
|
37
|
+
*
|
|
38
|
+
* The only banner-specific key accepted in the tsconfig plugin entry is
|
|
39
|
+
* `configFile`. Any other key that is not a known framework key is rejected
|
|
40
|
+
* with a specific error so users discover the correct configuration surface
|
|
41
|
+
* (the dedicated config file) rather than silently receiving no banner.
|
|
42
|
+
*
|
|
24
43
|
* @internal
|
|
25
44
|
*/
|
|
26
|
-
function createTtscBanner(
|
|
45
|
+
function createTtscBanner(context) {
|
|
46
|
+
const entry = context.plugin;
|
|
47
|
+
for (const key of Object.keys(entry)) {
|
|
48
|
+
if (!FRAMEWORK_KEYS.has(key) && key !== "configFile") {
|
|
49
|
+
throw new Error(`@ttsc/banner: tsconfig plugin entry contains unsupported key ${JSON.stringify(key)}. ` +
|
|
50
|
+
`Banner options must be placed in a banner.config.{ts,cts,mts,js,cjs,mjs,json} file. ` +
|
|
51
|
+
`The only accepted key in the tsconfig entry is "configFile" (optional path to the config file).`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
27
54
|
return {
|
|
28
55
|
name: "@ttsc/banner",
|
|
56
|
+
// Point at the `driver/` directory one level above `lib/` in the
|
|
57
|
+
// installed package tree (where the Go sources live).
|
|
29
58
|
source: node_path_1.default.resolve(__dirname, "..", "driver"),
|
|
30
59
|
stage: "transform",
|
|
31
60
|
};
|
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;;;;GAIG;AACH,MAAM,cAAc,GAAG,IAAI,GAAG,CAAS;IACrC,SAAS;IACT,MAAM;IACN,OAAO;IACP,WAAW;CACZ,CAAC,CAAC;AAEH;;;;;;;;;;GAUG;AACH,0BACE,OAA0D;IAE1D,MAAM,KAAK,GAAG,OAAO,CAAC,MAAiC,CAAC;IACxD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACrC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,KAAK,YAAY,EAAE,CAAC;YACrD,MAAM,IAAI,KAAK,CACb,gEAAgE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI;gBACrF,sFAAsF;gBACtF,iGAAiG,CACpG,CAAC;QACJ,CAAC;IACH,CAAC;IAED,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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ITtscBannerConfig.js","sourceRoot":"","sources":["../../src/structures/ITtscBannerConfig.ts"],"names":[],"mappings":""}
|
|
@@ -4,10 +4,11 @@ export interface ITtscBannerPluginConfig {
|
|
|
4
4
|
enabled?: boolean;
|
|
5
5
|
/** Plugin module specifier. */
|
|
6
6
|
transform?: string;
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Path to a `banner.config.*` file, resolved from the directory that contains
|
|
9
|
+
* the tsconfig. When omitted, `@ttsc/banner` walks up from that directory and
|
|
10
|
+
* uses the first `banner.config.{ts,cts,mts,js,cjs,mjs,json}` file it finds.
|
|
11
|
+
* Any other key in this entry is rejected with an error.
|
|
12
|
+
*/
|
|
13
|
+
configFile?: string;
|
|
13
14
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * from "./ITtscBannerPluginConfig";
|
|
2
|
-
export * from "./
|
|
2
|
+
export * from "./ITtscBannerConfig";
|
package/lib/structures/index.js
CHANGED
|
@@ -15,5 +15,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./ITtscBannerPluginConfig"), exports);
|
|
18
|
-
__exportStar(require("./
|
|
18
|
+
__exportStar(require("./ITtscBannerConfig"), exports);
|
|
19
19
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/structures/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,4DAA0C;AAC1C,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/structures/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,4DAA0C;AAC1C,sDAAoC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttsc/banner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "First-party ttsc plugin that adds package-documentation JSDoc banners during emit.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -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.20260522.1",
|
|
37
37
|
"rimraf": "^6.1.2",
|
|
38
|
-
"ttsc": "0.
|
|
38
|
+
"ttsc": "0.13.0"
|
|
39
39
|
},
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|
package/src/index.ts
CHANGED
|
@@ -4,28 +4,89 @@ 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
|
+
* Keys that the ttsc plugin host injects into every plugin entry and are not
|
|
51
|
+
* owned by `@ttsc/banner`. These pass through the factory without validation so
|
|
52
|
+
* the host can freely add new framework keys in the future.
|
|
53
|
+
*/
|
|
54
|
+
const FRAMEWORK_KEYS = new Set<string>([
|
|
55
|
+
"enabled",
|
|
56
|
+
"name",
|
|
57
|
+
"stage",
|
|
58
|
+
"transform",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Plugin factory for `@ttsc/banner` — called by the ttsc host to obtain the
|
|
63
|
+
* plugin descriptor.
|
|
64
|
+
*
|
|
65
|
+
* The only banner-specific key accepted in the tsconfig plugin entry is
|
|
66
|
+
* `configFile`. Any other key that is not a known framework key is rejected
|
|
67
|
+
* with a specific error so users discover the correct configuration surface
|
|
68
|
+
* (the dedicated config file) rather than silently receiving no banner.
|
|
69
|
+
*
|
|
22
70
|
* @internal
|
|
23
71
|
*/
|
|
24
72
|
export default function createTtscBanner(
|
|
25
|
-
|
|
73
|
+
context: TtscPluginFactoryContext<ITtscBannerPluginConfig>,
|
|
26
74
|
): TtscPluginDescriptor {
|
|
75
|
+
const entry = context.plugin as Record<string, unknown>;
|
|
76
|
+
for (const key of Object.keys(entry)) {
|
|
77
|
+
if (!FRAMEWORK_KEYS.has(key) && key !== "configFile") {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`@ttsc/banner: tsconfig plugin entry contains unsupported key ${JSON.stringify(key)}. ` +
|
|
80
|
+
`Banner options must be placed in a banner.config.{ts,cts,mts,js,cjs,mjs,json} file. ` +
|
|
81
|
+
`The only accepted key in the tsconfig entry is "configFile" (optional path to the config file).`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
27
86
|
return {
|
|
28
87
|
name: "@ttsc/banner",
|
|
88
|
+
// Point at the `driver/` directory one level above `lib/` in the
|
|
89
|
+
// installed package tree (where the Go sources live).
|
|
29
90
|
source: path.resolve(__dirname, "..", "driver"),
|
|
30
91
|
stage: "transform",
|
|
31
92
|
};
|
|
@@ -6,12 +6,11 @@ export interface ITtscBannerPluginConfig {
|
|
|
6
6
|
/** Plugin module specifier. */
|
|
7
7
|
transform?: string;
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
[key: string]: unknown;
|
|
9
|
+
/**
|
|
10
|
+
* Path to a `banner.config.*` file, resolved from the directory that contains
|
|
11
|
+
* the tsconfig. When omitted, `@ttsc/banner` walks up from that directory and
|
|
12
|
+
* uses the first `banner.config.{ts,cts,mts,js,cjs,mjs,json}` file it finds.
|
|
13
|
+
* Any other key in this entry is rejected with an error.
|
|
14
|
+
*/
|
|
15
|
+
configFile?: string;
|
|
17
16
|
}
|
package/src/structures/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * from "./ITtscBannerPluginConfig";
|
|
2
|
-
export * from "./
|
|
2
|
+
export * from "./ITtscBannerConfig";
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"TtscBannerConfig.js","sourceRoot":"","sources":["../../src/structures/TtscBannerConfig.ts"],"names":[],"mappings":""}
|