@ttsc/strip 0.12.2 → 0.12.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/driver/strip.go +245 -203
  2. package/package.json +1 -1
package/driver/strip.go CHANGED
@@ -1,268 +1,310 @@
1
1
  package strip
2
2
 
3
3
  import (
4
- "fmt"
5
- "strings"
4
+ "fmt"
5
+ "strings"
6
6
 
7
- shimast "github.com/microsoft/typescript-go/shim/ast"
7
+ shimast "github.com/microsoft/typescript-go/shim/ast"
8
8
 
9
- "github.com/samchon/ttsc/packages/ttsc/driver"
9
+ "github.com/samchon/ttsc/packages/ttsc/driver"
10
10
  )
11
11
 
12
12
  func init() {
13
- driver.RegisterPlugin(plugin{})
13
+ driver.RegisterPlugin(plugin{})
14
14
  }
15
15
 
16
+ // plugin implements driver.ProgramPlugin for @ttsc/strip.
16
17
  type plugin struct{}
17
18
 
19
+ // ApplyProgram strips configured call expressions and debugger statements from
20
+ // every source file in the program.
18
21
  func (plugin) ApplyProgram(prog *driver.Program, ctx driver.PluginContext) error {
19
- rewriter, err := parseStrip(ctx.Entry.Config)
20
- if err != nil {
21
- return err
22
- }
23
- for _, file := range prog.SourceFiles() {
24
- rewriter.apply(file)
25
- }
26
- return nil
22
+ rewriter, err := parseStrip(ctx.Entry.Config)
23
+ if err != nil {
24
+ return err
25
+ }
26
+ for _, file := range prog.SourceFiles() {
27
+ rewriter.apply(file)
28
+ }
29
+ return nil
27
30
  }
28
31
 
32
+ // stripRewriter holds the resolved strip configuration for a single build.
29
33
  type stripRewriter struct {
30
- calls []callPattern
31
- stripDebugger bool
34
+ calls []callPattern
35
+ stripDebugger bool
32
36
  }
33
37
 
38
+ // callPattern represents a parsed call-expression stripping rule such as
39
+ // "console.log" (exact) or "assert.*" (wildcard prefix).
34
40
  type callPattern struct {
35
- parts []string
36
- wildcard bool
41
+ parts []string
42
+ wildcard bool
37
43
  }
38
44
 
45
+ // parseStrip builds a stripRewriter from the plugin config map. When neither
46
+ // "calls" nor "statements" is present the default configuration is applied:
47
+ // strip console.log, console.debug, assert.*, and debugger statements.
39
48
  func parseStrip(config map[string]any) (*stripRewriter, error) {
40
- _, hasCalls := config["calls"]
41
- _, hasStatements := config["statements"]
42
- if !hasCalls && !hasStatements {
43
- config = map[string]any{
44
- "calls": []any{"console.log", "console.debug", "assert.*"},
45
- "statements": []any{"debugger"},
46
- }
47
- }
48
- calls, err := stringArrayConfig(config, "calls")
49
- if err != nil {
50
- return nil, fmt.Errorf("@ttsc/strip: %w", err)
51
- }
52
- statements, err := stringArrayConfig(config, "statements")
53
- if err != nil {
54
- return nil, fmt.Errorf("@ttsc/strip: %w", err)
55
- }
56
- out := &stripRewriter{}
57
- for _, call := range calls {
58
- pattern, err := parseCallPattern(call)
59
- if err != nil {
60
- return nil, fmt.Errorf("@ttsc/strip: %w", err)
61
- }
62
- out.calls = append(out.calls, pattern)
63
- }
64
- for _, statement := range statements {
65
- switch statement {
66
- case "debugger":
67
- out.stripDebugger = true
68
- default:
69
- return nil, fmt.Errorf("@ttsc/strip: unsupported statement pattern %q", statement)
70
- }
71
- }
72
- return out, nil
49
+ _, hasCalls := config["calls"]
50
+ _, hasStatements := config["statements"]
51
+ if !hasCalls && !hasStatements {
52
+ config = map[string]any{
53
+ "calls": []any{"console.log", "console.debug", "assert.*"},
54
+ "statements": []any{"debugger"},
55
+ }
56
+ }
57
+ calls, err := stringArrayConfig(config, "calls")
58
+ if err != nil {
59
+ return nil, fmt.Errorf("@ttsc/strip: %w", err)
60
+ }
61
+ statements, err := stringArrayConfig(config, "statements")
62
+ if err != nil {
63
+ return nil, fmt.Errorf("@ttsc/strip: %w", err)
64
+ }
65
+ out := &stripRewriter{}
66
+ for _, call := range calls {
67
+ pattern, err := parseCallPattern(call)
68
+ if err != nil {
69
+ return nil, fmt.Errorf("@ttsc/strip: %w", err)
70
+ }
71
+ out.calls = append(out.calls, pattern)
72
+ }
73
+ for _, statement := range statements {
74
+ switch statement {
75
+ case "debugger":
76
+ out.stripDebugger = true
77
+ default:
78
+ return nil, fmt.Errorf("@ttsc/strip: unsupported statement pattern %q", statement)
79
+ }
80
+ }
81
+ return out, nil
73
82
  }
