UNPKG

@platform/react.ssr

Version:

A lightweight SSR (server-side-rendering) system for react apps bundled with ParcelJS and hosted on S3.

160 lines (138 loc) 4.59 kB
import { bundler } from '../bundler'; import { Config } from '../config'; import { exec, fs, log, npm, t } from './common'; import * as push from './cmd.push'; /** * Bundle script. */ export async function run(args: { cli: t.ICmdApp; config?: Config; version?: string; push?: boolean; manifest?: boolean; }) { // Setup initial conditions. const { cli } = args; const config = args.config || (await Config.create()); const { endpoint } = config.s3; let manifest: t.IBundleManifest | undefined; // Load the NPM package closest to the bundle. await fs.ensureDir(config.builder.bundles); const pkgPath = await fs.ancestor(config.builder.bundles).first('package.json'); const pkg = npm.pkg(pkgPath); log.info.gray(fs.dirname(pkgPath)); log.info(); // Prompt user for version. const version = args.version || (await npm.prompt.incrementVersion({ path: pkgPath, noChange: true, save: true })); const bundleDir = fs.resolve(fs.join(config.builder.bundles, version)); // Prompt the user whether to push to S3. const isPush = args.push !== undefined ? args.push : (await cli.prompt.list({ message: 'push to S3', items: ['yes', 'no'] })) === 'yes'; // Ensure end-point exists. if (isPush && !endpoint) { throw new Error(`The S3 endpoint has not been configured in [ssr.yml].`); } // Task list. log.info(); const tasks = cli .task('build', async (e) => execScript(pkg, e, 'build'), { skip: args.manifest }) .task('bundle', async (e) => execScript(pkg, e, 'bundle'), { skip: args.manifest }) .task('manifest', async (e) => { const { entries, error } = await getEntries(config); if (error) { throw error; } else { const res = await bundler.prepare({ bundleDir, entries, silent: true }); manifest = res.manifest; } }); // Run tasks. const res = await tasks.run({ concurrent: false, exitOnError: true }); if (!res.ok) { // Task(s) failed. log.info(); res.errors.forEach((item) => { log.error(`ERROR ${item.title}`); log.warn(item.error); log.info(); }); return cli.exit(1); } // Push to S3. if (isPush) { await push.bundle({ cli, config, version }); } else if (manifest) { bundler.log.bundle({ bundleDir, manifest }); } // Finish up. log.info(); } /** * [Helpers] */ async function execScript(pkg: npm.NpmPackage, e: t.TaskArgs, scriptName: string) { // Ensure the script exists. const exists = Boolean(pkg.scripts.bundle); if (!exists) { e.error(`Package.json does not have a "${scriptName}" script.`); return; } // Run the bundle script. const cwd = pkg.dir; const res = await exec.command(`yarn ${scriptName}`).run({ cwd, silent: true }); // Ensure the command completed successfully. if (!res.ok) { throw new Error(`Failed while executing '${scriptName}' script of package '${pkg.name}'`); } } const getRootDir = async (source: string) => { let path = ''; await fs.ancestor(source).walk(async (e) => { if ((await fs.readdir(e.dir)).includes('package.json')) { return e.stop(); } else { path = e.dir; } }); return path; }; // type EntriesResponse = { entries: bundler.IBundleEntryElement[]; error?: Error }; const getEntries = async (config: Config) => { const done = (entries: bundler.IBundleEntryElement[], errorMessage?: string) => { const error = errorMessage ? new Error(errorMessage) : undefined; return { ok: !Boolean(error), entries, error }; }; let source = config.builder.entries; if (!source) { return done([]); } // Copy the source libs locally. // NB: This is necessary to ensure the [require] import works correctly. source = fs.resolve(source); const sourceRoot = await getRootDir(source); const sourcePath = source.substring(sourceRoot.length); const localRoot = fs.resolve(`tmp/${fs.basename(sourceRoot)}`); const localPath = fs.join(localRoot, sourcePath); await fs.ensureDir(fs.dirname(localRoot)); await fs.remove(localRoot); await fs.copy(sourceRoot, localRoot); // Import the entry react element(s). const err = `Failed to load bundle entries at path: ${source}.`; try { const res = require(localPath); // eslint-disable-line if (Array.isArray(res.default)) { return done(res.default); } return done([], `${err} Ensure the array is exported as the module default.`); } catch (error) { return done([], `${err} ${error.message}`); } finally { // Clean up. await fs.remove(fs.resolve('tmp')); } };