UNPKG

@platform/react.ssr

Version:

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

179 lines (157 loc) 4.85 kB
import { defaultValue, t, fs, semver, util } from './common'; import { Manifest } from './manifest'; const { stripSlashes } = util; const DEFAULT = { FILE: 'ssr.yml', }; const ERROR = { noFile: (path: string) => `An "${DEFAULT.FILE}" configuration file does not exist at: ${path}`, }; /** * A parser for the `ssr.yml` configuration file. */ export class Config { /** * [Lifecycle] */ public static create = async (options: { path?: string } = {}) => { const path = fs.resolve(options.path || DEFAULT.FILE); if (!(await fs.pathExists(path))) { throw new Error(ERROR.noFile(path)); } else { const def = await fs.file.loadAndParse<t.ISsrConfig>(path); return new Config({ def }); } }; public static createSync = (options: { path?: string } = {}) => { const path = fs.resolve(options.path || DEFAULT.FILE); if (!fs.pathExistsSync(path)) { throw new Error(`An "ssr.yml" configuration file does not exist at: ${path}`); } else { const def = fs.file.loadAndParseSync<t.ISsrConfig>(path); return new Config({ def }); } }; private constructor(args: { def: t.ISsrConfig }) { this.def = args.def; } /** * [Fields] */ private readonly def: t.ISsrConfig; /** * [Properties] */ public get secret() { return toValue(this.def.secret); } public get builder() { const builder = this.def.builder || {}; builder.bundles = builder.bundles || 'bundles'; builder.entries = builder.entries || ''; return builder; } public get s3() { const s3 = this.def.s3 || {}; const path = s3.path || {}; const endpoint = util.stripHttp(s3.endpoint || ''); const cdn = util.stripHttp(s3.cdn || ''); const accessKey = toValue(s3.accessKey); const secret = toValue(s3.secret); const bucket = s3.bucket || ''; const api = { endpoint, cdn, accessKey, secret, bucket, path: { base: stripSlashes(path.base || ''), manifest: stripSlashes(path.manifest || ''), bundles: stripSlashes(path.bundles || ''), }, get config(): t.IS3Config { return { endpoint, accessKey, secret }; }, get fs() { return fs.s3(api.config); }, async versions(options: { sort?: 'ASC' | 'DESC' } = {}) { const s3 = api.fs; const prefix = `${api.path.base}/${api.path.bundles}`; const list = s3.list({ bucket, prefix, }); const dirs = (await list.dirs).items.map(({ key }) => ({ key, version: fs.basename(key) })); const versions = semver.sort(dirs.map((item) => item.version)); return options.sort === 'DESC' ? versions.reverse() : versions; }, }; return api; } public get baseUrl() { const s3 = this.s3; return `https://${s3.cdn || s3.endpoint}/${s3.path.base}`; } public get manifest() { // Manifest file. const filePath = fs.resolve(this.def.manifest || 'manifest.yml'); // Manifest URL. const s3 = this.s3; const manifestUrl = `https://${s3.endpoint}/${s3.bucket}/${s3.path.base}/${s3.path.manifest}`; const baseUrl = this.baseUrl; const config = this; // eslint-disable-line const api = { local: { path: filePath, get exists() { return fs.pathExists(filePath); }, async load(args: { loadBundleManifest?: boolean } = {}) { const { loadBundleManifest } = args; return Manifest.fromFile({ path: filePath, baseUrl, loadBundleManifest }); }, async ensureLatest(args: { minimal?: boolean } = {}) { // Ensure local exists. if (!(await api.local.exists)) { await config.createFromTemplate(); } // Overwrite with latest cloud content (if it exists). const remote = await api.s3.pull({ force: true }); if (remote.ok) { const minimal = defaultValue(args.minimal, true); await remote.save(filePath, { minimal }); } // Load the manifest. return api.local.load(); }, }, s3: { url: manifestUrl, async pull(args: { force?: boolean; loadBundleManifest?: boolean } = {}) { return Manifest.get({ ...args, manifestUrl, baseUrl }); }, }, }; return api; } /** * [Methods] */ public async createFromTemplate() { const source = fs.join(__dirname, '../tmpl/manifest.yml'); const target = this.manifest.local.path; await fs.ensureDir(fs.dirname(target)); await fs.copy(source, target); return { source, target }; } } /** * [Helpers] */ const toValue = (value?: string) => { value = value || ''; value = process.env[value] ? process.env[value] : value; return value || ''; };