@ttsc/banner 0.11.0 → 0.12.1
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 +22 -35
- package/driver/banner.go +479 -0
- package/lib/index.d.ts +0 -14
- package/lib/index.js +4 -1
- package/lib/index.js.map +1 -1
- package/package.json +3 -2
- package/plugin/banner.go +1 -1
- package/src/index.ts +4 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@ttsc/banner`
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
|
|
5
5
|
[](https://github.com/samchon/ttsc/blob/master/LICENSE)
|
|
6
6
|
[](https://www.npmjs.com/package/@ttsc/banner)
|
|
@@ -20,7 +20,20 @@ npm install -D ttsc @typescript/native-preview
|
|
|
20
20
|
npm install -D @ttsc/banner
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Pass the banner text inline on the `compilerOptions.plugins[]` entry:
|
|
24
|
+
|
|
25
|
+
```jsonc
|
|
26
|
+
// tsconfig.json
|
|
27
|
+
{
|
|
28
|
+
"compilerOptions": {
|
|
29
|
+
"plugins": [
|
|
30
|
+
{ "transform": "@ttsc/banner", "text": "License MIT (c) 2026 Acme" }
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or keep the text in a separate file `banner.config.ts` next to your tsconfig:
|
|
24
37
|
|
|
25
38
|
```ts
|
|
26
39
|
// banner.config.ts
|
|
@@ -37,45 +50,19 @@ Run your normal `ttsc` command:
|
|
|
37
50
|
npx ttsc
|
|
38
51
|
```
|
|
39
52
|
|
|
40
|
-
If `@ttsc/banner` is installed and
|
|
53
|
+
If `@ttsc/banner` is installed and none of the three text sources (inline `text:`, `config:` path, or an auto-discovered `banner.config.*`) yields a banner, the compile fails.
|
|
41
54
|
|
|
42
55
|
## Configuration
|
|
43
56
|
|
|
44
|
-
|
|
57
|
+
`@ttsc/banner` resolves its text in this order:
|
|
45
58
|
|
|
46
|
-
|
|
59
|
+
1. the entry's inline `text` string;
|
|
60
|
+
2. the entry's `config: "./path/to/banner.config.ts"` path;
|
|
61
|
+
3. an upward walk for any `banner.config.{js,cjs,mjs,ts,mts,cts}` starting from the tsconfig directory.
|
|
47
62
|
|
|
48
|
-
|
|
49
|
-
{
|
|
50
|
-
"compilerOptions": {
|
|
51
|
-
"plugins": [
|
|
52
|
-
{
|
|
53
|
-
"transform": "@ttsc/banner",
|
|
54
|
-
"config": "./config/banner.config.ts",
|
|
55
|
-
},
|
|
56
|
-
],
|
|
57
|
-
},
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
The explicit `config` path resolves from the selected `tsconfig.json` directory.
|
|
62
|
-
|
|
63
|
-
Existing inline text config remains supported:
|
|
64
|
-
|
|
65
|
-
```jsonc
|
|
66
|
-
{
|
|
67
|
-
"compilerOptions": {
|
|
68
|
-
"plugins": [
|
|
69
|
-
{
|
|
70
|
-
"transform": "@ttsc/banner",
|
|
71
|
-
"text": "License MIT (c) 2026 Acme",
|
|
72
|
-
},
|
|
73
|
-
],
|
|
74
|
-
},
|
|
75
|
-
}
|
|
76
|
-
```
|
|
63
|
+
The plugin formats every line of the resolved text inside a JSDoc block and appends `@packageDocumentation`.
|
|
77
64
|
|
|
78
|
-
The
|
|
65
|
+
The banner follows TypeScript's normal comment emit policy, so `removeComments: true` removes it.
|
|
79
66
|
|
|
80
67
|
## Sponsors
|
|
81
68
|
|
package/driver/banner.go
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
package banner
|
|
2
|
+
|
|
3
|
+
import (
|
|
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
|
+
)
|
|
13
|
+
|
|
14
|
+
func init() {
|
|
15
|
+
driver.RegisterPlugin(plugin{})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type plugin struct{}
|
|
19
|
+
|
|
20
|
+
var (
|
|
21
|
+
linkConfigNodeModules = linkNearestNodeModules
|
|
22
|
+
writeConfigLoaderFile = os.WriteFile
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
func (plugin) SourcePreamble(ctx driver.PluginContext) (string, error) {
|
|
26
|
+
return parseBanner(ctx.Entry.Config, ctx.Cwd, ctx.Tsconfig)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func parseBanner(config map[string]any, cwd, tsconfigPath string) (string, error) {
|
|
30
|
+
text, err := resolveBannerText(config, cwd, tsconfigPath)
|
|
31
|
+
if err != nil {
|
|
32
|
+
return "", err
|
|
33
|
+
}
|
|
34
|
+
lines := strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n")
|
|
35
|
+
for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" {
|
|
36
|
+
lines = lines[:len(lines)-1]
|
|
37
|
+
}
|
|
38
|
+
var b strings.Builder
|
|
39
|
+
sep := strings.Repeat("-", 64)
|
|
40
|
+
b.WriteString("/**\n")
|
|
41
|
+
b.WriteString(" * ")
|
|
42
|
+
b.WriteString(sep)
|
|
43
|
+
b.WriteByte('\n')
|
|
44
|
+
for _, line := range lines {
|
|
45
|
+
b.WriteString(" * ")
|
|
46
|
+
b.WriteString(sanitizeJSDocLine(line))
|
|
47
|
+
b.WriteByte('\n')
|
|
48
|
+
}
|
|
49
|
+
b.WriteString(" *\n")
|
|
50
|
+
b.WriteString(" * @packageDocumentation\n ")
|
|
51
|
+
b.WriteString("*/\n")
|
|
52
|
+
return b.String(), nil
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func resolveBannerText(config map[string]any, cwd, tsconfigPath string) (string, error) {
|
|
56
|
+
if text, ok, err := bannerTextFromConfigValue(config["text"], `"text"`); ok || err != nil {
|
|
57
|
+
return text, err
|
|
58
|
+
}
|
|
59
|
+
if rawConfigPath, ok := config["config"]; ok {
|
|
60
|
+
configPath, ok := rawConfigPath.(string)
|
|
61
|
+
if !ok || strings.TrimSpace(configPath) == "" {
|
|
62
|
+
return "", fmt.Errorf("@ttsc/banner: \"config\" must be a non-empty string path")
|
|
63
|
+
}
|
|
64
|
+
location := resolveBannerConfigPath(configPath, cwd, tsconfigPath)
|
|
65
|
+
raw, err := loadBannerConfigFile(location)
|
|
66
|
+
if err != nil {
|
|
67
|
+
return "", err
|
|
68
|
+
}
|
|
69
|
+
text, ok, err := bannerTextFromConfigValue(raw, filepath.Base(location))
|
|
70
|
+
if err != nil {
|
|
71
|
+
return "", err
|
|
72
|
+
}
|
|
73
|
+
if !ok {
|
|
74
|
+
return "", fmt.Errorf("@ttsc/banner: %s must export a non-empty string or an object with a non-empty \"text\" string", location)
|
|
75
|
+
}
|
|
76
|
+
return text, nil
|
|
77
|
+
}
|
|
78
|
+
location, err := findBannerConfigFile(cwd, tsconfigPath)
|
|
79
|
+
if err != nil {
|
|
80
|
+
return "", err
|
|
81
|
+
}
|
|
82
|
+
if location == "" {
|
|
83
|
+
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")
|
|
84
|
+
}
|
|
85
|
+
raw, err := loadBannerConfigFile(location)
|
|
86
|
+
if err != nil {
|
|
87
|
+
return "", err
|
|
88
|
+
}
|
|
89
|
+
text, ok, err := bannerTextFromConfigValue(raw, filepath.Base(location))
|
|
90
|
+
if err != nil {
|
|
91
|
+
return "", err
|
|
92
|
+
}
|
|
93
|
+
if !ok {
|
|
94
|
+
return "", fmt.Errorf("@ttsc/banner: %s must export a non-empty string or an object with a non-empty \"text\" string", location)
|
|
95
|
+
}
|
|
96
|
+
return text, nil
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func bannerTextFromConfigValue(raw any, label string) (string, bool, error) {
|
|
100
|
+
if raw == nil {
|
|
101
|
+
return "", false, nil
|
|
102
|
+
}
|
|
103
|
+
text, ok := raw.(string)
|
|
104
|
+
if ok {
|
|
105
|
+
if strings.TrimSpace(text) == "" {
|
|
106
|
+
return "", true, fmt.Errorf("@ttsc/banner: %s must be a non-empty string", label)
|
|
107
|
+
}
|
|
108
|
+
return text, true, nil
|
|
109
|
+
}
|
|
110
|
+
object, ok := raw.(map[string]any)
|
|
111
|
+
if !ok {
|
|
112
|
+
return "", true, fmt.Errorf("@ttsc/banner: %s must be a string or an object with a non-empty \"text\" string", label)
|
|
113
|
+
}
|
|
114
|
+
rawText, ok := object["text"]
|
|
115
|
+
if !ok {
|
|
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
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func findBannerConfigFile(cwd, tsconfigPath string) (string, error) {
|
|
126
|
+
dir := discoveryConfigBaseDir(cwd, tsconfigPath)
|
|
127
|
+
for {
|
|
128
|
+
matches := make([]string, 0, 1)
|
|
129
|
+
for _, name := range []string{
|
|
130
|
+
"banner.config.js",
|
|
131
|
+
"banner.config.cjs",
|
|
132
|
+
"banner.config.mjs",
|
|
133
|
+
"banner.config.ts",
|
|
134
|
+
"banner.config.cts",
|
|
135
|
+
"banner.config.mts",
|
|
136
|
+
} {
|
|
137
|
+
candidate := filepath.Join(dir, name)
|
|
138
|
+
if stat, err := os.Stat(candidate); err == nil && !stat.IsDir() {
|
|
139
|
+
matches = append(matches, candidate)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if len(matches) > 1 {
|
|
143
|
+
return "", fmt.Errorf("@ttsc/banner: multiple banner.config.* files found in %s", dir)
|
|
144
|
+
}
|
|
145
|
+
if len(matches) == 1 {
|
|
146
|
+
return matches[0], nil
|
|
147
|
+
}
|
|
148
|
+
parent := filepath.Dir(dir)
|
|
149
|
+
if parent == dir {
|
|
150
|
+
return "", nil
|
|
151
|
+
}
|
|
152
|
+
dir = parent
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
func resolveBannerConfigPath(configPath, cwd, tsconfigPath string) string {
|
|
157
|
+
if filepath.IsAbs(configPath) {
|
|
158
|
+
return configPath
|
|
159
|
+
}
|
|
160
|
+
return filepath.Join(tsconfigBaseDir(cwd, tsconfigPath), configPath)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
func tsconfigBaseDir(cwd, tsconfigPath string) string {
|
|
164
|
+
if tsconfigPath == "" {
|
|
165
|
+
return cwd
|
|
166
|
+
}
|
|
167
|
+
resolvedTsconfig := tsconfigPath
|
|
168
|
+
if !filepath.IsAbs(resolvedTsconfig) {
|
|
169
|
+
resolvedTsconfig = filepath.Join(cwd, resolvedTsconfig)
|
|
170
|
+
}
|
|
171
|
+
return filepath.Dir(resolvedTsconfig)
|
|
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
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
func loadBannerConfigFile(location string) (any, error) {
|
|
186
|
+
if !isBannerConfigFileName(filepath.Base(location)) {
|
|
187
|
+
return nil, fmt.Errorf("@ttsc/banner: config file must be named banner.config.{js,cjs,mjs,ts,mts,cts}: %s", location)
|
|
188
|
+
}
|
|
189
|
+
ext := strings.ToLower(filepath.Ext(location))
|
|
190
|
+
switch ext {
|
|
191
|
+
case ".js", ".cjs", ".mjs":
|
|
192
|
+
return loadBannerScriptConfigFile(location)
|
|
193
|
+
}
|
|
194
|
+
return loadBannerTypeScriptConfigFile(location)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
func isBannerConfigFileName(name string) bool {
|
|
198
|
+
switch name {
|
|
199
|
+
case "banner.config.js",
|
|
200
|
+
"banner.config.cjs",
|
|
201
|
+
"banner.config.mjs",
|
|
202
|
+
"banner.config.ts",
|
|
203
|
+
"banner.config.cts",
|
|
204
|
+
"banner.config.mts":
|
|
205
|
+
return true
|
|
206
|
+
default:
|
|
207
|
+
return false
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
func loadBannerScriptConfigFile(location string) (any, error) {
|
|
212
|
+
const script = `
|
|
213
|
+
const { pathToFileURL } = require("node:url");
|
|
214
|
+
|
|
215
|
+
(async () => {
|
|
216
|
+
const mod = await import(pathToFileURL(process.argv[1]).href);
|
|
217
|
+
const candidate = Object.prototype.hasOwnProperty.call(mod, "default") ? mod.default : mod;
|
|
218
|
+
const value = typeof candidate === "function" ? await candidate() : candidate;
|
|
219
|
+
process.stdout.write(JSON.stringify(toSerializableBanner(value)));
|
|
220
|
+
})().catch((error) => {
|
|
221
|
+
process.stderr.write(error && error.stack ? error.stack : String(error));
|
|
222
|
+
process.exit(1);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
function toSerializableBanner(value) {
|
|
226
|
+
if (typeof value === "string") {
|
|
227
|
+
return value;
|
|
228
|
+
}
|
|
229
|
+
if (value !== null && typeof value === "object" && typeof value.text === "string") {
|
|
230
|
+
return { text: value.text };
|
|
231
|
+
}
|
|
232
|
+
throw new Error("config file must export a string or an object with a text string");
|
|
233
|
+
}
|
|
234
|
+
`
|
|
235
|
+
node := os.Getenv("TTSC_NODE_BINARY")
|
|
236
|
+
if node == "" {
|
|
237
|
+
node = "node"
|
|
238
|
+
}
|
|
239
|
+
cmd := exec.Command(node, "-e", script, location)
|
|
240
|
+
cmd.Env = nodeConfigLoaderEnv(location)
|
|
241
|
+
output, err := cmd.Output()
|
|
242
|
+
if err != nil {
|
|
243
|
+
stderr := ""
|
|
244
|
+
if exit, ok := err.(*exec.ExitError); ok {
|
|
245
|
+
stderr = strings.TrimSpace(string(exit.Stderr))
|
|
246
|
+
}
|
|
247
|
+
if stderr != "" {
|
|
248
|
+
return nil, fmt.Errorf("@ttsc/banner: load config file %s: %s", location, stderr)
|
|
249
|
+
}
|
|
250
|
+
return nil, fmt.Errorf("@ttsc/banner: load config file %s: %w", location, err)
|
|
251
|
+
}
|
|
252
|
+
var out any
|
|
253
|
+
if err := json.Unmarshal(output, &out); err != nil {
|
|
254
|
+
return nil, fmt.Errorf("@ttsc/banner: parse config file %s output: %w", location, err)
|
|
255
|
+
}
|
|
256
|
+
return out, nil
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
func loadBannerTypeScriptConfigFile(location string) (any, error) {
|
|
260
|
+
tempDir, err := os.MkdirTemp("", "ttsc-banner-config-")
|
|
261
|
+
if err != nil {
|
|
262
|
+
return nil, fmt.Errorf("@ttsc/banner: create config loader tempdir: %w", err)
|
|
263
|
+
}
|
|
264
|
+
defer os.RemoveAll(tempDir)
|
|
265
|
+
|
|
266
|
+
if err := linkConfigNodeModules(tempDir, filepath.Dir(location)); err != nil {
|
|
267
|
+
return nil, err
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
loader := filepath.Join(tempDir, "loader.mts")
|
|
271
|
+
tsconfig := filepath.Join(tempDir, "tsconfig.json")
|
|
272
|
+
importSpecifier, err := relativeImportSpecifier(tempDir, location)
|
|
273
|
+
if err != nil {
|
|
274
|
+
return nil, err
|
|
275
|
+
}
|
|
276
|
+
importLiteral, _ := json.Marshal(importSpecifier)
|
|
277
|
+
if err := writeConfigLoaderFile(loader, []byte(bannerTypeScriptConfigLoaderSource(string(importLiteral))), 0o644); err != nil {
|
|
278
|
+
return nil, fmt.Errorf("@ttsc/banner: write config loader: %w", err)
|
|
279
|
+
}
|
|
280
|
+
if err := writeConfigLoaderFile(tsconfig, []byte(typeScriptConfigLoaderTsconfig(loader, location, tempDir)), 0o644); err != nil {
|
|
281
|
+
return nil, fmt.Errorf("@ttsc/banner: write config loader tsconfig: %w", err)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
args := []string{
|
|
285
|
+
"--project", tsconfig,
|
|
286
|
+
"--cwd", tempDir,
|
|
287
|
+
"--cache-dir", filepath.Join(tempDir, "cache"),
|
|
288
|
+
}
|
|
289
|
+
if tsgo := os.Getenv("TTSC_TSGO_BINARY"); tsgo != "" {
|
|
290
|
+
args = append(args, "--binary", tsgo)
|
|
291
|
+
}
|
|
292
|
+
args = append(args, loader)
|
|
293
|
+
|
|
294
|
+
cmd := ttsxCommand(args...)
|
|
295
|
+
cmd.Env = nodeConfigLoaderEnv(location)
|
|
296
|
+
output, err := cmd.Output()
|
|
297
|
+
if err != nil {
|
|
298
|
+
stderr := ""
|
|
299
|
+
if exit, ok := err.(*exec.ExitError); ok {
|
|
300
|
+
stderr = strings.TrimSpace(string(exit.Stderr))
|
|
301
|
+
}
|
|
302
|
+
if stderr != "" {
|
|
303
|
+
return nil, fmt.Errorf("@ttsc/banner: load TypeScript config file %s: %s", location, stderr)
|
|
304
|
+
}
|
|
305
|
+
return nil, fmt.Errorf("@ttsc/banner: load TypeScript config file %s: %w", location, err)
|
|
306
|
+
}
|
|
307
|
+
var out any
|
|
308
|
+
if err := json.Unmarshal(output, &out); err != nil {
|
|
309
|
+
return nil, fmt.Errorf("@ttsc/banner: parse TypeScript config file %s output: %w", location, err)
|
|
310
|
+
}
|
|
311
|
+
return out, nil
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
func relativeImportSpecifier(fromDir, location string) (string, error) {
|
|
315
|
+
relative, err := filepath.Rel(fromDir, location)
|
|
316
|
+
if err != nil {
|
|
317
|
+
return "", fmt.Errorf("@ttsc/banner: resolve relative config import %s: %w", location, err)
|
|
318
|
+
}
|
|
319
|
+
relative = filepath.ToSlash(relative)
|
|
320
|
+
if strings.HasPrefix(relative, "../") || strings.HasPrefix(relative, "./") {
|
|
321
|
+
return relative, nil
|
|
322
|
+
}
|
|
323
|
+
return "./" + relative, nil
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
func bannerTypeScriptConfigLoaderSource(importLiteral string) string {
|
|
327
|
+
return fmt.Sprintf(`import * as importedConfig from %s;
|
|
328
|
+
|
|
329
|
+
declare const process: {
|
|
330
|
+
stdout: { write(value: string): void };
|
|
331
|
+
stderr: { write(value: string): void };
|
|
332
|
+
exit(code?: number): never;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const value = await resolveConfig(importedConfig);
|
|
337
|
+
process.stdout.write(JSON.stringify(toSerializableBanner(value)));
|
|
338
|
+
} catch (error) {
|
|
339
|
+
process.stderr.write(error instanceof Error && error.stack ? error.stack : String(error));
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function resolveConfig(value: unknown): Promise<unknown> {
|
|
344
|
+
let current = value;
|
|
345
|
+
for (let i = 0; i < 8; i++) {
|
|
346
|
+
if (isObject(current) && hasOwn(current, "default")) {
|
|
347
|
+
current = current.default;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
if (typeof current === "function") {
|
|
353
|
+
return await (current as () => unknown | Promise<unknown>)();
|
|
354
|
+
}
|
|
355
|
+
return current;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
359
|
+
return value !== null && typeof value === "object";
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function hasOwn(value: Record<string, unknown>, key: string): boolean {
|
|
363
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function toSerializableBanner(value: unknown): unknown {
|
|
367
|
+
if (typeof value === "string") {
|
|
368
|
+
return value;
|
|
369
|
+
}
|
|
370
|
+
if (isObject(value) && typeof value.text === "string") {
|
|
371
|
+
return { text: value.text };
|
|
372
|
+
}
|
|
373
|
+
throw new Error("config file must export a string or an object with a text string");
|
|
374
|
+
}
|
|
375
|
+
`, importLiteral)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
func typeScriptConfigLoaderTsconfig(loader, location, outDir string) string {
|
|
379
|
+
content := map[string]any{
|
|
380
|
+
"compilerOptions": map[string]any{
|
|
381
|
+
"allowImportingTsExtensions": true,
|
|
382
|
+
"module": "ESNext",
|
|
383
|
+
"moduleResolution": "bundler",
|
|
384
|
+
"outDir": filepath.ToSlash(filepath.Join(outDir, "out")),
|
|
385
|
+
"rewriteRelativeImportExtensions": true,
|
|
386
|
+
"rootDir": "/",
|
|
387
|
+
"skipLibCheck": true,
|
|
388
|
+
"strict": true,
|
|
389
|
+
"target": "ES2022",
|
|
390
|
+
},
|
|
391
|
+
"files": []string{
|
|
392
|
+
filepath.ToSlash(loader),
|
|
393
|
+
filepath.ToSlash(location),
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
body, _ := json.MarshalIndent(content, "", " ")
|
|
397
|
+
return string(body)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
func ttsxCommand(args ...string) *exec.Cmd {
|
|
401
|
+
ttsx := os.Getenv("TTSC_TTSX_BINARY")
|
|
402
|
+
if ttsx == "" {
|
|
403
|
+
ttsx = "ttsx"
|
|
404
|
+
}
|
|
405
|
+
if shouldRunTtsxThroughNode(ttsx) {
|
|
406
|
+
node := os.Getenv("TTSC_NODE_BINARY")
|
|
407
|
+
if node == "" {
|
|
408
|
+
node = "node"
|
|
409
|
+
}
|
|
410
|
+
return exec.Command(node, append([]string{ttsx}, args...)...)
|
|
411
|
+
}
|
|
412
|
+
return exec.Command(ttsx, args...)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
func shouldRunTtsxThroughNode(binary string) bool {
|
|
416
|
+
switch strings.ToLower(filepath.Ext(binary)) {
|
|
417
|
+
case ".js", ".cjs", ".mjs", ".ts", ".cts", ".mts":
|
|
418
|
+
return true
|
|
419
|
+
default:
|
|
420
|
+
return false
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
func nodeConfigLoaderEnv(location string) []string {
|
|
425
|
+
env := os.Environ()
|
|
426
|
+
parts := make([]string, 0, 2)
|
|
427
|
+
if nodeModules := findNearestNodeModules(filepath.Dir(location)); nodeModules != "" {
|
|
428
|
+
parts = append(parts, nodeModules)
|
|
429
|
+
}
|
|
430
|
+
if existing := os.Getenv("NODE_PATH"); existing != "" {
|
|
431
|
+
parts = append(parts, existing)
|
|
432
|
+
}
|
|
433
|
+
if len(parts) == 0 {
|
|
434
|
+
return env
|
|
435
|
+
}
|
|
436
|
+
return setEnv(env, "NODE_PATH", strings.Join(parts, string(os.PathListSeparator)))
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
func linkNearestNodeModules(tempDir, sourceDir string) error {
|
|
440
|
+
nodeModules := findNearestNodeModules(sourceDir)
|
|
441
|
+
if nodeModules == "" {
|
|
442
|
+
return nil
|
|
443
|
+
}
|
|
444
|
+
link := filepath.Join(tempDir, "node_modules")
|
|
445
|
+
if err := os.Symlink(nodeModules, link); err != nil {
|
|
446
|
+
return fmt.Errorf("@ttsc/banner: link config node_modules %s: %w", nodeModules, err)
|
|
447
|
+
}
|
|
448
|
+
return nil
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
func findNearestNodeModules(start string) string {
|
|
452
|
+
dir := filepath.Clean(start)
|
|
453
|
+
for {
|
|
454
|
+
candidate := filepath.Join(dir, "node_modules")
|
|
455
|
+
if stat, err := os.Stat(candidate); err == nil && stat.IsDir() {
|
|
456
|
+
return candidate
|
|
457
|
+
}
|
|
458
|
+
parent := filepath.Dir(dir)
|
|
459
|
+
if parent == dir {
|
|
460
|
+
return ""
|
|
461
|
+
}
|
|
462
|
+
dir = parent
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
func setEnv(env []string, key, value string) []string {
|
|
467
|
+
prefix := key + "="
|
|
468
|
+
for i, entry := range env {
|
|
469
|
+
if strings.HasPrefix(entry, prefix) {
|
|
470
|
+
env[i] = prefix + value
|
|
471
|
+
return env
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return append(env, prefix+value)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
func sanitizeJSDocLine(line string) string {
|
|
478
|
+
return strings.ReplaceAll(line, "*/", "* /")
|
|
479
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,15 +1 @@
|
|
|
1
|
-
import type { ITtscBannerPluginConfig } from "./structures";
|
|
2
1
|
export * from "./structures/index";
|
|
3
|
-
type TtscPluginDescriptor = {
|
|
4
|
-
name: string;
|
|
5
|
-
source: string;
|
|
6
|
-
stage?: "check" | "transform";
|
|
7
|
-
};
|
|
8
|
-
type TtscPluginFactoryContext<TConfig> = {
|
|
9
|
-
binary: string;
|
|
10
|
-
cwd: string;
|
|
11
|
-
plugin: TConfig;
|
|
12
|
-
projectRoot: string;
|
|
13
|
-
tsconfig: string;
|
|
14
|
-
};
|
|
15
|
-
export default function createTtscBanner(_context: TtscPluginFactoryContext<ITtscBannerPluginConfig>): TtscPluginDescriptor;
|
package/lib/index.js
CHANGED
|
@@ -20,10 +20,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
20
20
|
exports.default = createTtscBanner;
|
|
21
21
|
const node_path_1 = __importDefault(require("node:path"));
|
|
22
22
|
__exportStar(require("./structures/index"), exports);
|
|
23
|
+
/**
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
23
26
|
function createTtscBanner(_context) {
|
|
24
27
|
return {
|
|
25
28
|
name: "@ttsc/banner",
|
|
26
|
-
source: node_path_1.default.resolve(__dirname, "..", "
|
|
29
|
+
source: node_path_1.default.resolve(__dirname, "..", "driver"),
|
|
27
30
|
stage: "transform",
|
|
28
31
|
};
|
|
29
32
|
}
|
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;AAgBnC,0BACE,QAA2D;IAE3D,OAAO;QACL,IAAI,EAAE,cAAc;QACpB,MAAM,EAAE,mBAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,QAAQ,CAAC;QAC/C,KAAK,EAAE,WAAW;KACnB,CAAC;AACJ,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA,0DAA6B;AAI7B,qDAAmC;AAgBnC;;GAEG;AACH,0BACE,QAA2D;IAE3D,OAAO;QACL,IAAI,EAAE,cAAc;QACpB,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.
|
|
3
|
+
"version": "0.12.1",
|
|
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",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"README.md",
|
|
27
|
+
"driver/banner.go",
|
|
27
28
|
"lib",
|
|
28
29
|
"src",
|
|
29
30
|
"go.mod",
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
"@types/node": "^25.3.0",
|
|
35
36
|
"@typescript/native-preview": "7.0.0-dev.20260515.1",
|
|
36
37
|
"rimraf": "^6.1.2",
|
|
37
|
-
"ttsc": "0.
|
|
38
|
+
"ttsc": "0.12.1"
|
|
38
39
|
},
|
|
39
40
|
"repository": {
|
|
40
41
|
"type": "git",
|
package/plugin/banner.go
CHANGED
package/src/index.ts
CHANGED
|
@@ -18,12 +18,15 @@ type TtscPluginFactoryContext<TConfig> = {
|
|
|
18
18
|
tsconfig: string;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
21
24
|
export default function createTtscBanner(
|
|
22
25
|
_context: TtscPluginFactoryContext<ITtscBannerPluginConfig>,
|
|
23
26
|
): TtscPluginDescriptor {
|
|
24
27
|
return {
|
|
25
28
|
name: "@ttsc/banner",
|
|
26
|
-
source: path.resolve(__dirname, "..", "
|
|
29
|
+
source: path.resolve(__dirname, "..", "driver"),
|
|
27
30
|
stage: "transform",
|
|
28
31
|
};
|
|
29
32
|
}
|