tjs-lang 0.6.28 → 0.6.31
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/bin/dev.ts +107 -2
- package/demo/src/imports.test.ts +46 -161
- package/demo/src/imports.ts +46 -451
- package/demo/src/index.ts +4 -0
- package/demo/src/tfs-worker.js +221 -0
- package/demo/src/tjs-playground.ts +7 -27
- package/demo/src/ts-playground.ts +5 -23
- package/dist/index.js +48 -47
- package/dist/index.js.map +4 -4
- package/dist/tjs-full.js +48 -47
- package/dist/tjs-full.js.map +4 -4
- package/package.json +1 -1
- package/src/bun-plugin/tjs-plugin.ts +6 -0
- package/src/cli/tjs.ts +1 -1
- package/src/lang/emitters/js.ts +101 -22
- package/src/lang/runtime.ts +7 -1
package/bin/dev.ts
CHANGED
|
@@ -77,8 +77,8 @@ async function buildDemo() {
|
|
|
77
77
|
naming: 'tjs-runtime.js',
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
-
// Copy static files
|
|
81
|
-
await $`cp demo/index.html demo/static/favicon.svg demo/static/photo-*.jpg tjs-lang.svg .demo/`
|
|
80
|
+
// Copy static files (including TFS service worker — must not be bundled)
|
|
81
|
+
await $`cp demo/index.html demo/static/favicon.svg demo/static/photo-*.jpg tjs-lang.svg demo/src/tfs-worker.js .demo/`
|
|
82
82
|
await $`cp -r demo/static/texts .demo/`
|
|
83
83
|
|
|
84
84
|
console.log('Build complete!')
|
|
@@ -195,6 +195,111 @@ const server = Bun.serve({
|
|
|
195
195
|
})
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
// TFS proxy — resolve npm packages from jsdelivr CDN
|
|
199
|
+
// This is the server-side fallback when the service worker can't intercept
|
|
200
|
+
// (e.g. blob iframes, first load before SW is active)
|
|
201
|
+
if (pathname.startsWith('/tfs/')) {
|
|
202
|
+
const tfsPath = pathname.slice(5)
|
|
203
|
+
const CDN_BASE = 'https://cdn.jsdelivr.net/npm'
|
|
204
|
+
|
|
205
|
+
// Parse package@version/subpath
|
|
206
|
+
let name: string, version: string, subpath: string
|
|
207
|
+
if (tfsPath.startsWith('@')) {
|
|
208
|
+
const match = tfsPath.match(/^(@[^/]+\/[^/@]+)(?:@([^/]+))?(\/.*)?$/)
|
|
209
|
+
if (match) {
|
|
210
|
+
name = match[1]
|
|
211
|
+
version = match[2] || 'latest'
|
|
212
|
+
subpath = match[3] || ''
|
|
213
|
+
} else {
|
|
214
|
+
return new Response('invalid tfs path', { status: 400 })
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
const match = tfsPath.match(/^([^/@]+)(?:@([^/]+))?(\/.*)?$/)
|
|
218
|
+
if (match) {
|
|
219
|
+
name = match[1]
|
|
220
|
+
version = match[2] || 'latest'
|
|
221
|
+
subpath = match[3] || ''
|
|
222
|
+
} else {
|
|
223
|
+
return new Response('invalid tfs path', { status: 400 })
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
// If no subpath, resolve ESM entry point from package.json
|
|
229
|
+
if (!subpath) {
|
|
230
|
+
const pkgRes = await fetch(
|
|
231
|
+
`${CDN_BASE}/${name}@${version}/package.json`
|
|
232
|
+
)
|
|
233
|
+
if (pkgRes.ok) {
|
|
234
|
+
const pkg = await pkgRes.json()
|
|
235
|
+
const exp = pkg.exports
|
|
236
|
+
let entryPath: string | null = null
|
|
237
|
+
|
|
238
|
+
if (exp) {
|
|
239
|
+
// exports can be { ".": { import: "..." } } or { import: "..." }
|
|
240
|
+
const dot = exp['.'] ?? exp
|
|
241
|
+
if (typeof dot === 'string') entryPath = dot
|
|
242
|
+
else if (dot?.import)
|
|
243
|
+
entryPath =
|
|
244
|
+
typeof dot.import === 'string'
|
|
245
|
+
? dot.import
|
|
246
|
+
: dot.import?.default
|
|
247
|
+
else if (dot?.default) entryPath = dot.default
|
|
248
|
+
}
|
|
249
|
+
if (!entryPath) entryPath = pkg.module || pkg.main || '/index.js'
|
|
250
|
+
subpath = entryPath!.startsWith('/')
|
|
251
|
+
? entryPath!
|
|
252
|
+
: entryPath!.startsWith('./')
|
|
253
|
+
? entryPath!.slice(1)
|
|
254
|
+
: `/${entryPath}`
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const cdnUrl = `${CDN_BASE}/${name}@${version}${subpath}`
|
|
259
|
+
const cdnRes = await fetch(cdnUrl)
|
|
260
|
+
if (!cdnRes.ok) {
|
|
261
|
+
return new Response(`package not found: ${name}@${version}`, {
|
|
262
|
+
status: 404,
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let body = await cdnRes.text()
|
|
267
|
+
const origin = new URL(req.url).origin
|
|
268
|
+
const pkgBase = `${CDN_BASE}/${name}@${version}`
|
|
269
|
+
|
|
270
|
+
// Rewrite imports in the fetched module:
|
|
271
|
+
// - Bare specifiers → /tfs/ (transitive deps)
|
|
272
|
+
// - Relative imports → absolute CDN URLs (sibling files)
|
|
273
|
+
body = body.replace(
|
|
274
|
+
/((?:import|export)\s+(?:[\w\s{},*]+\s+from\s+)?)(['"])([^'"]+)\2/g,
|
|
275
|
+
(match: string, prefix: string, quote: string, spec: string) => {
|
|
276
|
+
if (spec.startsWith('http://') || spec.startsWith('https://'))
|
|
277
|
+
return match
|
|
278
|
+
if (spec.startsWith('./') || spec.startsWith('../')) {
|
|
279
|
+
// Relative import → resolve against CDN package path
|
|
280
|
+
const dir = subpath ? subpath.replace(/\/[^/]*$/, '') : '/dist'
|
|
281
|
+
// Add .js extension if missing (CDN requires it)
|
|
282
|
+
const specWithExt = /\.\w+$/.test(spec) ? spec : `${spec}.js`
|
|
283
|
+
const resolved = new URL(specWithExt, `${pkgBase}${dir}/`).href
|
|
284
|
+
return `${prefix}${quote}${resolved}${quote}`
|
|
285
|
+
}
|
|
286
|
+
if (spec.startsWith('/')) return match
|
|
287
|
+
// Bare specifier → route through /tfs/
|
|
288
|
+
return `${prefix}${quote}${origin}/tfs/${spec}${quote}`
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
return new Response(body, {
|
|
293
|
+
headers: {
|
|
294
|
+
'Content-Type': 'application/javascript',
|
|
295
|
+
'Access-Control-Allow-Origin': '*',
|
|
296
|
+
},
|
|
297
|
+
})
|
|
298
|
+
} catch (err: any) {
|
|
299
|
+
return new Response(`tfs error: ${err.message}`, { status: 502 })
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
198
303
|
// For SPA routing, serve index.html for unknown paths
|
|
199
304
|
const indexFile = Bun.file(join(DOCS_DIR, 'index.html'))
|
|
200
305
|
if (await indexFile.exists()) {
|
package/demo/src/imports.test.ts
CHANGED
|
@@ -1,206 +1,91 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unit tests for import resolution
|
|
3
|
-
*
|
|
4
|
-
* Tests the synchronous functions that don't require browser/network.
|
|
2
|
+
* Unit tests for TFS import resolution
|
|
5
3
|
*/
|
|
6
4
|
|
|
7
5
|
import { describe, it, expect } from 'bun:test'
|
|
8
|
-
import {
|
|
9
|
-
extractImports,
|
|
10
|
-
getCDNUrl,
|
|
11
|
-
generateImportMap,
|
|
12
|
-
generateImportMapScript,
|
|
13
|
-
wrapAsModule,
|
|
14
|
-
clearModuleCache,
|
|
15
|
-
getCacheStats,
|
|
16
|
-
} from './imports'
|
|
6
|
+
import { extractImports, rewriteImports } from './imports'
|
|
17
7
|
|
|
18
8
|
describe('extractImports', () => {
|
|
19
9
|
it('should extract named imports', () => {
|
|
20
|
-
|
|
21
|
-
expect(extractImports(source)).toEqual(['some-package'])
|
|
10
|
+
expect(extractImports(`import { foo } from 'pkg'`)).toEqual(['pkg'])
|
|
22
11
|
})
|
|
23
12
|
|
|
24
13
|
it('should extract default imports', () => {
|
|
25
|
-
|
|
26
|
-
expect(extractImports(source)).toEqual(['react'])
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('should extract namespace imports', () => {
|
|
30
|
-
const source = `import * as lodash from 'lodash'`
|
|
31
|
-
expect(extractImports(source)).toEqual(['lodash'])
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('should extract side-effect imports', () => {
|
|
35
|
-
const source = `import 'polyfill'`
|
|
36
|
-
expect(extractImports(source)).toEqual(['polyfill'])
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('should extract re-exports', () => {
|
|
40
|
-
const source = `export { foo } from 'some-package'`
|
|
41
|
-
expect(extractImports(source)).toEqual(['some-package'])
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('should handle multiple imports', () => {
|
|
45
|
-
const source = `
|
|
46
|
-
import { add } from 'lodash'
|
|
47
|
-
import { format } from 'date-fns'
|
|
48
|
-
import React from 'react'
|
|
49
|
-
`
|
|
50
|
-
expect(extractImports(source)).toEqual(['lodash', 'date-fns', 'react'])
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('should deduplicate imports', () => {
|
|
54
|
-
const source = `
|
|
55
|
-
import { add } from 'lodash'
|
|
56
|
-
import { subtract } from 'lodash'
|
|
57
|
-
`
|
|
58
|
-
expect(extractImports(source)).toEqual(['lodash'])
|
|
14
|
+
expect(extractImports(`import React from 'react'`)).toEqual(['react'])
|
|
59
15
|
})
|
|
60
16
|
|
|
61
17
|
it('should ignore relative imports', () => {
|
|
62
|
-
|
|
63
|
-
import {
|
|
64
|
-
|
|
65
|
-
import { baz } from '/absolute'
|
|
66
|
-
import { qux } from 'npm-package'
|
|
67
|
-
`
|
|
68
|
-
expect(extractImports(source)).toEqual(['npm-package'])
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('should handle subpath imports', () => {
|
|
72
|
-
const source = `import { debounce } from 'lodash/debounce'`
|
|
73
|
-
expect(extractImports(source)).toEqual(['lodash/debounce'])
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('should handle scoped packages', () => {
|
|
77
|
-
const source = `import { something } from '@scope/package'`
|
|
78
|
-
expect(extractImports(source)).toEqual(['@scope/package'])
|
|
18
|
+
expect(
|
|
19
|
+
extractImports(`import { a } from './local'\nimport { b } from 'pkg'`)
|
|
20
|
+
).toEqual(['pkg'])
|
|
79
21
|
})
|
|
80
22
|
|
|
81
|
-
it('should handle
|
|
82
|
-
|
|
83
|
-
|
|
23
|
+
it('should handle versioned specifiers', () => {
|
|
24
|
+
expect(extractImports(`import { x } from 'tosijs@1.3.11'`)).toEqual([
|
|
25
|
+
'tosijs@1.3.11',
|
|
26
|
+
])
|
|
84
27
|
})
|
|
85
28
|
|
|
86
|
-
it('should
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
it('should return empty array for no imports', () => {
|
|
92
|
-
const source = `const x = 1; function foo() { return x }`
|
|
93
|
-
expect(extractImports(source)).toEqual([])
|
|
29
|
+
it('should deduplicate', () => {
|
|
30
|
+
expect(
|
|
31
|
+
extractImports(`import { a } from 'pkg'\nimport { b } from 'pkg'`)
|
|
32
|
+
).toEqual(['pkg'])
|
|
94
33
|
})
|
|
95
34
|
})
|
|
96
35
|
|
|
97
|
-
describe('
|
|
98
|
-
it('should
|
|
99
|
-
expect(
|
|
100
|
-
'
|
|
36
|
+
describe('rewriteImports', () => {
|
|
37
|
+
it('should rewrite bare specifiers to /tfs/', () => {
|
|
38
|
+
expect(rewriteImports(`import { foo } from 'tosijs'`)).toBe(
|
|
39
|
+
`import { foo } from '/tfs/tosijs'`
|
|
101
40
|
)
|
|
102
41
|
})
|
|
103
42
|
|
|
104
|
-
it('should
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
'https://cdn.jsdelivr.net/npm/tosijs@1.2.0/dist/module.js'
|
|
108
|
-
)
|
|
109
|
-
// date-fns has pinned version but no path
|
|
110
|
-
expect(getCDNUrl('date-fns')).toBe(
|
|
111
|
-
'https://cdn.jsdelivr.net/npm/date-fns@3.6.0'
|
|
43
|
+
it('should handle versioned specifiers', () => {
|
|
44
|
+
expect(rewriteImports(`import { x } from 'tosijs@1.3.11'`)).toBe(
|
|
45
|
+
`import { x } from '/tfs/tosijs@1.3.11'`
|
|
112
46
|
)
|
|
113
47
|
})
|
|
114
48
|
|
|
115
|
-
it('should handle subpath imports
|
|
116
|
-
expect(
|
|
117
|
-
'
|
|
118
|
-
)
|
|
49
|
+
it('should handle subpath imports', () => {
|
|
50
|
+
expect(
|
|
51
|
+
rewriteImports(`import { debounce } from 'lodash-es/debounce'`)
|
|
52
|
+
).toBe(`import { debounce } from '/tfs/lodash-es/debounce'`)
|
|
119
53
|
})
|
|
120
54
|
|
|
121
55
|
it('should handle scoped packages', () => {
|
|
122
|
-
expect(
|
|
123
|
-
'
|
|
56
|
+
expect(rewriteImports(`import { x } from '@scope/pkg'`)).toBe(
|
|
57
|
+
`import { x } from '/tfs/@scope/pkg'`
|
|
124
58
|
)
|
|
125
59
|
})
|
|
126
60
|
|
|
127
|
-
it('should
|
|
128
|
-
expect(
|
|
129
|
-
'
|
|
61
|
+
it('should not rewrite relative imports', () => {
|
|
62
|
+
expect(rewriteImports(`import { x } from './local'`)).toBe(
|
|
63
|
+
`import { x } from './local'`
|
|
130
64
|
)
|
|
131
65
|
})
|
|
132
66
|
|
|
133
|
-
it('should
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
'https://cdn.jsdelivr.net/npm/lodash-es@4.17.21'
|
|
67
|
+
it('should not rewrite absolute imports', () => {
|
|
68
|
+
expect(rewriteImports(`import { x } from '/abs/path'`)).toBe(
|
|
69
|
+
`import { x } from '/abs/path'`
|
|
137
70
|
)
|
|
138
71
|
})
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
describe('generateImportMap', () => {
|
|
142
|
-
it('should generate import map for specifiers', () => {
|
|
143
|
-
const result = generateImportMap(['tosijs', 'date-fns'])
|
|
144
|
-
expect(result).toEqual({
|
|
145
|
-
imports: {
|
|
146
|
-
tosijs: 'https://cdn.jsdelivr.net/npm/tosijs@1.2.0/dist/module.js',
|
|
147
|
-
'date-fns': 'https://cdn.jsdelivr.net/npm/date-fns@3.6.0',
|
|
148
|
-
},
|
|
149
|
-
})
|
|
150
|
-
})
|
|
151
72
|
|
|
152
|
-
it('should
|
|
153
|
-
expect(
|
|
73
|
+
it('should not rewrite http imports', () => {
|
|
74
|
+
expect(
|
|
75
|
+
rewriteImports(`import { x } from 'https://cdn.example.com/pkg.js'`)
|
|
76
|
+
).toBe(`import { x } from 'https://cdn.example.com/pkg.js'`)
|
|
154
77
|
})
|
|
155
78
|
|
|
156
|
-
it('should handle
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
)
|
|
79
|
+
it('should handle multiple imports', () => {
|
|
80
|
+
const source = `import { a } from 'pkg-a'\nimport { b } from 'pkg-b'`
|
|
81
|
+
const result = rewriteImports(source)
|
|
82
|
+
expect(result).toContain("from '/tfs/pkg-a'")
|
|
83
|
+
expect(result).toContain("from '/tfs/pkg-b'")
|
|
161
84
|
})
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
describe('generateImportMapScript', () => {
|
|
165
|
-
it('should generate script tag with import map', () => {
|
|
166
|
-
const importMap = {
|
|
167
|
-
imports: {
|
|
168
|
-
tosijs: 'https://cdn.jsdelivr.net/npm/tosijs@1.0.10/dist/module.js',
|
|
169
|
-
},
|
|
170
|
-
}
|
|
171
|
-
const script = generateImportMapScript(importMap)
|
|
172
85
|
|
|
173
|
-
|
|
174
|
-
expect(
|
|
175
|
-
|
|
176
|
-
expect(script).toContain(
|
|
177
|
-
'https://cdn.jsdelivr.net/npm/tosijs@1.0.10/dist/module.js'
|
|
86
|
+
it('should handle re-exports', () => {
|
|
87
|
+
expect(rewriteImports(`export { foo } from 'pkg'`)).toBe(
|
|
88
|
+
`export { foo } from '/tfs/pkg'`
|
|
178
89
|
)
|
|
179
90
|
})
|
|
180
91
|
})
|
|
181
|
-
|
|
182
|
-
describe('wrapAsModule', () => {
|
|
183
|
-
it('should wrap code in module script tag', () => {
|
|
184
|
-
const code = 'console.log("hello")'
|
|
185
|
-
const wrapped = wrapAsModule(code)
|
|
186
|
-
|
|
187
|
-
expect(wrapped).toContain('<script type="module">')
|
|
188
|
-
expect(wrapped).toContain('</script>')
|
|
189
|
-
expect(wrapped).toContain(code)
|
|
190
|
-
})
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
describe('module cache', () => {
|
|
194
|
-
it('should start empty', () => {
|
|
195
|
-
clearModuleCache()
|
|
196
|
-
const stats = getCacheStats()
|
|
197
|
-
expect(stats.size).toBe(0)
|
|
198
|
-
expect(stats.entries).toEqual([])
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
it('should clear cache', () => {
|
|
202
|
-
// Just verify it doesn't throw
|
|
203
|
-
clearModuleCache()
|
|
204
|
-
expect(getCacheStats().size).toBe(0)
|
|
205
|
-
})
|
|
206
|
-
})
|