Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ parameters:
default: false

orbs:
codecov: codecov/codecov@5.4.3
codecov: codecov/codecov@6.0.0

executors:
base:
Expand Down
249 changes: 249 additions & 0 deletions cli/src/__tests__/commands/xlr-compile-packages.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import fs from "fs";
import os from "os";
import path from "path";
import { test, expect, describe, beforeEach, afterEach, vi } from "vitest";
import { Errors } from "@oclif/core";
import XLRCompile from "../../commands/xlr/compile";

/** A plugin package with one asset, laid out the way `xlr compile` expects */
function writeFixture(dir: string, packageJson?: Record<string, unknown>) {
fs.mkdirSync(path.join(dir, "src"), { recursive: true });

if (packageJson) {
fs.writeFileSync(
path.join(dir, "package.json"),
JSON.stringify(packageJson),
);
}

fs.writeFileSync(
path.join(dir, "src", "index.ts"),
`
import type { ExtendedPlayerPlugin } from "@player-ui/player";

export interface TestAsset {
id: string;
type: "test";
}

export class TestPlugin implements ExtendedPlayerPlugin<[TestAsset]> {
name = "test-plugin";
}
`,
);
}

/** Silences `Errors.warn` while capturing what it was called with */
function spyOnWarn() {
return vi.spyOn(Errors, "warn").mockImplementation(() => undefined);
}

function readManifest(dir: string) {
return JSON.parse(
fs.readFileSync(path.join(dir, "dist", "xlr", "manifest.json"), "utf-8"),
);
}

describe("xlr compile package info", () => {
/** An isolated root, so nothing on the ambient filesystem can be picked up */
let workspace: string;
let cwd: string;
let warn: ReturnType<typeof spyOnWarn>;
const env = { ...process.env };

beforeEach(() => {
workspace = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), "xlr-compile-")),
);
cwd = process.cwd();
warn = spyOnWarn();
delete process.env.BAZEL_STABLE_STATUS_FILE;
delete process.env.BAZEL_PACKAGE;
delete process.env.XLR_PACKAGE_NAME;
delete process.env.JS_BINARY__EXECROOT;
});

afterEach(() => {
process.chdir(cwd);
fs.rmSync(workspace, { recursive: true, force: true });
process.env = { ...env };
warn.mockRestore();
});

describe("non-bazel", () => {
beforeEach(() => {
process.chdir(workspace);
});

test("records the name and version from package.json", async () => {
writeFixture(workspace, { name: "@test/plugin", version: "2.3.4" });

await XLRCompile.run(["-i", "src", "-o", "dist"]);

expect(readManifest(workspace).packages).toStrictEqual({
react: { name: "@test/plugin", version: "2.3.4" },
});
});

test("records the name alone when package.json has no version", async () => {
writeFixture(workspace, { name: "@test/plugin" });

await XLRCompile.run(["-i", "src", "-o", "dist"]);

expect(readManifest(workspace).packages).toStrictEqual({
react: { name: "@test/plugin" },
});
});

// The omission must be noisy: a manifest silently losing its `packages` key is the
// failure mode this whole path exists to prevent.
describe("when package.json is missing or incomplete", () => {
test("omits packages and warns when there is no package.json", async () => {
writeFixture(workspace);

await XLRCompile.run(["-i", "src", "-o", "dist"]);

expect(readManifest(workspace).packages).toBeUndefined();
expect(warn).toHaveBeenCalledWith(
expect.stringContaining("Could not read"),
);
expect(warn).toHaveBeenCalledWith(
expect.stringContaining("Omitting package information"),
);
});

test("omits packages and warns when package.json has no name", async () => {
writeFixture(workspace, { version: "2.3.4" });

await XLRCompile.run(["-i", "src", "-o", "dist"]);

expect(readManifest(workspace).packages).toBeUndefined();
expect(warn).toHaveBeenCalledWith(
expect.stringContaining('No "name" in'),
);
});
});
});

