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 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()) {
@@ -1,206 +1,91 @@
1
1
  /**
2
- * Unit tests for import resolution infrastructure
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
- const source = `import { foo, bar } from 'some-package'`
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
- const source = `import React from 'react'`
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
- const source = `
63
- import { foo } from './local'
64
- import { bar } from '../parent'
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 scoped packages with subpaths', () => {
82
- const source = `import { util } from '@scope/package/utils'`
83
- expect(extractImports(source)).toEqual(['@scope/package/utils'])
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 handle double quotes', () => {
87
- const source = `import { foo } from "some-package"`
88
- expect(extractImports(source)).toEqual(['some-package'])
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('getCDNUrl', () => {
98
- it('should generate URL for simple package', () => {
99
- expect(getCDNUrl('some-unknown-package')).toBe(
100
- 'https://cdn.jsdelivr.net/npm/some-unknown-package'
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 use pinned version and path for known packages', () => {
105
- // tosijs has pinned version and path
106
- expect(getCDNUrl('tosijs')).toBe(
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 with pinned version', () => {
116
- expect(getCDNUrl('lodash-es/debounce')).toBe(
117
- 'https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/debounce'
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(getCDNUrl('@scope/package')).toBe(
123
- 'https://cdn.jsdelivr.net/npm/@scope/package'
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 handle scoped packages with subpaths', () => {
128
- expect(getCDNUrl('@scope/package/utils')).toBe(
129
- 'https://cdn.jsdelivr.net/npm/@scope/package/utils'
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 handle packages with pinned version but no path', () => {
134
- // lodash-es is pinned but has no explicit path
135
- expect(getCDNUrl('lodash-es')).toBe(
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 handle empty array', () => {
153
- expect(generateImportMap([])).toEqual({ imports: {} })
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 subpath imports', () => {
157
- const result = generateImportMap(['lodash-es/debounce'])
158
- expect(result.imports['lodash-es/debounce']).toBe(
159
- 'https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/debounce'
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
- expect(script).toContain('<script type="importmap">')
174
- expect(script).toContain('</script>')
175
- expect(script).toContain('"tosijs"')
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
- })