@ttsc/strip 0.12.4 → 0.13.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 CHANGED
@@ -43,19 +43,27 @@ Default behavior removes these statement patterns:
43
43
 
44
44
  Call patterns match statement-level calls such as `console.log("debug")` or `assert.equal(left, right)`. A wildcard is supported at the end of a dotted call pattern, such as `assert.*`.
45
45
 
46
- Add a direct plugin config only when the project needs a different strip list:
46
+ To customize the strip list, add a `strip.config.ts` next to your `tsconfig.json`:
47
+
48
+ ```ts
49
+ // strip.config.ts
50
+ import type { ITtscStripConfig } from "@ttsc/strip";
51
+
52
+ export default {
53
+ calls: ["console.log", "console.debug", "assert.*"],
54
+ statements: ["debugger"],
55
+ } satisfies ITtscStripConfig;
56
+ ```
57
+
58
+ `@ttsc/strip` discovers its config by walking upward from the tsconfig directory, looking for `strip.config.{ts,cts,mts,js,cjs,mjs,json}`. To point at a specific file, set `configFile` on the tsconfig entry:
47
59
 
48
60
  ```jsonc
49
61
  {
50
62
  "compilerOptions": {
51
63
  "plugins": [
52
- {
53
- "transform": "@ttsc/strip",
54
- "calls": ["console.log", "console.debug", "assert.*"],
55
- "statements": ["debugger"],
56
- },
57
- ],
58
- },
64
+ { "transform": "@ttsc/strip", "configFile": "./config/strip.config.ts" }
65
+ ]
66
+ }
59
67
  }
60
68
  ```
61
69
 
