tjs-lang 0.6.27 → 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/docs.json +37 -1
- 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/docs.json
CHANGED
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"type": "example",
|
|
116
116
|
"group": "basics",
|
|
117
117
|
"order": 15,
|
|
118
|
-
"code": "TjsEquals\n\n/*#\n## The Problem with JavaScript ==\n\nJavaScript's `==` does type coercion, producing surprises:\n\n 0 == '' // true in JS (!)\n false == [] // true in JS (!)\n '' == false // true in JS (!)\n null == 0 // false in JS (but null == undefined is true)\n\nJavaScript's `===` fixes coercion but can't compare values:\n\n new String('hi') === 'hi' // false in JS (different types)\n new Boolean(false) === false // false in JS (object vs primitive)\n\n## TJS Equality (TjsEquals)\n\n`==` becomes **honest equality**: no coercion, but unwraps\nboxed primitives. Fast O(1) — no deep comparison.\n\n`Is` / `IsNot` are **structural equality**: deep comparison\nfor when you explicitly need it. O(n) cost is visible.\n*/\n\n// --- Honest equality (==) fixes coercion ---\nconsole.log('== fixes JS coercion:')\nconsole.log(' 0 == \"\":', 0 == '') // false (JS: true)\nconsole.log(' false == []:', false == []) // false (JS: true)\nconsole.log(' false == \"\":', false == '') // false (JS: true)\nconsole.log(' 1 == \"1\":', 1 == '1') // false (JS: true)\n\n// --- Boxed primitives unwrap ---\nconsole.log('')\nconsole.log('== unwraps boxed primitives:')\nconsole.log(' new String(\"hi\") == \"hi\":', new String('hi') == 'hi') // true\nconsole.log(' new Boolean(false) == false:', new Boolean(false) == false) // true\nconsole.log(' new Number(42) == 42:', new Number(42) == 42) // true\n\n// --- Nullish equality preserved ---\nconsole.log('')\nconsole.log('Nullish equality (useful pattern preserved):')\nconsole.log(' null == undefined:', null == undefined) // true\nconsole.log(' null == 0:', null == 0) // false\nconsole.log(' null == \"\":', null == '') // false\n\n// --- Objects/arrays: reference equality (fast, O(1)) ---\nconsole.log('')\nconsole.log('== on objects/arrays is reference equality (fast):')\nconst obj = {x: 1}\nconsole.log(' obj == obj:', obj == obj) // true (same ref)\nconsole.log(' {x:1} == {x:1}:', {x: 1} == {x: 1}) // false (different refs)\nconsole.log(' [1,2] == [1,2]:', [1, 2] == [1, 2]) // false (different refs)\n\n// --- Is/IsNot: explicit deep structural comparison ---\nconsole.log('')\nconsole.log('Is/IsNot for deep structural comparison (explicit):')\nconsole.log(' {x:1} Is {x:1}:', Is({x: 1}, {x: 1})) // true\nconsole.log(' [1,2,3] Is [1,2,3]:', Is([1,2,3], [1,2,3])) // true\nconsole.log(' [1,2] Is [2,1]:', Is([1,2], [2,1])) // false (order matters)\n\n// Sets compare by membership, not order\nconsole.log(' Set([1,2]) Is Set([2,1]):', Is(new Set([1,2]), new Set([2,1]))) // true\n\n// --- typeof null fixed ---\nconsole.log('')\nconsole.log('typeof null fixed:')\nconsole.log(' typeof null:', typeof null) // 'null' (JS: 'object')\nconsole.log(' typeof undefined:', typeof undefined) // 'undefined'\nconsole.log(' typeof 42:', typeof 42) // 'number' (unchanged)\n\n// --- === unchanged: identity comparison ---\nconsole.log('')\nconsole.log('=== is unchanged (identity):')\nconsole.log(' obj === obj:', obj === obj) // true\nconsole.log(' {x:1} === {x:1}:', {x: 1} === {x: 1}) // false\n\ntest 'typeof null is null, not object' {\n expect(TypeOf(null)).toBe('null')\n expect(TypeOf(undefined)).toBe('undefined')\n expect(TypeOf(42)).toBe('number')\n expect(TypeOf('hi')).toBe('string')\n expect(TypeOf(true)).toBe('boolean')\n expect(TypeOf({})).toBe('object')\n}\n\ntest 'Eq fixes coercion' {\n expect(Eq(0, '')).toBe(false)\n expect(Eq(false, [])).toBe(false)\n expect(Eq('', false)).toBe(false)\n}\n\ntest 'Eq unwraps boxed primitives' {\n expect(Eq(new String('hi'), 'hi')).toBe(true)\n expect(Eq(new Boolean(false), false)).toBe(true)\n expect(Eq(new Number(42), 42)).toBe(true)\n}\n\ntest 'Eq preserves nullish equality' {\n expect(Eq(null, undefined)).toBe(true)\n expect(Eq(null, 0)).toBe(false)\n}\n\ntest 'Is does deep structural comparison' {\n expect(Is({a: 1, b: 2}, {a: 1, b: 2})).toBe(true)\n expect(Is([1, 2, 3], [1, 2, 3])).toBe(true)\n expect(Is([1, 2], [2, 1])).toBe(false)\n}",
|
|
118
|
+
"code": "TjsEquals\n\n/*#\n## The Problem with JavaScript ==\n\nJavaScript's `==` does type coercion, producing surprises:\n\n 0 == '' // true in JS (!)\n false == [] // true in JS (!)\n '' == false // true in JS (!)\n null == 0 // false in JS (but null == undefined is true)\n\nJavaScript's `===` fixes coercion but can't compare values:\n\n new String('hi') === 'hi' // false in JS (different types)\n new Boolean(false) === false // false in JS (object vs primitive)\n\n## TJS Equality (TjsEquals)\n\n`==` becomes **honest equality**: no coercion, but unwraps\nboxed primitives. Fast O(1) — no deep comparison.\n\n`Is` / `IsNot` are **structural equality**: deep comparison\nfor when you explicitly need it. O(n) cost is visible.\n*/\n\n// --- Honest equality (==) fixes coercion ---\nconsole.log('== fixes JS coercion:')\nconsole.log(' [] == ![]:', [] == ![]) // false (JS: true!)\nconsole.log(' 0 == \"\":', 0 == '') // false (JS: true)\nconsole.log(' false == []:', false == []) // false (JS: true)\nconsole.log(' false == \"\":', false == '') // false (JS: true)\nconsole.log(' 1 == \"1\":', 1 == '1') // false (JS: true)\n\n// --- Boxed primitives unwrap ---\nconsole.log('')\nconsole.log('== unwraps boxed primitives:')\nconsole.log(' new String(\"hi\") == \"hi\":', new String('hi') == 'hi') // true\nconsole.log(' new Boolean(false) == false:', new Boolean(false) == false) // true\nconsole.log(' new Number(42) == 42:', new Number(42) == 42) // true\n\n// --- Nullish equality preserved ---\nconsole.log('')\nconsole.log('Nullish equality (useful pattern preserved):')\nconsole.log(' null == undefined:', null == undefined) // true\nconsole.log(' null == 0:', null == 0) // false\nconsole.log(' null == \"\":', null == '') // false\n\n// --- Objects/arrays: reference equality (fast, O(1)) ---\nconsole.log('')\nconsole.log('== on objects/arrays is reference equality (fast):')\nconst obj = {x: 1}\nconsole.log(' obj == obj:', obj == obj) // true (same ref)\nconsole.log(' {x:1} == {x:1}:', {x: 1} == {x: 1}) // false (different refs)\nconsole.log(' [1,2] == [1,2]:', [1, 2] == [1, 2]) // false (different refs)\n\n// --- Is/IsNot: explicit deep structural comparison ---\nconsole.log('')\nconsole.log('Is/IsNot for deep structural comparison (explicit):')\nconsole.log(' {x:1} Is {x:1}:', Is({x: 1}, {x: 1})) // true\nconsole.log(' [1,2,3] Is [1,2,3]:', Is([1,2,3], [1,2,3])) // true\nconsole.log(' [1,2] Is [2,1]:', Is([1,2], [2,1])) // false (order matters)\n\n// Sets compare by membership, not order\nconsole.log(' Set([1,2]) Is Set([2,1]):', Is(new Set([1,2]), new Set([2,1]))) // true\n\n// --- typeof null fixed ---\nconsole.log('')\nconsole.log('typeof null fixed:')\nconsole.log(' typeof null:', typeof null) // 'null' (JS: 'object')\nconsole.log(' typeof undefined:', typeof undefined) // 'undefined'\nconsole.log(' typeof 42:', typeof 42) // 'number' (unchanged)\n\n// --- === unchanged: identity comparison ---\nconsole.log('')\nconsole.log('=== is unchanged (identity):')\nconsole.log(' obj === obj:', obj === obj) // true\nconsole.log(' {x:1} === {x:1}:', {x: 1} === {x: 1}) // false\n\ntest 'typeof null is null, not object' {\n expect(TypeOf(null)).toBe('null')\n expect(TypeOf(undefined)).toBe('undefined')\n expect(TypeOf(42)).toBe('number')\n expect(TypeOf('hi')).toBe('string')\n expect(TypeOf(true)).toBe('boolean')\n expect(TypeOf({})).toBe('object')\n}\n\ntest 'Eq fixes coercion' {\n expect(Eq(0, '')).toBe(false)\n expect(Eq(false, [])).toBe(false)\n expect(Eq('', false)).toBe(false)\n}\n\ntest 'Eq unwraps boxed primitives' {\n expect(Eq(new String('hi'), 'hi')).toBe(true)\n expect(Eq(new Boolean(false), false)).toBe(true)\n expect(Eq(new Number(42), 42)).toBe(true)\n}\n\ntest 'Eq preserves nullish equality' {\n expect(Eq(null, undefined)).toBe(true)\n expect(Eq(null, 0)).toBe(false)\n}\n\ntest 'Is does deep structural comparison' {\n expect(Is({a: 1, b: 2}, {a: 1, b: 2})).toBe(true)\n expect(Is([1, 2, 3], [1, 2, 3])).toBe(true)\n expect(Is([1, 2], [2, 1])).toBe(false)\n}",
|
|
119
119
|
"language": "tjs",
|
|
120
120
|
"description": "JavaScript `==` is broken. TJS fixes it without breaking anything."
|
|
121
121
|
},
|
|
@@ -357,6 +357,42 @@
|
|
|
357
357
|
"language": "tjs",
|
|
358
358
|
"description": "Named types with runtime validation. Type, Generic, FunctionPredicate, Enum, Union."
|
|
359
359
|
},
|
|
360
|
+
{
|
|
361
|
+
"title": "Inline WASM",
|
|
362
|
+
"filename": "wasm-basics.md",
|
|
363
|
+
"path": "guides/examples/tjs/wasm-basics.md",
|
|
364
|
+
"section": "tjs",
|
|
365
|
+
"type": "example",
|
|
366
|
+
"group": "patterns",
|
|
367
|
+
"order": 25,
|
|
368
|
+
"code": "/*#\n## The Problem\n\nUsing WebAssembly in JavaScript requires:\n1. Write WAT or compile from C/Rust\n2. Load a separate .wasm file\n3. Instantiate the module async\n4. Marshal data between JS and WASM heaps\n\nTJS does all of this for you. Write WASM inline using JS-like\nsyntax, it compiles at transpile time and embeds as base64.\n\n## Syntax\n\n function fast(x: 0, y: 0) {\n wasm {\n // JS-like syntax compiled to WASM bytecode\n } fallback {\n // JS fallback if WASM unavailable\n return x + y\n }\n }\n\n // Or with a return value:\n function compute(x: 0) {\n return wasm {\n x * x + 1\n } fallback {\n return x * x + 1\n }\n }\n\nParam types: `i32` (integer), `f32`/`f64` (float), `Float32Array`, etc.\n*/\n\n// --- Basic: integer math in WASM ---\n\nfunction addInts(! a: 0, b: 0) -! 0 {\n return wasm {\n a + b\n } fallback {\n return a + b\n }\n}\n\nfunction factorial(! n: 0) -! 0 {\n return wasm {\n let result = 1\n for (let i = 2; i <= n; i++) {\n result = result * i\n }\n } fallback {\n let result = 1\n for (let i = 2; i <= n; i++) result *= i\n return result\n }\n}\n\n// --- Float math ---\n\nfunction lerp(! a: 0.0, b: 0.0, t: 0.0) -! 0.0 {\n return wasm {\n a + (b - a) * t\n } fallback {\n return a + (b - a) * t\n }\n}\n\n// --- Array processing ---\n\nfunction sumArray(! arr: Float32Array, len: 0) -! 0.0 {\n return wasm {\n let sum = 0.0\n for (let i = 0; i < len; i++) {\n let off = i * 4\n sum = sum + f32x4_extract_lane(f32x4_load(arr, off), 0)\n }\n } fallback {\n let sum = 0\n for (let i = 0; i < len; i++) sum += arr[i]\n return sum\n }\n}\n\nconsole.log('addInts(3, 4):', addInts(3, 4))\nconsole.log('factorial(10):', factorial(10))\nconsole.log('lerp(0, 100, 0.25):', lerp(0.0, 100.0, 0.25))\nconsole.log('sumArray([1,2,3,4]):', sumArray(new Float32Array([1, 2, 3, 4]), 4))",
|
|
369
|
+
"language": "tjs",
|
|
370
|
+
"description": "Write WebAssembly inline — compiled at transpile time, embedded in the output."
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
"title": "WASM SIMD",
|
|
374
|
+
"filename": "wasm-simd.md",
|
|
375
|
+
"path": "guides/examples/tjs/wasm-simd.md",
|
|
376
|
+
"section": "tjs",
|
|
377
|
+
"type": "example",
|
|
378
|
+
"group": "patterns",
|
|
379
|
+
"order": 26,
|
|
380
|
+
"code": "/*#\n## SIMD: Single Instruction, Multiple Data\n\nSIMD processes 4 float values per instruction — a 4x throughput\nimprovement for vectorized math. TJS provides SIMD via `f32x4_*`\nintrinsics that compile directly to WASM SIMD opcodes.\n\n### Available Intrinsics\n\n| Intrinsic | Operation |\n|-----------|-----------|\n| `f32x4_load(arr, offset)` | Load 4 floats from array |\n| `f32x4_store(arr, offset, vec)` | Store 4 floats to array |\n| `f32x4_splat(value)` | Fill all 4 lanes with one value |\n| `f32x4_add(a, b)` | Add 4 pairs |\n| `f32x4_sub(a, b)` | Subtract 4 pairs |\n| `f32x4_mul(a, b)` | Multiply 4 pairs |\n| `f32x4_div(a, b)` | Divide 4 pairs |\n| `f32x4_neg(a)` | Negate 4 values |\n| `f32x4_sqrt(a)` | Square root of 4 values |\n| `f32x4_extract_lane(vec, lane)` | Get one float (0-3) |\n| `f32x4_replace_lane(vec, lane, val)` | Set one float |\n*/\n\n// --- Scale an array by a constant (SIMD: 4 elements per step) ---\n\nfunction scale(! arr: Float32Array, len: 0, factor: 0.0) {\n wasm {\n let s = f32x4_splat(factor)\n for (let i = 0; i < len; i += 4) {\n let off = i * 4\n let v = f32x4_load(arr, off)\n f32x4_store(arr, off, f32x4_mul(v, s))\n }\n } fallback {\n for (let i = 0; i < len; i++) arr[i] *= factor\n }\n}\n\n// --- Dot product (sum of element-wise products) ---\n\nfunction dot(! a: Float32Array, b: Float32Array, len: 0) -! 0.0 {\n return wasm {\n let acc = f32x4_splat(0.0)\n for (let i = 0; i < len; i += 4) {\n let off = i * 4\n let va = f32x4_load(a, off)\n let vb = f32x4_load(b, off)\n acc = f32x4_add(acc, f32x4_mul(va, vb))\n }\n // Sum the 4 lanes\n f32x4_extract_lane(acc, 0)\n + f32x4_extract_lane(acc, 1)\n + f32x4_extract_lane(acc, 2)\n + f32x4_extract_lane(acc, 3)\n } fallback {\n let sum = 0\n for (let i = 0; i < len; i++) sum += a[i] * b[i]\n return sum\n }\n}\n\n// --- Demo ---\n\nconst SIZE = 1024\n\n// Create test arrays\nconst arr = new Float32Array(SIZE)\nconst a = new Float32Array(SIZE)\nconst b = new Float32Array(SIZE)\n\nfor (let i = 0; i < SIZE; i++) {\n arr[i] = i + 1\n a[i] = 1.0\n b[i] = 2.0\n}\n\n// Scale\nscale(arr, SIZE, 0.5)\nconsole.log('scale([1..1024], 0.5) first 4:', arr[0], arr[1], arr[2], arr[3])\n\n// Dot product: 1.0 * 2.0 * 1024 = 2048\nconst d = dot(a, b, SIZE)\nconsole.log('dot([1,1,...], [2,2,...], 1024):', d)\n\n// Benchmark\nconst iters = 1000\nconst t0 = performance.now()\nfor (let i = 0; i < iters; i++) scale(arr, SIZE, 1.001)\nconst elapsed = performance.now() - t0\nconsole.log(`${iters} scale ops on ${SIZE} floats: ${elapsed.toFixed(1)}ms`)",
|
|
381
|
+
"language": "tjs",
|
|
382
|
+
"description": "Process 4 floats per instruction. No setup, no toolchain."
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
"title": "WASM Memory",
|
|
386
|
+
"filename": "wasm-memory.md",
|
|
387
|
+
"path": "guides/examples/tjs/wasm-memory.md",
|
|
388
|
+
"section": "tjs",
|
|
389
|
+
"type": "example",
|
|
390
|
+
"group": "patterns",
|
|
391
|
+
"order": 27,
|
|
392
|
+
"code": "/*#\n## How Data Moves Between JS and WASM\n\nThe #1 WebAssembly question: \"How do I get my data into WASM?\"\nTJS handles it automatically. Three modes:\n\n### 1. Scalars — pass through\n`i32`, `f32`, `f64` go directly as WASM parameters. No marshaling.\n\n### 2. Regular typed arrays — transparent copy\nPass a normal `Float32Array` and TJS copies it into WASM memory\nbefore the call, then copies results back out after. You don't\nhave to think about it.\n\n### 3. `wasmBuffer()` — zero-copy shared memory\nAllocate directly in WASM memory. Both JS and WASM see the same\nbytes. No copy in, no copy out. Mutations are instantly visible.\n\n const xs = wasmBuffer(Float32Array, 50000)\n xs[0] = 3.14 // JS writes to WASM memory\n wasmFunction(xs) // WASM reads/writes the same memory\n console.log(xs[0]) // JS sees WASM's mutations immediately\n\n### Supported types\n`Float32Array`, `Float64Array`, `Int32Array`, `Uint8Array`\n\n### How it works internally\nAll WASM blocks in a file share one `WebAssembly.Memory` (64MB).\n`wasmBuffer` is a bump allocator — it hands out slices of this memory.\nWhen a typed array argument's `.buffer === wasmMemory.buffer`, the\nwrapper skips the copy and passes the byte offset directly.\n*/\n\n// --- Regular arrays: transparent copy ---\n\nfunction addOne(! arr: Float32Array, len: 0) {\n wasm {\n for (let i = 0; i < len; i += 4) {\n let off = i * 4\n let v = f32x4_load(arr, off)\n let ones = f32x4_splat(1.0)\n f32x4_store(arr, off, f32x4_add(v, ones))\n }\n } fallback {\n for (let i = 0; i < len; i++) arr[i] += 1\n }\n}\n\n// Regular Float32Array — TJS copies in before, copies out after\nconst regular = new Float32Array([10, 20, 30, 40])\naddOne(regular, 4)\nconsole.log('Regular array after WASM:', Array.from(regular))\n// [11, 21, 31, 41] — changes visible in JS\n\n// --- wasmBuffer: zero-copy shared memory ---\n\nconst shared = wasmBuffer(Float32Array, 4)\nshared[0] = 100\nshared[1] = 200\nshared[2] = 300\nshared[3] = 400\n\naddOne(shared, 4)\nconsole.log('wasmBuffer after WASM:', Array.from(shared))\n// [101, 201, 301, 401] — zero copy, same memory\n\n// --- Practical: large array processing ---\n\nconst SIZE = 10000\nconst data = wasmBuffer(Float32Array, SIZE)\nfor (let i = 0; i < SIZE; i++) data[i] = i * 0.01\n\n// Process in WASM — no marshaling overhead\nfunction normalize(! arr: Float32Array, len: 0) {\n wasm {\n // Find max (scalar — SIMD max needs horizontal reduction)\n let max = 0.0\n for (let i = 0; i < len; i++) {\n let off = i * 4\n let v = f32x4_extract_lane(f32x4_load(arr, off), 0)\n if (v > max) { max = v }\n }\n // Scale to [0, 1]\n if (max > 0.0) {\n let inv = f32x4_splat(1.0 / max)\n for (let i = 0; i < len; i += 4) {\n let off = i * 4\n f32x4_store(arr, off, f32x4_mul(f32x4_load(arr, off), inv))\n }\n }\n } fallback {\n let max = 0\n for (let i = 0; i < len; i++) if (arr[i] > max) max = arr[i]\n if (max > 0) for (let i = 0; i < len; i++) arr[i] /= max\n }\n}\n\nconst t0 = performance.now()\nnormalize(data, SIZE)\nconst elapsed = performance.now() - t0\n\nconsole.log(`Normalized ${SIZE} floats in ${elapsed.toFixed(2)}ms`)\nconsole.log('First 4:', data[0].toFixed(4), data[1].toFixed(4), data[2].toFixed(4), data[3].toFixed(4))\nconsole.log('Last:', data[SIZE - 1].toFixed(4))",
|
|
393
|
+
"language": "tjs",
|
|
394
|
+
"description": "Zero-copy arrays and automatic data marshaling between JS and WASM."
|
|
395
|
+
},
|
|
360
396
|
{
|
|
361
397
|
"title": "React Todo (Comparison)",
|
|
362
398
|
"filename": "react-todo-comparison.md",
|
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
|
-
})
|