import assert from 'node:assert/strict' import { execFileSync } from 'node:child_process' import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import path from 'node:path' import test from 'node:test' import { fileURLToPath } from 'node:url' const cli = fileURLToPath(new URL('./xuqm-rn.mjs', import.meta.url)) test('embed creates a complete versioned manifest from a host fixture', () => { const root = mkdtempSync(path.join(tmpdir(), 'xuqm-cli-')) const output = path.join(root, 'generated', 'rn-bundles') try { writeFileSync( path.join(root, 'package.json'), JSON.stringify({ name: 'fixture', version: '7.2.14', dependencies: { '@xuqm/rn-update': '0.5.0-alpha.5' }, }), ) writeFileSync(path.join(root, 'common.ts'), '') writeFileSync(path.join(root, 'app.ts'), '') writeFileSync( path.join(root, 'xuqm.config.json'), JSON.stringify({ schemaVersion: 3, appId: 'fixture.app', mainModuleName: 'Fixture', pluginVersion: '1.0.0', appVersionRange: '>=7.2.14 <8.0.0', outputDir: './bundle', embeddedOutput: { android: './unused' }, modules: [ { id: 'common', type: 'common', entry: './common.ts' }, { id: 'app', type: 'app', entry: './app.ts' }, ], }), ) for (const moduleId of ['common', 'app']) { const directory = path.join(root, 'bundle', 'android', moduleId) mkdirSync(directory, { recursive: true }) writeFileSync(path.join(directory, `${moduleId}.android.bundle`), moduleId) } execFileSync(process.execPath, [cli, 'embed', 'android', '--skip-build', '--output', output], { cwd: root, stdio: 'pipe', }) const manifest = JSON.parse(readFileSync(path.join(output, 'manifest.json'), 'utf8')) assert.equal(manifest.appVersion, '7.2.14') assert.equal(manifest.bundleFormat, 'javascript') assert.equal(manifest.modules.common.moduleVersion, '1.0.0') assert.equal(manifest.modules.app.moduleVersion, '1.0.0') assert.equal(manifest.modules.app.commonVersionRange, '>=1.0.0 <2.0.0') assert.equal(manifest.modules.app.minNativeApiLevel, 1) assert.equal(manifest.modules.app.appVersionRange, '>=7.2.14 <8.0.0') assert.equal(readFileSync(path.join(output, 'app.android.bundle'), 'utf8'), 'app') } finally { rmSync(root, { recursive: true, force: true }) } }) test('Android Hermes hosts compile plugin bundles to bytecode', () => { const root = mkdtempSync(path.join(tmpdir(), 'xuqm-cli-hermes-')) try { writeFileSync( path.join(root, 'package.json'), JSON.stringify({ name: 'fixture', version: '1.0.0', dependencies: { '@xuqm/rn-update': '0.5.0-alpha.10' }, }), ) writeFileSync(path.join(root, 'common.ts'), '') mkdirSync(path.join(root, 'android'), { recursive: true }) writeFileSync(path.join(root, 'android', 'gradle.properties'), 'hermesEnabled=true\n') writeFileSync( path.join(root, 'xuqm.config.json'), JSON.stringify({ schemaVersion: 3, appId: 'fixture.app', mainModuleName: 'Fixture', pluginVersion: '1.0.0', appVersionRange: '>=1.0.0 <2.0.0', outputDir: './bundle', embeddedOutput: { android: './generated' }, modules: [{ id: 'common', type: 'common', entry: './common.ts' }], }), ) const reactNativeDirectory = path.join(root, 'node_modules', 'react-native') const compilerDirectory = path.join(root, 'node_modules', 'hermes-compiler') mkdirSync(path.join(reactNativeDirectory, 'scripts'), { recursive: true }) mkdirSync( path.join( compilerDirectory, 'hermesc', process.platform === 'win32' ? 'win64-bin' : process.platform === 'darwin' ? 'osx-bin' : 'linux64-bin', ), { recursive: true }, ) writeFileSync( path.join(reactNativeDirectory, 'package.json'), JSON.stringify({ name: 'react-native', version: '0.86.0', dependencies: { 'hermes-compiler': '1.0.0' }, }), ) writeFileSync( path.join(reactNativeDirectory, 'cli.js'), ` const fs = require('node:fs'); const path = require('node:path'); const args = process.argv.slice(2); const output = args[args.indexOf('--bundle-output') + 1]; fs.mkdirSync(path.dirname(output), { recursive: true }); fs.writeFileSync(output, 'javascript'); `, ) writeFileSync( path.join(compilerDirectory, 'package.json'), JSON.stringify({ name: 'hermes-compiler', version: '1.0.0' }), ) const compiler = path.join( compilerDirectory, 'hermesc', process.platform === 'win32' ? 'win64-bin' : process.platform === 'darwin' ? 'osx-bin' : 'linux64-bin', process.platform === 'win32' ? 'hermesc.exe' : 'hermesc', ) writeFileSync( compiler, `#!/usr/bin/env node const fs = require('node:fs'); const args = process.argv.slice(2); fs.writeFileSync(args[args.indexOf('-out') + 1], 'hermes-bytecode'); `, { mode: 0o755 }, ) execFileSync(process.execPath, [cli, 'embed', 'android'], { cwd: root, stdio: 'pipe', }) assert.equal( readFileSync(path.join(root, 'generated', 'common.android.bundle'), 'utf8'), 'hermes-bytecode', ) const manifest = JSON.parse(readFileSync(path.join(root, 'generated', 'manifest.json'), 'utf8')) assert.equal(manifest.bundleFormat, 'hermes-bytecode') } finally { rmSync(root, { recursive: true, force: true }) } }) test('build invokes the host-local React Native CLI with a consistent color environment', () => { const root = mkdtempSync(path.join(tmpdir(), 'xuqm-cli-local-rn-')) try { writeFileSync( path.join(root, 'package.json'), JSON.stringify({ name: 'fixture', version: '1.0.0', dependencies: { '@xuqm/rn-update': '0.5.0-alpha.8' }, }), ) writeFileSync(path.join(root, 'common.ts'), '') writeFileSync( path.join(root, 'xuqm.config.json'), JSON.stringify({ schemaVersion: 3, appId: 'fixture.app', mainModuleName: 'Fixture', pluginVersion: '1.0.0', appVersionRange: '>=1.0.0 <2.0.0', outputDir: './bundle', embeddedOutput: { android: './generated' }, modules: [{ id: 'common', type: 'common', entry: './common.ts' }], }), ) const reactNativeDirectory = path.join(root, 'node_modules', 'react-native') mkdirSync(reactNativeDirectory, { recursive: true }) writeFileSync( path.join(reactNativeDirectory, 'cli.js'), ` const fs = require('node:fs'); const path = require('node:path'); const args = process.argv.slice(2); const output = args[args.indexOf('--bundle-output') + 1]; fs.mkdirSync(path.dirname(output), { recursive: true }); fs.writeFileSync(output, 'bundle'); fs.writeFileSync(path.join(process.cwd(), 'child.json'), JSON.stringify({ args, forceColor: process.env.FORCE_COLOR ?? null, noColor: process.env.NO_COLOR ?? null, })); `, ) execFileSync(process.execPath, [cli, 'build', 'android'], { cwd: root, env: { ...process.env, FORCE_COLOR: '1', NO_COLOR: '1' }, stdio: 'pipe', }) const child = JSON.parse(readFileSync(path.join(root, 'child.json'), 'utf8')) assert.equal(child.args[0], 'bundle') assert.equal(child.forceColor, '0') assert.equal(child.noColor, null) } finally { rmSync(root, { recursive: true, force: true }) } }) test('start and run forward arguments through the host-local React Native CLI', () => { const root = mkdtempSync(path.join(tmpdir(), 'xuqm-cli-run-rn-')) try { const reactNativeDirectory = path.join(root, 'node_modules', 'react-native') mkdirSync(reactNativeDirectory, { recursive: true }) writeFileSync( path.join(reactNativeDirectory, 'cli.js'), ` const fs = require('node:fs'); const callsFile = require('node:path').join(process.cwd(), 'calls.json'); const calls = fs.existsSync(callsFile) ? JSON.parse(fs.readFileSync(callsFile, 'utf8')) : []; calls.push({ args: process.argv.slice(2), forceColor: process.env.FORCE_COLOR ?? null, noColor: process.env.NO_COLOR ?? null, }); fs.writeFileSync(callsFile, JSON.stringify(calls)); `, ) const environment = { ...process.env, FORCE_COLOR: '1', NO_COLOR: '1' } execFileSync(process.execPath, [cli, 'start', '--reset-cache'], { cwd: root, env: environment, stdio: 'pipe', }) execFileSync(process.execPath, [cli, 'run', 'android', '--device', 'emulator-5554'], { cwd: root, env: environment, stdio: 'pipe', }) const calls = JSON.parse(readFileSync(path.join(root, 'calls.json'), 'utf8')) assert.deepEqual(calls[0].args, ['start', '--reset-cache']) assert.deepEqual(calls[1].args, ['run-android', '--device', 'emulator-5554']) assert.equal(calls[0].forceColor, '0') assert.equal(calls[1].forceColor, '0') assert.equal(calls[0].noColor, null) assert.equal(calls[1].noColor, null) } finally { rmSync(root, { recursive: true, force: true }) } })