@@ -0,0 +1,506 @@
1
+ package strip
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/json"
7
+ "fmt"
8
+ "os"
9
+ "os/exec"
10
+ "path/filepath"
11
+ "runtime"
12
+ "strings"
13
+ "time"
14
+ )
15
+
16
+ // configLoaderTimeout caps every `ttsx`/`node -e` subprocess that evaluates a
17
+ // user-supplied strip config. Mirrors the lint package budget: 60 s is generous
18
+ // for cold ttsx starts on CI runners and tight enough to keep user-visible
19
+ // feedback under a minute.
20
+ const configLoaderTimeout = 60 * time.Second
21
+
22
+ // stripConfigFilenames is the ordered list of candidate filenames that
23
+ // findStripConfigFile checks in each directory during upward discovery.
24
+ var stripConfigFilenames = []string{
25
+ "strip.config.ts",
26
+ "strip.config.mts",
27
+ "strip.config.cts",
28
+ "strip.config.js",
29
+ "strip.config.mjs",
30
+ "strip.config.cjs",
31
+ "strip.config.json",
32
+ }
33
+
34
+ // allowedTsconfigKeys lists the tsconfig plugin-entry keys that @ttsc/strip
35
+ // accepts. Any other key is a hard error.
36
+ var allowedTsconfigKeys = map[string]struct{}{
37
+ "configFile": {},
38
+ "enabled": {},
39
+ "name": {},
40
+ "stage": {},
41
+ "transform": {},
42
+ }
43
+
44
+ // loadStripConfigMap validates the tsconfig plugin entry and loads the strip
45
+ // configuration from either an explicit configFile or an auto-discovered
46
+ // strip.config.* file. Returns the raw config map ready for parseStrip.
47
+ func loadStripConfigMap(pluginConfig map[string]any, cwd, tsconfigPath string) (map[string]any, error) {
48
+ // Reject any key that @ttsc/strip does not recognise. This surfaces
49
+ // stale inline keys (calls, statements) with a clear error so users
50
+ // migrate to a config file instead of silently using defaults.
51
+ for key := range pluginConfig {
52
+ if _, ok := allowedTsconfigKeys[key]; !ok {
53
+ return nil, fmt.Errorf(
54
+ "@ttsc/strip: tsconfig plugin entry contains unsupported key %q; "+
55
+ "strip configuration must be supplied via a strip.config.* file "+
56
+ "(use the \"configFile\" key to point at a custom path)",
57
+ key,
58
+ )
59
+ }
60
+ }
61
+
62
+ // Resolve the config file: explicit configFile wins over discovery.
63
+ configFilePath := ""
64
+ if rawCF, ok := pluginConfig["configFile"]; ok {
65
+ cf, ok := rawCF.(string)
66
+ if !ok || strings.TrimSpace(cf) == "" {
67
+ return nil, fmt.Errorf("@ttsc/strip: \"configFile\" must be a non-empty string path")
68
+ }
69
+ configFilePath = resolveStripConfigFilePath(cf, cwd, tsconfigPath)
70
+ } else {
71
+ discovered, err := findStripConfigFile(cwd, tsconfigPath)
72
+ if err != nil {
73
+ return nil, err
74
+ }
75
+ configFilePath = discovered
76
+ }
77
+
78
+ // No config file found → use defaults (parseStrip treats an empty map
79
+ // as "apply built-in defaults").
80
+ if configFilePath == "" {
81
+ return map[string]any{}, nil
82
+ }
83
+
84
+ raw, err := loadStripConfigFile(configFilePath)
85
+ if err != nil {
86
+ return nil, err
87
+ }
88
+ cfg, ok := raw.(map[string]any)
89
+ if !ok {
90
+ return nil, fmt.Errorf("@ttsc/strip: config file %s must export an object", configFilePath)
91
+ }
92
+ return cfg, nil
93
+ }
94
+
95
+ // findStripConfigFile walks upward from the tsconfig directory (or cwd when no
96
+ // tsconfig is set) and returns the first directory that contains exactly one
97
+ // strip.config.* file. Multiple candidates in the same directory is an error.
98
+ // Returns "" (no error) when the filesystem root is reached without a match.
99
+ func findStripConfigFile(cwd, tsconfigPath string) (string, error) {
100
+ dir := stripDiscoveryBaseDir(cwd, tsconfigPath)
101
+ for {
102
+ matches := make([]string, 0, 1)
103
+ for _, name := range stripConfigFilenames {
104
+ candidate := filepath.Join(dir, name)
105
+ if stat, err := os.Stat(candidate); err == nil && !stat.IsDir() {
106
+ matches = append(matches, candidate)
107
+ }
108
+ }
109
+ if len(matches) > 1 {
110
+ names := make([]string, 0, len(matches))
111
+ for _, m := range matches {
112
+ names = append(names, filepath.Base(m))
113
+ }
114
+ return "", fmt.Errorf(
115
+ "@ttsc/strip: multiple strip config files found in %s (%s); "+
116
+ "set \"configFile\" explicitly in the tsconfig plugin entry",
117
+ dir, strings.Join(names, ", "),
118
+ )
119
+ }
120
+ if len(matches) == 1 {
121
+ return matches[0], nil
122
+ }
123
+ parent := filepath.Dir(dir)
124
+ if parent == dir {
125
+ return "", nil
126
+ }
127
+ dir = parent
128
+ }
129
+ }
130
+
131
+ // stripDiscoveryBaseDir returns the directory from which auto-discovery walks
132
+ // upward. Prefers the tsconfig directory over cwd so nested package configs are
133
+ // found relative to the tsconfig that triggered the strip run.
134
+ func stripDiscoveryBaseDir(cwd, tsconfigPath string) string {
135
+ if tsconfigPath != "" {
136
+ resolved := tsconfigPath
137
+ if !filepath.IsAbs(resolved) {
138
+ resolved = filepath.Join(cwd, resolved)
139
+ }
140
+ return filepath.Dir(resolved)
141
+ }
142
+ return cwd
143
+ }
144
+
145
+ // resolveStripConfigFilePath resolves a user-supplied config path to an
146
+ // absolute path. Absolute paths are returned unchanged; relative paths are
147
+ // joined to the tsconfig directory (or cwd when no tsconfig is set).
148
+ func resolveStripConfigFilePath(configPath, cwd, tsconfigPath string) string {
149
+ if filepath.IsAbs(configPath) {
150
+ return configPath
151
+ }
152
+ return filepath.Join(stripDiscoveryBaseDir(cwd, tsconfigPath), configPath)
153
+ }
154
+
155
+ // loadStripConfigFile loads and deserializes a strip config file at location.
156
+ // The format is determined by extension: .json is parsed natively; .js/.cjs/.mjs
157
+ // run through a Node subprocess; .ts/.cts/.mts run through ttsx.
158
+ func loadStripConfigFile(location string) (any, error) {
159
+ ext := strings.ToLower(filepath.Ext(location))
160
+ switch ext {
161
+ case ".json":
162
+ return loadStripJSONConfigFile(location)
163
+ case ".js", ".cjs", ".mjs":
164
+ return loadStripScriptConfigFile(location)
165
+ case ".ts", ".cts", ".mts":
166
+ return loadStripTypeScriptConfigFile(location)
167
+ default:
168
+ return nil, fmt.Errorf("@ttsc/strip: unsupported config file extension %q for %s", ext, location)
169
+ }
170
+ }
171
+
172
+ // loadStripJSONConfigFile reads and JSON-parses a strip config file. A leading
173
+ // UTF-8 BOM is stripped before parsing so files saved by Windows editors are
174
+ // accepted.
175
+ func loadStripJSONConfigFile(location string) (any, error) {
176
+ body, err := os.ReadFile(location)
177
+ if err != nil {
178
+ return nil, fmt.Errorf("@ttsc/strip: read config file %s: %w", location, err)
179
+ }
180
+ body = bytes.TrimPrefix(body, []byte{0xEF, 0xBB, 0xBF})
181
+ var out any
182
+ if err := json.Unmarshal(body, &out); err != nil {
183
+ return nil, fmt.Errorf("@ttsc/strip: parse config file %s: %w", location, err)
184
+ }
185
+ return out, nil
186
+ }
187
+
188
+ // stripScriptLoaderSource is the inline Node.js script used by
189
+ // loadStripScriptConfigFile to evaluate a .js/.cjs/.mjs strip config and
190
+ // serialize the result to stdout as JSON.
191
+ const stripScriptLoaderSource = `
192
+ const { pathToFileURL } = require("node:url");
193
+
194
+ (async () => {
195
+ const mod = await import(pathToFileURL(process.argv[1]).href);
196
+ let current = mod;
197
+ for (let i = 0; i < 8; i++) {
198
+ if (current !== null && typeof current === "object" && Object.prototype.hasOwnProperty.call(current, "default")) {
199
+ current = current.default;
200
+ continue;
201
+ }
202
+ break;
203
+ }
204
+ const value = typeof current === "function" ? await current() : current;
205
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
206
+ throw new Error("strip config file must export an object");
207
+ }
208
+ process.stdout.write(JSON.stringify(value));
209
+ })().catch((error) => {
210
+ process.stderr.write(error && error.stack ? error.stack : String(error));
211
+ process.exit(1);
212
+ });
213
+ `
214
+
215
+ // loadStripScriptConfigFile evaluates a .js/.cjs/.mjs config file by running a
216
+ // Node subprocess that dynamic-imports the file, resolves the default export,
217
+ // and serializes the result as JSON to stdout.
218
+ func loadStripScriptConfigFile(location string) (any, error) {
219
+ node := os.Getenv("TTSC_NODE_BINARY")
220
+ if node == "" {
221
+ node = "node"
222
+ }
223
+ ctx, cancel := context.WithTimeout(context.Background(), configLoaderTimeout)
224
+ defer cancel()
225
+ cmd := exec.CommandContext(ctx, node, "-e", stripScriptLoaderSource, location)
226
+ cmd.Env = stripNodeConfigLoaderEnv(location)
227
+ output, err := cmd.Output()
228
+ if err != nil {
229
+ if ctx.Err() == context.DeadlineExceeded {
230
+ return nil, fmt.Errorf("@ttsc/strip: load config file %s: timed out after %s", location, configLoaderTimeout)
231
+ }
232
+ stderr := ""
233
+ if exit, ok := err.(*exec.ExitError); ok {
234
+ stderr = strings.TrimSpace(string(exit.Stderr))
235
+ }
236
+ if stderr != "" {
237
+ return nil, fmt.Errorf("@ttsc/strip: load config file %s: %s", location, stderr)
238
+ }
239
+ return nil, fmt.Errorf("@ttsc/strip: load config file %s: %w", location, err)
240
+ }
241
+ var out any
242
+ if err := json.Unmarshal(output, &out); err != nil {
243
+ return nil, fmt.Errorf("@ttsc/strip: parse config file %s output: %w", location, err)
244
+ }
245
+ return out, nil
246
+ }
247
+
248
+ // stripTypeScriptLoaderSource returns the TypeScript source of the ephemeral
249
+ // loader script that ttsx executes to evaluate a TypeScript strip config file.
250
+ // importLiteral must be a JSON-encoded relative import path (e.g.
251
+ // `"./strip.config.ts"`) produced by json.Marshal.
252
+ func stripTypeScriptLoaderSource(importLiteral string) string {
253
+ return fmt.Sprintf(`import * as importedConfig from %s;
254
+
255
+ declare const process: {
256
+ stdout: { write(value: string): void };
257
+ stderr: { write(value: string): void };
258
+ exit(code?: number): never;
259
+ };
260
+
261
+ try {
262
+ let current: unknown = importedConfig;
263
+ for (let i = 0; i < 8; i++) {
264
+ if (current !== null && typeof current === "object" && Object.prototype.hasOwnProperty.call(current as Record<string, unknown>, "default")) {
265
+ current = (current as Record<string, unknown>).default;
266
+ continue;
267
+ }
268
+ break;
269
+ }
270
+ if (typeof current === "function") {
271
+ current = await (current as () => unknown | Promise<unknown>)();
272
+ }
273
+ if (current === null || typeof current !== "object" || Array.isArray(current)) {
274
+ throw new Error("strip config file must export an object");
275
+ }
276
+ process.stdout.write(JSON.stringify(current));
277
+ } catch (error) {
278
+ process.stderr.write(error instanceof Error && error.stack ? error.stack : String(error));
279
+ process.exit(1);
280
+ }
281
+ `, importLiteral)
282
+ }
283
+
284
+ // loadStripTypeScriptConfigFile evaluates a .ts/.cts/.mts config file by writing
285
+ // an ephemeral loader script and tsconfig into a temp directory, symlinking the
286
+ // nearest node_modules, then running ttsx with a configLoaderTimeout deadline.
287
+ //
288
+ // The ttsx build runs with `--no-plugins`: the loader only needs to
289
+ // type-check and execute the strip config file, so loading the host
290
+ // project's transform/check plugins would be wasteful and could fail the
291
+ // build against this deliberately lenient loader tsconfig.
292
+ func loadStripTypeScriptConfigFile(location string) (any, error) {
293
+ tempDir, err := os.MkdirTemp("", "ttsc-strip-config-")
294
+ if err != nil {
295
+ return nil, fmt.Errorf("@ttsc/strip: create config loader tempdir: %w", err)
296
+ }
297
+ defer os.RemoveAll(tempDir)
298
+
299
+ if err := stripLinkNearestNodeModules(tempDir, filepath.Dir(location)); err != nil {
300
+ return nil, err
301
+ }
302
+
303
+ loader := filepath.Join(tempDir, "loader.mts")
304
+ tsconfig := filepath.Join(tempDir, "tsconfig.json")
305
+ importSpecifier, err := stripRelativeImportSpecifier(tempDir, location)
306
+ if err != nil {
307
+ return nil, err
308
+ }
309
+ importLiteral, err := json.Marshal(importSpecifier)
310
+ if err != nil {
311
+ return nil, fmt.Errorf("@ttsc/strip: encode config import %s: %w", location, err)
312
+ }
313
+ if err := os.WriteFile(loader, []byte(stripTypeScriptLoaderSource(string(importLiteral))), 0o644); err != nil {
314
+ return nil, fmt.Errorf("@ttsc/strip: write config loader: %w", err)
315
+ }
316
+ if err := os.WriteFile(tsconfig, []byte(stripTypeScriptLoaderTsconfig(loader, location, tempDir)), 0o644); err != nil {
317
+ return nil, fmt.Errorf("@ttsc/strip: write config loader tsconfig: %w", err)
318
+ }
319
+
320
+ args := []string{
321
+ "--project", tsconfig,
322
+ "--cwd", tempDir,
323
+ "--cache-dir", filepath.Join(tempDir, "cache"),
324
+ "--no-plugins",
325
+ }
326
+ if tsgo := os.Getenv("TTSC_TSGO_BINARY"); tsgo != "" {
327
+ args = append(args, "--binary", tsgo)
328
+ }
329
+ args = append(args, loader)
330
+
331
+ ctx, cancel := context.WithTimeout(context.Background(), configLoaderTimeout)
332
+ defer cancel()
333
+ cmd := stripTtsxCommandContext(ctx, args...)
334
+ cmd.Env = stripNodeConfigLoaderEnv(location)
335
+ output, err := cmd.Output()
336
+ if err != nil {
337
+ if ctx.Err() == context.DeadlineExceeded {
338
+ return nil, fmt.Errorf("@ttsc/strip: load TypeScript config file %s: timed out after %s", location, configLoaderTimeout)
339
+ }
340
+ stderr := ""
341
+ if exit, ok := err.(*exec.ExitError); ok {
342
+ stderr = strings.TrimSpace(string(exit.Stderr))
343
+ }
344
+ if stderr != "" {
345
+ return nil, fmt.Errorf("@ttsc/strip: load TypeScript config file %s: %s", location, stderr)
346
+ }
347
+ return nil, fmt.Errorf("@ttsc/strip: load TypeScript config file %s: %w", location, err)
348
+ }
349
+ var out any
350
+ if err := json.Unmarshal(output, &out); err != nil {
351
+ return nil, fmt.Errorf("@ttsc/strip: parse TypeScript config file %s output: %w", location, err)
352
+ }
353
+ return out, nil
354
+ }
355
+
356
+ // stripTypeScriptLoaderTsconfig generates the JSON content of the ephemeral
357
+ // tsconfig used by the loader script.
358
+ func stripTypeScriptLoaderTsconfig(loader, location, outDir string) string {
359
+ content := map[string]any{
360
+ "compilerOptions": map[string]any{
361
+ "allowImportingTsExtensions": true,
362
+ "allowJs": true,
363
+ "checkJs": false,
364
+ "module": "ESNext",
365
+ "moduleResolution": "bundler",
366
+ "noImplicitAny": false,
367
+ "outDir": filepath.ToSlash(filepath.Join(outDir, "out")),
368
+ "rewriteRelativeImportExtensions": true,
369
+ "rootDir": "/",
370
+ "skipLibCheck": true,
371
+ "strict": false,
372
+ "target": "ES2022",
373
+ },
374
+ "files": []string{
375
+ filepath.ToSlash(loader),
376
+ filepath.ToSlash(location),
377
+ },
378
+ }
379
+ body, err := json.MarshalIndent(content, "", " ")
380
+ if err != nil {
381
+ panic(err)
382
+ }
383
+ return string(body)
384
+ }
385
+
386
+ // stripTtsxCommandContext returns an exec.Cmd that runs ttsx with the given
387
+ // arguments, routing through node when the resolved binary is a script file.
388
+ func stripTtsxCommandContext(ctx context.Context, args ...string) *exec.Cmd {
389
+ ttsx := os.Getenv("TTSC_TTSX_BINARY")
390
+ if ttsx == "" {
391
+ ttsx = "ttsx"
392
+ }
393
+ if stripShouldRunThroughNode(ttsx) {
394
+ node := os.Getenv("TTSC_NODE_BINARY")
395
+ if node == "" {
396
+ node = "node"
397
+ }
398
+ return exec.CommandContext(ctx, node, append([]string{ttsx}, args...)...)
399
+ }
400
+ return exec.CommandContext(ctx, ttsx, args...)
401
+ }
402
+
403
+ // stripShouldRunThroughNode reports whether the resolved ttsx binary is a
404
+ // script (JS or TS extension) that must be executed via node.
405
+ func stripShouldRunThroughNode(binary string) bool {
406
+ switch strings.ToLower(filepath.Ext(binary)) {
407
+ case ".js", ".cjs", ".mjs", ".ts", ".cts", ".mts":
408
+ return true
409
+ default:
410
+ return false
411
+ }
412
+ }
413
+
414
+ // stripNodeConfigLoaderEnv builds the environment for a Node.js config-loader
415
+ // subprocess. Prepends the nearest node_modules to NODE_PATH so imports in
416
+ // .js/.cjs/.mjs config files resolve correctly.
417
+ func stripNodeConfigLoaderEnv(location string) []string {
418
+ env := os.Environ()
419
+ parts := make([]string, 0, 2)
420
+ if nodeModules := stripFindNearestNodeModules(filepath.Dir(location)); nodeModules != "" {
421
+ parts = append(parts, nodeModules)
422
+ }
423
+ if existing := os.Getenv("NODE_PATH"); existing != "" {
424
+ parts = append(parts, existing)
425
+ }
426
+ if len(parts) == 0 {
427
+ return env
428
+ }
429
+ return stripSetEnv(env, "NODE_PATH", strings.Join(parts, string(os.PathListSeparator)))
430
+ }
431
+
432
+ // stripFindNearestNodeModules walks upward from start and returns the first
433
+ // node_modules directory found, or "" when the filesystem root is reached.
434
+ func stripFindNearestNodeModules(start string) string {
435
+ dir := filepath.Clean(start)
436
+ for {
437
+ candidate := filepath.Join(dir, "node_modules")
438
+ if stat, err := os.Stat(candidate); err == nil && stat.IsDir() {
439
+ return candidate
440
+ }
441
+ parent := filepath.Dir(dir)
442
+ if parent == dir {
443
+ return ""
444
+ }
445
+ dir = parent
446
+ }
447
+ }
448
+
449
+ // stripLinkNearestNodeModules creates a node_modules symlink (or Windows
450
+ // junction) inside tempDir pointing at the nearest node_modules upward from
451
+ // sourceDir. No-op when no node_modules is found.
452
+ func stripLinkNearestNodeModules(tempDir, sourceDir string) error {
453
+ nodeModules := stripFindNearestNodeModules(sourceDir)
454
+ if nodeModules == "" {
455
+ return nil
456
+ }
457
+ link := filepath.Join(tempDir, "node_modules")
458
+ err := os.Symlink(nodeModules, link)
459
+ if err == nil {
460
+ return nil
461
+ }
462
+ if runtime.GOOS == "windows" {
463
+ jerr := stripCreateWindowsJunction(link, nodeModules)
464
+ if jerr == nil {
465
+ return nil
466
+ }
467
+ err = fmt.Errorf("%w (junction fallback: %v)", err, jerr)
468
+ }
469
+ return fmt.Errorf("@ttsc/strip: link config node_modules %s: %w", nodeModules, err)
470
+ }
471
+
472
+ // stripCreateWindowsJunction creates a directory junction on Windows.
473
+ func stripCreateWindowsJunction(link, target string) error {
474
+ cmd := exec.Command("cmd", "/c", "mklink", "/J", link, target)
475
+ if out, err := cmd.CombinedOutput(); err != nil {
476
+ return fmt.Errorf("mklink /J failed: %v: %s", err, strings.TrimSpace(string(out)))
477
+ }
478
+ return nil
479
+ }
480
+
481
+ // stripRelativeImportSpecifier computes the ESM import specifier for location
482
+ // relative to fromDir, always prefixed with "./" or "../".
483
+ func stripRelativeImportSpecifier(fromDir, location string) (string, error) {
484
+ relative, err := filepath.Rel(fromDir, location)
485
+ if err != nil {
486
+ return "", fmt.Errorf("@ttsc/strip: resolve relative config import %s: %w", location, err)
487
+ }
488
+ relative = filepath.ToSlash(relative)
489
+ if strings.HasPrefix(relative, "../") || strings.HasPrefix(relative, "./") {
490
+ return relative, nil
491
+ }
492
+ return "./" + relative, nil
493
+ }
494
+
495
+ // stripSetEnv updates an existing key=value entry in env (in-place) or appends
496
+ // a new one.
497
+ func stripSetEnv(env []string, key, value string) []string {
498
+ prefix := key + "="
499
+ for i, entry := range env {
500
+ if strings.HasPrefix(entry, prefix) {
501
+ env[i] = prefix + value
502
+ return env
503
+ }
504
+ }
505
+ return append(env, prefix+value)
506
+ }
package/driver/strip.go CHANGED
@@ -19,7 +19,11 @@ type plugin struct{}
19
19
  // ApplyProgram strips configured call expressions and debugger statements from
