also looking at this
fix(sdk): keep version checks working in relocated bundles
#290
3 files
+63
-17
| @@ -0,0 +1,55 @@ | ||
| 1 | import { expect, test } from 'bun:test' | |
| 2 | import { chmodSync, cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' | |
| 3 | import { tmpdir } from 'node:os' | |
| 4 | import { join, resolve } from 'node:path' | |
| 5 | ||
| 6 | test('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) | |
| @@ -4,11 +4,12 @@ | ||
| 4 | 4 | */ |
| 5 | 5 | |
| 6 | 6 | import { execFile, spawn, type ChildProcess } from 'node:child_process' |
| 7 | import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs' | |
| 7 | import { mkdtempSync, writeFileSync } from 'node:fs' | |
| 8 | 8 | import { tmpdir } from 'node:os' |
| 9 | 9 | import { join } from 'node:path' |
| 10 | 10 | import { promisify } from 'node:util' |
| 11 | 11 | import { CRAFT_CLI_SPAWN_MARKER, craftBinaryNotFoundMessage, resolveCraftBinary } from './binary-resolver.js' |
| 12 | import { SDK_VERSION } from './sdk-version.js' | |
| 12 | 13 | import type { AppConfig, WindowOptions } from './types.js' |
| 13 | 14 | |
| 14 | 15 | const execFileAsync = promisify(execFile) |
| @@ -16,21 +17,6 @@ const execFileAsync = promisify(execFile) | ||
| 16 | 17 | /** Tracks whether we've already warned about a binary/SDK version mismatch so each process logs once. */ |
| 17 | 18 | let versionMismatchWarned = false |
| 18 | 19 | |
| 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 | */ | |
| 24 | function 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 | ||
| 34 | 20 | /** |
| 35 | 21 | * Probe the native binary for its `--version`, compare it to the SDK |
| 36 | 22 | * version, and warn (once) when they don't match. Best-effort: a binary |
| @@ -41,7 +27,7 @@ async function probeBinaryVersion(craftPath: string): Promise<void> { | ||
| 41 | 27 | try { |
| 42 | 28 | const { stdout } = await execFileAsync(craftPath, ['--version'], { timeout: 2000 }) |
| 43 | 29 | const nativeVersion = parseCraftVersionOutput(stdout) |
| 44 | const sdkVersion = getSdkVersion() | |
| 30 | const sdkVersion = SDK_VERSION | |
| 45 | 31 | if (sdkVersion !== 'unknown' && nativeVersion && nativeVersion !== sdkVersion) { |
| 46 | 32 | versionMismatchWarned = true |
| 47 | 33 | console.warn( |