describe("bazel", () => {
// Bazel runs from the workspace root and names the package in BAZEL_PACKAGE, so the
// working directory alone does not identify the package.
const pkgPath = path.join("plugins", "test-plugin");

beforeEach(() => {
// a workspace root package.json that must never be picked up
fs.writeFileSync(
path.join(workspace, "package.json"),
JSON.stringify({ name: "workspace-root", version: "0.0.0" }),
);
process.chdir(workspace);
process.env.BAZEL_PACKAGE = pkgPath;
});

test("takes the name from XLR_PACKAGE_NAME and the version from the stamp", async () => {
// No package.json in the package: Bazel does not stage one, it passes the name instead
writeFixture(path.join(workspace, pkgPath));
process.env.XLR_PACKAGE_NAME = "@test/plugin";
const statusFile = path.join(workspace, "stable-status.txt");
fs.writeFileSync(statusFile, "STABLE_VERSION 1.1.0\n");
process.env.BAZEL_STABLE_STATUS_FILE = statusFile;

await XLRCompile.run([
"-i",
path.join(pkgPath, "src"),
"-o",
path.join(pkgPath, "dist"),
]);

expect(
readManifest(path.join(workspace, pkgPath)).packages,
).toStrictEqual({
react: { name: "@test/plugin", version: "1.1.0" },
});
});

test("resolves an execroot-relative stamp path against the execroot", async () => {
// Bazel names the status file relative to the execroot, but the js_binary launcher
// runs the tool from BAZEL_BINDIR, so a relative path does not resolve against cwd.
writeFixture(path.join(workspace, pkgPath));
process.env.XLR_PACKAGE_NAME = "@test/plugin";
fs.mkdirSync(path.join(workspace, "bazel-out"), { recursive: true });
fs.writeFileSync(
path.join(workspace, "bazel-out", "stable-status.txt"),
"STABLE_VERSION 1.2.0-next.7\n",
);
process.env.BAZEL_STABLE_STATUS_FILE = "bazel-out/stable-status.txt";
process.env.JS_BINARY__EXECROOT = workspace;

const bindir = path.join(workspace, "bazel-out", "bin");
fs.mkdirSync(bindir, { recursive: true });
process.chdir(bindir);

await XLRCompile.run([
"-i",
path.join(workspace, pkgPath, "src"),
"-o",
path.join(workspace, pkgPath, "dist"),
]);

expect(
readManifest(path.join(workspace, pkgPath)).packages,
).toStrictEqual({
react: { name: "@test/plugin", version: "1.2.0-next.7" },
});
});

test("omits the version when not stamped", async () => {
writeFixture(path.join(workspace, pkgPath));
process.env.XLR_PACKAGE_NAME = "@test/plugin";

await XLRCompile.run([
"-i",
path.join(pkgPath, "src"),
"-o",
path.join(pkgPath, "dist"),
]);

expect(
readManifest(path.join(workspace, pkgPath)).packages,
).toStrictEqual({
react: { name: "@test/plugin" },
});
});

describe("when package.json is missing or incomplete", () => {
test("omits packages and warns when neither XLR_PACKAGE_NAME nor package.json is available", async () => {
writeFixture(path.join(workspace, pkgPath));

await XLRCompile.run([
"-i",
path.join(pkgPath, "src"),
"-o",
path.join(pkgPath, "dist"),
]);

expect(
readManifest(path.join(workspace, pkgPath)).packages,
).toBeUndefined();
expect(warn).toHaveBeenCalledWith(
expect.stringContaining(path.join(pkgPath, "package.json")),
);
});

test("omits packages when package.json has no name", async () => {
writeFixture(path.join(workspace, pkgPath), { version: "2.3.4" });

await XLRCompile.run([
"-i",
path.join(pkgPath, "src"),
"-o",
path.join(pkgPath, "dist"),
]);

expect(
readManifest(path.join(workspace, pkgPath)).packages,
).toBeUndefined();
});
});
});
});
26 changes: 19 additions & 7 deletions cli/src/commands/xlr/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import path from "path";
import globby from "globby";
import logSymbols from "log-symbols";
import { TsConverter } from "@xlr-lib/xlr-converters";
import type { Manifest } from "@xlr-lib/xlr";
import type { Manifest, PlatformPackages } from "@xlr-lib/xlr";
import chalk from "chalk";
import { BaseCommand } from "../../utils/base-command";
import { pluginVisitor, fileVisitor } from "../../utils/xlr/visitors";
import { Mode, customPrimitives } from "../../utils/xlr/consts";
import { getPackages } from "../../utils/xlr/packages";

/**
* Exports TS Interfaces/Types to XLR format
Expand Down Expand Up @@ -61,8 +62,9 @@ export default class XLRCompile extends BaseCommand {
`${inputPath}/**/*.ts`,
`${inputPath}/**/*.tsx`,
]);
const packages = getPackages();
try {
this.processTypes(inputFiles, outputDir, {}, mode);
this.processTypes(inputFiles, outputDir, {}, mode, packages);
} catch (e: any) {
console.log("");
console.log(
Expand Down Expand Up @@ -90,6 +92,7 @@ export default class XLRCompile extends BaseCommand {
outputDirectory: string,
options: ts.CompilerOptions,
mode: Mode = Mode.PLUGIN,
packages?: PlatformPackages,
): void {
// Build a program using the set of root file names in fileNames
const program = ts.createProgram(fileNames, options);
Expand Down Expand Up @@ -148,28 +151,37 @@ export default class XLRCompile extends BaseCommand {
throw new Error("Error: Unable to parse any XLRs in package");
}

const manifest: Manifest = {
...capabilities,
...(packages ? { packages } : {}),
};

// print out the manifest files
const jsonManifest = JSON.stringify(capabilities, this.replacer, 4);
const jsonManifest = JSON.stringify(manifest, this.replacer, 4);
fs.writeFileSync(path.join(outputDirectory, "manifest.json"), jsonManifest);

const tsManifestFile = `${[...(capabilities.capabilities?.values() ?? [])]
const tsManifestFile = `${[...(manifest.capabilities?.values() ?? [])]
.flat(2)
.map((capability) => {
return `const ${capability.replace(".", "_")} = require("./${capability}.json")`;
})
.join("\n")}

module.exports = {
"pluginName": "${capabilities.pluginName}",
"pluginName": "${manifest.pluginName}",${
manifest.packages
? `\n "packages": ${JSON.stringify(manifest.packages)},`
: ""
}
"capabilities": {
${[...(capabilities.capabilities?.entries() ?? [])]
${[...(manifest.capabilities?.entries() ?? [])]
.map(([capabilityName, provides]) => {
return `"${capabilityName}":[${provides.join(",").replaceAll(".", "_")}],`;
})
.join("\n\t\t")}
},
"customPrimitives": [
${[capabilities.customPrimitives?.map((i) => `"${i}"`).join(",") ?? ""]}
${[manifest.customPrimitives?.map((i) => `"${i}"`).join(",") ?? ""]}
]
}
`;
Expand Down
Loading