UNPKG

one

Version:

One is a React framework for web, iOS, and Android apps.

235 lines (232 loc) • 8.72 kB
import { execFileSync } from "node:child_process"; import { createHash, randomUUID } from "node:crypto"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs"; import { createRequire } from "node:module"; import { tmpdir } from "node:os"; import { join, relative, resolve, sep } from "node:path"; import { validateNativeApp } from "../native/appManifest.mjs"; import { loadUserOneOptions } from "../vite/loadConfig.mjs"; function lastValue(value) { return Array.isArray(value) ? value[value.length - 1] : value; } function allValues(value) { if (value === void 0) return []; return Array.isArray(value) ? value : [value]; } function base64Url(bytes) { return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } function sha256Base64Url(path) { return base64Url(createHash("sha256").update(readFileSync(path)).digest()); } function parseMetadata(entries) { const metadata = {}; for (const entry of entries) { const equals = entry.indexOf("="); if (equals <= 0) { throw new Error(`[one] --metadata must look like key=value, got "${entry}"`); } const key = entry.slice(0, equals); const raw = entry.slice(equals + 1); let value = raw; try { const parsed = JSON.parse(raw); if (typeof parsed === "string" || typeof parsed === "number" || typeof parsed === "boolean") { value = parsed; } } catch {} metadata[key] = value; } return metadata; } function collectFiles(root) { const files = []; const visit = (dir) => { for (const entry of readdirSync(dir, { withFileTypes: true })) { const path = join(dir, entry.name); if (entry.isDirectory()) visit(path); else if (entry.isFile()) files.push(path); } }; if (existsSync(root)) visit(root); return files.sort(); } function resolveCliJs(root) { const require2 = createRequire(join(root, "package.json")); const packageJson = require2.resolve("react-native/package.json"); return join(packageJson.slice(0, -"package.json".length), "cli.js"); } function resolveNpmHermesc(root) { const require2 = createRequire(join(root, "package.json")); const reactNativeJson = require2.resolve("react-native/package.json"); const nested = createRequire(reactNativeJson); const compilerJson = nested.resolve("hermes-compiler/package.json"); const dir = compilerJson.slice(0, -"package.json".length); const bin = process.platform === "darwin" ? "osx-bin" : process.platform === "win32" ? "win64-bin" : "linux64-bin"; return join(dir, "hermesc", bin, process.platform === "win32" ? "hermesc.exe" : "hermesc"); } function resolveHermesc(root, platform) { if (platform === "ios") { const pod = join(root, "ios", "Pods", "hermes-engine", "destroot", "bin", "hermesc"); if (existsSync(pod)) return pod; } const npm = resolveNpmHermesc(root); if (!existsSync(npm)) { throw new Error(`[one] hermesc not found at ${npm}: install dependencies${platform === "ios" ? " and run one prebuild with pod install" : ""} first`); } return npm; } async function resolveRuntimeVersion(platform, flag) { const { oneOptions } = await loadUserOneOptions("build", true); const native = oneOptions?.native; const app = typeof native === "object" ? native.app : void 0; if (!app) { if (!flag) { throw new Error("[one] one updates publish needs native.app.updates.runtimeVersion, or --runtime-version for an app built with expo prebuild: the binary only takes updates published for its own runtime version"); } return flag; } validateNativeApp(app, platform); const runtimeVersion = app.updates?.runtimeVersion; if (!runtimeVersion) { throw new Error("[one] one updates publish needs native.app.updates.runtimeVersion: the binary only takes updates published for its own runtime version"); } if (flag !== void 0 && flag !== runtimeVersion) { throw new Error(`[one] --runtime-version ${flag} differs from native.app.updates.runtimeVersion ${runtimeVersion}`); } return runtimeVersion; } async function runUpdatesPublish(args) { const platform = lastValue(args.platform); if (platform !== "ios" && platform !== "android") { throw new Error("[one] one updates publish needs --platform ios|android"); } const out = lastValue(args.out); if (!out) { throw new Error("[one] one updates publish needs --out <dir>"); } const intermediatesOut = lastValue(args.intermediatesOut); if (intermediatesOut) { const published = resolve(out); const debug = resolve(intermediatesOut); if (published === debug || published.startsWith(`${debug}${sep}`) || debug.startsWith(`${published}${sep}`)) { throw new Error("[one] --intermediates-out must be separate from --out"); } if (existsSync(debug) && readdirSync(debug).length > 0) { throw new Error(`[one] refusing to write intermediates into non-empty ${debug}`); } } const root = process.cwd(); const runtimeVersion = await resolveRuntimeVersion(platform, lastValue(args.runtimeVersion)); if (existsSync(out) && readdirSync(out).length > 0) { throw new Error(`[one] refusing to publish into non-empty ${out}: every publish writes a fresh directory`); } mkdirSync(out, { recursive: true }); if (intermediatesOut) { mkdirSync(intermediatesOut, { recursive: true }); const published = realpathSync(out); const debug = realpathSync(intermediatesOut); if (published === debug || published.startsWith(`${debug}${sep}`) || debug.startsWith(`${published}${sep}`)) { throw new Error("[one] --intermediates-out must be separate from --out"); } } const assetsOut = join(out, "assets"); mkdirSync(assetsOut, { recursive: true }); const metadata = parseMetadata(allValues(args.metadata)); const work = mkdtempSync(join(tmpdir(), "one-updates-publish-")); const bundleJs = join(work, "bundle.js"); const packagerMap = join(work, "bundle.js.map"); const assetsDir = join(work, "assets"); const bytecode = join(work, "main.jsbundle"); const composedMap = join(work, "main.jsbundle.map"); try { execFileSync(process.execPath, [ resolveCliJs(root), "bundle", "--entry-file", "index.js", "--platform", platform, "--dev", "false", "--reset-cache", "--bundle-output", bundleJs, ...intermediatesOut ? ["--sourcemap-output", packagerMap] : [], "--assets-dest", assetsDir, "--minify", platform === "ios" ? "true" : "false" ], { cwd: root, stdio: "inherit" }); const hermesc = resolveHermesc(root, platform); const hermescArgs = ["-emit-binary", "-max-diagnostic-width=80"]; if (platform === "android") hermescArgs.push("-w"); hermescArgs.push("-O"); if (intermediatesOut) hermescArgs.push("-output-source-map"); hermescArgs.push("-out", bytecode, bundleJs); try { execFileSync(hermesc, hermescArgs, { cwd: root, stdio: "inherit" }); } catch (error) { throw new Error(`[one] hermesc failed to compile the ${platform} update bundle${error instanceof Error ? `: ${error.message}` : ""}`); } if (intermediatesOut) { const require2 = createRequire(join(root, "package.json")); const reactNativeJson = require2.resolve("react-native/package.json"); const composeSourceMaps = join(reactNativeJson.slice(0, -"package.json".length), "scripts", "compose-source-maps.js"); execFileSync(process.execPath, [ composeSourceMaps, packagerMap, `${bytecode}.map`, "-o", composedMap ], { cwd: root, stdio: "inherit" }); writeFileSync(join(intermediatesOut, "bundle.js"), readFileSync(bundleJs)); writeFileSync(join(intermediatesOut, "main.jsbundle.map"), readFileSync(composedMap)); } const bundleHash = sha256Base64Url(bytecode); writeFileSync(join(assetsOut, bundleHash), readFileSync(bytecode)); const assets = []; for (const file of collectFiles(assetsDir)) { const hash = sha256Base64Url(file); const target = join(assetsOut, hash); if (!existsSync(target)) writeFileSync(target, readFileSync(file)); assets.push({ hash, url: `assets/${hash}`, path: relative(assetsDir, file).split(sep).join("/") }); } assets.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0); const manifest = { id: randomUUID(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), runtimeVersion, launchAsset: { hash: bundleHash, url: `assets/${bundleHash}`, path: "main.jsbundle" }, assets, metadata }; writeFileSync(join(out, "manifest.json"), `${JSON.stringify(manifest, null, 2)} `); console.info(`[one] published ${platform} update ${manifest.id} (${assets.length} assets) to ${out}`); } finally { rmSync(work, { recursive: true, force: true }); } } export { runUpdatesPublish }; //# sourceMappingURL=updatesPublish.mjs.map