74
83
 
84
+ // apply removes matching statements from file's top-level statement list.
75
85
  func (s *stripRewriter) apply(file *shimast.SourceFile) {
76
- if s == nil || file == nil || (len(s.calls) == 0 && !s.stripDebugger) {
77
- return
78
- }
79
- filterStatements(file.Statements, s)
86
+ if s == nil || file == nil || (len(s.calls) == 0 && !s.stripDebugger) {
87
+ return
88
+ }
89
+ filterStatements(file.Statements, s)
80
90
  }
81
91
 
92
+ // filterStatements removes stripped statements from list in-place, preserving
93
+ // order. Children of retained statements are recursively filtered.
82
94
  func filterStatements(list *shimast.NodeList, strip *stripRewriter) {
83
- if list == nil || len(list.Nodes) == 0 {
84
- return
85
- }
86
- out := make([]*shimast.Node, 0, len(list.Nodes))
87
- for _, stmt := range list.Nodes {
88
- if shouldStripStatement(stmt, strip) {
89
- continue
90
- }
91
- filterChildStatements(stmt, strip)
92
- out = append(out, stmt)
93
- }
94
- list.Nodes = out
95
+ if list == nil || len(list.Nodes) == 0 {
96
+ return
97
+ }
98
+ out := make([]*shimast.Node, 0, len(list.Nodes))
99
+ for _, stmt := range list.Nodes {
100
+ if shouldStripStatement(stmt, strip) {
101
+ continue
102
+ }
103
+ filterChildStatements(stmt, strip)
104
+ out = append(out, stmt)
105
+ }
106
+ list.Nodes = out
95
107
  }
96
108
 
109
+ // filterChildStatements recurses into node's children, filtering embedded
110
+ // single-statement bodies (if, while, for, etc.) and nested statement lists.
97
111
  func filterChildStatements(node *shimast.Node, strip *stripRewriter) {
98
- if node == nil {
99
- return
100
- }
101
- filterEmbeddedStatements(node, strip)
102
- if node.CanHaveStatements() {
103
- filterStatements(node.StatementList(), strip)
104
- }
105
- node.ForEachChild(func(child *shimast.Node) bool {
106
- filterChildStatements(child, strip)
107
- return false
108
- })
112
+ if node == nil {
113
+ return
114
+ }
115
+ filterEmbeddedStatements(node, strip)
116
+ if node.CanHaveStatements() {
117
+ filterStatements(node.StatementList(), strip)
118
+ }
119
+ node.ForEachChild(func(child *shimast.Node) bool {
120
+ filterChildStatements(child, strip)
121
+ return false
122
+ })
109
123
  }
110
124
 
