tjs-lang 0.2.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/CONTEXT.md +594 -0
- package/LICENSE +190 -0
- package/README.md +220 -0
- package/bin/benchmarks.ts +351 -0
- package/bin/dev.ts +205 -0
- package/bin/docs.js +170 -0
- package/bin/install-cursor.sh +71 -0
- package/bin/install-vscode.sh +71 -0
- package/bin/select-local-models.d.ts +1 -0
- package/bin/select-local-models.js +28 -0
- package/bin/select-local-models.ts +31 -0
- package/demo/autocomplete.test.ts +232 -0
- package/demo/docs.json +186 -0
- package/demo/examples.test.ts +598 -0
- package/demo/index.html +91 -0
- package/demo/src/autocomplete.ts +482 -0
- package/demo/src/capabilities.ts +859 -0
- package/demo/src/demo-nav.ts +2097 -0
- package/demo/src/examples.test.ts +161 -0
- package/demo/src/examples.ts +476 -0
- package/demo/src/imports.test.ts +196 -0
- package/demo/src/imports.ts +421 -0
- package/demo/src/index.ts +639 -0
- package/demo/src/module-store.ts +635 -0
- package/demo/src/module-sw.ts +132 -0
- package/demo/src/playground.ts +949 -0
- package/demo/src/service-host.ts +389 -0
- package/demo/src/settings.ts +440 -0
- package/demo/src/style.ts +280 -0
- package/demo/src/tjs-playground.ts +1605 -0
- package/demo/src/ts-examples.ts +478 -0
- package/demo/src/ts-playground.ts +1092 -0
- package/demo/static/favicon.svg +30 -0
- package/demo/static/photo-1.jpg +0 -0
- package/demo/static/photo-2.jpg +0 -0
- package/demo/static/texts/ai-history.txt +9 -0
- package/demo/static/texts/coffee-origins.txt +9 -0
- package/demo/static/texts/renewable-energy.txt +9 -0
- package/dist/index.js +256 -0
- package/dist/index.js.map +37 -0
- package/dist/tjs-batteries.js +4 -0
- package/dist/tjs-batteries.js.map +15 -0
- package/dist/tjs-full.js +256 -0
- package/dist/tjs-full.js.map +37 -0
- package/dist/tjs-transpiler.js +220 -0
- package/dist/tjs-transpiler.js.map +21 -0
- package/dist/tjs-vm.js +4 -0
- package/dist/tjs-vm.js.map +14 -0
- package/docs/CNAME +1 -0
- package/docs/favicon.svg +30 -0
- package/docs/index.html +91 -0
- package/docs/index.js +10468 -0
- package/docs/index.js.map +92 -0
- package/docs/photo-1.jpg +0 -0
- package/docs/photo-1.webp +0 -0
- package/docs/photo-2.jpg +0 -0
- package/docs/photo-2.webp +0 -0
- package/docs/texts/ai-history.txt +9 -0
- package/docs/texts/coffee-origins.txt +9 -0
- package/docs/texts/renewable-energy.txt +9 -0
- package/docs/tjs-lang.svg +31 -0
- package/docs/tosijs-agent.svg +31 -0
- package/editors/README.md +325 -0
- package/editors/ace/ajs-mode.js +328 -0
- package/editors/ace/ajs-mode.ts +269 -0
- package/editors/ajs-syntax.ts +212 -0
- package/editors/build-grammars.ts +510 -0
- package/editors/codemirror/ajs-language.js +287 -0
- package/editors/codemirror/ajs-language.ts +1447 -0
- package/editors/codemirror/autocomplete.test.ts +531 -0
- package/editors/codemirror/component.ts +404 -0
- package/editors/monaco/ajs-monarch.js +243 -0
- package/editors/monaco/ajs-monarch.ts +225 -0
- package/editors/tjs-syntax.ts +115 -0
- package/editors/vscode/language-configuration.json +37 -0
- package/editors/vscode/package.json +65 -0
- package/editors/vscode/syntaxes/ajs-injection.tmLanguage.json +107 -0
- package/editors/vscode/syntaxes/ajs.tmLanguage.json +252 -0
- package/editors/vscode/syntaxes/tjs.tmLanguage.json +333 -0
- package/package.json +83 -0
- package/src/cli/commands/check.ts +41 -0
- package/src/cli/commands/convert.ts +133 -0
- package/src/cli/commands/emit.ts +260 -0
- package/src/cli/commands/run.ts +68 -0
- package/src/cli/commands/test.ts +194 -0
- package/src/cli/commands/types.ts +20 -0
- package/src/cli/create-app.ts +236 -0
- package/src/cli/playground.ts +250 -0
- package/src/cli/tjs.ts +166 -0
- package/src/cli/tjsx.ts +160 -0
- package/tjs-lang.svg +31 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for import resolution infrastructure
|
|
3
|
+
*
|
|
4
|
+
* Tests the synchronous functions that don't require browser/network.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
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'
|
|
17
|
+
|
|
18
|
+
describe('extractImports', () => {
|
|
19
|
+
it('should extract named imports', () => {
|
|
20
|
+
const source = `import { foo, bar } from 'some-package'`
|
|
21
|
+
expect(extractImports(source)).toEqual(['some-package'])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
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'])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
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'])
|
|
79
|
+
})
|
|
80
|
+
|
|
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'])
|
|
84
|
+
})
|
|
85
|
+
|
|
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([])
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('getCDNUrl', () => {
|
|
98
|
+
it('should generate URL for simple package', () => {
|
|
99
|
+
expect(getCDNUrl('some-unknown-package')).toBe(
|
|
100
|
+
'https://unpkg.com/some-unknown-package'
|
|
101
|
+
)
|
|
102
|
+
})
|
|
103
|
+
|
|
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://unpkg.com/tosijs@1.0.10/dist/module.js'
|
|
108
|
+
)
|
|
109
|
+
// date-fns has pinned version but no path
|
|
110
|
+
expect(getCDNUrl('date-fns')).toBe('https://unpkg.com/date-fns@3.6.0')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should handle subpath imports with pinned version', () => {
|
|
114
|
+
expect(getCDNUrl('lodash-es/debounce')).toBe(
|
|
115
|
+
'https://unpkg.com/lodash-es@4.17.21/debounce'
|
|
116
|
+
)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('should handle scoped packages', () => {
|
|
120
|
+
expect(getCDNUrl('@scope/package')).toBe('https://unpkg.com/@scope/package')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should handle scoped packages with subpaths', () => {
|
|
124
|
+
expect(getCDNUrl('@scope/package/utils')).toBe(
|
|
125
|
+
'https://unpkg.com/@scope/package/utils'
|
|
126
|
+
)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should handle packages with pinned version but no path', () => {
|
|
130
|
+
// lodash-es is pinned but has no explicit path
|
|
131
|
+
expect(getCDNUrl('lodash-es')).toBe('https://unpkg.com/lodash-es@4.17.21')
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
describe('generateImportMap', () => {
|
|
136
|
+
it('should generate import map for specifiers', () => {
|
|
137
|
+
const result = generateImportMap(['tosijs', 'date-fns'])
|
|
138
|
+
expect(result).toEqual({
|
|
139
|
+
imports: {
|
|
140
|
+
tosijs: 'https://unpkg.com/tosijs@1.0.10/dist/module.js',
|
|
141
|
+
'date-fns': 'https://unpkg.com/date-fns@3.6.0',
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('should handle empty array', () => {
|
|
147
|
+
expect(generateImportMap([])).toEqual({ imports: {} })
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should handle subpath imports', () => {
|
|
151
|
+
const result = generateImportMap(['lodash-es/debounce'])
|
|
152
|
+
expect(result.imports['lodash-es/debounce']).toBe(
|
|
153
|
+
'https://unpkg.com/lodash-es@4.17.21/debounce'
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('generateImportMapScript', () => {
|
|
159
|
+
it('should generate script tag with import map', () => {
|
|
160
|
+
const importMap = {
|
|
161
|
+
imports: { tosijs: 'https://unpkg.com/tosijs@1.0.10/dist/module.js' },
|
|
162
|
+
}
|
|
163
|
+
const script = generateImportMapScript(importMap)
|
|
164
|
+
|
|
165
|
+
expect(script).toContain('<script type="importmap">')
|
|
166
|
+
expect(script).toContain('</script>')
|
|
167
|
+
expect(script).toContain('"tosijs"')
|
|
168
|
+
expect(script).toContain('https://unpkg.com/tosijs@1.0.10/dist/module.js')
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('wrapAsModule', () => {
|
|
173
|
+
it('should wrap code in module script tag', () => {
|
|
174
|
+
const code = 'console.log("hello")'
|
|
175
|
+
const wrapped = wrapAsModule(code)
|
|
176
|
+
|
|
177
|
+
expect(wrapped).toContain('<script type="module">')
|
|
178
|
+
expect(wrapped).toContain('</script>')
|
|
179
|
+
expect(wrapped).toContain(code)
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe('module cache', () => {
|
|
184
|
+
it('should start empty', () => {
|
|
185
|
+
clearModuleCache()
|
|
186
|
+
const stats = getCacheStats()
|
|
187
|
+
expect(stats.size).toBe(0)
|
|
188
|
+
expect(stats.entries).toEqual([])
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should clear cache', () => {
|
|
192
|
+
// Just verify it doesn't throw
|
|
193
|
+
clearModuleCache()
|
|
194
|
+
expect(getCacheStats().size).toBe(0)
|
|
195
|
+
})
|
|
196
|
+
})
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playground Import Resolver
|
|
3
|
+
*
|
|
4
|
+
* Resolves and fetches external modules for the TJS playground.
|
|
5
|
+
* Uses unpkg.com CDN for npm packages.
|
|
6
|
+
*
|
|
7
|
+
* Note: unpkg serves files directly from npm, unlike esm.sh which
|
|
8
|
+
* converts CJS to ESM. For packages without proper ESM exports,
|
|
9
|
+
* you need to specify the exact path to the ESM bundle.
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Detects import statements in code
|
|
13
|
+
* - Fetches modules from unpkg with pinned versions
|
|
14
|
+
* - Caches fetched modules in memory
|
|
15
|
+
* - Returns import map for browser
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// CDN base URL - unpkg serves files directly from npm
|
|
19
|
+
const CDN_BASE = 'https://unpkg.com'
|
|
20
|
+
|
|
21
|
+
// In-memory cache for fetched module URLs
|
|
22
|
+
const moduleCache = new Map<string, string>()
|
|
23
|
+
|
|
24
|
+
// Common packages that should use specific versions and ESM paths
|
|
25
|
+
// unpkg serves files directly, so we need explicit paths to ESM bundles
|
|
26
|
+
// Packages with proper "exports" or "module" fields in package.json
|
|
27
|
+
// may work without explicit paths, but it's safer to specify them
|
|
28
|
+
const PINNED_PACKAGES: Record<string, { version: string; path?: string }> = {
|
|
29
|
+
// tosijs ecosystem
|
|
30
|
+
tosijs: { version: '1.0.10', path: '/dist/module.js' },
|
|
31
|
+
'tosijs-ui': { version: '1.0.10', path: '/dist/index.js' },
|
|
32
|
+
|
|
33
|
+
// Utilities - lodash-es is native ESM
|
|
34
|
+
'lodash-es': { version: '4.17.21' },
|
|
35
|
+
'date-fns': { version: '3.6.0' }, // v3+ is native ESM
|
|
36
|
+
|
|
37
|
+
// Validation
|
|
38
|
+
zod: { version: '3.23.8', path: '/lib/index.mjs' },
|
|
39
|
+
|
|
40
|
+
// Markdown
|
|
41
|
+
marked: { version: '9.1.6', path: '/lib/marked.esm.js' },
|
|
42
|
+
|
|
43
|
+
// UI frameworks - these have proper ESM exports
|
|
44
|
+
preact: { version: '10.19.0', path: '/dist/preact.module.js' },
|
|
45
|
+
'preact/hooks': { version: '10.19.0', path: '/hooks/dist/hooks.module.js' },
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Extract import specifiers from source code
|
|
50
|
+
*/
|
|
51
|
+
export function extractImports(source: string): string[] {
|
|
52
|
+
const imports: string[] = []
|
|
53
|
+
|
|
54
|
+
// Match: import ... from 'package'
|
|
55
|
+
// Match: import ... from "package"
|
|
56
|
+
// Match: import 'package'
|
|
57
|
+
// Match: export ... from 'package'
|
|
58
|
+
const importRegex =
|
|
59
|
+
/(?:import|export)\s+(?:[\w\s{},*]+\s+from\s+)?['"]([^'"]+)['"]/g
|
|
60
|
+
|
|
61
|
+
let match
|
|
62
|
+
while ((match = importRegex.exec(source)) !== null) {
|
|
63
|
+
const specifier = match[1]
|
|
64
|
+
// Only resolve bare specifiers (not relative or absolute paths)
|
|
65
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
66
|
+
imports.push(specifier)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return [...new Set(imports)] // Dedupe
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse a package specifier into name and subpath
|
|
75
|
+
*
|
|
76
|
+
* Examples:
|
|
77
|
+
* 'lodash' -> { name: 'lodash', subpath: '' }
|
|
78
|
+
* 'lodash/debounce' -> { name: 'lodash', subpath: '/debounce' }
|
|
79
|
+
* '@scope/pkg' -> { name: '@scope/pkg', subpath: '' }
|
|
80
|
+
* '@scope/pkg/util' -> { name: '@scope/pkg', subpath: '/util' }
|
|
81
|
+
*/
|
|
82
|
+
function parseSpecifier(specifier: string): { name: string; subpath: string } {
|
|
83
|
+
if (specifier.startsWith('@')) {
|
|
84
|
+
// Scoped package: @scope/name or @scope/name/path
|
|
85
|
+
const parts = specifier.split('/')
|
|
86
|
+
const name = `${parts[0]}/${parts[1]}`
|
|
87
|
+
const subpath = parts.slice(2).join('/')
|
|
88
|
+
return { name, subpath: subpath ? `/${subpath}` : '' }
|
|
89
|
+
} else {
|
|
90
|
+
// Regular package: name or name/path
|
|
91
|
+
const slashIndex = specifier.indexOf('/')
|
|
92
|
+
if (slashIndex === -1) {
|
|
93
|
+
return { name: specifier, subpath: '' }
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
name: specifier.slice(0, slashIndex),
|
|
97
|
+
subpath: specifier.slice(slashIndex),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get CDN URL for a package specifier
|
|
104
|
+
*
|
|
105
|
+
* unpkg serves files directly from npm packages. For ESM packages,
|
|
106
|
+
* we often need to specify the exact path to the ESM bundle since
|
|
107
|
+
* unpkg doesn't do automatic ESM conversion like esm.sh.
|
|
108
|
+
*/
|
|
109
|
+
export function getCDNUrl(specifier: string): string {
|
|
110
|
+
const { name, subpath } = parseSpecifier(specifier)
|
|
111
|
+
|
|
112
|
+
// Check for pinned package config
|
|
113
|
+
const pinned = PINNED_PACKAGES[name]
|
|
114
|
+
|
|
115
|
+
if (pinned) {
|
|
116
|
+
// If subpath provided in specifier, use that; otherwise use pinned path
|
|
117
|
+
const path = subpath || pinned.path || ''
|
|
118
|
+
return `${CDN_BASE}/${name}@${pinned.version}${path}`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// For unknown packages, try unpkg's default resolution
|
|
122
|
+
// This may not work for all packages if they don't have proper ESM exports
|
|
123
|
+
return `${CDN_BASE}/${name}${subpath}`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Generate an import map for the browser
|
|
128
|
+
*
|
|
129
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap
|
|
130
|
+
*/
|
|
131
|
+
export function generateImportMap(specifiers: string[]): {
|
|
132
|
+
imports: Record<string, string>
|
|
133
|
+
} {
|
|
134
|
+
const imports: Record<string, string> = {}
|
|
135
|
+
|
|
136
|
+
for (const specifier of specifiers) {
|
|
137
|
+
imports[specifier] = getCDNUrl(specifier)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { imports }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Cache for package.json data
|
|
144
|
+
const packageJsonCache = new Map<string, any>()
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Fetch and cache package.json for a package
|
|
148
|
+
*/
|
|
149
|
+
async function getPackageJson(name: string, version?: string): Promise<any> {
|
|
150
|
+
const cacheKey = version ? `${name}@${version}` : name
|
|
151
|
+
|
|
152
|
+
if (packageJsonCache.has(cacheKey)) {
|
|
153
|
+
return packageJsonCache.get(cacheKey)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const url = `${CDN_BASE}/${cacheKey}/package.json`
|
|
157
|
+
const response = await fetch(url)
|
|
158
|
+
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
throw new Error(`Package not found: ${cacheKey}`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const pkg = await response.json()
|
|
164
|
+
packageJsonCache.set(cacheKey, pkg)
|
|
165
|
+
return pkg
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Resolve the ESM entry point from package.json
|
|
170
|
+
* Checks exports, module, and main fields in order of preference
|
|
171
|
+
*/
|
|
172
|
+
function resolveEntryPoint(pkg: any): string | null {
|
|
173
|
+
// Check exports field first (modern packages)
|
|
174
|
+
if (pkg.exports) {
|
|
175
|
+
// Handle string exports
|
|
176
|
+
if (typeof pkg.exports === 'string') {
|
|
177
|
+
return pkg.exports
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Handle exports object - look for ESM entry
|
|
181
|
+
const exp = pkg.exports['.'] ?? pkg.exports
|
|
182
|
+
if (typeof exp === 'string') {
|
|
183
|
+
return exp
|
|
184
|
+
}
|
|
185
|
+
if (exp?.import) {
|
|
186
|
+
return typeof exp.import === 'string' ? exp.import : exp.import?.default
|
|
187
|
+
}
|
|
188
|
+
if (exp?.module) {
|
|
189
|
+
return exp.module
|
|
190
|
+
}
|
|
191
|
+
if (exp?.default) {
|
|
192
|
+
return typeof exp.default === 'string' ? exp.default : null
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check module field (ES modules)
|
|
197
|
+
if (pkg.module) {
|
|
198
|
+
return pkg.module
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check main field (may be CJS, but worth trying)
|
|
202
|
+
if (pkg.main) {
|
|
203
|
+
return pkg.main
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return null
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Fetch and cache a module, returning its resolved URL
|
|
211
|
+
* Uses package.json to find the correct ESM entry point
|
|
212
|
+
*/
|
|
213
|
+
export async function resolveModule(specifier: string): Promise<string> {
|
|
214
|
+
// Check cache first
|
|
215
|
+
if (moduleCache.has(specifier)) {
|
|
216
|
+
return moduleCache.get(specifier)!
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const { name, subpath } = parseSpecifier(specifier)
|
|
220
|
+
const pinned = PINNED_PACKAGES[name]
|
|
221
|
+
const version = pinned?.version
|
|
222
|
+
|
|
223
|
+
// If there's a subpath in the specifier, use it directly
|
|
224
|
+
if (subpath) {
|
|
225
|
+
const url = `${CDN_BASE}/${name}${version ? `@${version}` : ''}${subpath}`
|
|
226
|
+
moduleCache.set(specifier, url)
|
|
227
|
+
return url
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// If we have a pinned path, use it directly (skip package.json lookup)
|
|
231
|
+
if (pinned?.path) {
|
|
232
|
+
const url = `${CDN_BASE}/${name}@${version}${pinned.path}`
|
|
233
|
+
moduleCache.set(specifier, url)
|
|
234
|
+
return url
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Fetch package.json and resolve the entry point
|
|
238
|
+
const pkg = await getPackageJson(name, version)
|
|
239
|
+
const entryPoint = resolveEntryPoint(pkg)
|
|
240
|
+
|
|
241
|
+
if (!entryPoint) {
|
|
242
|
+
throw new Error(`No ESM entry point found in package.json for ${name}`)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Normalize path (ensure it starts with /)
|
|
246
|
+
const path = entryPoint.startsWith('./')
|
|
247
|
+
? entryPoint.slice(1)
|
|
248
|
+
: entryPoint.startsWith('/')
|
|
249
|
+
? entryPoint
|
|
250
|
+
: `/${entryPoint}`
|
|
251
|
+
|
|
252
|
+
const url = `${CDN_BASE}/${name}${version ? `@${version}` : ''}${path}`
|
|
253
|
+
moduleCache.set(specifier, url)
|
|
254
|
+
return url
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Resolve all imports in source code and return import map
|
|
259
|
+
* Checks local module store first, then falls back to CDN
|
|
260
|
+
*/
|
|
261
|
+
export async function resolveImports(source: string): Promise<{
|
|
262
|
+
importMap: { imports: Record<string, string> }
|
|
263
|
+
errors: string[]
|
|
264
|
+
localModules: string[]
|
|
265
|
+
}> {
|
|
266
|
+
const specifiers = extractImports(source)
|
|
267
|
+
const errors: string[] = []
|
|
268
|
+
const imports: Record<string, string> = {}
|
|
269
|
+
const localModules: string[] = []
|
|
270
|
+
|
|
271
|
+
// Lazy import to avoid circular deps
|
|
272
|
+
const { resolveLocalImports } = await import('./module-store')
|
|
273
|
+
|
|
274
|
+
// First, resolve local modules
|
|
275
|
+
try {
|
|
276
|
+
const localImports = await resolveLocalImports(specifiers)
|
|
277
|
+
Object.assign(imports, localImports)
|
|
278
|
+
localModules.push(...Object.keys(localImports))
|
|
279
|
+
} catch (error: any) {
|
|
280
|
+
errors.push(error.message)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Then resolve remaining from CDN
|
|
284
|
+
const remaining = specifiers.filter((s) => !localModules.includes(s))
|
|
285
|
+
|
|
286
|
+
await Promise.all(
|
|
287
|
+
remaining.map(async (specifier) => {
|
|
288
|
+
try {
|
|
289
|
+
imports[specifier] = await resolveModule(specifier)
|
|
290
|
+
} catch (error: any) {
|
|
291
|
+
errors.push(error.message)
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
return { importMap: { imports }, errors, localModules }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Generate HTML script tag with import map
|
|
301
|
+
*/
|
|
302
|
+
export function generateImportMapScript(importMap: {
|
|
303
|
+
imports: Record<string, string>
|
|
304
|
+
}): string {
|
|
305
|
+
return `<script type="importmap">
|
|
306
|
+
${JSON.stringify(importMap, null, 2)}
|
|
307
|
+
</script>`
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Transform source code to use module script
|
|
312
|
+
*/
|
|
313
|
+
export function wrapAsModule(code: string): string {
|
|
314
|
+
return `<script type="module">
|
|
315
|
+
${code}
|
|
316
|
+
</script>`
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Clear the in-memory module cache
|
|
321
|
+
*/
|
|
322
|
+
export function clearModuleCache(): void {
|
|
323
|
+
moduleCache.clear()
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get in-memory cache statistics
|
|
328
|
+
*/
|
|
329
|
+
export function getCacheStats(): { size: number; entries: string[] } {
|
|
330
|
+
return {
|
|
331
|
+
size: moduleCache.size,
|
|
332
|
+
entries: [...moduleCache.keys()],
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Service Worker integration for persistent caching
|
|
337
|
+
|
|
338
|
+
let swRegistration: ServiceWorkerRegistration | null = null
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Register the module cache service worker
|
|
342
|
+
*/
|
|
343
|
+
export async function registerServiceWorker(): Promise<boolean> {
|
|
344
|
+
if (!('serviceWorker' in navigator)) {
|
|
345
|
+
console.warn('Service workers not supported')
|
|
346
|
+
return false
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
swRegistration = await navigator.serviceWorker.register('/module-sw.js', {
|
|
351
|
+
scope: '/',
|
|
352
|
+
})
|
|
353
|
+
console.log('Module cache SW registered')
|
|
354
|
+
return true
|
|
355
|
+
} catch (error) {
|
|
356
|
+
console.error('SW registration failed:', error)
|
|
357
|
+
return false
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Send message to service worker and wait for response
|
|
363
|
+
*/
|
|
364
|
+
function sendSWMessage<T>(type: string, payload?: any): Promise<T> {
|
|
365
|
+
return new Promise((resolve, reject) => {
|
|
366
|
+
if (!navigator.serviceWorker.controller) {
|
|
367
|
+
reject(new Error('No active service worker'))
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const channel = new MessageChannel()
|
|
372
|
+
channel.port1.onmessage = (event) => resolve(event.data)
|
|
373
|
+
|
|
374
|
+
navigator.serviceWorker.controller.postMessage({ type, payload }, [
|
|
375
|
+
channel.port2,
|
|
376
|
+
])
|
|
377
|
+
|
|
378
|
+
// Timeout after 5 seconds
|
|
379
|
+
setTimeout(() => reject(new Error('SW message timeout')), 5000)
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Clear the service worker cache
|
|
385
|
+
*/
|
|
386
|
+
export async function clearSWCache(): Promise<boolean> {
|
|
387
|
+
try {
|
|
388
|
+
const result = await sendSWMessage<{ success: boolean }>('CLEAR_CACHE')
|
|
389
|
+
return result.success
|
|
390
|
+
} catch {
|
|
391
|
+
return false
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Get service worker cache statistics
|
|
397
|
+
*/
|
|
398
|
+
export async function getSWCacheStats(): Promise<{
|
|
399
|
+
size: number
|
|
400
|
+
entries: string[]
|
|
401
|
+
} | null> {
|
|
402
|
+
try {
|
|
403
|
+
return await sendSWMessage('GET_CACHE_STATS')
|
|
404
|
+
} catch {
|
|
405
|
+
return null
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Prefetch modules into service worker cache
|
|
411
|
+
*/
|
|
412
|
+
export async function prefetchModules(
|
|
413
|
+
specifiers: string[]
|
|
414
|
+
): Promise<{ success: number; failed: number }> {
|
|
415
|
+
const urls = specifiers.map(getCDNUrl)
|
|
416
|
+
try {
|
|
417
|
+
return await sendSWMessage('PREFETCH', { urls })
|
|
418
|
+
} catch {
|
|
419
|
+
return { success: 0, failed: specifiers.length }
|
|
420
|
+
}
|
|
421
|
+
}
|