@ttsc/strip 0.7.2 → 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 +3 -3
- package/go.mod +1 -5
- package/package.json +1 -1
- package/plugin/main.go +24 -19
- package/plugin/strip.go +1 -357
- package/src/index.cjs +4 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://github.com/samchon/ttsc/tree/master/docs)
|
|
10
10
|
[](https://discord.gg/E94XhzrUCZ)
|
|
11
11
|
|
|
12
|
-
`@ttsc/strip` removes configured call-expression statements and debugger statements from
|
|
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
|
-
//
|
|
58
|
-
{ "transform": "@ttsc/banner", "banner": "
|
|
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
package/plugin/main.go
CHANGED
|
@@ -1,31 +1,36 @@
|
|
|
1
|
+
// Native sidecar entrypoint for `@ttsc/strip`.
|
|
1
2
|
package main
|
|
2
3
|
|
|
3
4
|
import (
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
|
|
8
|
+
"github.com/samchon/ttsc/packages/ttsc/utility"
|
|
6
9
|
)
|
|
7
10
|
|
|
8
11
|
const version = "0.0.1"
|
|
9
12
|
|
|
10
13
|
func main() {
|
|
11
|
-
|
|
14
|
+
os.Exit(run(os.Args[1:]))
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
func run(args []string) int {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
if len(args) == 0 {
|
|
19
|
+
fmt.Fprintln(os.Stderr, "@ttsc/strip: command required (expected build|transform|check|version)")
|
|
20
|
+
return 2
|
|
21
|
+
}
|
|
22
|
+
switch args[0] {
|
|
23
|
+
case "-v", "--version", "version":
|
|
24
|
+
fmt.Fprintf(os.Stdout, "@ttsc/strip %s\n", version)
|
|
25
|
+
return 0
|
|
26
|
+
case "build":
|
|
27
|
+
return utility.RunBuild(args[1:])
|
|
28
|
+
case "transform":
|
|
29
|
+
return utility.RunTransform(args[1:])
|
|
30
|
+
case "check":
|
|
31
|
+
return 0
|
|
32
|
+
default:
|
|
33
|
+
fmt.Fprintf(os.Stderr, "@ttsc/strip: unknown command %q\n", args[0])
|
|
34
|
+
return 2
|
|
35
|
+
}
|
|
31
36
|
}
|
package/plugin/strip.go
CHANGED
|
@@ -1,359 +1,3 @@
|
|
|
1
1
|
package main
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
"encoding/json"
|
|
5
|
-
"flag"
|
|
6
|
-
"fmt"
|
|
7
|
-
"os"
|
|
8
|
-
"path/filepath"
|
|
9
|
-
"sort"
|
|
10
|
-
"strings"
|
|
11
|
-
|
|
12
|
-
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
13
|
-
shimcore "github.com/microsoft/typescript-go/shim/core"
|
|
14
|
-
shimparser "github.com/microsoft/typescript-go/shim/parser"
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
type pluginEntry struct {
|
|
18
|
-
Config map[string]any `json:"config"`
|
|
19
|
-
Name string `json:"name"`
|
|
20
|
-
Stage string `json:"stage"`
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type stripTransform struct {
|
|
24
|
-
calls []callPattern
|
|
25
|
-
stripDebugger bool
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
type callPattern struct {
|
|
29
|
-
parts []string
|
|
30
|
-
wildcard bool
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
type textEdit struct {
|
|
34
|
-
start int
|
|
35
|
-
end int
|
|
36
|
-
text string
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
func RunOutput(args []string) int {
|
|
40
|
-
fs := flag.NewFlagSet("output", flag.ContinueOnError)
|
|
41
|
-
fs.SetOutput(os.Stderr)
|
|
42
|
-
file := fs.String("file", "", "emitted file to transform")
|
|
43
|
-
out := fs.String("out", "", "write transformed text to this file instead of updating --file")
|
|
44
|
-
_ = fs.String("cwd", "", "project directory")
|
|
45
|
-
_ = fs.String("outDir", "", "emit directory override")
|
|
46
|
-
pluginsJSON := fs.String("plugins-json", "", "ttsc plugin manifest JSON")
|
|
47
|
-
_ = fs.String("tsconfig", "tsconfig.json", "project tsconfig")
|
|
48
|
-
if err := fs.Parse(args); err != nil {
|
|
49
|
-
return 2
|
|
50
|
-
}
|
|
51
|
-
if *file == "" {
|
|
52
|
-
fmt.Fprintln(os.Stderr, "@ttsc/strip: output requires --file")
|
|
53
|
-
return 2
|
|
54
|
-
}
|
|
55
|
-
config, err := findConfig(*pluginsJSON)
|
|
56
|
-
if err != nil {
|
|
57
|
-
fmt.Fprintln(os.Stderr, err)
|
|
58
|
-
return 2
|
|
59
|
-
}
|
|
60
|
-
text, err := os.ReadFile(*file)
|
|
61
|
-
if err != nil {
|
|
62
|
-
fmt.Fprintf(os.Stderr, "@ttsc/strip: read %s: %v\n", *file, err)
|
|
63
|
-
return 2
|
|
64
|
-
}
|
|
65
|
-
patched, err := Apply(*file, string(text), config)
|
|
66
|
-
if err != nil {
|
|
67
|
-
fmt.Fprintln(os.Stderr, err)
|
|
68
|
-
return 2
|
|
69
|
-
}
|
|
70
|
-
target := *file
|
|
71
|
-
if *out != "" {
|
|
72
|
-
target = *out
|
|
73
|
-
}
|
|
74
|
-
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
|
75
|
-
fmt.Fprintf(os.Stderr, "@ttsc/strip: mkdir: %v\n", err)
|
|
76
|
-
return 2
|
|
77
|
-
}
|
|
78
|
-
if err := os.WriteFile(target, []byte(patched), 0o644); err != nil {
|
|
79
|
-
fmt.Fprintf(os.Stderr, "@ttsc/strip: write %s: %v\n", target, err)
|
|
80
|
-
return 2
|
|
81
|
-
}
|
|
82
|
-
return 0
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
func Apply(fileName string, text string, config map[string]any) (string, error) {
|
|
86
|
-
strip, err := parseStrip(config)
|
|
87
|
-
if err != nil {
|
|
88
|
-
return "", err
|
|
89
|
-
}
|
|
90
|
-
return strip.apply(fileName, text)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
func parseStrip(config map[string]any) (*stripTransform, error) {
|
|
94
|
-
calls, err := stringArrayConfig(config, "calls")
|
|
95
|
-
if err != nil {
|
|
96
|
-
return nil, fmt.Errorf("@ttsc/strip: %w", err)
|
|
97
|
-
}
|
|
98
|
-
statements, err := stringArrayConfig(config, "statements")
|
|
99
|
-
if err != nil {
|
|
100
|
-
return nil, fmt.Errorf("@ttsc/strip: %w", err)
|
|
101
|
-
}
|
|
102
|
-
out := &stripTransform{}
|
|
103
|
-
for _, call := range calls {
|
|
104
|
-
pattern, err := parseCallPattern(call)
|
|
105
|
-
if err != nil {
|
|
106
|
-
return nil, fmt.Errorf("@ttsc/strip: %w", err)
|
|
107
|
-
}
|
|
108
|
-
out.calls = append(out.calls, pattern)
|
|
109
|
-
}
|
|
110
|
-
for _, statement := range statements {
|
|
111
|
-
switch statement {
|
|
112
|
-
case "debugger":
|
|
113
|
-
out.stripDebugger = true
|
|
114
|
-
default:
|
|
115
|
-
return nil, fmt.Errorf("@ttsc/strip: unsupported statement pattern %q", statement)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return out, nil
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
func (s *stripTransform) apply(fileName string, text string) (string, error) {
|
|
122
|
-
if s == nil || !isJavaScriptOutput(fileName) || (len(s.calls) == 0 && !s.stripDebugger) {
|
|
123
|
-
return text, nil
|
|
124
|
-
}
|
|
125
|
-
file := parseJS(fileName, text)
|
|
126
|
-
if file == nil {
|
|
127
|
-
return text, nil
|
|
128
|
-
}
|
|
129
|
-
edits := make([]textEdit, 0)
|
|
130
|
-
var walk func(*shimast.Node)
|
|
131
|
-
walk = func(node *shimast.Node) {
|
|
132
|
-
if node == nil {
|
|
133
|
-
return
|
|
134
|
-
}
|
|
135
|
-
switch node.Kind {
|
|
136
|
-
case shimast.KindDebuggerStatement:
|
|
137
|
-
if s.stripDebugger {
|
|
138
|
-
start, end := statementRemovalRange(text, node)
|
|
139
|
-
edits = append(edits, textEdit{start: start, end: end})
|
|
140
|
-
}
|
|
141
|
-
case shimast.KindExpressionStatement:
|
|
142
|
-
expr := node.AsExpressionStatement().Expression
|
|
143
|
-
name, ok := callExpressionName(expr)
|
|
144
|
-
if ok && s.matchesCall(name) {
|
|
145
|
-
start, end := statementRemovalRange(text, node)
|
|
146
|
-
edits = append(edits, textEdit{start: start, end: end})
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
node.ForEachChild(func(child *shimast.Node) bool {
|
|
150
|
-
walk(child)
|
|
151
|
-
return false
|
|
152
|
-
})
|
|
153
|
-
}
|
|
154
|
-
for _, stmt := range file.Statements.Nodes {
|
|
155
|
-
walk(stmt)
|
|
156
|
-
}
|
|
157
|
-
return applyTextEdits(text, edits), nil
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
func (s *stripTransform) matchesCall(name string) bool {
|
|
161
|
-
for _, pattern := range s.calls {
|
|
162
|
-
if pattern.matches(name) {
|
|
163
|
-
return true
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
return false
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
func parseCallPattern(text string) (callPattern, error) {
|
|
170
|
-
parts := strings.Split(text, ".")
|
|
171
|
-
if len(parts) == 0 {
|
|
172
|
-
return callPattern{}, fmt.Errorf("empty call pattern")
|
|
173
|
-
}
|
|
174
|
-
for i, part := range parts {
|
|
175
|
-
if part == "" {
|
|
176
|
-
return callPattern{}, fmt.Errorf("invalid call pattern %q", text)
|
|
177
|
-
}
|
|
178
|
-
if part == "*" && i != len(parts)-1 {
|
|
179
|
-
return callPattern{}, fmt.Errorf("wildcard is only supported at the end of call pattern %q", text)
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
wildcard := parts[len(parts)-1] == "*"
|
|
183
|
-
if wildcard {
|
|
184
|
-
parts = parts[:len(parts)-1]
|
|
185
|
-
}
|
|
186
|
-
return callPattern{parts: parts, wildcard: wildcard}, nil
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
func (p callPattern) matches(name string) bool {
|
|
190
|
-
parts := strings.Split(name, ".")
|
|
191
|
-
if p.wildcard {
|
|
192
|
-
if len(parts) <= len(p.parts) {
|
|
193
|
-
return false
|
|
194
|
-
}
|
|
195
|
-
return equalStringSlices(parts[:len(p.parts)], p.parts)
|
|
196
|
-
}
|
|
197
|
-
return equalStringSlices(parts, p.parts)
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
func callExpressionName(expr *shimast.Node) (string, bool) {
|
|
201
|
-
if expr == nil || expr.Kind != shimast.KindCallExpression {
|
|
202
|
-
return "", false
|
|
203
|
-
}
|
|
204
|
-
call := expr.AsCallExpression()
|
|
205
|
-
return dottedName(call.Expression)
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
func dottedName(expr *shimast.Node) (string, bool) {
|
|
209
|
-
if expr == nil {
|
|
210
|
-
return "", false
|
|
211
|
-
}
|
|
212
|
-
switch expr.Kind {
|
|
213
|
-
case shimast.KindIdentifier:
|
|
214
|
-
return expr.Text(), true
|
|
215
|
-
case shimast.KindPropertyAccessExpression:
|
|
216
|
-
prop := expr.AsPropertyAccessExpression()
|
|
217
|
-
left, ok := dottedName(prop.Expression)
|
|
218
|
-
if !ok || prop.Name() == nil {
|
|
219
|
-
return "", false
|
|
220
|
-
}
|
|
221
|
-
return left + "." + prop.Name().Text(), true
|
|
222
|
-
default:
|
|
223
|
-
return "", false
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
func parseJS(fileName string, text string) *shimast.SourceFile {
|
|
228
|
-
normalized := filepath.ToSlash(fileName)
|
|
229
|
-
if !filepath.IsAbs(normalized) {
|
|
230
|
-
if abs, err := filepath.Abs(normalized); err == nil {
|
|
231
|
-
normalized = filepath.ToSlash(abs)
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
opts := shimast.SourceFileParseOptions{FileName: normalized}
|
|
235
|
-
return shimparser.ParseSourceFile(opts, text, shimcore.ScriptKindJS)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
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
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
func statementRemovalRange(text string, node *shimast.Node) (int, int) {
|
|
259
|
-
start := clamp(node.Pos(), 0, len(text))
|
|
260
|
-
end := clamp(node.End(), start, len(text))
|
|
261
|
-
lineStart := start
|
|
262
|
-
for lineStart > 0 && text[lineStart-1] != '\n' && text[lineStart-1] != '\r' {
|
|
263
|
-
lineStart--
|
|
264
|
-
}
|
|
265
|
-
if strings.TrimSpace(text[lineStart:start]) == "" {
|
|
266
|
-
start = lineStart
|
|
267
|
-
}
|
|
268
|
-
if end < len(text) && text[end] == ';' {
|
|
269
|
-
end++
|
|
270
|
-
}
|
|
271
|
-
for end < len(text) && (text[end] == ' ' || text[end] == '\t') {
|
|
272
|
-
end++
|
|
273
|
-
}
|
|
274
|
-
if end < len(text) && text[end] == '\r' {
|
|
275
|
-
end++
|
|
276
|
-
}
|
|
277
|
-
if end < len(text) && text[end] == '\n' {
|
|
278
|
-
end++
|
|
279
|
-
}
|
|
280
|
-
return start, end
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
func applyTextEdits(text string, edits []textEdit) string {
|
|
284
|
-
if len(edits) == 0 {
|
|
285
|
-
return text
|
|
286
|
-
}
|
|
287
|
-
sort.SliceStable(edits, func(i, j int) bool {
|
|
288
|
-
if edits[i].start == edits[j].start {
|
|
289
|
-
return edits[i].end > edits[j].end
|
|
290
|
-
}
|
|
291
|
-
return edits[i].start > edits[j].start
|
|
292
|
-
})
|
|
293
|
-
out := text
|
|
294
|
-
lastStart := len(text) + 1
|
|
295
|
-
for _, edit := range edits {
|
|
296
|
-
if edit.start < 0 || edit.end < edit.start || edit.start > len(out) {
|
|
297
|
-
continue
|
|
298
|
-
}
|
|
299
|
-
if edit.end > lastStart {
|
|
300
|
-
edit.end = lastStart
|
|
301
|
-
}
|
|
302
|
-
if edit.end > len(out) {
|
|
303
|
-
edit.end = len(out)
|
|
304
|
-
}
|
|
305
|
-
out = out[:edit.start] + edit.text + out[edit.end:]
|
|
306
|
-
lastStart = edit.start
|
|
307
|
-
}
|
|
308
|
-
return out
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
func findConfig(pluginsJSON string) (map[string]any, error) {
|
|
312
|
-
if strings.TrimSpace(pluginsJSON) == "" {
|
|
313
|
-
return nil, fmt.Errorf("@ttsc/strip: missing --plugins-json")
|
|
314
|
-
}
|
|
315
|
-
var entries []pluginEntry
|
|
316
|
-
if err := json.Unmarshal([]byte(pluginsJSON), &entries); err != nil {
|
|
317
|
-
return nil, fmt.Errorf("@ttsc/strip: invalid --plugins-json: %w", err)
|
|
318
|
-
}
|
|
319
|
-
for _, entry := range entries {
|
|
320
|
-
if entry.Name == "@ttsc/strip" {
|
|
321
|
-
if entry.Config == nil {
|
|
322
|
-
return map[string]any{}, nil
|
|
323
|
-
}
|
|
324
|
-
return entry.Config, nil
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
return nil, fmt.Errorf("@ttsc/strip: plugin entry not found")
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
func isJavaScriptOutput(fileName string) bool {
|
|
331
|
-
switch strings.ToLower(filepath.Ext(fileName)) {
|
|
332
|
-
case ".js", ".mjs", ".cjs":
|
|
333
|
-
return true
|
|
334
|
-
default:
|
|
335
|
-
return false
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
func equalStringSlices(a []string, b []string) bool {
|
|
340
|
-
if len(a) != len(b) {
|
|
341
|
-
return false
|
|
342
|
-
}
|
|
343
|
-
for i := range a {
|
|
344
|
-
if a[i] != b[i] {
|
|
345
|
-
return false
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return true
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
func clamp(value int, min int, max int) int {
|
|
352
|
-
if value < min {
|
|
353
|
-
return min
|
|
354
|
-
}
|
|
355
|
-
if value > max {
|
|
356
|
-
return max
|
|
357
|
-
}
|
|
358
|
-
return value
|
|
359
|
-
}
|
|
3
|
+
// Strip transform logic lives in github.com/samchon/ttsc/packages/ttsc/utility.
|