125
+ // filterEmbeddedStatements handles statement nodes that embed a single child
126
+ // statement (if/else, do, while, for, with, labeled). A stripped child is
127
+ // replaced with an empty synthesized statement to preserve the AST shape.
111
128
  func filterEmbeddedStatements(node *shimast.Node, strip *stripRewriter) {
112
- switch node.Kind {
113
- case shimast.KindIfStatement:
114
- stmt := node.AsIfStatement()
115
- stmt.ThenStatement = filterEmbeddedStatement(stmt.ThenStatement, strip)
116
- stmt.ElseStatement = filterEmbeddedStatement(stmt.ElseStatement, strip)
117
- case shimast.KindDoStatement:
118
- stmt := node.AsDoStatement()
119
- stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
120
- case shimast.KindWhileStatement:
121
- stmt := node.AsWhileStatement()
122
- stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
123
- case shimast.KindForStatement:
124
- stmt := node.AsForStatement()
125
- stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
126
- case shimast.KindForInStatement, shimast.KindForOfStatement:
127
- stmt := node.AsForInOrOfStatement()
128
- stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
129
- case shimast.KindWithStatement:
130
- stmt := node.AsWithStatement()
131
- stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
132
- case shimast.KindLabeledStatement:
133
- stmt := node.AsLabeledStatement()
134
- stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
135
- }
129
+ switch node.Kind {
130
+ case shimast.KindIfStatement:
131
+ stmt := node.AsIfStatement()
132
+ stmt.ThenStatement = filterEmbeddedStatement(stmt.ThenStatement, strip)
133
+ stmt.ElseStatement = filterEmbeddedStatement(stmt.ElseStatement, strip)
134
+ case shimast.KindDoStatement:
135
+ stmt := node.AsDoStatement()
136
+ stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
137
+ case shimast.KindWhileStatement:
138
+ stmt := node.AsWhileStatement()
139
+ stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
140
+ case shimast.KindForStatement:
141
+ stmt := node.AsForStatement()
142
+ stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
143
+ case shimast.KindForInStatement, shimast.KindForOfStatement:
144
+ stmt := node.AsForInOrOfStatement()
145
+ stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
146
+ case shimast.KindWithStatement:
147
+ stmt := node.AsWithStatement()
148
+ stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
149
+ case shimast.KindLabeledStatement:
150
+ stmt := node.AsLabeledStatement()
151
+ stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
152
+ }
136
153
  }
137
154
 
155
+ // filterEmbeddedStatement strips or recurses into a single embedded statement.
156
+ // Returns an empty synthesized statement when stmt is to be stripped, preserving
157
+ // the original source location for downstream source-map accuracy.
138
158
  func filterEmbeddedStatement(stmt *shimast.Statement, strip *stripRewriter) *shimast.Statement {
139
- if stmt == nil {
140
- return nil
141
- }
142
- if shouldStripStatement(stmt, strip) {
143
- return emptyStatement(stmt)
144
- }
145
- filterChildStatements(stmt, strip)
146
- return stmt
159
+ if stmt == nil {
160
+ return nil
161
+ }
162
+ if shouldStripStatement(stmt, strip) {
163
+ return emptyStatement(stmt)
164
+ }
165
+ filterChildStatements(stmt, strip)
166
+ return stmt
147
167
  }
148
168
 
169
+ // emptyStatement creates a synthesized empty statement (";") that inherits
170
+ // original's source location, used as a no-op placeholder after stripping.
149
171
  func emptyStatement(original *shimast.Node) *shimast.Statement {
150
- empty := shimast.NewNodeFactory(shimast.NodeFactoryHooks{}).NewEmptyStatement()
151
- empty.Flags |= shimast.NodeFlagsSynthesized
152
- if original != nil {
153
- empty.Loc = original.Loc
154
- }
155
- return empty
172
+ empty := shimast.NewNodeFactory(shimast.NodeFactoryHooks{}).NewEmptyStatement()
173
+ empty.Flags |= shimast.NodeFlagsSynthesized
174
+ if original != nil {
175
+ empty.Loc = original.Loc
176
+ }
177
+ return empty
156
178
  }
157
179
 
