ReviewOS

also looking at this

craft-native/craft

fix(sdk): keep version checks working in relocated bundles

#290
Merged glennmichael123 wants to merge codex/relocatable-sdk-version into main
3 files +63 -17
packages/typescript/src/__tests__/package-version.test.tsadded+55-0
Changes to packages/typescript/src/__tests__/package-version.test.ts
@@ -0,0 +1,55 @@
1import { expect, test } from 'bun:test'
2import { chmodSync, cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
3import { tmpdir } from 'node:os'
4import { join, resolve } from 'node:path'
5
6test('relocated ESM and CJS bundles retain their build-time SDK version', async () => {
7 const packageRoot = resolve(import.meta.dir, '../..')
8 const temp = mkdtempSync(join(tmpdir(), 'craft-version-package-'))
9 try {
10 const source = join(temp, 'source')
11 mkdirSync(source)
12 cpSync(join(packageRoot, 'src'), join(source, 'src'), { recursive: true })
13 writeFileSync(join(source, 'package.json'), JSON.stringify({ name: 'craft-native', version: '7.8.9', type: 'module' }))
14 symlinkSync(join(packageRoot, 'node_modules'), join(source, 'node_modules'), 'dir')
15 for (const format of ['esm', 'cjs'] as const) {
16 const result = await Bun.build({
17 root: source,
18 entrypoints: [join(source, 'src/index.ts')],
19 outdir: join(temp, format),
20 naming: format === 'cjs' ? 'sdk.cjs' : 'sdk.mjs',
21 format,
22 target: format === 'cjs' ? 'node' : 'bun',
23 })
24 expect(result.success).toBe(true)
25 }
26 // Neither installed bundle may read the now-absent build checkout.
27 rmSync(source, { recursive: true, force: true })
28 const native = join(temp, 'native')
29 writeFileSync(native, '#!/bin/sh\nif [ "$1" = "--version" ]; then echo "craft version $CRAFT_TEST_VERSION"; fi\n')
30 chmodSync(native, 0o755)
31 for (const format of ['esm', 'cjs'] as const) {
32 const bundle = join(temp, format, format === 'cjs' ? 'sdk.cjs' : 'sdk.mjs')
33 const consumer = join(temp, format === 'cjs' ? 'consumer.cjs' : 'consumer.mjs')
34 const load = format === 'cjs' ? `const sdk = require(${JSON.stringify(bundle)})` : `import * as sdk from ${JSON.stringify(bundle)}`
35 writeFileSync(consumer, `${load}\nsdk.createApp({ url: 'https://example.test', craftPath: ${JSON.stringify(native)}, quiet: true }).show().catch(error => { console.error(error); process.exitCode = 1 })\n`)
36 for (const nativeVersion of ['7.8.9', '7.8.10']) {
37 const result = Bun.spawnSync([format === 'cjs' ? 'node' : process.execPath, consumer], {
38 cwd: temp,
39 env: { ...process.env, CRAFT_TEST_VERSION: nativeVersion },
40 stdout: 'pipe', stderr: 'pipe',
41 })
42 expect(result.exitCode).toBe(0)
43 const stderr = result.stderr.toString()
44 if (nativeVersion === '7.8.9') expect(stderr).not.toContain('version mismatch')
45 else expect(stderr).toContain('SDK=7.8.9, native=7.8.10')
46 }
47 const runtimePaths = readFileSync(bundle, 'utf8').split('\n')
48 .filter(line => !line.trimStart().startsWith('//') && line.includes(source))
49 expect(runtimePaths).toEqual([])
50 }
51 }
52 finally {
53 rmSync(temp, { recursive: true, force: true })
54 }
55}, 60_000)
packages/typescript/src/index.tsmodified+3-17
Changes to packages/typescript/src/index.ts
@@ -4,11 +4,12 @@
44 */
55
66import { execFile, spawn, type ChildProcess } from 'node:child_process'
7import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
7import { mkdtempSync, writeFileSync } from 'node:fs'
88import { tmpdir } from 'node:os'
99import { join } from 'node:path'
1010import { promisify } from 'node:util'
1111import { CRAFT_CLI_SPAWN_MARKER, craftBinaryNotFoundMessage, resolveCraftBinary } from './binary-resolver.js'
12import { SDK_VERSION } from './sdk-version.js'
1213import type { AppConfig, WindowOptions } from './types.js'
1314
1415const execFileAsync = promisify(execFile)
@@ -16,21 +17,6 @@ const execFileAsync = promisify(execFile)
1617/** Tracks whether we've already warned about a binary/SDK version mismatch so each process logs once. */
1718let versionMismatchWarned = false
1819
19/**
20 * Read the SDK's own version from `package.json`. Falls back to `'unknown'`
21 * when not running from the source tree (the import-meta.dir path may not
22 * resolve in some bundler edge cases).
23 */
24function getSdkVersion(): string {
25 try {
26 const pkgPath = join(import.meta.dir, '..', 'package.json')
27 return JSON.parse(readFileSync(pkgPath, 'utf-8')).version ?? 'unknown'
28 }
29 catch {
30 return 'unknown'
31 }
32}
33
3420/**
3521 * Probe the native binary for its `--version`, compare it to the SDK
3622 * version, and warn (once) when they don't match. Best-effort: a binary
@@ -41,7 +27,7 @@ async function probeBinaryVersion(craftPath: string): Promise<void> {
4127 try {
4228 const { stdout } = await execFileAsync(craftPath, ['--version'], { timeout: 2000 })
4329 const nativeVersion = parseCraftVersionOutput(stdout)
44 const sdkVersion = getSdkVersion()
30 const sdkVersion = SDK_VERSION
4531 if (sdkVersion !== 'unknown' && nativeVersion && nativeVersion !== sdkVersion) {
4632 versionMismatchWarned = true
4733 console.warn(
packages/typescript/src/sdk-version.tsadded+5-0
Changes to packages/typescript/src/sdk-version.ts
@@ -0,0 +1,5 @@
1import { version } from '../package.json'
2
3// Bundlers embed the value, so both ESM and CJS work after the build checkout
4// disappears. Source consumers still read the same package manifest.
5export const SDK_VERSION: string = version