one
Version:
One is a new React Framework that makes Vite serve both native and web.
328 lines (327 loc) • 11.6 kB
JavaScript
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { generateBundlerConfig, isCiEnvironment, maybeGenerateBundlerConfigOnInstall, ONE_GENERATED_MARKER } from "./generateBundlerConfig.mjs";
describe("generateBundlerConfig", () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "one-gen-bundler-test-"));
});
afterEach(() => {
fs.rmSync(tmpDir, {
recursive: true,
force: true
});
});
it("writes both files when missing", () => {
const {
ok,
results
} = generateBundlerConfig({
cwd: tmpDir,
quiet: true
});
expect(ok).toBe(true);
expect(results.map(r => r.action)).toEqual(["wrote", "wrote"]);
const babel = fs.readFileSync(path.join(tmpDir, "babel.config.cjs"), "utf8");
expect(babel).toContain(ONE_GENERATED_MARKER);
expect(babel).toContain("require('one/babel-preset')");
expect(babel).toContain("oneBundlerOptions");
const metro = fs.readFileSync(path.join(tmpDir, "metro.config.cjs"), "utf8");
expect(metro).toContain(ONE_GENERATED_MARKER);
expect(metro).toContain("require('one/metro-config')");
expect(metro).toContain("withOne");
});
it("is idempotent on second run", () => {
generateBundlerConfig({
cwd: tmpDir,
quiet: true
});
const before = fs.readFileSync(path.join(tmpDir, "babel.config.cjs"), "utf8");
const {
results
} = generateBundlerConfig({
cwd: tmpDir,
quiet: true
});
expect(results.map(r => r.action)).toEqual(["kept", "kept"]);
const after = fs.readFileSync(path.join(tmpDir, "babel.config.cjs"), "utf8");
expect(after).toBe(before);
});
it("refuses to overwrite a customized file without --force", () => {
fs.writeFileSync(path.join(tmpDir, "babel.config.cjs"), "// hand-written\nmodule.exports = { presets: [] }\n");
const {
ok,
results
} = generateBundlerConfig({
cwd: tmpDir,
quiet: true
});
const babelResult = results.find(r => r.filePath.endsWith("babel.config.cjs"));
expect(babelResult.action).toBe("skipped-customized");
expect(ok).toBe(true);
const after = fs.readFileSync(path.join(tmpDir, "babel.config.cjs"), "utf8");
expect(after).toContain("hand-written");
});
it("overwrites a customized file when --force is set", () => {
fs.writeFileSync(path.join(tmpDir, "babel.config.cjs"), "// hand-written\nmodule.exports = { presets: [] }\n");
const {
results
} = generateBundlerConfig({
cwd: tmpDir,
force: true,
quiet: true
});
const babelResult = results.find(r => r.filePath.endsWith("babel.config.cjs"));
expect(babelResult.action).toBe("wrote");
const after = fs.readFileSync(path.join(tmpDir, "babel.config.cjs"), "utf8");
expect(after).toContain(ONE_GENERATED_MARKER);
expect(after).not.toContain("hand-written");
});
it("rewrites a stale marked file", () => {
fs.writeFileSync(path.join(tmpDir, "babel.config.cjs"), `// ${ONE_GENERATED_MARKER}
// old content from previous version
module.exports = {}
`);
const {
results
} = generateBundlerConfig({
cwd: tmpDir,
quiet: true
});
const babelResult = results.find(r => r.filePath.endsWith("babel.config.cjs"));
expect(babelResult.action).toBe("wrote");
const after = fs.readFileSync(path.join(tmpDir, "babel.config.cjs"), "utf8");
expect(after).toContain("require('one/babel-preset')");
expect(after).not.toContain("old content from previous version");
});
describe("--check mode", () => {
it("exits ok when files exist and match", () => {
generateBundlerConfig({
cwd: tmpDir,
quiet: true
});
const {
ok,
results
} = generateBundlerConfig({
cwd: tmpDir,
check: true,
quiet: true
});
expect(ok).toBe(true);
expect(results.every(r => r.action === "kept")).toBe(true);
});
it("exits not-ok when files are missing", () => {
const {
ok,
results
} = generateBundlerConfig({
cwd: tmpDir,
check: true,
quiet: true
});
expect(ok).toBe(false);
expect(results.every(r => r.action === "would-write")).toBe(true);
expect(fs.existsSync(path.join(tmpDir, "babel.config.cjs"))).toBe(false);
});
it("exits not-ok when a marked file is stale", () => {
fs.writeFileSync(path.join(tmpDir, "babel.config.cjs"), `// ${ONE_GENERATED_MARKER}
module.exports = {}
`);
fs.writeFileSync(path.join(tmpDir, "metro.config.cjs"), `// ${ONE_GENERATED_MARKER}
module.exports = {}
`);
const {
ok,
results
} = generateBundlerConfig({
cwd: tmpDir,
check: true,
quiet: true
});
expect(ok).toBe(false);
expect(results.every(r => r.action === "would-overwrite")).toBe(true);
});
});
describe("--eject mode", () => {
it("writes files WITHOUT the @one/generated marker", () => {
const {
results
} = generateBundlerConfig({
cwd: tmpDir,
eject: true,
quiet: true
});
expect(results.map(r => r.action)).toEqual(["wrote", "wrote"]);
const babel = fs.readFileSync(path.join(tmpDir, "babel.config.cjs"), "utf8");
expect(babel).not.toContain(ONE_GENERATED_MARKER);
expect(babel).toContain("you own this file");
expect(babel).toContain("require('one/babel-preset')");
const metro = fs.readFileSync(path.join(tmpDir, "metro.config.cjs"), "utf8");
expect(metro).not.toContain(ONE_GENERATED_MARKER);
expect(metro).toContain("withOne");
});
it("subsequent auto-gen run (no --eject) treats ejected files as customized", () => {
generateBundlerConfig({
cwd: tmpDir,
eject: true,
quiet: true
});
const before = fs.readFileSync(path.join(tmpDir, "babel.config.cjs"), "utf8");
const {
results
} = generateBundlerConfig({
cwd: tmpDir,
quiet: true
});
expect(results.every(r => r.action === "skipped-customized")).toBe(true);
const after = fs.readFileSync(path.join(tmpDir, "babel.config.cjs"), "utf8");
expect(after).toBe(before);
});
});
it("does not clobber a non-.cjs config in the same family", () => {
fs.writeFileSync(path.join(tmpDir, "babel.config.js"), "module.exports = { presets: [] }\n");
const {
results
} = generateBundlerConfig({
cwd: tmpDir,
quiet: true
});
const babelResult = results.find(r => r.filePath.endsWith("babel.config.js") || r.filePath.endsWith("babel.config.cjs"));
expect(babelResult.action).toBe("skipped-other-format");
expect(fs.existsSync(path.join(tmpDir, "babel.config.cjs"))).toBe(false);
});
it("embeds loaded One router/setup options into both config files", () => {
generateBundlerConfig({
cwd: tmpDir,
quiet: true,
oneOptions: {
router: {
root: "src/routes",
ignoredRouteFiles: ["**/*.native-test.*"],
linking: {
scheme: "myapp",
prefixes: ["https://example.com/app"]
}
},
setupFile: {
native: "src/setup.native.ts"
}
}
});
const babel = fs.readFileSync(path.join(tmpDir, "babel.config.cjs"), "utf8");
const metro = fs.readFileSync(path.join(tmpDir, "metro.config.cjs"), "utf8");
for (const file of [babel, metro]) {
expect(file).toContain('"routerRoot": "src/routes"');
expect(file).toContain('"ignoredRouteFiles"');
expect(file).toContain('"**/*.native-test.*"');
expect(file).toContain('"scheme": "myapp"');
expect(file).toContain('"native": "src/setup.native.ts"');
}
});
it("refuses to silently drop non-serializable options", () => {
expect(() => generateBundlerConfig({
cwd: tmpDir,
quiet: true,
oneOptions: {
router: {
linking: {
// the plugin API only accepts serializable router.linking fields
filter: () => true
}
}
}
})).toThrow(/JSON-serializable/);
});
});
describe("isCiEnvironment", () => {
const originalCi = process.env.CI;
const originalEasBuild = process.env.EAS_BUILD;
afterEach(() => {
if (originalCi === void 0) delete process.env.CI;else process.env.CI = originalCi;
if (originalEasBuild === void 0) delete process.env.EAS_BUILD;else process.env.EAS_BUILD = originalEasBuild;
});
it("is true when CI=true", () => {
process.env.CI = "true";
delete process.env.EAS_BUILD;
expect(isCiEnvironment()).toBe(true);
});
it("is true when CI=1", () => {
process.env.CI = "1";
delete process.env.EAS_BUILD;
expect(isCiEnvironment()).toBe(true);
});
it("is true when EAS_BUILD=true", () => {
delete process.env.CI;
process.env.EAS_BUILD = "true";
expect(isCiEnvironment()).toBe(true);
});
it("is false when neither is set", () => {
delete process.env.CI;
delete process.env.EAS_BUILD;
expect(isCiEnvironment()).toBe(false);
});
it("is false when CI=false or CI=0", () => {
delete process.env.EAS_BUILD;
process.env.CI = "false";
expect(isCiEnvironment()).toBe(false);
process.env.CI = "0";
expect(isCiEnvironment()).toBe(false);
});
});
describe("maybeGenerateBundlerConfigOnInstall", () => {
let tmpDir;
const originalCi = process.env.CI;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "one-gen-bundler-ci-test-"));
});
afterEach(() => {
fs.rmSync(tmpDir, {
recursive: true,
force: true
});
if (originalCi === void 0) delete process.env.CI;else process.env.CI = originalCi;
delete process.env.EAS_BUILD;
});
it("does NOT generate files when not in CI even if expo-updates is present", () => {
delete process.env.CI;
delete process.env.EAS_BUILD;
const fakeUpdates = path.join(tmpDir, "node_modules/expo-updates");
fs.mkdirSync(fakeUpdates, {
recursive: true
});
fs.writeFileSync(path.join(fakeUpdates, "package.json"), JSON.stringify({
name: "expo-updates"
}));
maybeGenerateBundlerConfigOnInstall(tmpDir);
expect(fs.existsSync(path.join(tmpDir, "babel.config.cjs"))).toBe(false);
expect(fs.existsSync(path.join(tmpDir, "metro.config.cjs"))).toBe(false);
});
it("does NOT generate files in CI when expo-updates is absent", () => {
process.env.CI = "true";
maybeGenerateBundlerConfigOnInstall(tmpDir);
expect(fs.existsSync(path.join(tmpDir, "babel.config.cjs"))).toBe(false);
expect(fs.existsSync(path.join(tmpDir, "metro.config.cjs"))).toBe(false);
});
it("generates files in CI when expo-updates is present", () => {
process.env.CI = "true";
const fakeUpdates = path.join(tmpDir, "node_modules/expo-updates");
fs.mkdirSync(fakeUpdates, {
recursive: true
});
fs.writeFileSync(path.join(fakeUpdates, "package.json"), JSON.stringify({
name: "expo-updates"
}));
const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => {});
try {
maybeGenerateBundlerConfigOnInstall(tmpDir);
} finally {
consoleInfo.mockRestore();
}
expect(fs.existsSync(path.join(tmpDir, "babel.config.cjs"))).toBe(true);
expect(fs.existsSync(path.join(tmpDir, "metro.config.cjs"))).toBe(true);
});
});
//# sourceMappingURL=generateBundlerConfig.test.mjs.map