Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/editor/src/lib/measurement-parser.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Must precede the lingo import: lingo clones its kind table with
// structuredClone while its module body evaluates. See the file's own comment.
import './structured-clone-fallback'
import { type Kind, parseQuantity, quantity } from '@pascal-app/lingo'

/**
Expand Down
45 changes: 45 additions & 0 deletions packages/editor/src/lib/structured-clone-fallback.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { afterEach, describe, expect, test } from 'bun:test'

const native = globalThis.structuredClone

afterEach(() => {
globalThis.structuredClone = native
})

async function installFallback() {
// Fresh module instance each time — the shim is a module side effect.
await import(`./structured-clone-fallback?${Math.random()}`)
}

describe('structuredClone fallback', () => {
test('leaves a native implementation alone', async () => {
await installFallback()
expect(globalThis.structuredClone).toBe(native)
})

test('installs a JSON-based clone when the global is missing', async () => {
// @ts-expect-error — simulating a browser without the global.
globalThis.structuredClone = undefined
await installFallback()

const source = { units: [{ factor: 1, id: 'm' }], kind: 'length' }
const copy = structuredClone(source)

expect(copy).toEqual(source)
expect(copy).not.toBe(source)
expect(copy.units).not.toBe(source.units)
})

test('lets lingo build its registry without the global', async () => {
// @ts-expect-error — simulating a browser without the global.
globalThis.structuredClone = undefined
await installFallback()

// The actual regression: lingo clones its kind table at module-eval time,
// so this import throws on a browser lacking structuredClone.
const { parseQuantity } = await import('@pascal-app/lingo')
const result = parseQuantity('180cm', { kind: 'length', unit: 'm' })
expect(result.ok).toBe(true)
if (result.ok) expect(result.quantity.to('m').value).toBeCloseTo(1.8, 6)
})
})
24 changes: 24 additions & 0 deletions packages/editor/src/lib/structured-clone-fallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* `@pascal-app/lingo` builds its unit registry at module-eval time, and
* `registerKind` deep-copies each kind definition with `structuredClone`. On a
* browser without that global (Chromium <98 — reported from Honor Browser 9.8
* as `ReferenceError: structuredClone is not defined`) the throw happens while
* the module graph is still evaluating, so a component-level guard can never
* run in time and the whole editor bundle fails to load.
*
* This has to be imported for its side effect from the module that pulls lingo
* in, so it is installed wherever `@pascal-app/editor` is loaded — the OSS app,
* the hosted app, and any npm consumer. An app-level polyfill would only cover
* the app that declares it.
*
* Scoped deliberately narrowly: the kind definitions lingo clones are plain
* JSON (strings, numbers, arrays of those), so a JSON round-trip is sufficient
* and avoids a dependency. This is NOT a spec-compliant `structuredClone` — it
* has no support for Map/Set/Date/ArrayBuffer/cycles and throws a `TypeError`
* rather than a `DataCloneError`. Anything needing real structured-clone
* semantics must not rely on this shim.
*/
if (typeof globalThis.structuredClone !== 'function') {
globalThis.structuredClone = (<T>(value: T): T =>
JSON.parse(JSON.stringify(value)) as T) as typeof structuredClone
}
Loading