20
20
  // every source file in the program.
21
21
  func (plugin) ApplyProgram(prog *driver.Program, ctx driver.PluginContext) error {
22
- rewriter, err := parseStrip(ctx.Entry.Config)
22
+ config, err := loadStripConfigMap(ctx.Entry.Config, ctx.Cwd, ctx.Tsconfig)
23
+ if err != nil {
24
+ return err
25
+ }
26
+ rewriter, err := parseStrip(config)
23
27
  if err != nil {
24
28
  return err
25
29
  }
package/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "@ttsc/strip",
3
- "version": "0.12.4",
3
+ "version": "0.13.1",
4
4
  "description": "First-party ttsc plugin that removes configured calls and statements from emitted JavaScript.",
5
5
  "main": "src/index.cjs",
6
+ "types": "src/index.d.ts",
6
7
  "exports": {
7
- ".": "./src/index.cjs",
8
+ ".": {
9
+ "types": "./src/index.d.ts",
10
+ "default": "./src/index.cjs"
11
+ },
8
12
  "./package.json": "./package.json"
9
13
  },
10
14
  "keywords": [
@@ -21,8 +25,10 @@
21
25
  },
22
26
  "files": [
23
27
  "README.md",
28
+ "driver/config.go",
24
29
  "driver/strip.go",
25
30
  "src/index.cjs",
31
+ "src/index.d.ts",
26
32
  "go.mod",
27
33
  "plugin/main.go",
28
34
  "plugin/strip.go"
package/src/index.cjs CHANGED
@@ -3,7 +3,31 @@
3
3
 
4
4
  const path = require("node:path");
5
5
 
6
- module.exports = function createTtscStrip() {
6
+ // Keys from the tsconfig plugin entry that @ttsc/strip accepts. All other keys
7
+ // are rejected so that stale inline options (calls, statements) surface as a
8
+ // clear error rather than silently falling back to defaults.
9
+ const ALLOWED_TSCONFIG_KEYS = new Set([
10
+ "configFile",
11
+ "enabled",
12
+ "name",
13
+ "stage",
14
+ "transform",
15
+ ]);
16
+
17
+ module.exports = function createTtscStrip(context) {
18
+ const plugin =
19
+ context && typeof context === "object" && context.plugin != null
20
+ ? context.plugin
21
+ : {};
22
+ for (const key of Object.keys(plugin)) {
23
+ if (!ALLOWED_TSCONFIG_KEYS.has(key)) {
24
+ throw new Error(
25
+ `@ttsc/strip: tsconfig plugin entry contains unsupported key ${JSON.stringify(key)}; ` +
26
+ `strip configuration must be supplied via a strip.config.* file ` +
27
+ `(use the "configFile" key to point at a custom path)`,
28
+ );
29
+ }
30
+ }
7
31
  return {
8
32
  name: "@ttsc/strip",
9
33
  source: path.resolve(__dirname, "..", "driver"),
package/src/index.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Plugin descriptor factory consumed by ttsc's package discovery.
3
+ *
4
+ * `@ttsc/strip` is configured through a `strip.config.*` file, never through
5
+ * the factory context — the factory only returns the native descriptor.
6
+ *
7
+ * @internal
8
+ */
9
+ declare function createTtscStrip(context: unknown): {
10
+ name: string;
11
+ source: string;
12
+ stage: "transform";
13
+ };
14
+
15
+ declare namespace createTtscStrip {
16
+ /**
17
+ * Standalone `strip.config.{ts,cts,mts,js,cjs,mjs,json}` file shape consumed
18
+ * by `@ttsc/strip`.
19
+ *
20
+ * Both keys are optional. The built-in defaults (`calls: ["console.log",
21
+ * "console.debug", "assert.*"]`, `statements: ["debugger"]`) apply only when
22
+ * *both* keys are omitted; declaring either key replaces both defaults with
23
+ * exactly what the file lists.
24
+ */
25
+ export interface ITtscStripConfig {
26
+ /**
27
+ * Statement-level call patterns to remove, written as dotted names. A
28
+ * trailing `.*` wildcard matches any final property — e.g. `"assert.*"`
29
+ * matches `assert.equal`, `assert.deepStrictEqual`, …
30
+ */
31
+ calls?: readonly string[];
32
+
33
+ /** Bare statement kinds to remove. Currently only `"debugger"` is supported. */
34
+ statements?: readonly string[];
35
+ }
36
+ }
37
+
38
+ export = createTtscStrip;