180
+ // shouldStripStatement reports whether node should be removed based on the
181
+ // current strip configuration. Only debugger and expression statements are
182
+ // candidates; all other statement kinds are retained.
158
183
  func shouldStripStatement(node *shimast.Node, strip *stripRewriter) bool {
159
- if node == nil {
160
- return false
161
- }
162
- switch node.Kind {
163
- case shimast.KindDebuggerStatement:
164
- return strip.stripDebugger
165
- case shimast.KindExpressionStatement:
166
- expr := node.AsExpressionStatement().Expression
167
- name, ok := callExpressionName(expr)
168
- return ok && strip.matchesCall(name)
169
- default:
170
- return false
171
- }
184
+ if node == nil {
185
+ return false
186
+ }
187
+ switch node.Kind {
188
+ case shimast.KindDebuggerStatement:
189
+ return strip.stripDebugger
190
+ case shimast.KindExpressionStatement:
191
+ expr := node.AsExpressionStatement().Expression
192
+ name, ok := callExpressionName(expr)
193
+ return ok && strip.matchesCall(name)
194
+ default:
195
+ return false
196
+ }
172
197
  }
173
198
 
199
+ // matchesCall reports whether name matches any of the configured call patterns.
174
200
  func (s *stripRewriter) matchesCall(name string) bool {
175
- for _, pattern := range s.calls {
176
- if pattern.matches(name) {
177
- return true
178
- }
179
- }
180
- return false
201
+ for _, pattern := range s.calls {
202
+ if pattern.matches(name) {
203
+ return true
204
+ }
205
+ }
206
+ return false
181
207
  }
182
208
 
209
+ // parseCallPattern parses a dot-separated call pattern string such as
210
+ // "console.log" or "assert.*". A wildcard ("*") is only allowed as the
211
+ // final segment. Empty segments are rejected.
183
212
  func parseCallPattern(text string) (callPattern, error) {
184
- parts := strings.Split(text, ".")
185
- for i, part := range parts {
186
- if part == "" {
187
- return callPattern{}, fmt.Errorf("invalid call pattern %q", text)
188
- }
189
- if part == "*" && i != len(parts)-1 {
190
- return callPattern{}, fmt.Errorf("wildcard is only supported at the end of call pattern %q", text)
191
- }
192
- }
193
- wildcard := parts[len(parts)-1] == "*"
194
- if wildcard {
195
- parts = parts[:len(parts)-1]
196
- }
197
- return callPattern{parts: parts, wildcard: wildcard}, nil
213
+ parts := strings.Split(text, ".")
214
+ for i, part := range parts {
215
+ if part == "" {
216
+ return callPattern{}, fmt.Errorf("invalid call pattern %q", text)
217
+ }
218
+ if part == "*" && i != len(parts)-1 {
219
+ return callPattern{}, fmt.Errorf("wildcard is only supported at the end of call pattern %q", text)
220
+ }
221
+ }
222
+ wildcard := parts[len(parts)-1] == "*"
223
+ if wildcard {
224
+ parts = parts[:len(parts)-1]
225
+ }
226
+ return callPattern{parts: parts, wildcard: wildcard}, nil
198
227
  }
199
228
 
229
+ // matches reports whether a dotted call name (e.g. "console.log") matches
230
+ // the pattern. Wildcard patterns require at least one extra segment beyond
231
+ // the pattern prefix.
200
232
  func (p callPattern) matches(name string) bool {
201
- parts := strings.Split(name, ".")
202
- if p.wildcard {
203
- if len(parts) <= len(p.parts) {
204
- return false
205
- }
206
- return equalStringSlices(parts[:len(p.parts)], p.parts)
207
- }
208
- return equalStringSlices(parts, p.parts)
233
+ parts := strings.Split(name, ".")
234
+ if p.wildcard {
235
+ if len(parts) <= len(p.parts) {
236
+ return false
237
+ }
238
+ return equalStringSlices(parts[:len(p.parts)], p.parts)
239
+ }
240
+ return equalStringSlices(parts, p.parts)
209
241
  }
210
242
 
