ttsc 0.12.3 → 0.13.0
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/cmd/platform/main.go +9 -0
- package/cmd/ttsc/api_compile.go +122 -101
- package/cmd/ttsc/api_compile_branches_test.go +55 -57
- package/cmd/ttsc/api_transform.go +67 -49
- package/cmd/ttsc/api_transform_branches_test.go +30 -32
- package/cmd/ttsc/build.go +141 -108
- package/cmd/ttsc/build_branches_test.go +95 -97
- package/cmd/ttsc/filter.go +70 -0
- package/cmd/ttsc/filter_strips_unknown_forwarded_flag_test.go +61 -0
- package/cmd/ttsc/flags_gen.go +30 -0
- package/cmd/ttsc/helpers_test.go +39 -28
- package/cmd/ttsc/main.go +111 -104
- package/cmd/ttsc/main_alias_branches_test.go +21 -23
- package/cmd/ttscserver/main.go +96 -90
- package/cmd/ttscserver/run_lsp_prefers_tsgo_flag_test.go +22 -22
- package/cmd/ttscserver/run_lsp_reports_server_error_test.go +22 -22
- package/cmd/ttscserver/run_lsp_uses_tsgo_environment_test.go +23 -23
- package/cmd/utility-host/main.go +30 -23
- package/driver/lsp.go +53 -7
- package/driver/plugins.go +88 -72
- package/driver/program.go +464 -340
- package/driver/rewrite.go +477 -380
- package/internal/lspserver/lsp_envelope.go +64 -64
- package/internal/lspserver/lsp_frame.go +60 -60
- package/internal/lspserver/lsp_plugin.go +57 -57
- package/internal/lspserver/lsp_proxy.go +265 -251
- package/internal/lspserver/lsp_server.go +155 -144
- package/lib/TtscCompiler.d.ts +7 -0
- package/lib/TtscCompiler.js +7 -0
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/compiler/internal/buildNativeCompiler.d.ts +11 -1
- package/lib/compiler/internal/buildNativeCompiler.js +23 -3
- package/lib/compiler/internal/buildNativeCompiler.js.map +1 -1
- package/lib/compiler/internal/compileProjectInMemory.d.ts +9 -0
- package/lib/compiler/internal/compileProjectInMemory.js +36 -21
- package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
- package/lib/compiler/internal/paths.d.ts +27 -0
- package/lib/compiler/internal/paths.js +70 -0
- package/lib/compiler/internal/paths.js.map +1 -0
- package/lib/compiler/internal/project/readProjectConfig.d.ts +9 -1
- package/lib/compiler/internal/project/readProjectConfig.js +80 -1
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/project/resolveProjectConfig.d.ts +13 -1
- package/lib/compiler/internal/project/resolveProjectConfig.js +42 -1
- package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
- package/lib/compiler/internal/resolveBinary.d.ts +9 -1
- package/lib/compiler/internal/resolveBinary.js +17 -1
- package/lib/compiler/internal/resolveBinary.js.map +1 -1
- package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +16 -1
- package/lib/compiler/internal/resolveEmittedJavaScript.js +42 -10
- package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
- package/lib/compiler/internal/resolveTsgo.d.ts +21 -1
- package/lib/compiler/internal/resolveTsgo.js +23 -1
- package/lib/compiler/internal/resolveTsgo.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +24 -4
- package/lib/compiler/internal/runBuild.js +355 -93
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.d.ts +10 -1
- package/lib/compiler/internal/runSingleFileEmit.js +14 -1
- package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
- package/lib/compiler/internal/sharedHostHelpers.d.ts +5 -0
- package/lib/compiler/internal/sharedHostHelpers.js +6 -2
- package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
- package/lib/compiler/internal/spawnNative.d.ts +21 -0
- package/lib/compiler/internal/spawnNative.js +59 -0
- package/lib/compiler/internal/spawnNative.js.map +1 -0
- package/lib/compiler/internal/transformProjectInMemory.d.ts +15 -1
- package/lib/compiler/internal/transformProjectInMemory.js +57 -61
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/flags/parser.d.ts +63 -0
- package/lib/flags/parser.js +254 -0
- package/lib/flags/parser.js.map +1 -0
- package/lib/flags/schema.d.ts +147 -0
- package/lib/flags/schema.js +405 -0
- package/lib/flags/schema.js.map +1 -0
- package/lib/launcher/internal/getCompilerVersionText.js +7 -0
- package/lib/launcher/internal/getCompilerVersionText.js.map +1 -1
- package/lib/launcher/internal/prepareExecution.js +35 -0
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/resolveCacheDir.d.ts +9 -0
- package/lib/launcher/internal/resolveCacheDir.js +22 -0
- package/lib/launcher/internal/resolveCacheDir.js.map +1 -0
- package/lib/launcher/internal/resolveTtscserverBinary.js +2 -0
- package/lib/launcher/internal/resolveTtscserverBinary.js.map +1 -1
- package/lib/launcher/internal/runTtsc.d.ts +10 -0
- package/lib/launcher/internal/runTtsc.js +90 -153
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.js +10 -35
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/runTtsx.d.ts +8 -0
- package/lib/launcher/internal/runTtsx.js +137 -79
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/plugin/internal/buildSourcePlugin.d.ts +38 -0
- package/lib/plugin/internal/buildSourcePlugin.js +40 -17
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +26 -0
- package/lib/plugin/internal/loadProjectPlugins.js +90 -41
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscCompilerTransformation.d.ts +15 -2
- package/lib/structures/ITtscPlugin.d.ts +24 -0
- package/lib/structures/ITtscPluginCapabilities.d.ts +29 -0
- package/lib/structures/ITtscPluginCapabilities.js +3 -0
- package/lib/structures/ITtscPluginCapabilities.js.map +1 -0
- package/lib/structures/index.d.ts +1 -0
- package/lib/structures/index.js +1 -0
- package/lib/structures/index.js.map +1 -1
- package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +5 -0
- package/lib/structures/internal/TtscCommonOptions.d.ts +19 -0
- package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +0 -2
- package/package.json +9 -9
- package/shim/ast/shim.go +24 -0
- package/shim/checker/shim.go +35 -0
- package/shim/core/shim.go +17 -0
- package/shim/diagnosticwriter/shim.go +4 -0
- package/shim/lsp/shim.go +3 -3
- package/shim/printer/shim.go +20 -0
- package/src/TtscCompiler.ts +7 -0
- package/src/compiler/internal/buildNativeCompiler.ts +23 -3
- package/src/compiler/internal/compileProjectInMemory.ts +34 -25
- package/src/compiler/internal/paths.ts +68 -0
- package/src/compiler/internal/project/readProjectConfig.ts +86 -1
- package/src/compiler/internal/project/resolveProjectConfig.ts +44 -1
- package/src/compiler/internal/resolveBinary.ts +17 -1
- package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -12
- package/src/compiler/internal/resolveTsgo.ts +29 -1
- package/src/compiler/internal/runBuild.ts +398 -112
- package/src/compiler/internal/runSingleFileEmit.ts +14 -1
- package/src/compiler/internal/sharedHostHelpers.ts +7 -7
- package/src/compiler/internal/spawnNative.ts +64 -0
- package/src/compiler/internal/transformProjectInMemory.ts +47 -67
- package/src/flags/parser.ts +367 -0
- package/src/flags/schema.ts +570 -0
- package/src/launcher/internal/getCompilerVersionText.ts +7 -0
- package/src/launcher/internal/prepareExecution.ts +35 -0
- package/src/launcher/internal/resolveCacheDir.ts +19 -0
- package/src/launcher/internal/resolveTtscserverBinary.ts +2 -0
- package/src/launcher/internal/runTtsc.ts +97 -143
- package/src/launcher/internal/runTtscserver.ts +9 -1
- package/src/launcher/internal/runTtsx.ts +149 -82
- package/src/plugin/internal/buildSourcePlugin.ts +40 -14
- package/src/plugin/internal/loadProjectPlugins.ts +116 -45
- package/src/structures/ITtscCompilerTransformation.ts +15 -2
- package/src/structures/ITtscPlugin.ts +26 -1
- package/src/structures/ITtscPluginCapabilities.ts +29 -0
- package/src/structures/index.ts +1 -0
- package/src/structures/internal/ITtscLoadedNativePlugin.ts +5 -0
- package/src/structures/internal/TtscCommonOptions.ts +19 -0
- package/src/structures/internal/TtscSingleFileEmitOptions.ts +0 -2
- package/tools/gen_shims/main.go +43 -8
- package/utility/flags_gen.go +30 -0
- package/utility/host.go +304 -245
package/driver/rewrite.go
CHANGED
|
@@ -14,34 +14,34 @@
|
|
|
14
14
|
package driver
|
|
15
15
|
|
|
16
16
|
import (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
"context"
|
|
18
|
+
"errors"
|
|
19
|
+
"fmt"
|
|
20
|
+
"os"
|
|
21
|
+
"path/filepath"
|
|
22
|
+
"regexp"
|
|
23
|
+
"strings"
|
|
24
|
+
"sync"
|
|
25
|
+
"unicode"
|
|
26
|
+
|
|
27
|
+
"github.com/microsoft/typescript-go/shim/ast"
|
|
28
|
+
shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
// Rewrite describes one emit-time patch. Produced by CollectCallSites after
|
|
32
32
|
// the engine has generated a replacement JS fragment for the call.
|
|
33
33
|
type Rewrite struct {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
File *ast.SourceFile
|
|
35
|
+
RootName string
|
|
36
|
+
Namespaces []string
|
|
37
|
+
Method string
|
|
38
|
+
Replacement string
|
|
39
|
+
ConsumeParens bool
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// RewriteSet groups rewrites by file, preserving source order.
|
|
43
43
|
type RewriteSet struct {
|
|
44
|
-
|
|
44
|
+
byPath map[string][]Rewrite
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// NewRewriteSet returns an empty set.
|
|
@@ -49,20 +49,20 @@ func NewRewriteSet() *RewriteSet { return &RewriteSet{byPath: map[string][]Rewri
|
|
|
49
49
|
|
|
50
50
|
// Add registers a rewrite under the absolute path of its source file.
|
|
51
51
|
func (rs *RewriteSet) Add(r Rewrite) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
if r.File == nil {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
path := filepath.ToSlash(r.File.FileName())
|
|
56
|
+
rs.byPath[path] = append(rs.byPath[path], r)
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// Len returns the total number of rewrites across every file.
|
|
60
60
|
func (rs *RewriteSet) Len() int {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
n := 0
|
|
62
|
+
for _, rws := range rs.byPath {
|
|
63
|
+
n += len(rws)
|
|
64
|
+
}
|
|
65
|
+
return n
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// RewriteSentinel is the marker inserted at the top of a patched file so
|
|
@@ -73,128 +73,171 @@ const RewriteSentinel = "/* @ttsc-rewritten */"
|
|
|
73
73
|
// the output. Returns the tsgo diagnostics and any patch-time error. When
|
|
74
74
|
// `writeFile` is nil, the patched JS is written to disk via the standard
|
|
75
75
|
// tsgo WriteFile.
|
|
76
|
+
//
|
|
77
|
+
// `writeFile` does not need to be concurrency-safe: emit() funnels every
|
|
78
|
+
// invocation through one mutex, so the callback never runs on two goroutines
|
|
79
|
+
// at once even though TypeScript-Go emits files in parallel.
|
|
76
80
|
func (p *Program) EmitAll(rs *RewriteSet, writeFile shimcompiler.WriteFile) (*shimcompiler.EmitResult, []Diagnostic, error) {
|
|
77
|
-
|
|
81
|
+
return p.emit(rs, nil, writeFile)
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
// EmitAllRaw runs TypeScript-Go emit without ttsc output-text rewrites.
|
|
85
|
+
//
|
|
86
|
+
// `writeFile` does not need to be concurrency-safe: like EmitAll, EmitAllRaw
|
|
87
|
+
// funnels every invocation through one mutex, so the callback never runs on
|
|
88
|
+
// two goroutines at once even though TypeScript-Go emits files in parallel.
|
|
89
|
+
// This is the contract a plugin's output rewriter relies on — it is the
|
|
90
|
+
// emit-stage phase ttsc guarantees runs single-threaded (a plugin's WriteFile
|
|
91
|
+
// is the standard place to carry per-file cursors or an output map), so ttsc
|
|
92
|
+
// owns the serialization rather than pushing goroutine-safety onto every
|
|
93
|
+
// plugin author. See the emit-concurrency contract in the plugin docs.
|
|
81
94
|
func (p *Program) EmitAllRaw(writeFile shimcompiler.WriteFile) (*shimcompiler.EmitResult, []Diagnostic, error) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
if p == nil || p.TSProgram == nil {
|
|
96
|
+
return nil, nil, errors.New("driver: nil program")
|
|
97
|
+
}
|
|
98
|
+
if err := p.ApplyLinkedPlugins(); err != nil {
|
|
99
|
+
return nil, nil, err
|
|
100
|
+
}
|
|
101
|
+
// TypeScript-Go's parallel emit invokes WriteFile once per emitted file,
|
|
102
|
+
// concurrently — one goroutine per source file. Serialize the whole callback
|
|
103
|
+
// under wfMu so a plugin's output rewriter sees one writer at a time: a
|
|
104
|
+
// callback that mutates shared state (e.g. @nestia/core's per-file rewrite
|
|
105
|
+
// cursors and runtime-alias cache) would otherwise trip `fatal error:
|
|
106
|
+
// concurrent map read and map write`. The callback is cheap I/O, so
|
|
107
|
+
// serializing it costs ~nothing while parse/check/emit-text still parallelize
|
|
108
|
+
// — the same trade EmitAll makes for its own WriteFile.
|
|
109
|
+
var wfMu sync.Mutex
|
|
110
|
+
wf := func(fileName, text string, data *shimcompiler.WriteFileData) error {
|
|
111
|
+
wfMu.Lock()
|
|
112
|
+
defer wfMu.Unlock()
|
|
113
|
+
if writeFile != nil {
|
|
114
|
+
return writeFile(fileName, text, data)
|
|
115
|
+
}
|
|
116
|
+
return DefaultWriteFile(fileName, text)
|
|
117
|
+
}
|
|
118
|
+
result := p.TSProgram.Emit(context.Background(), shimcompiler.EmitOptions{
|
|
119
|
+
WriteFile: wf,
|
|
120
|
+
})
|
|
121
|
+
return result, convertDiagnostics(result.Diagnostics), nil
|
|
98
122
|
}
|
|
99
123
|
|
|
100
124
|
// EmitFile runs tsgo's emitter for one source file, applying the same rewrite
|
|
101
125
|
// pipeline as EmitAll.
|
|
102
126
|
func (p *Program) EmitFile(rs *RewriteSet, target *ast.SourceFile, writeFile shimcompiler.WriteFile) (*shimcompiler.EmitResult, []Diagnostic, error) {
|
|
103
|
-
|
|
127
|
+
return p.emit(rs, target, writeFile)
|
|
104
128
|
}
|
|
105
129
|
|
|
106
130
|
func (p *Program) emit(rs *RewriteSet, target *ast.SourceFile, writeFile shimcompiler.WriteFile) (*shimcompiler.EmitResult, []Diagnostic, error) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
131
|
+
if p == nil || p.TSProgram == nil {
|
|
132
|
+
return nil, nil, errors.New("driver: nil program")
|
|
133
|
+
}
|
|
134
|
+
if err := p.ApplyLinkedPlugins(); err != nil {
|
|
135
|
+
return nil, nil, err
|
|
136
|
+
}
|
|
137
|
+
if rs == nil {
|
|
138
|
+
rs = NewRewriteSet()
|
|
139
|
+
}
|
|
140
|
+
cursors := map[string]int{}
|
|
141
|
+
// TypeScript-Go's parallel emit invokes this WriteFile callback once per
|
|
142
|
+
// emitted file, concurrently — one goroutine per source file. Serialize the
|
|
143
|
+
// whole callback body under wfMu: the `cursors` map would otherwise trip
|
|
144
|
+
// `fatal error: concurrent map writes`, and the wrapped `writeFile` (which a
|
|
145
|
+
// caller may back with its own non-thread-safe state, e.g. api-compile's
|
|
146
|
+
// output map) must likewise see one writer at a time. The patch work here is
|
|
147
|
+
// cheap, so serializing only the callback costs ~nothing while parsing,
|
|
148
|
+
// checking, and emit-text generation still parallelize.
|
|
149
|
+
var wfMu sync.Mutex
|
|
150
|
+
wf := func(fileName, text string, data *shimcompiler.WriteFileData) error {
|
|
151
|
+
wfMu.Lock()
|
|
152
|
+
defer wfMu.Unlock()
|
|
153
|
+
// A patched file is idempotent: once the sentinel exists, the emitted text
|
|
154
|
+
// is passed through unchanged. This matters for watch/rebuild loops and
|
|
155
|
+
// tests that re-run emit over the same output directory.
|
|
156
|
+
if strings.Contains(text, RewriteSentinel) {
|
|
157
|
+
if writeFile != nil {
|
|
158
|
+
return writeFile(fileName, text, data)
|
|
159
|
+
}
|
|
160
|
+
return DefaultWriteFile(fileName, text)
|
|
161
|
+
}
|
|
162
|
+
// Rewrites are matched after tsgo has printed JavaScript. The source-file
|
|
163
|
+
// association is recovered from the output path because WriteFile receives
|
|
164
|
+
// only the final file name and text.
|
|
165
|
+
patched, err := applyRewrites(fileName, text, rs, cursors)
|
|
166
|
+
if err != nil {
|
|
167
|
+
return err
|
|
168
|
+
}
|
|
169
|
+
if patched != text {
|
|
170
|
+
patched = insertSentinel(patched)
|
|
171
|
+
}
|
|
172
|
+
if writeFile != nil {
|
|
173
|
+
return writeFile(fileName, patched, data)
|
|
174
|
+
}
|
|
175
|
+
return DefaultWriteFile(fileName, patched)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
result := p.TSProgram.Emit(context.Background(), shimcompiler.EmitOptions{
|
|
179
|
+
TargetSourceFile: target,
|
|
180
|
+
WriteFile: wf,
|
|
181
|
+
})
|
|
182
|
+
return result, convertDiagnostics(result.Diagnostics), nil
|
|
148
183
|
}
|
|
149
184
|
|
|
150
185
|
// DefaultWriteFile is the default disk writer used when EmitAll's caller does not
|
|
151
186
|
// supply a custom WriteFile callback.
|
|
152
187
|
func DefaultWriteFile(fileName, text string) error {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
188
|
+
if dir := filepath.Dir(fileName); dir != "" {
|
|
189
|
+
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
190
|
+
return err
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return os.WriteFile(fileName, []byte(text), 0o644)
|
|
159
194
|
}
|
|
160
195
|
|
|
196
|
+
// insertSentinel prepends RewriteSentinel to the output text. When the file
|
|
197
|
+
// starts with a "use strict" directive (either quote style), the sentinel is
|
|
198
|
+
// inserted after it so the directive remains the first statement — ES modules
|
|
199
|
+
// and bundlers expect it at position zero.
|
|
161
200
|
func insertSentinel(text string) string {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
201
|
+
for _, prefix := range []string{"\"use strict\";\n", "'use strict';\n"} {
|
|
202
|
+
if strings.HasPrefix(text, prefix) {
|
|
203
|
+
return prefix + RewriteSentinel + "\n" + text[len(prefix):]
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return RewriteSentinel + "\n" + text
|
|
168
207
|
}
|
|
169
208
|
|
|
209
|
+
// applyRewrites applies all registered rewrites for the source file that
|
|
210
|
+
// corresponds to outputName. cursors tracks how many rewrites have already
|
|
211
|
+
// been applied per source path across multiple WriteFile calls so incremental
|
|
212
|
+
// watch rebuilds resume at the right offset rather than re-scanning from zero.
|
|
170
213
|
func applyRewrites(outputName, text string, rs *RewriteSet, cursors map[string]int) (string, error) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
214
|
+
srcPath, ok := findSourceForOutput(outputName, rs)
|
|
215
|
+
if !ok || len(rs.byPath[srcPath]) == 0 {
|
|
216
|
+
return text, nil
|
|
217
|
+
}
|
|
218
|
+
rewrites := rs.byPath[srcPath]
|
|
219
|
+
pos := cursors[srcPath]
|
|
220
|
+
out := text
|
|
221
|
+
searchFrom := 0
|
|
222
|
+
for pos < len(rewrites) {
|
|
223
|
+
r := rewrites[pos]
|
|
224
|
+
replaced, nextSearchFrom, ok, err := spliceCall(out, r, searchFrom)
|
|
225
|
+
if err != nil {
|
|
226
|
+
return "", err
|
|
227
|
+
}
|
|
228
|
+
if !ok {
|
|
229
|
+
preview := out
|
|
230
|
+
if len(preview) > 400 {
|
|
231
|
+
preview = preview[:400] + "…"
|
|
232
|
+
}
|
|
233
|
+
return "", fmt.Errorf("driver: could not locate %s.%s(…) call in %s (tried roots %v; preview: %q)", joinRootAndNamespaces(r), r.Method, outputName, candidateRoots(r.RootName), preview)
|
|
234
|
+
}
|
|
235
|
+
out = replaced
|
|
236
|
+
searchFrom = nextSearchFrom
|
|
237
|
+
pos++
|
|
238
|
+
}
|
|
239
|
+
cursors[srcPath] = pos
|
|
240
|
+
return out, nil
|
|
198
241
|
}
|
|
199
242
|
|
|
200
243
|
// findSourceForOutput recovers which registered source file produced a given
|
|
@@ -213,93 +256,99 @@ func applyRewrites(outputName, text string, rs *RewriteSet, cursors map[string]i
|
|
|
213
256
|
// Ambiguous matches (two or more registered sources with the same tail) return
|
|
214
257
|
// no match so the caller treats the output as having no rewrites.
|
|
215
258
|
func findSourceForOutput(outputName string, rs *RewriteSet) (string, bool) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
259
|
+
if len(rs.byPath) == 0 {
|
|
260
|
+
return "", false
|
|
261
|
+
}
|
|
262
|
+
outStem := strings.TrimSuffix(filepath.ToSlash(outputName), filepath.Ext(outputName))
|
|
263
|
+
commonDir := commonSourceDirectoryFor(rs)
|
|
264
|
+
var matched string
|
|
265
|
+
hits := 0
|
|
266
|
+
for srcPath := range rs.byPath {
|
|
267
|
+
tail := sourceTail(srcPath, commonDir)
|
|
268
|
+
if tail == "" {
|
|
269
|
+
continue
|
|
270
|
+
}
|
|
271
|
+
if outStem == tail || strings.HasSuffix(outStem, "/"+tail) {
|
|
272
|
+
matched = srcPath
|
|
273
|
+
hits++
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if hits != 1 {
|
|
277
|
+
return "", false
|
|
278
|
+
}
|
|
279
|
+
return matched, true
|
|
237
280
|
}
|
|
238
281
|
|
|
239
282
|
// commonSourceDirectoryFor returns the deepest directory (with trailing "/")
|
|
240
283
|
// shared by every source path in rs.byPath. When rs has a single source this is
|
|
241
284
|
// just that source's directory.
|
|
242
285
|
func commonSourceDirectoryFor(rs *RewriteSet) string {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
286
|
+
var dirs [][]string
|
|
287
|
+
for srcPath := range rs.byPath {
|
|
288
|
+
dirs = append(dirs, strings.Split(filepath.ToSlash(filepath.Dir(srcPath)), "/"))
|
|
289
|
+
}
|
|
290
|
+
if len(dirs) == 0 {
|
|
291
|
+
return ""
|
|
292
|
+
}
|
|
293
|
+
common := dirs[0]
|
|
294
|
+
for _, other := range dirs[1:] {
|
|
295
|
+
n := len(common)
|
|
296
|
+
if len(other) < n {
|
|
297
|
+
n = len(other)
|
|
298
|
+
}
|
|
299
|
+
shared := 0
|
|
300
|
+
for i := 0; i < n; i++ {
|
|
301
|
+
if common[i] != other[i] {
|
|
302
|
+
break
|
|
303
|
+
}
|
|
304
|
+
shared++
|
|
305
|
+
}
|
|
306
|
+
common = common[:shared]
|
|
307
|
+
if len(common) == 0 {
|
|
308
|
+
break
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if len(common) == 0 {
|
|
312
|
+
return ""
|
|
313
|
+
}
|
|
314
|
+
return strings.Join(common, "/") + "/"
|
|
272
315
|
}
|
|
273
316
|
|
|
274
317
|
// sourceTail returns the source stem (extension dropped) without the common
|
|
275
318
|
// directory prefix. The leading "/" is also stripped so callers can match it as
|
|
276
319
|
// a suffix segment.
|
|
277
320
|
func sourceTail(srcPath, commonDir string) string {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
321
|
+
stem := strings.TrimSuffix(filepath.ToSlash(srcPath), filepath.Ext(srcPath))
|
|
322
|
+
if commonDir != "" && strings.HasPrefix(stem, commonDir) {
|
|
323
|
+
return stem[len(commonDir):]
|
|
324
|
+
}
|
|
325
|
+
return strings.TrimPrefix(stem, "/")
|
|
283
326
|
}
|
|
284
327
|
|
|
328
|
+
// spliceCall locates the next call expression for r in text starting at
|
|
329
|
+
// searchFrom and splices in r.Replacement. When ConsumeParens is true the
|
|
330
|
+
// replacement covers the entire call including arguments; otherwise only the
|
|
331
|
+
// head (root.namespaces.method) is replaced and the argument list is kept.
|
|
332
|
+
// Returns the patched text, the byte position to resume from on the next
|
|
333
|
+
// call, a found flag, and any error from the paren-matching step.
|
|
285
334
|
func spliceCall(text string, r Rewrite, searchFrom int) (string, int, bool, error) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
335
|
+
aliases := candidateRoots(r.RootName)
|
|
336
|
+
pattern := callRegexFor(aliases, r.Namespaces, r.Method)
|
|
337
|
+
idx, needleLen := findCallMatch(text, pattern, searchFrom)
|
|
338
|
+
if idx < 0 {
|
|
339
|
+
return text, searchFrom, false, nil
|
|
340
|
+
}
|
|
341
|
+
parenPos := idx + needleLen
|
|
342
|
+
closePos, ok := matchParen(text, parenPos)
|
|
343
|
+
if !ok {
|
|
344
|
+
return text, searchFrom, false, errors.New("driver: unbalanced parens while locating plugin call")
|
|
345
|
+
}
|
|
346
|
+
if r.ConsumeParens {
|
|
347
|
+
replaced := text[:idx] + r.Replacement + text[closePos+1:]
|
|
348
|
+
return replaced, idx + len(r.Replacement), true, nil
|
|
349
|
+
}
|
|
350
|
+
replaced := text[:idx] + r.Replacement + text[idx+needleLen:]
|
|
351
|
+
return replaced, idx + len(r.Replacement), true, nil
|
|
303
352
|
}
|
|
304
353
|
|
|
305
354
|
// callRegexFor compiles the loose-match needle pattern used by spliceCall.
|
|
@@ -317,30 +366,30 @@ func spliceCall(text string, r Rewrite, searchFrom int) (string, int, bool, erro
|
|
|
317
366
|
// same rewrite descriptor is re-checked once per emitted file in incremental
|
|
318
367
|
// watch builds.
|
|
319
368
|
func callRegexFor(aliases, namespaces []string, method string) *regexp.Regexp {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
369
|
+
key := strings.Join(aliases, "|") + "\x00" + strings.Join(namespaces, ".") + "\x00" + method
|
|
370
|
+
if cached, ok := callRegexCache.Load(key); ok {
|
|
371
|
+
return cached.(*regexp.Regexp)
|
|
372
|
+
}
|
|
373
|
+
rootAlternation := make([]string, 0, len(aliases))
|
|
374
|
+
for _, alias := range aliases {
|
|
375
|
+
rootAlternation = append(rootAlternation, regexp.QuoteMeta(alias))
|
|
376
|
+
}
|
|
377
|
+
var b strings.Builder
|
|
378
|
+
b.WriteString(`(?:`)
|
|
379
|
+
b.WriteString(strings.Join(rootAlternation, `|`))
|
|
380
|
+
b.WriteString(`)`)
|
|
381
|
+
for _, ns := range namespaces {
|
|
382
|
+
b.WriteString(`\s*\.\s*`)
|
|
383
|
+
b.WriteString(regexp.QuoteMeta(ns))
|
|
384
|
+
}
|
|
385
|
+
b.WriteString(`\s*\.\s*`)
|
|
386
|
+
b.WriteString(regexp.QuoteMeta(method))
|
|
387
|
+
b.WriteString(`\s*(\()`)
|
|
388
|
+
// MustCompile is safe because every contributing string was QuoteMeta'd
|
|
389
|
+
// and the surrounding template is a fixed regex grammar.
|
|
390
|
+
re := regexp.MustCompile(b.String())
|
|
391
|
+
callRegexCache.Store(key, re)
|
|
392
|
+
return re
|
|
344
393
|
}
|
|
345
394
|
|
|
346
395
|
var callRegexCache sync.Map
|
|
@@ -351,197 +400,245 @@ var callRegexCache sync.Map
|
|
|
351
400
|
// like `mytypia.foo(` don't shadow `typia.foo(`. Returns the start byte of the
|
|
352
401
|
// match and the length up to (but not including) the captured `(`.
|
|
353
402
|
func findCallMatch(text string, pattern *regexp.Regexp, searchFrom int) (int, int) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
403
|
+
start := searchFrom
|
|
404
|
+
if start < 0 {
|
|
405
|
+
start = 0
|
|
406
|
+
}
|
|
407
|
+
for start <= len(text) {
|
|
408
|
+
loc := pattern.FindStringSubmatchIndex(text[start:])
|
|
409
|
+
if loc == nil {
|
|
410
|
+
return -1, 0
|
|
411
|
+
}
|
|
412
|
+
matchStart := start + loc[0]
|
|
413
|
+
parenStart := start + loc[2]
|
|
414
|
+
if matchStart > 0 && isIdentifierPart(rune(text[matchStart-1])) {
|
|
415
|
+
start = matchStart + 1
|
|
416
|
+
continue
|
|
417
|
+
}
|
|
418
|
+
return matchStart, parenStart - matchStart
|
|
419
|
+
}
|
|
420
|
+
return -1, 0
|
|
372
421
|
}
|
|
373
422
|
|
|
423
|
+
// candidateRoots expands a bare module name into the set of identifier stems
|
|
424
|
+
// tsgo's CommonJS emitter may produce for it. For example "typia" becomes
|
|
425
|
+
// ["typia", "typia_1.default", "typia_2.default", "typia.default",
|
|
426
|
+
// "typia_1", "typia_2"]. The regex alternation built from these candidates
|
|
427
|
+
// matches the emitted call regardless of which deduplication suffix tsgo chose.
|
|
374
428
|
func candidateRoots(root string) []string {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
429
|
+
return []string{
|
|
430
|
+
root,
|
|
431
|
+
root + "_1.default",
|
|
432
|
+
root + "_2.default",
|
|
433
|
+
root + ".default",
|
|
434
|
+
root + "_1",
|
|
435
|
+
root + "_2",
|
|
436
|
+
}
|
|
383
437
|
}
|
|
384
438
|
|
|
439
|
+
// joinRootAndNamespaces returns the human-readable "root.ns1.ns2" form of
|
|
440
|
+
// the rewrite's call head, used in error messages only.
|
|
385
441
|
func joinRootAndNamespaces(r Rewrite) string {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
442
|
+
if len(r.Namespaces) == 0 {
|
|
443
|
+
return r.RootName
|
|
444
|
+
}
|
|
445
|
+
return r.RootName + "." + strings.Join(r.Namespaces, ".")
|
|
390
446
|
}
|
|
391
447
|
|
|
448
|
+
// needleTail returns the literal suffix of a call expression head, e.g.
|
|
449
|
+
// ".ns.method(". This was the original literal-search needle before the regex
|
|
450
|
+
// rewriter was introduced; it is kept for potential future use by callers
|
|
451
|
+
// that do a quick pre-filter before invoking the regex path.
|
|
392
452
|
func needleTail(r Rewrite) string {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
453
|
+
if len(r.Namespaces) == 0 {
|
|
454
|
+
return "." + r.Method + "("
|
|
455
|
+
}
|
|
456
|
+
return "." + strings.Join(r.Namespaces, ".") + "." + r.Method + "("
|
|
397
457
|
}
|
|
398
458
|
|
|
459
|
+
// isIdentifierPart reports whether r can appear inside a JavaScript identifier.
|
|
460
|
+
// Used to ensure a regex match does not begin mid-identifier (e.g. "mytypia"
|
|
461
|
+
// must not match as "typia").
|
|
399
462
|
func isIdentifierPart(r rune) bool {
|
|
400
|
-
|
|
463
|
+
return r == '_' || r == '$' || unicode.IsLetter(r) || unicode.IsDigit(r)
|
|
401
464
|
}
|
|
402
465
|
|
|
466
|
+
// matchParen finds the closing ")" that matches the "(" at text[pos],
|
|
467
|
+
// skipping over nested parentheses, strings, template literals, comments, and
|
|
468
|
+
// regex literals. Returns the byte index of the closing ")" and true on
|
|
469
|
+
// success; (0, false) when pos does not point at "(" or the text ends before
|
|
470
|
+
// the paren is closed. The lastSignificant variable tracks the most recently
|
|
471
|
+
// seen non-whitespace byte so canStartRegexLiteral can distinguish division
|
|
472
|
+
// from a regex literal opener.
|
|
403
473
|
func matchParen(text string, pos int) (int, bool) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
474
|
+
if pos >= len(text) || text[pos] != '(' {
|
|
475
|
+
return 0, false
|
|
476
|
+
}
|
|
477
|
+
depth := 1
|
|
478
|
+
lastSignificant := byte('(')
|
|
479
|
+
for i := pos + 1; i < len(text); i++ {
|
|
480
|
+
ch := text[i]
|
|
481
|
+
switch ch {
|
|
482
|
+
case ' ', '\t', '\n', '\r', '\f':
|
|
483
|
+
continue
|
|
484
|
+
case '(':
|
|
485
|
+
depth++
|
|
486
|
+
lastSignificant = ch
|
|
487
|
+
case ')':
|
|
488
|
+
depth--
|
|
489
|
+
if depth == 0 {
|
|
490
|
+
return i, true
|
|
491
|
+
}
|
|
492
|
+
lastSignificant = ch
|
|
493
|
+
case '"', '\'':
|
|
494
|
+
end, ok := skipQuoted(text, i, ch)
|
|
495
|
+
if !ok {
|
|
496
|
+
return 0, false
|
|
497
|
+
}
|
|
498
|
+
i = end
|
|
499
|
+
lastSignificant = 'x'
|
|
500
|
+
case '`':
|
|
501
|
+
end, ok := skipTemplate(text, i)
|
|
502
|
+
if !ok {
|
|
503
|
+
return 0, false
|
|
504
|
+
}
|
|
505
|
+
i = end
|
|
506
|
+
lastSignificant = 'x'
|
|
507
|
+
case '/':
|
|
508
|
+
if i+1 < len(text) {
|
|
509
|
+
switch text[i+1] {
|
|
510
|
+
case '/':
|
|
511
|
+
i = skipLineComment(text, i+2)
|
|
512
|
+
continue
|
|
513
|
+
case '*':
|
|
514
|
+
end, ok := skipBlockComment(text, i+2)
|
|
515
|
+
if !ok {
|
|
516
|
+
return 0, false
|
|
517
|
+
}
|
|
518
|
+
i = end
|
|
519
|
+
continue
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if canStartRegexLiteral(lastSignificant) {
|
|
523
|
+
end, ok := skipRegexLiteral(text, i)
|
|
524
|
+
if !ok {
|
|
525
|
+
return 0, false
|
|
526
|
+
}
|
|
527
|
+
i = end
|
|
528
|
+
lastSignificant = 'x'
|
|
529
|
+
continue
|
|
530
|
+
}
|
|
531
|
+
lastSignificant = ch
|
|
532
|
+
default:
|
|
533
|
+
lastSignificant = ch
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return 0, false
|
|
467
537
|
}
|
|
468
538
|
|
|
539
|
+
// skipQuoted advances past a single- or double-quoted string literal starting
|
|
540
|
+
// at pos. Returns the index of the closing quote and true, or (0, false) on
|
|
541
|
+
// unterminated literals. Newlines inside a non-template string are illegal in
|
|
542
|
+
// JS and also terminate the scan as a failure.
|
|
469
543
|
func skipQuoted(text string, pos int, quote byte) (int, bool) {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
544
|
+
for i := pos + 1; i < len(text); i++ {
|
|
545
|
+
switch text[i] {
|
|
546
|
+
case '\\':
|
|
547
|
+
i++
|
|
548
|
+
case quote:
|
|
549
|
+
return i, true
|
|
550
|
+
case '\n', '\r':
|
|
551
|
+
return 0, false
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return 0, false
|
|
481
555
|
}
|
|
482
556
|
|
|
557
|
+
// skipTemplate advances past a backtick template literal starting at pos.
|
|
558
|
+
// Nested template expressions (${...}) are not recursed into — the rewriter
|
|
559
|
+
// only needs to balance the outer backtick so it does not misinterpret a
|
|
560
|
+
// backtick inside the template as the end of a surrounding construct.
|
|
483
561
|
func skipTemplate(text string, pos int) (int, bool) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
562
|
+
for i := pos + 1; i < len(text); i++ {
|
|
563
|
+
switch text[i] {
|
|
564
|
+
case '\\':
|
|
565
|
+
i++
|
|
566
|
+
case '`':
|
|
567
|
+
return i, true
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return 0, false
|
|
493
571
|
}
|
|
494
572
|
|
|
573
|
+
// skipLineComment advances past a "//" line comment starting at pos (pos
|
|
574
|
+
// should be the character after the second "/"). Returns the index of the
|
|
575
|
+
// line terminator, or the last valid index when no newline is found.
|
|
495
576
|
func skipLineComment(text string, pos int) int {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
577
|
+
for i := pos; i < len(text); i++ {
|
|
578
|
+
if text[i] == '\n' || text[i] == '\r' {
|
|
579
|
+
return i
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return len(text) - 1
|
|
502
583
|
}
|
|
503
584
|
|
|
585
|
+
// skipBlockComment advances past a "/* … */" block comment starting at pos
|
|
586
|
+
// (pos should be the character after the opening "/*"). Returns the index of
|
|
587
|
+
// the closing "/" and true, or (0, false) when the comment is unterminated.
|
|
504
588
|
func skipBlockComment(text string, pos int) (int, bool) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
589
|
+
for i := pos; i+1 < len(text); i++ {
|
|
590
|
+
if text[i] == '*' && text[i+1] == '/' {
|
|
591
|
+
return i + 1, true
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return 0, false
|
|
511
595
|
}
|
|
512
596
|
|
|
597
|
+
// canStartRegexLiteral reports whether the byte previous (the last
|
|
598
|
+
// non-whitespace character seen before a "/") allows a regex literal to
|
|
599
|
+
// start. This is the minimal set of characters that unambiguously precede a
|
|
600
|
+
// regex in emitted CommonJS output; false positives are safe (they cause an
|
|
601
|
+
// extra skipRegexLiteral attempt that quickly fails), false negatives would
|
|
602
|
+
// misparse a "/" as the start of a comment or regex.
|
|
513
603
|
func canStartRegexLiteral(previous byte) bool {
|
|
514
|
-
|
|
604
|
+
return strings.ContainsRune("([{=,:;!&|?+-*~^<>%", rune(previous))
|
|
515
605
|
}
|
|
516
606
|
|
|
607
|
+
// skipRegexLiteral advances past a "/" regex literal starting at pos.
|
|
608
|
+
// Character classes ("[…]") are tracked so a "/" inside them is not treated
|
|
609
|
+
// as the closing delimiter. Returns the index of the last flag character (or
|
|
610
|
+
// the closing "/" when no flags follow) and true, or (0, false) for unterminated
|
|
611
|
+
// or newline-terminated literals.
|
|
517
612
|
func skipRegexLiteral(text string, pos int) (int, bool) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
613
|
+
inClass := false
|
|
614
|
+
for i := pos + 1; i < len(text); i++ {
|
|
615
|
+
switch text[i] {
|
|
616
|
+
case '\\':
|
|
617
|
+
i++
|
|
618
|
+
case '[':
|
|
619
|
+
inClass = true
|
|
620
|
+
case ']':
|
|
621
|
+
inClass = false
|
|
622
|
+
case '/':
|
|
623
|
+
if inClass {
|
|
624
|
+
continue
|
|
625
|
+
}
|
|
626
|
+
for i+1 < len(text) && isRegexFlag(text[i+1]) {
|
|
627
|
+
i++
|
|
628
|
+
}
|
|
629
|
+
return i, true
|
|
630
|
+
case '\n', '\r':
|
|
631
|
+
return 0, false
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return 0, false
|
|
540
635
|
}
|
|
541
636
|
|
|
637
|
+
// isRegexFlag reports whether ch is a valid regex flag character (letter,
|
|
638
|
+
// digit, "_" or "$"). ES2025 allows any IdentifierPart after the closing "/".
|
|
542
639
|
func isRegexFlag(ch byte) bool {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
640
|
+
return ch == '_' ||
|
|
641
|
+
ch == '$' ||
|
|
642
|
+
unicode.IsLetter(rune(ch)) ||
|
|
643
|
+
unicode.IsDigit(rune(ch))
|
|
547
644
|
}
|