@ttsc/strip 0.5.0-dev.20260429

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jeongho Nam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # `@ttsc/strip`
2
+
3
+ ![banner of @ttsc/strip](https://raw.githubusercontent.com/samchon/ttsc/refs/heads/master/assets/og.jpg)
4
+
5
+ [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/ttsc/blob/master/LICENSE)
6
+ [![NPM Version](https://img.shields.io/npm/v/@ttsc/strip.svg)](https://www.npmjs.com/package/@ttsc/strip)
7
+ [![NPM Downloads](https://img.shields.io/npm/dm/@ttsc/strip.svg)](https://www.npmjs.com/package/@ttsc/strip)
8
+ [![Build Status](https://github.com/samchon/ttsc/workflows/test/badge.svg)](https://github.com/samchon/ttsc/actions?query=workflow%3Atest)
9
+ [![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)
10
+
11
+ `@ttsc/strip` removes configured call-expression statements and debugger statements from emitted JavaScript.
12
+
13
+ ## Setup
14
+
15
+ Install `ttsc`, TypeScript-Go, and the strip plugin:
16
+
17
+ ```bash
18
+ npm install -D ttsc @typescript/native-preview @ttsc/strip
19
+ ```
20
+
21
+ Open your project's `tsconfig.json`, then add this entry under `compilerOptions.plugins`. If the file already has `compilerOptions`, merge this into the existing object:
22
+
23
+ ```jsonc
24
+ {
25
+ "compilerOptions": {
26
+ "plugins": [
27
+ {
28
+ "transform": "@ttsc/strip",
29
+ "calls": ["console.log", "console.debug", "assert.*"],
30
+ "statements": ["debugger"]
31
+ }
32
+ ]
33
+ }
34
+ }
35
+ ```
36
+
37
+ Run your normal `ttsc` command:
38
+
39
+ ```bash
40
+ npx ttsc
41
+ ```
42
+
43
+ Only the configured patterns are removed. `@ttsc/strip` is not a minifier, tree-shaker, or dead-code-elimination pass.
44
+
45
+ ## Notes
46
+
47
+ 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.*`.
48
+
49
+ ```jsonc
50
+ {
51
+ "compilerOptions": {
52
+ "plugins": [
53
+ // Keep lint first.
54
+ { "transform": "@ttsc/lint", "rules": { "no-var": "error" } },
55
+
56
+ // Output plugins run after emit, in order.
57
+ { "transform": "@ttsc/banner", "banner": "/*! @license MIT */" },
58
+ { "transform": "@ttsc/paths" },
59
+ {
60
+ "transform": "@ttsc/strip",
61
+ "calls": ["console.log", "console.debug", "assert.*"],
62
+ "statements": ["debugger"]
63
+ }
64
+ ]
65
+ }
66
+ }
67
+ ```
@@ -0,0 +1,19 @@
1
+ module github.com/samchon/ttsc/packages/strip/go-plugin
2
+
3
+ go 1.26
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
+ )
10
+
11
+ require (
12
+ github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect
13
+ github.com/klauspost/cpuid/v2 v2.2.10 // indirect
14
+ github.com/microsoft/typescript-go v0.0.0-20260424234512-515d036f927a // indirect
15
+ github.com/zeebo/xxh3 v1.1.0 // indirect
16
+ golang.org/x/sync v0.20.0 // indirect
17
+ golang.org/x/sys v0.42.0 // indirect
18
+ golang.org/x/text v0.35.0 // indirect
19
+ )
@@ -0,0 +1,33 @@
1
+ package main
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+
7
+ "github.com/samchon/ttsc/packages/strip/go-plugin/strip"
8
+ )
9
+
10
+ const version = "0.0.1"
11
+
12
+ func main() {
13
+ os.Exit(run(os.Args[1:]))
14
+ }
15
+
16
+ func run(args []string) int {
17
+ if len(args) == 0 {
18
+ fmt.Fprintln(os.Stderr, "@ttsc/strip: command required (expected output|version)")
19
+ return 2
20
+ }
21
+ switch args[0] {
22
+ case "-v", "--version", "version":
23
+ fmt.Fprintf(os.Stdout, "@ttsc/strip %s\n", version)
24
+ return 0
25
+ case "check":
26
+ return 0
27
+ case "output":
28
+ return strip.RunOutput(args[1:])
29
+ default:
30
+ fmt.Fprintf(os.Stderr, "@ttsc/strip: unknown command %q\n", args[0])
31
+ return 2
32
+ }
33
+ }
@@ -0,0 +1,362 @@
1
+ package strip
2
+
3
+ import (
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
+ const modeStrip = "ttsc-strip"
18
+
19
+ type pluginEntry struct {
20
+ Config map[string]any `json:"config"`
21
+ Mode string `json:"mode"`
22
+ Name string `json:"name"`
23
+ }
24
+
25
+ type stripTransform struct {
26
+ calls []callPattern
27
+ stripDebugger bool
28
+ }
29
+
30
+ type callPattern struct {
31
+ parts []string
32
+ wildcard bool
33
+ }
34
+
35
+ type textEdit struct {
36
+ start int
37
+ end int
38
+ text string
39
+ }
40
+
41
+ func RunOutput(args []string) int {
42
+ fs := flag.NewFlagSet("output", flag.ContinueOnError)
43
+ fs.SetOutput(os.Stderr)
44
+ file := fs.String("file", "", "emitted file to transform")
45
+ out := fs.String("out", "", "write transformed text to this file instead of updating --file")
46
+ _ = fs.String("cwd", "", "project directory")
47
+ _ = fs.String("outDir", "", "emit directory override")
48
+ pluginsJSON := fs.String("plugins-json", "", "ttsc plugin manifest JSON")
49
+ _ = fs.String("rewrite-mode", modeStrip, "native mode")
50
+ _ = fs.String("tsconfig", "tsconfig.json", "project tsconfig")
51
+ if err := fs.Parse(args); err != nil {
52
+ return 2
53
+ }
54
+ if *file == "" {
55
+ fmt.Fprintln(os.Stderr, "@ttsc/strip: output requires --file")
56
+ return 2
57
+ }
58
+ config, err := findConfig(*pluginsJSON)
59
+ if err != nil {
60
+ fmt.Fprintln(os.Stderr, err)
61
+ return 2
62
+ }
63
+ text, err := os.ReadFile(*file)
64
+ if err != nil {
65
+ fmt.Fprintf(os.Stderr, "@ttsc/strip: read %s: %v\n", *file, err)
66
+ return 2
67
+ }
68
+ patched, err := Apply(*file, string(text), config)
69
+ if err != nil {
70
+ fmt.Fprintln(os.Stderr, err)
71
+ return 2
72
+ }
73
+ target := *file
74
+ if *out != "" {
75
+ target = *out
76
+ }
77
+ if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
78
+ fmt.Fprintf(os.Stderr, "@ttsc/strip: mkdir: %v\n", err)
79
+ return 2
80
+ }
81
+ if err := os.WriteFile(target, []byte(patched), 0o644); err != nil {
82
+ fmt.Fprintf(os.Stderr, "@ttsc/strip: write %s: %v\n", target, err)
83
+ return 2
84
+ }
85
+ return 0
86
+ }
87
+
88
+ func Apply(fileName string, text string, config map[string]any) (string, error) {
89
+ strip, err := parseStrip(config)
90
+ if err != nil {
91
+ return "", err
92
+ }
93
+ return strip.apply(fileName, text)
94
+ }
95
+
96
+ func parseStrip(config map[string]any) (*stripTransform, error) {
97
+ calls, err := stringArrayConfig(config, "calls")
98
+ if err != nil {
99
+ return nil, fmt.Errorf("@ttsc/strip: %w", err)
100
+ }
101
+ statements, err := stringArrayConfig(config, "statements")
102
+ if err != nil {
103
+ return nil, fmt.Errorf("@ttsc/strip: %w", err)
104
+ }
105
+ out := &stripTransform{}
106
+ for _, call := range calls {
107
+ pattern, err := parseCallPattern(call)
108
+ if err != nil {
109
+ return nil, fmt.Errorf("@ttsc/strip: %w", err)
110
+ }
111
+ out.calls = append(out.calls, pattern)
112
+ }
113
+ for _, statement := range statements {
114
+ switch statement {
115
+ case "debugger":
116
+ out.stripDebugger = true
117
+ default:
118
+ return nil, fmt.Errorf("@ttsc/strip: unsupported statement pattern %q", statement)
119
+ }
120
+ }
121
+ return out, nil
122
+ }
123
+
124
+ func (s *stripTransform) apply(fileName string, text string) (string, error) {
125
+ if s == nil || !isJavaScriptOutput(fileName) || (len(s.calls) == 0 && !s.stripDebugger) {
126
+ return text, nil
127
+ }
128
+ file := parseJS(fileName, text)
129
+ if file == nil {
130
+ return text, nil
131
+ }
132
+ edits := make([]textEdit, 0)
133
+ var walk func(*shimast.Node)
134
+ walk = func(node *shimast.Node) {
135
+ if node == nil {
136
+ return
137
+ }
138
+ switch node.Kind {
139
+ case shimast.KindDebuggerStatement:
140
+ if s.stripDebugger {
141
+ start, end := statementRemovalRange(text, node)
142
+ edits = append(edits, textEdit{start: start, end: end})
143
+ }
144
+ case shimast.KindExpressionStatement:
145
+ expr := node.AsExpressionStatement().Expression
146
+ name, ok := callExpressionName(expr)
147
+ if ok && s.matchesCall(name) {
148
+ start, end := statementRemovalRange(text, node)
149
+ edits = append(edits, textEdit{start: start, end: end})
150
+ }
151
+ }
152
+ node.ForEachChild(func(child *shimast.Node) bool {
153
+ walk(child)
154
+ return false
155
+ })
156
+ }
157
+ for _, stmt := range file.Statements.Nodes {
158
+ walk(stmt)
159
+ }
160
+ return applyTextEdits(text, edits), nil
161
+ }
162
+
163
+ func (s *stripTransform) matchesCall(name string) bool {
164
+ for _, pattern := range s.calls {
165
+ if pattern.matches(name) {
166
+ return true
167
+ }
168
+ }
169
+ return false
170
+ }
171
+
172
+ func parseCallPattern(text string) (callPattern, error) {
173
+ parts := strings.Split(text, ".")
174
+ if len(parts) == 0 {
175
+ return callPattern{}, fmt.Errorf("empty call pattern")
176
+ }
177
+ for i, part := range parts {
178
+ if part == "" {
179
+ return callPattern{}, fmt.Errorf("invalid call pattern %q", text)
180
+ }
181
+ if part == "*" && i != len(parts)-1 {
182
+ return callPattern{}, fmt.Errorf("wildcard is only supported at the end of call pattern %q", text)
183
+ }
184
+ }
185
+ wildcard := parts[len(parts)-1] == "*"
186
+ if wildcard {
187
+ parts = parts[:len(parts)-1]
188
+ }
189
+ return callPattern{parts: parts, wildcard: wildcard}, nil
190
+ }
191
+
192
+ func (p callPattern) matches(name string) bool {
193
+ parts := strings.Split(name, ".")
194
+ if p.wildcard {
195
+ if len(parts) <= len(p.parts) {
196
+ return false
197
+ }
198
+ return equalStringSlices(parts[:len(p.parts)], p.parts)
199
+ }
200
+ return equalStringSlices(parts, p.parts)
201
+ }
202
+
203
+ func callExpressionName(expr *shimast.Node) (string, bool) {
204
+ if expr == nil || expr.Kind != shimast.KindCallExpression {
205
+ return "", false
206
+ }
207
+ call := expr.AsCallExpression()
208
+ return dottedName(call.Expression)
209
+ }
210
+
211
+ func dottedName(expr *shimast.Node) (string, bool) {
212
+ if expr == nil {
213
+ return "", false
214
+ }
215
+ switch expr.Kind {
216
+ case shimast.KindIdentifier:
217
+ return expr.Text(), true
218
+ case shimast.KindPropertyAccessExpression:
219
+ prop := expr.AsPropertyAccessExpression()
220
+ left, ok := dottedName(prop.Expression)
221
+ if !ok || prop.Name() == nil {
222
+ return "", false
223
+ }
224
+ return left + "." + prop.Name().Text(), true
225
+ default:
226
+ return "", false
227
+ }
228
+ }
229
+
230
+ func parseJS(fileName string, text string) *shimast.SourceFile {
231
+ normalized := filepath.ToSlash(fileName)
232
+ if !filepath.IsAbs(normalized) {
233
+ if abs, err := filepath.Abs(normalized); err == nil {
234
+ normalized = filepath.ToSlash(abs)
235
+ }
236
+ }
237
+ opts := shimast.SourceFileParseOptions{FileName: normalized}
238
+ return shimparser.ParseSourceFile(opts, text, shimcore.ScriptKindJS)
239
+ }
240
+
241
+ func stringArrayConfig(config map[string]any, key string) ([]string, error) {
242
+ raw, ok := config[key]
243
+ if !ok || raw == nil {
244
+ return nil, nil
245
+ }
246
+ values, ok := raw.([]any)
247
+ if !ok {
248
+ return nil, fmt.Errorf("%q must be an array of strings", key)
249
+ }
250
+ out := make([]string, 0, len(values))
251
+ for i, value := range values {
252
+ text, ok := value.(string)
253
+ if !ok || strings.TrimSpace(text) == "" {
254
+ return nil, fmt.Errorf("%q[%d] must be a non-empty string", key, i)
255
+ }
256
+ out = append(out, text)
257
+ }
258
+ return out, nil
259
+ }
260
+
261
+ func statementRemovalRange(text string, node *shimast.Node) (int, int) {
262
+ start := clamp(node.Pos(), 0, len(text))
263
+ end := clamp(node.End(), start, len(text))
264
+ lineStart := start
265
+ for lineStart > 0 && text[lineStart-1] != '\n' && text[lineStart-1] != '\r' {
266
+ lineStart--
267
+ }
268
+ if strings.TrimSpace(text[lineStart:start]) == "" {
269
+ start = lineStart
270
+ }
271
+ if end < len(text) && text[end] == ';' {
272
+ end++
273
+ }
274
+ for end < len(text) && (text[end] == ' ' || text[end] == '\t') {
275
+ end++
276
+ }
277
+ if end < len(text) && text[end] == '\r' {
278
+ end++
279
+ }
280
+ if end < len(text) && text[end] == '\n' {
281
+ end++
282
+ }
283
+ return start, end
284
+ }
285
+
286
+ func applyTextEdits(text string, edits []textEdit) string {
287
+ if len(edits) == 0 {
288
+ return text
289
+ }
290
+ sort.SliceStable(edits, func(i, j int) bool {
291
+ if edits[i].start == edits[j].start {
292
+ return edits[i].end > edits[j].end
293
+ }
294
+ return edits[i].start > edits[j].start
295
+ })
296
+ out := text
297
+ lastStart := len(text) + 1
298
+ for _, edit := range edits {
299
+ if edit.start < 0 || edit.end < edit.start || edit.start > len(out) {
300
+ continue
301
+ }
302
+ if edit.end > lastStart {
303
+ edit.end = lastStart
304
+ }
305
+ if edit.end > len(out) {
306
+ edit.end = len(out)
307
+ }
308
+ out = out[:edit.start] + edit.text + out[edit.end:]
309
+ lastStart = edit.start
310
+ }
311
+ return out
312
+ }
313
+
314
+ func findConfig(pluginsJSON string) (map[string]any, error) {
315
+ if strings.TrimSpace(pluginsJSON) == "" {
316
+ return nil, fmt.Errorf("@ttsc/strip: missing --plugins-json")
317
+ }
318
+ var entries []pluginEntry
319
+ if err := json.Unmarshal([]byte(pluginsJSON), &entries); err != nil {
320
+ return nil, fmt.Errorf("@ttsc/strip: invalid --plugins-json: %w", err)
321
+ }
322
+ for _, entry := range entries {
323
+ if entry.Mode == modeStrip || entry.Name == "@ttsc/strip" {
324
+ if entry.Config == nil {
325
+ return map[string]any{}, nil
326
+ }
327
+ return entry.Config, nil
328
+ }
329
+ }
330
+ return nil, fmt.Errorf("@ttsc/strip: plugin entry not found")
331
+ }
332
+
333
+ func isJavaScriptOutput(fileName string) bool {
334
+ switch strings.ToLower(filepath.Ext(fileName)) {
335
+ case ".js", ".mjs", ".cjs":
336
+ return true
337
+ default:
338
+ return false
339
+ }
340
+ }
341
+
342
+ func equalStringSlices(a []string, b []string) bool {
343
+ if len(a) != len(b) {
344
+ return false
345
+ }
346
+ for i := range a {
347
+ if a[i] != b[i] {
348
+ return false
349
+ }
350
+ }
351
+ return true
352
+ }
353
+
354
+ func clamp(value int, min int, max int) int {
355
+ if value < min {
356
+ return min
357
+ }
358
+ if value > max {
359
+ return max
360
+ }
361
+ return value
362
+ }
package/index.cjs ADDED
@@ -0,0 +1,18 @@
1
+ // @ts-check
2
+ "use strict";
3
+
4
+ const path = require("node:path");
5
+
6
+ module.exports = function createTtscStrip() {
7
+ return {
8
+ name: "@ttsc/strip",
9
+ native: {
10
+ mode: "ttsc-strip",
11
+ source: {
12
+ dir: path.resolve(__dirname, "go-plugin"),
13
+ },
14
+ contractVersion: 1,
15
+ capabilities: ["output"],
16
+ },
17
+ };
18
+ };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@ttsc/strip",
3
+ "version": "0.5.0-dev.20260429",
4
+ "description": "First-party ttsc plugin that removes configured calls and statements from emitted JavaScript.",
5
+ "main": "index.cjs",
6
+ "exports": {
7
+ ".": "./index.cjs",
8
+ "./package.json": "./package.json"
9
+ },
10
+ "keywords": [
11
+ "ttsc",
12
+ "strip",
13
+ "console",
14
+ "typescript",
15
+ "tsgo"
16
+ ],
17
+ "files": [
18
+ "README.md",
19
+ "index.cjs",
20
+ "go-plugin/go.mod",
21
+ "go-plugin/main.go",
22
+ "go-plugin/strip"
23
+ ],
24
+ "peerDependencies": {
25
+ "ttsc": "^0.4.4"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/samchon/ttsc"
30
+ },
31
+ "author": "Jeongho Nam",
32
+ "license": "MIT",
33
+ "bugs": {
34
+ "url": "https://github.com/samchon/ttsc/issues"
35
+ },
36
+ "homepage": "https://github.com/samchon/ttsc/tree/master/packages/strip#readme"
37
+ }