UNPKG

@rechunk/cli

Version:

Command-line interface for managing ReChunk projects, chunks, and deployments

512 lines (498 loc) 18.4 kB
#!/bin/env node 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); });