@ttsc/paths 0.11.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@ttsc/paths`
2
2
 
3
- ![banner of @ttsc/paths](https://raw.githubusercontent.com/samchon/ttsc/refs/heads/master/assets/og.jpg)
3
+ ![banner of @ttsc/paths](https://ttsc.dev/og.jpg)
4
4
 
5
5
  [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/ttsc/blob/master/LICENSE)
6
6
  [![NPM Version](https://img.shields.io/npm/v/@ttsc/paths.svg)](https://www.npmjs.com/package/@ttsc/paths)
@@ -0,0 +1,288 @@
1
+ package paths
2
+
3
+ import (
4
+ "path/filepath"
5
+ "sort"
6
+ "strings"
7
+
8
+ shimast "github.com/microsoft/typescript-go/shim/ast"
9
+ shimcore "github.com/microsoft/typescript-go/shim/core"
10
+
11
+ "github.com/samchon/ttsc/packages/ttsc/driver"
12
+ )
13
+
14
+ func init() {
15
+ driver.RegisterPlugin(plugin{})
16
+ }
17
+
18
+ type plugin struct{}
19
+
20
+ func (plugin) ApplyProgram(prog *driver.Program, _ driver.PluginContext) error {
21
+ rewriter := newRewriter(prog)
22
+ for _, file := range prog.SourceFiles() {
23
+ rewriter.apply(file)
24
+ }
25
+ return nil
26
+ }
27
+
28
+ type rewriter struct {
29
+ basePath string
30
+ outDir string
31
+ patterns []pathPattern
32
+ rootDir string
33
+ sourceFiles map[string]string
34
+ }
35
+
36
+ type pathPattern struct {
37
+ pattern string
38
+ targets []string
39
+ }
40
+
41
+ func newRewriter(prog *driver.Program) *rewriter {
42
+ out := &rewriter{sourceFiles: map[string]string{}}
43
+ if prog == nil || prog.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig.CompilerOptions == nil {
44
+ return out
45
+ }
46
+ options := prog.ParsedConfig.ParsedConfig.CompilerOptions
47
+ out.basePath = filepath.Clean(options.GetPathsBasePath(prog.Host.GetCurrentDirectory()))
48
+ out.outDir = optionalPath(options.OutDir, prog.Host.GetCurrentDirectory())
49
+ out.rootDir = optionalPath(options.RootDir, prog.Host.GetCurrentDirectory())
50
+ files := prog.SourceFiles()
51
+ if out.rootDir == "" {
52
+ out.rootDir = commonSourceDir(files)
53
+ }
54
+ for _, file := range files {
55
+ name := normalizePath(file.FileName())
56
+ out.sourceFiles[name] = name
57
+ out.sourceFiles[stripKnownSourceExtension(name)] = name
58
+ }
59
+ if options.Paths != nil {
60
+ for key, targets := range options.Paths.Entries() {
61
+ out.patterns = append(out.patterns, pathPattern{
62
+ pattern: key,
63
+ targets: append([]string(nil), targets...),
64
+ })
65
+ }
66
+ }
67
+ sort.SliceStable(out.patterns, func(i, j int) bool {
68
+ return patternRank(out.patterns[i].pattern) > patternRank(out.patterns[j].pattern)
69
+ })
70
+ return out
71
+ }
72
+
73
+ func (r *rewriter) apply(file *shimast.SourceFile) {
74
+ if r == nil || file == nil || len(r.patterns) == 0 {
75
+ return
76
+ }
77
+ visitModuleSpecifiers(file.AsNode(), func(lit *shimast.Node) {
78
+ if lit == nil || lit.Kind != shimast.KindStringLiteral {
79
+ return
80
+ }
81
+ spec := lit.Text()
82
+ rewritten, ok := r.rewrite(file.FileName(), spec)
83
+ if ok && rewritten != spec {
84
+ lit.AsStringLiteral().Text = rewritten
85
+ lit.Flags |= shimast.NodeFlagsSynthesized
86
+ lit.Loc = shimcore.UndefinedTextRange()
87
+ }
88
+ })
89
+ }
90
+
91
+ func visitModuleSpecifiers(node *shimast.Node, visit func(*shimast.Node)) {
92
+ if node == nil {
93
+ return
94
+ }
95
+ switch node.Kind {
96
+ case shimast.KindImportDeclaration:
97
+ visit(node.AsImportDeclaration().ModuleSpecifier)
98
+ case shimast.KindExportDeclaration:
99
+ visit(node.AsExportDeclaration().ModuleSpecifier)
100
+ case shimast.KindImportEqualsDeclaration:
101
+ ref := node.AsImportEqualsDeclaration().ModuleReference
102
+ if ref != nil && ref.Kind == shimast.KindExternalModuleReference {
103
+ visit(ref.AsExternalModuleReference().Expression)
104
+ }
105
+ case shimast.KindImportType:
106
+ arg := node.AsImportTypeNode().Argument
107
+ if arg != nil && arg.Kind == shimast.KindLiteralType {
108
+ visit(arg.AsLiteralTypeNode().Literal)
109
+ }
110
+ case shimast.KindModuleDeclaration:
111
+ decl := node.AsModuleDeclaration()
112
+ if decl != nil {
113
+ visit(decl.Name())
114
+ }
115
+ case shimast.KindCallExpression:
116
+ call := node.AsCallExpression()
117
+ if isModuleSpecifierCall(call) && call.Arguments != nil && len(call.Arguments.Nodes) > 0 {
118
+ visit(call.Arguments.Nodes[0])
119
+ }
120
+ }
121
+ node.ForEachChild(func(child *shimast.Node) bool {
122
+ visitModuleSpecifiers(child, visit)
123
+ return false
124
+ })
125
+ }
126
+
127
+ func isModuleSpecifierCall(call *shimast.CallExpression) bool {
128
+ if call == nil || call.Expression == nil {
129
+ return false
130
+ }
131
+ switch call.Expression.Kind {
132
+ case shimast.KindImportKeyword:
133
+ return true
134
+ case shimast.KindIdentifier:
135
+ return call.Expression.Text() == "require"
136
+ default:
137
+ return false
138
+ }
139
+ }
140
+
141
+ func (r *rewriter) rewrite(fromSource string, specifier string) (string, bool) {
142
+ if specifier == "" || strings.HasPrefix(specifier, ".") || strings.HasPrefix(specifier, "/") {
143
+ return specifier, false
144
+ }
145
+ targetSource, ok := r.resolveSource(specifier)
146
+ if !ok {
147
+ return specifier, false
148
+ }
149
+ fromOut := r.outputPathForSource(fromSource)
150
+ targetOut := r.outputPathForSource(targetSource)
151
+ if fromOut == "" || targetOut == "" {
152
+ return specifier, false
153
+ }
154
+ rel, _ := filepath.Rel(filepath.Dir(fromOut), targetOut)
155
+ rel = filepath.ToSlash(rel)
156
+ if !strings.HasPrefix(rel, ".") {
157
+ rel = "./" + rel
158
+ }
159
+ return rel, true
160
+ }
161
+
162
+ func (r *rewriter) resolveSource(specifier string) (string, bool) {
163
+ for _, pattern := range r.patterns {
164
+ star, ok := matchPattern(pattern.pattern, specifier)
165
+ if !ok {
166
+ continue
167
+ }
168
+ for _, target := range pattern.targets {
169
+ candidate := strings.Replace(target, "*", star, 1)
170
+ resolved := normalizePath(filepath.Join(r.basePath, candidate))
171
+ if source, ok := r.lookupSource(resolved); ok {
172
+ return source, true
173
+ }
174
+ }
175
+ }
176
+ return "", false
177
+ }
178
+
179
+ func (r *rewriter) lookupSource(candidate string) (string, bool) {
180
+ if source, ok := r.sourceFiles[normalizePath(candidate)]; ok {
181
+ return source, true
182
+ }
183
+ stem := stripKnownSourceExtension(normalizePath(candidate))
184
+ if source, ok := r.sourceFiles[stem]; ok {
185
+ return source, true
186
+ }
187
+ for _, ext := range []string{".ts", ".tsx", ".mts", ".cts"} {
188
+ if source, ok := r.sourceFiles[stem+ext]; ok {
189
+ return source, true
190
+ }
191
+ }
192
+ for _, ext := range []string{".ts", ".tsx", ".mts", ".cts"} {
193
+ if source, ok := r.sourceFiles[normalizePath(filepath.Join(stem, "index"+ext))]; ok {
194
+ return source, true
195
+ }
196
+ }
197
+ return "", false
198
+ }
199
+
200
+ func (r *rewriter) outputPathForSource(source string) string {
201
+ if r.outDir == "" || r.rootDir == "" {
202
+ return ""
203
+ }
204
+ rel, err := filepath.Rel(r.rootDir, source)
205
+ if err != nil || isOutsideRelativePath(rel) {
206
+ return ""
207
+ }
208
+ return normalizePath(filepath.Join(r.outDir, replaceSourceExtension(rel, emittedJavaScriptExtension(rel))))
209
+ }
210
+
211
+ func emittedJavaScriptExtension(source string) string {
212
+ switch strings.ToLower(filepath.Ext(source)) {
213
+ case ".mts":
214
+ return ".mjs"
215
+ case ".cts":
216
+ return ".cjs"
217
+ default:
218
+ return ".js"
219
+ }
220
+ }
221
+
222
+ func matchPattern(pattern string, specifier string) (string, bool) {
223
+ if !strings.Contains(pattern, "*") {
224
+ return "", pattern == specifier
225
+ }
226
+ parts := strings.SplitN(pattern, "*", 2)
227
+ if !strings.HasPrefix(specifier, parts[0]) || !strings.HasSuffix(specifier, parts[1]) {
228
+ return "", false
229
+ }
230
+ return specifier[len(parts[0]) : len(specifier)-len(parts[1])], true
231
+ }
232
+
233
+ func patternRank(pattern string) int {
234
+ return len(strings.ReplaceAll(pattern, "*", ""))
235
+ }
236
+
237
+ func optionalPath(value string, cwd string) string {
238
+ if value == "" {
239
+ return ""
240
+ }
241
+ if filepath.IsAbs(value) {
242
+ return normalizePath(value)
243
+ }
244
+ return normalizePath(filepath.Join(cwd, value))
245
+ }
246
+
247
+ func commonSourceDir(files []*shimast.SourceFile) string {
248
+ if len(files) == 0 {
249
+ return ""
250
+ }
251
+ common := normalizePath(filepath.Dir(files[0].FileName()))
252
+ for _, file := range files[1:] {
253
+ dir := normalizePath(filepath.Dir(file.FileName()))
254
+ for common != "" && !strings.HasPrefix(dir+"/", common+"/") {
255
+ next := filepath.Dir(common)
256
+ if next == common {
257
+ return common
258
+ }
259
+ common = normalizePath(next)
260
+ }
261
+ }
262
+ return common
263
+ }
264
+
265
+ func normalizePath(value string) string {
266
+ if value == "" {
267
+ return ""
268
+ }
269
+ return filepath.ToSlash(filepath.Clean(value))
270
+ }
271
+
272
+ func stripKnownSourceExtension(value string) string {
273
+ lower := strings.ToLower(value)
274
+ for _, ext := range []string{".d.ts", ".d.mts", ".d.cts", ".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"} {
275
+ if strings.HasSuffix(lower, ext) {
276
+ return value[:len(value)-len(ext)]
277
+ }
278
+ }
279
+ return strings.TrimSuffix(value, filepath.Ext(value))
280
+ }
281
+
282
+ func replaceSourceExtension(value string, ext string) string {
283
+ return stripKnownSourceExtension(filepath.ToSlash(value)) + ext
284
+ }
285
+
286
+ func isOutsideRelativePath(rel string) bool {
287
+ return rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator))
288
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/paths",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "description": "First-party ttsc plugin that rewrites emitted module specifiers from tsconfig paths.",
5
5
  "main": "src/index.cjs",
6
6
  "exports": {
@@ -20,6 +20,7 @@
20
20
  },
21
21
  "files": [
22
22
  "README.md",
23
+ "driver/paths.go",
23
24
  "src/index.cjs",
24
25
  "go.mod",
25
26
  "plugin/main.go",
package/plugin/paths.go CHANGED
@@ -1,3 +1,3 @@
1
1
  package main
2
2
 
3
- // Paths transform logic lives in github.com/samchon/ttsc/packages/ttsc/utility.
3
+ import _ "github.com/samchon/ttsc/packages/paths/driver"
package/src/index.cjs CHANGED
@@ -6,7 +6,7 @@ const path = require("node:path");
6
6
  module.exports = function createTtscPaths() {
7
7
  return {
8
8
  name: "@ttsc/paths",
9
- source: path.resolve(__dirname, "..", "plugin"),
9
+ source: path.resolve(__dirname, "..", "driver"),
10
10
  stage: "transform",
11
11
  };
12
12
  };