one
Version:
One is a React framework for web, iOS, and Android apps.
235 lines (232 loc) • 8.72 kB
JavaScript
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