@ttsc/strip 0.7.3 → 0.8.0-dev.20260505

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
@@ -9,7 +9,7 @@
9
9
  [![Guide Documents](https://img.shields.io/badge/Guide-Documents-forestgreen)](https://github.com/samchon/ttsc/tree/master/docs)
10
10
  [![Discord Badge](https://img.shields.io/badge/discord-samchon-d91965?style=flat&labelColor=5866f2&logo=discord&logoColor=white&link=https://discord.gg/E94XhzrUCZ)](https://discord.gg/E94XhzrUCZ)
11
11
 
12
- `@ttsc/strip` removes configured call-expression statements and debugger statements from emitted JavaScript.
12
+ `@ttsc/strip` removes configured call-expression statements and debugger statements from TypeScript source before emit.
13
13
 
14
14
  ## Setup
15
15
 
@@ -54,8 +54,8 @@ Call patterns match statement-level calls such as `console.log("debug")` or `ass
54
54
  // Keep lint first.
55
55
  { "transform": "@ttsc/lint", "config": { "no-var": "error" } },
56
56
 
57
- // Output plugins run after emit, in order.
58
- { "transform": "@ttsc/banner", "banner": "/*! @license MIT */" },
57
+ // First-party utilities use their documented source/emit hook order.
58
+ { "transform": "@ttsc/banner", "banner": "License MIT" },
59
59
  { "transform": "@ttsc/paths" },
60
60
  {
61
61
  "transform": "@ttsc/strip",
package/go.mod CHANGED
@@ -2,11 +2,7 @@ module github.com/samchon/ttsc/packages/strip
2
2
 
3
3
  go 1.26
4
4
 
5
- require (
6
- github.com/microsoft/typescript-go/shim/ast v0.0.0
7
- github.com/microsoft/typescript-go/shim/core v0.0.0
8
- github.com/microsoft/typescript-go/shim/parser v0.0.0
9
- )
5
+ require github.com/samchon/ttsc/packages/ttsc v0.0.0
10
6
 
11
7
  require (
12
8
  github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/strip",
3
- "version": "0.7.3",
3
+ "version": "0.8.0-dev.20260505",
4
4
  "description": "First-party ttsc plugin that removes configured calls and statements from emitted JavaScript.",
5
5
  "main": "src/index.cjs",
6
6
  "exports": {
package/plugin/main.go CHANGED
@@ -1,13 +1,11 @@
1
1
  // Native sidecar entrypoint for `@ttsc/strip`.
2
- //
3
- // The sidecar implements an output-stage transform for JavaScript emit. It
4
- // removes configured call statements and optional debugger statements after
5
- // TypeScript-Go prints JS, leaving typechecking and project loading to ttsc.
6
2
  package main
7
3
 
8
4
  import (
9
5
  "fmt"
10
6
  "os"
7
+
8
+ "github.com/samchon/ttsc/packages/ttsc/utility"
11
9
  )
12
10
 
13
11
  const version = "0.0.1"
@@ -18,19 +16,19 @@ func main() {
18
16
 
19
17
  func run(args []string) int {
20
18
  if len(args) == 0 {
21
- fmt.Fprintln(os.Stderr, "@ttsc/strip: command required (expected output|version)")
19
+ fmt.Fprintln(os.Stderr, "@ttsc/strip: command required (expected build|transform|check|version)")
22
20
  return 2
23
21
  }
24
22
  switch args[0] {
25
23
  case "-v", "--version", "version":
26
24
  fmt.Fprintf(os.Stdout, "@ttsc/strip %s\n", version)
27
25
  return 0
26
+ case "build":
27
+ return utility.RunBuild(args[1:])
28
+ case "transform":
29
+ return utility.RunTransform(args[1:])
28
30
  case "check":
29
- // Strip rewriting is output-only; configuration is validated when output
30
- // text is actually transformed.
31
31
  return 0
32
- case "output":
33
- return RunOutput(args[1:])
34
32
  default:
35
33
  fmt.Fprintf(os.Stderr, "@ttsc/strip: unknown command %q\n", args[0])
36
34
  return 2
package/plugin/strip.go CHANGED
@@ -1,385 +1,3 @@
1
- // Output transformer for `@ttsc/strip`.
2
- //
3
- // The transform parses emitted JavaScript, removes complete statements that
4
- // match configured call patterns, and preserves all other text. It operates on
5
- // JS output rather than TS source so the runtime file shape remains consistent
6
- // with TypeScript-Go's printer.
7
1
  package main
8
2
 
9
- import (
10
- "encoding/json"
11
- "flag"
12
- "fmt"
13
- "os"
14
- "path/filepath"
15
- "sort"
16
- "strings"
17
-
18
- shimast "github.com/microsoft/typescript-go/shim/ast"
19
- shimcore "github.com/microsoft/typescript-go/shim/core"
20
- shimparser "github.com/microsoft/typescript-go/shim/parser"
21
- )
22
-
23
- type pluginEntry struct {
24
- // Config is the plugin-specific entry from compilerOptions.plugins[] once
25
- // the sidecar locates the `@ttsc/strip` descriptor in the ordered manifest.
26
- Config map[string]any `json:"config"`
27
- Name string `json:"name"`
28
- Stage string `json:"stage"`
29
- }
30
-
31
- type stripTransform struct {
32
- // calls stores dotted call patterns such as console.log or invariant.*.
33
- // stripDebugger controls DebuggerStatement removal independently.
34
- calls []callPattern
35
- stripDebugger bool
36
- }
37
-
38
- type callPattern struct {
39
- // parts contains the dotted name split on '.'. wildcard means the final
40
- // source token was '*' and therefore matches any deeper property call.
41
- parts []string
42
- wildcard bool
43
- }
44
-
45
- type textEdit struct {
46
- start int
47
- end int
48
- text string
49
- }
50
-
51
- func RunOutput(args []string) int {
52
- fs := flag.NewFlagSet("output", flag.ContinueOnError)
53
- fs.SetOutput(os.Stderr)
54
- file := fs.String("file", "", "emitted file to transform")
55
- out := fs.String("out", "", "write transformed text to this file instead of updating --file")
56
- _ = fs.String("cwd", "", "project directory")
57
- _ = fs.String("outDir", "", "emit directory override")
58
- pluginsJSON := fs.String("plugins-json", "", "ttsc plugin manifest JSON")
59
- _ = fs.String("tsconfig", "tsconfig.json", "project tsconfig")
60
- if err := fs.Parse(args); err != nil {
61
- return 2
62
- }
63
- if *file == "" {
64
- fmt.Fprintln(os.Stderr, "@ttsc/strip: output requires --file")
65
- return 2
66
- }
67
- config, err := findConfig(*pluginsJSON)
68
- if err != nil {
69
- fmt.Fprintln(os.Stderr, err)
70
- return 2
71
- }
72
- text, err := os.ReadFile(*file)
73
- if err != nil {
74
- fmt.Fprintf(os.Stderr, "@ttsc/strip: read %s: %v\n", *file, err)
75
- return 2
76
- }
77
- patched, err := Apply(*file, string(text), config)
78
- if err != nil {
79
- fmt.Fprintln(os.Stderr, err)
80
- return 2
81
- }
82
- target := *file
83
- if *out != "" {
84
- target = *out
85
- }
86
- if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
87
- fmt.Fprintf(os.Stderr, "@ttsc/strip: mkdir: %v\n", err)
88
- return 2
89
- }
90
- if err := os.WriteFile(target, []byte(patched), 0o644); err != nil {
91
- fmt.Fprintf(os.Stderr, "@ttsc/strip: write %s: %v\n", target, err)
92
- return 2
93
- }
94
- return 0
95
- }
96
-
97
- func Apply(fileName string, text string, config map[string]any) (string, error) {
98
- // Apply is deterministic and has no filesystem side effects. RunOutput owns
99
- // file reads/writes so unit tests can exercise the transform directly.
100
- strip, err := parseStrip(config)
101
- if err != nil {
102
- return "", err
103
- }
104
- return strip.apply(fileName, text)
105
- }
106
-
107
- func parseStrip(config map[string]any) (*stripTransform, error) {
108
- calls, err := stringArrayConfig(config, "calls")
109
- if err != nil {
110
- return nil, fmt.Errorf("@ttsc/strip: %w", err)
111
- }
112
- statements, err := stringArrayConfig(config, "statements")
113
- if err != nil {
114
- return nil, fmt.Errorf("@ttsc/strip: %w", err)
115
- }
116
- out := &stripTransform{}
117
- for _, call := range calls {
118
- pattern, err := parseCallPattern(call)
119
- if err != nil {
120
- return nil, fmt.Errorf("@ttsc/strip: %w", err)
121
- }
122
- out.calls = append(out.calls, pattern)
123
- }
124
- for _, statement := range statements {
125
- switch statement {
126
- case "debugger":
127
- out.stripDebugger = true
128
- default:
129
- return nil, fmt.Errorf("@ttsc/strip: unsupported statement pattern %q", statement)
130
- }
131
- }
132
- return out, nil
133
- }
134
-
135
- func (s *stripTransform) apply(fileName string, text string) (string, error) {
136
- // Declaration files and maps are intentionally skipped. Strip patterns are
137
- // JavaScript runtime statements, not type-level declarations.
138
- if s == nil || !isJavaScriptOutput(fileName) || (len(s.calls) == 0 && !s.stripDebugger) {
139
- return text, nil
140
- }
141
- file := parseJS(fileName, text)
142
- if file == nil {
143
- return text, nil
144
- }
145
- edits := make([]textEdit, 0)
146
- // The AST tells us which statements are safe to remove; the original text is
147
- // still used for editing so comments, whitespace, and unrelated printer
148
- // details remain untouched.
149
- var walk func(*shimast.Node)
150
- walk = func(node *shimast.Node) {
151
- if node == nil {
152
- return
153
- }
154
- switch node.Kind {
155
- case shimast.KindDebuggerStatement:
156
- if s.stripDebugger {
157
- start, end := statementRemovalRange(text, node)
158
- edits = append(edits, textEdit{start: start, end: end})
159
- }
160
- case shimast.KindExpressionStatement:
161
- expr := node.AsExpressionStatement().Expression
162
- name, ok := callExpressionName(expr)
163
- if ok && s.matchesCall(name) {
164
- start, end := statementRemovalRange(text, node)
165
- edits = append(edits, textEdit{start: start, end: end})
166
- }
167
- }
168
- node.ForEachChild(func(child *shimast.Node) bool {
169
- walk(child)
170
- return false
171
- })
172
- }
173
- for _, stmt := range file.Statements.Nodes {
174
- walk(stmt)
175
- }
176
- return applyTextEdits(text, edits), nil
177
- }
178
-
179
- func (s *stripTransform) matchesCall(name string) bool {
180
- for _, pattern := range s.calls {
181
- if pattern.matches(name) {
182
- return true
183
- }
184
- }
185
- return false
186
- }
187
-
188
- func parseCallPattern(text string) (callPattern, error) {
189
- parts := strings.Split(text, ".")
190
- if len(parts) == 0 {
191
- return callPattern{}, fmt.Errorf("empty call pattern")
192
- }
193
- for i, part := range parts {
194
- if part == "" {
195
- return callPattern{}, fmt.Errorf("invalid call pattern %q", text)
196
- }
197
- if part == "*" && i != len(parts)-1 {
198
- // Only terminal wildcards have a clear statement-level meaning:
199
- // logger.* matches logger.info(), logger.warn(), etc.
200
- return callPattern{}, fmt.Errorf("wildcard is only supported at the end of call pattern %q", text)
201
- }
202
- }
203
- wildcard := parts[len(parts)-1] == "*"
204
- if wildcard {
205
- parts = parts[:len(parts)-1]
206
- }
207
- return callPattern{parts: parts, wildcard: wildcard}, nil
208
- }
209
-
210
- func (p callPattern) matches(name string) bool {
211
- parts := strings.Split(name, ".")
212
- if p.wildcard {
213
- if len(parts) <= len(p.parts) {
214
- return false
215
- }
216
- return equalStringSlices(parts[:len(p.parts)], p.parts)
217
- }
218
- return equalStringSlices(parts, p.parts)
219
- }
220
-
221
- func callExpressionName(expr *shimast.Node) (string, bool) {
222
- if expr == nil || expr.Kind != shimast.KindCallExpression {
223
- return "", false
224
- }
225
- call := expr.AsCallExpression()
226
- return dottedName(call.Expression)
227
- }
228
-
229
- func dottedName(expr *shimast.Node) (string, bool) {
230
- if expr == nil {
231
- return "", false
232
- }
233
- switch expr.Kind {
234
- case shimast.KindIdentifier:
235
- return expr.Text(), true
236
- case shimast.KindPropertyAccessExpression:
237
- prop := expr.AsPropertyAccessExpression()
238
- left, ok := dottedName(prop.Expression)
239
- if !ok || prop.Name() == nil {
240
- return "", false
241
- }
242
- return left + "." + prop.Name().Text(), true
243
- default:
244
- return "", false
245
- }
246
- }
247
-
248
- func parseJS(fileName string, text string) *shimast.SourceFile {
249
- normalized := filepath.ToSlash(fileName)
250
- if !filepath.IsAbs(normalized) {
251
- if abs, err := filepath.Abs(normalized); err == nil {
252
- normalized = filepath.ToSlash(abs)
253
- }
254
- }
255
- opts := shimast.SourceFileParseOptions{FileName: normalized}
256
- return shimparser.ParseSourceFile(opts, text, shimcore.ScriptKindJS)
257
- }
258
-
259
- func stringArrayConfig(config map[string]any, key string) ([]string, error) {
260
- raw, ok := config[key]
261
- if !ok || raw == nil {
262
- return nil, nil
263
- }
264
- values, ok := raw.([]any)
265
- if !ok {
266
- return nil, fmt.Errorf("%q must be an array of strings", key)
267
- }
268
- out := make([]string, 0, len(values))
269
- for i, value := range values {
270
- text, ok := value.(string)
271
- if !ok || strings.TrimSpace(text) == "" {
272
- return nil, fmt.Errorf("%q[%d] must be a non-empty string", key, i)
273
- }
274
- out = append(out, text)
275
- }
276
- return out, nil
277
- }
278
-
279
- func statementRemovalRange(text string, node *shimast.Node) (int, int) {
280
- // Remove the whole physical statement line when the statement starts after
281
- // indentation. This keeps blank-line churn small for common one-statement
282
- // debug calls.
283
- start := clamp(node.Pos(), 0, len(text))
284
- end := clamp(node.End(), start, len(text))
285
- lineStart := start
286
- for lineStart > 0 && text[lineStart-1] != '\n' && text[lineStart-1] != '\r' {
287
- lineStart--
288
- }
289
- if strings.TrimSpace(text[lineStart:start]) == "" {
290
- start = lineStart
291
- }
292
- if end < len(text) && text[end] == ';' {
293
- end++
294
- }
295
- for end < len(text) && (text[end] == ' ' || text[end] == '\t') {
296
- end++
297
- }
298
- if end < len(text) && text[end] == '\r' {
299
- end++
300
- }
301
- if end < len(text) && text[end] == '\n' {
302
- end++
303
- }
304
- return start, end
305
- }
306
-
307
- func applyTextEdits(text string, edits []textEdit) string {
308
- if len(edits) == 0 {
309
- return text
310
- }
311
- sort.SliceStable(edits, func(i, j int) bool {
312
- if edits[i].start == edits[j].start {
313
- return edits[i].end > edits[j].end
314
- }
315
- return edits[i].start > edits[j].start
316
- })
317
- out := text
318
- lastStart := len(text) + 1
319
- for _, edit := range edits {
320
- if edit.start < 0 || edit.end < edit.start || edit.start > len(out) {
321
- continue
322
- }
323
- if edit.end > lastStart {
324
- // Nested edits are collapsed by letting the earlier source range own the
325
- // bytes. This keeps repeated rule matches from corrupting offsets.
326
- edit.end = lastStart
327
- }
328
- if edit.end > len(out) {
329
- edit.end = len(out)
330
- }
331
- out = out[:edit.start] + edit.text + out[edit.end:]
332
- lastStart = edit.start
333
- }
334
- return out
335
- }
336
-
337
- func findConfig(pluginsJSON string) (map[string]any, error) {
338
- if strings.TrimSpace(pluginsJSON) == "" {
339
- return nil, fmt.Errorf("@ttsc/strip: missing --plugins-json")
340
- }
341
- var entries []pluginEntry
342
- if err := json.Unmarshal([]byte(pluginsJSON), &entries); err != nil {
343
- return nil, fmt.Errorf("@ttsc/strip: invalid --plugins-json: %w", err)
344
- }
345
- for _, entry := range entries {
346
- if entry.Name == "@ttsc/strip" {
347
- if entry.Config == nil {
348
- return map[string]any{}, nil
349
- }
350
- return entry.Config, nil
351
- }
352
- }
353
- return nil, fmt.Errorf("@ttsc/strip: plugin entry not found")
354
- }
355
-
356
- func isJavaScriptOutput(fileName string) bool {
357
- switch strings.ToLower(filepath.Ext(fileName)) {
358
- case ".js", ".mjs", ".cjs":
359
- return true
360
- default:
361
- return false
362
- }
363
- }
364
-
365
- func equalStringSlices(a []string, b []string) bool {
366
- if len(a) != len(b) {
367
- return false
368
- }
369
- for i := range a {
370
- if a[i] != b[i] {
371
- return false
372
- }
373
- }
374
- return true
375
- }
376
-
377
- func clamp(value int, min int, max int) int {
378
- if value < min {
379
- return min
380
- }
381
- if value > max {
382
- return max
383
- }
384
- return value
385
- }
3
+ // Strip transform logic lives in github.com/samchon/ttsc/packages/ttsc/utility.
package/src/index.cjs CHANGED
@@ -7,6 +7,9 @@ module.exports = function createTtscStrip() {
7
7
  return {
8
8
  name: "@ttsc/strip",
9
9
  source: path.resolve(__dirname, "..", "plugin"),
10
- stage: "output",
10
+ stage: "transform",
11
+ hooks: {
12
+ source: true,
13
+ },
11
14
  };
12
15
  };