@rechunk/cli
Version:
Command-line interface for managing ReChunk projects, chunks, and deployments
512 lines (498 loc) • 18.4 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
// src/cmds/dev-server.ts
var import_rollup_preset = __toESM(require("@rechunk/rollup-preset"));
var import_chalk2 = __toESM(require("chalk"));
var import_commander = require("commander");
var import_crypto = require("crypto");
var import_http = __toESM(require("http"));
var import_jsrsasign = require("jsrsasign");
var import_path3 = __toESM(require("path"));
var import_rollup = require("rollup");
var import_url = __toESM(require("url"));
// src/lib/config.ts
var import_api_client = require("@rechunk/api-client");
var import_fs = __toESM(require("fs"));
var import_path = __toESM(require("path"));
function getRechunkConfig(dir = process.cwd()) {
const rechunkConfigPath = import_path.default.resolve(dir, ".rechunkrc.json");
if (!import_fs.default.existsSync(rechunkConfigPath)) {
throw new Error(
"[ReChunk]: cannot find rechunk configuration, ensure there is a .rechunkrc.json in the root of your project. If there is not, please generate a ReChunk project with the init command."
);
}
const rechunkConfig = require(rechunkConfigPath);
return rechunkConfig;
}
function createBaseConfiguration(host, username, password) {
return new import_api_client.Configuration({
basePath: host,
headers: {
Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
}
});
}
function configureReChunkChunksApi(host, username, password) {
return new import_api_client.ChunksApi(createBaseConfiguration(host, username, password));
}
function configureReChunkProjectsApi(host, username, password) {
return new import_api_client.ProjectsApi(createBaseConfiguration(host, username, password));
}
function configureReChunkAuthenticationApi(host, username, password) {
return new import_api_client.AuthenticationApi(
createBaseConfiguration(host, username, password)
);
}
// src/lib/constants.ts
var import_chalk = __toESM(require("chalk"));
// package.json
var package_default = {
name: "@rechunk/cli",
description: "Command-line interface for managing ReChunk projects, chunks, and deployments",
version: "0.2.0",
author: "Chris Herman",
bin: {
rechunk: "bin/rechunk"
},
dependencies: {
"@babel/parser": "^7.27.0",
"@babel/traverse": "^7.27.0",
"@rechunk/api-client": "workspace:*",
"@rechunk/rollup-preset": "workspace:*",
chalk: "^4.0.0",
commander: "^12.0.0",
glob: "^11.0.0",
inquirer: "^12.2.0",
jsrsasign: "^11.1.0",
open: "8.4.2",
ora: "^8.1.1",
rollup: "^4.13.0",
typescript: "^5.4.3"
},
devDependencies: {
"@repo/typescript-config": "workspace:*",
"@types/jsrsasign": "^10.5.14"
},
keywords: [
"chunks",
"cli",
"code-splitting",
"deployment",
"management",
"react-native",
"rechunk"
],
license: "MIT",
main: "dist/index.js",
publishConfig: {
access: "public",
registry: "https://registry.npmjs.org"
},
scripts: {
build: "tsup",
"check-types": "tsc --noEmit"
},
types: "dist/index.d.ts"
};
// src/lib/constants.ts
var LOGO = import_chalk.default.green`
████████████
████████████
██████████████████
████ ████
████ ████
████ ████
████ ████
████ ████
██████████████████
███████████████
███████████████
████ ████
████ ████████
████ █████
████ █████
Welcome to ReChunk ${import_chalk.default.bold.white`v${package_default.version}`}
${import_chalk.default.white.dim`React Native - Remote Chunks - Secure`}
`;
// src/lib/parsers.ts
var import_typescript = __toESM(require("typescript"));
// src/lib/paths.ts
var import_parser = require("@babel/parser");
var import_traverse = __toESM(require("@babel/traverse"));
var import_fs2 = __toESM(require("fs"));
var import_glob = require("glob");
var import_path2 = require("path");
async function findTSAndTSXFiles(dir) {
const globPattern = (0, import_path2.join)(dir, "**/*.{ts,tsx}");
const ignorePattern = "**/node_modules/**";
return (0, import_glob.glob)(globPattern, { ignore: ignorePattern });
}
async function containsUseRechunkDirective(filePath) {
try {
const code = await import_fs2.default.promises.readFile(filePath, "utf-8");
const ast = (0, import_parser.parse)(code, {
sourceType: "module",
plugins: ["jsx", "typescript"]
});
let hasUseDomDirective = false;
(0, import_traverse.default)(ast, {
Directive(path5) {
if (path5.node.value.value === "use rechunk") {
hasUseDomDirective = true;
path5.stop();
}
}
});
return hasUseDomDirective;
} catch (error) {
console.error(`Error processing file ${filePath}:`, error);
return false;
}
}
async function aggregateUseRechunkFiles(rootDir) {
const files = await findTSAndTSXFiles(rootDir);
const matchingFiles = [];
for (const file of files) {
if (await containsUseRechunkDirective(file)) {
let relativePath = (0, import_path2.relative)(process.cwd(), file);
if (!relativePath.startsWith(".")) {
relativePath = `./${relativePath}`;
}
matchingFiles.push(relativePath);
}
}
return matchingFiles;
}
function constructAuthTokenURL(host, project, token) {
const url2 = new URL("/auth/token", host);
const params = new URLSearchParams({
projectId: project,
token
});
url2.search = params.toString();
return url2.toString();
}
// src/cmds/dev-server.ts
var PORT = 49904;
import_commander.program.command("dev-server").description(
"ReChunk development server to serve and sign React Native chunks."
).action(() => {
const rc = getRechunkConfig();
startDevServer(rc);
});
function startDevServer(rc) {
const server = import_http.default.createServer((req, res) => handleRequest(req, res, rc));
server.listen(PORT, () => {
console.log();
console.log(LOGO);
console.log(
` ${import_chalk2.default.green`→`} host: http://localhost
${import_chalk2.default.green`→`} port: ${PORT}
${import_chalk2.default.green`→`} path: /projects/:project/chunks/:chunkId`
);
console.log();
});
}
async function handleRequest(req, res, rc) {
const { projectId, chunkId } = parseUrl(req.url);
if (!chunkId) {
res.writeHead(400, { "Content-Type": "text/plain" });
res.end("Bad Request");
return;
}
console.log(
`${import_chalk2.default.green` ⑇`} ${(/* @__PURE__ */ new Date()).toISOString()}: Serving /projects/${projectId}/chunks/${chunkId}`
);
try {
const decodeChunkId = Buffer.from(chunkId, "base64").toString("utf-8");
const code = await bundleChunk(decodeChunkId);
const token = generateToken(code, rc.privateKey);
sendJsonResponse(res, { token, data: code });
} catch (error) {
logError(res, error.message);
}
}
function parseUrl(requestUrl) {
const parsedUrl = import_url.default.parse(requestUrl || "", true);
const matches = parsedUrl.pathname?.match(/\/projects\/(.*)\/chunks\/(\w+)/);
if (!matches) {
throw new Error("[ReChunk]: Unable to parse URL");
}
return { projectId: matches[1], chunkId: matches[2] };
}
async function bundleChunk(entryPath) {
const input = import_path3.default.resolve(process.cwd(), entryPath);
const rollupBuild = await (0, import_rollup.rollup)(await (0, import_rollup_preset.default)({ input }));
const {
output: [{ code }]
} = await rollupBuild.generate({ interop: "auto", format: "cjs" });
return code;
}
function generateToken(code, privateKey) {
const prvKey = import_jsrsasign.KEYUTIL.getKey(privateKey);
const sPayload = (0, import_crypto.createHash)("sha256").update(code).digest("hex");
return import_jsrsasign.KJUR.jws.JWS.sign(
"RS256",
JSON.stringify({ alg: "RS256" }),
sPayload,
prvKey
);
}
function sendJsonResponse(res, data) {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(data));
}
function logError(res, message) {
console.error(`\u274C Error: ${message}`);
res.writeHead(500, { "Content-Type": "text/plain" });
res.end(`Server Error: ${message}`);
}
// src/cmds/init.ts
var import_chalk3 = __toESM(require("chalk"));
var import_commander2 = require("commander");
var import_fs3 = __toESM(require("fs"));
var import_path4 = __toESM(require("path"));
import_commander2.program.command("init").description("Initializes a ReChunk project").requiredOption("-h, --host <host>", "ReChunk server host URL").requiredOption("-u, --username <username>", "Username for basic auth").requiredOption("-p, --password <password>", "Password for basic auth").action(async (options) => {
console.log(LOGO);
const ora = await import("ora");
const spinner = ora.default();
const { host, username, password } = options;
const ctx = process.cwd();
const rcPath = import_path4.default.resolve(ctx, ".rechunkrc.json");
try {
spinner.start("Validating host URL...");
validateHost(host);
spinner.succeed("Host URL is valid.");
spinner.start("Checking for existing project...");
checkProjectExists(rcPath);
spinner.succeed("No existing project found.");
spinner.start("Creating project on the ReChunk server...");
const { createdAt, updatedAt, id, ...projectData } = await createProject(
host,
username,
password
);
spinner.succeed("Project created successfully.");
spinner.start("Saving project configuration...");
saveProjectFile(rcPath, {
...projectData,
$schema: "https://crherman7.github.io/rechunk/schema.json",
project: id,
host,
external: []
});
spinner.succeed("Project configuration saved.");
console.log(
import_chalk3.default.green(
"\n\u{1F389} Successfully initialized a new ReChunk project! Generated .rechunkrc.json."
)
);
} catch (error) {
spinner.fail(import_chalk3.default.red(`Error: ${error.message}`));
}
});
function validateHost(host) {
const urlPattern = /^http(|s):\/\/\w+(\.\w+)*(:[0-9]+)?\/?(\/[.\w]*)*$/;
if (!host.match(urlPattern)) {
throw new Error(
"The provided host URL does not match the expected format (e.g., https://rechunk.onrender.com, https://localhost:3000)."
);
}
}
function checkProjectExists(rcPath) {
if (import_fs3.default.existsSync(rcPath)) {
throw new Error(
"Project already exists. Please remove .rechunkrc.json before initializing a new project."
);
}
}
async function createProject(host, username, password) {
const api = configureReChunkProjectsApi(host, username, password);
try {
return await api.createProject();
} catch (error) {
throw new Error(
`Failed to initialize project: ${error.message}`
);
}
}
function saveProjectFile(rcPath, data) {
import_fs3.default.writeFileSync(
rcPath,
JSON.stringify(
Object.keys(data).sort().reduce((obj, key) => {
obj[key] = data[key];
return obj;
}, {}),
null,
2
) + "\n"
);
}
// src/cmds/manage.ts
var import_commander3 = require("commander");
var import_open = __toESM(require("open"));
import_commander3.program.command("manage").description(
"Opens the browser to the chunks management page with a verified token"
).action(async () => {
console.log();
console.log(LOGO);
const ora = await import("ora");
const spinner = ora.default("Generating auth token...").start();
const { host, project, writeKey } = getRechunkConfig();
const { token } = await manageCunks(host, project, writeKey);
spinner.succeed("Generated auth token.");
spinner.start(`Opening browser at ${host}/auth/token...`);
await (0, import_open.default)(constructAuthTokenURL(host, project, token));
spinner.succeed(`Opened browser at ${host}/auth/token.`);
});
async function manageCunks(host, project, writeKey) {
const api = configureReChunkAuthenticationApi(host, project, writeKey);
try {
return api.createToken();
} catch (error) {
throw new Error(`Failed to publish chunk: ${error.message}`);
}
}
// src/cmds/publish.ts
var import_rollup_preset2 = __toESM(require("@rechunk/rollup-preset"));
var import_chalk4 = __toESM(require("chalk"));
var import_commander4 = require("commander");
var import_inquirer = __toESM(require("inquirer"));
var import_path5 = __toESM(require("path"));
var import_rollup2 = require("rollup");
import_commander4.program.command("publish").description("Aggregates and publishes selected ReChunk components").action(async () => {
console.log();
console.log(LOGO);
const ora = await import("ora");
const spinner = ora.default("Aggregating ReChunk files...").start();
try {
const files = await aggregateUseRechunkFiles(process.cwd());
if (files.length === 0) {
spinner.fail('No files containing "use rechunk" directive found.');
return;
}
spinner.succeed(`Found ${files.length} files.`);
const { selectedFiles } = await import_inquirer.default.prompt([
{
type: "checkbox",
theme: {
style: {
renderSelectedChoices: (selectedChoices) => selectedChoices.map(
(choice, index) => index !== 0 ? ` ${choice.short}` : ` ${choice.short}`
).join("\n")
}
},
name: "selectedFiles",
message: `${import_chalk4.default.bold("Select the components to publish:\n")}`,
choices: files.map((file) => ({
name: ` ${import_chalk4.default.cyan(file)}`,
value: file
})),
validate: (choices) => choices.length > 0 ? true : "You must select at least one component."
}
]);
if (selectedFiles.length === 0) {
console.log(
import_chalk4.default.yellow("No components selected for publishing. Exiting.")
);
return;
}
const confirm = await import_inquirer.default.prompt([
{
type: "confirm",
name: "proceed",
message: `You selected ${selectedFiles.length} components. Do you want to proceed with publishing?`,
default: true
}
]);
if (!confirm.proceed) {
console.log(import_chalk4.default.yellow("Publishing cancelled."));
return;
}
const rc = getRechunkConfig();
for (const file of selectedFiles) {
const componentName = import_path5.default.basename(file, import_path5.default.extname(file));
const base64String = Buffer.from(file, "utf8").toString("base64");
spinner.start(
`Bundling and publishing ${componentName} as ${base64String}...`
);
try {
const input = import_path5.default.resolve(process.cwd(), file);
const rollupBuild = await (0, import_rollup2.rollup)(await (0, import_rollup_preset2.default)({ input }));
const {
output: [{ code }]
} = await rollupBuild.generate({ interop: "auto", format: "cjs" });
await publishChunk(
rc.host,
base64String,
code,
rc.project,
rc.writeKey
);
spinner.succeed(
`Successfully published ${componentName} as ${base64String}`
);
} catch (err) {
spinner.fail(
`Failed to publish ${componentName}: ${err.message}`
);
continue;
}
}
console.log(
import_chalk4.default.green("\n\u{1F389} All selected components have been processed!")
);
} catch (error) {
spinner.fail(`Error: ${error.message}`);
}
});
async function publishChunk(host, chunk, code, project, writeKey) {
const api = configureReChunkChunksApi(host, project, writeKey);
try {
await api.createChunkForProject({
projectId: project,
chunkId: chunk,
chunkCreate: {
data: code
}
});
} catch (error) {
throw new Error(`Failed to publish chunk: ${error.message}`);
}
}
// src/index.ts
var import_chalk5 = __toESM(require("chalk"));
var import_commander5 = require("commander");
var import_process = __toESM(require("process"));
import_commander5.program.name("rechunk").description("command-line interface for rechunk").version(package_default.version, "-v, --version", "output the current version");
import_commander5.program.parseAsync().catch(async (error) => {
console.log();
console.log(
import_chalk5.default.red`Unexpected error. Please report it as a bug: https://github.com/crherman7/rechunk/issues`
);
console.log();
console.log(import_chalk5.default.red(error.message));
import_process.default.exit(1);
});