@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
text/typescript
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'));
}
};