243
+ // callExpressionName extracts the dotted callee name from a call expression
244
+ // node, e.g. "console.log" from `console.log(...)`. Returns ("", false) when
245
+ // expr is not a call expression or the callee is not a dotted identifier chain.
211
246
  func callExpressionName(expr *shimast.Node) (string, bool) {
212
- if expr == nil || expr.Kind != shimast.KindCallExpression {
213
- return "", false
214
- }
215
- call := expr.AsCallExpression()
216
- return dottedName(call.Expression)
247
+ if expr == nil || expr.Kind != shimast.KindCallExpression {
248
+ return "", false
249
+ }
250
+ call := expr.AsCallExpression()
251
+ return dottedName(call.Expression)
217
252
  }
218
253
 
254
+ // dottedName recursively extracts a dot-joined identifier chain from an
255
+ // expression node. Returns ("", false) for any non-identifier, non-property-
256
+ // access node.
219
257
  func dottedName(expr *shimast.Node) (string, bool) {
220
- if expr == nil {
221
- return "", false
222
- }
223
- switch expr.Kind {
224
- case shimast.KindIdentifier:
225
- return expr.Text(), true
226
- case shimast.KindPropertyAccessExpression:
227
- prop := expr.AsPropertyAccessExpression()
228
- left, ok := dottedName(prop.Expression)
229
- if !ok || prop.Name() == nil {
230
- return "", false
231
- }
232
- return left + "." + prop.Name().Text(), true
233
- default:
234
- return "", false
235
- }
258
+ if expr == nil {
259
+ return "", false
260
+ }
261
+ switch expr.Kind {
262
+ case shimast.KindIdentifier:
263
+ return expr.Text(), true
264
+ case shimast.KindPropertyAccessExpression:
265
+ prop := expr.AsPropertyAccessExpression()
266
+ left, ok := dottedName(prop.Expression)
267
+ if !ok || prop.Name() == nil {
268
+ return "", false
269
+ }
270
+ return left + "." + prop.Name().Text(), true
271
+ default:
272
+ return "", false
273
+ }
236
274
  }
237
275
 
276
+ // stringArrayConfig reads a string array from config[key]. Returns nil when the
277
+ // key is absent. Returns an error when the value is not an array of non-empty strings.
238
278
  func stringArrayConfig(config map[string]any, key string) ([]string, error) {
239
- raw, ok := config[key]
240
- if !ok || raw == nil {
241
- return nil, nil
242
- }
243
- values, ok := raw.([]any)
244
- if !ok {
245
- return nil, fmt.Errorf("%q must be an array of strings", key)
246
- }
247
- out := make([]string, 0, len(values))
248
- for i, value := range values {
249
- text, ok := value.(string)
250
- if !ok || strings.TrimSpace(text) == "" {
251
- return nil, fmt.Errorf("%q[%d] must be a non-empty string", key, i)
252
- }
253
- out = append(out, text)
254
- }
255
- return out, nil
279
+ raw, ok := config[key]
280
+ if !ok || raw == nil {
281
+ return nil, nil
282
+ }
283
+ values, ok := raw.([]any)
284
+ if !ok {
285
+ return nil, fmt.Errorf("%q must be an array of strings", key)
286
+ }
287
+ out := make([]string, 0, len(values))
288
+ for i, value := range values {
289
+ text, ok := value.(string)
290
+ if !ok || strings.TrimSpace(text) == "" {
291
+ return nil, fmt.Errorf("%q[%d] must be a non-empty string", key, i)
292
+ }
293
+ out = append(out, text)
294
+ }
295
+ return out, nil
256
296
  }
257
297
 
298
+ // equalStringSlices reports whether left and right contain the same strings in
299
+ // the same order.
258
300
  func equalStringSlices(left, right []string) bool {
259
- if len(left) != len(right) {
260
- return false
261
- }
262
- for i := range left {
263
- if left[i] != right[i] {
264
- return false
265
- }
266
- }
267
- return true
301
+ if len(left) != len(right) {
302
+ return false
303
+ }
304
+ for i := range left {
305
+ if left[i] != right[i] {
306
+ return false
307
+ }
308
+ }
309
+ return true
268
310
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/strip",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
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": {