@platform/react.ssr
Version:
A lightweight SSR (server-side-rendering) system for react apps bundled with ParcelJS and hosted on S3.
156 lines (155 loc) • 5.62 kB
JavaScript
import { defaultValue, fs, http, time, util } from '../common';
import { Site } from './Site';
let URL_CACHE = {};
export class Manifest {
constructor(args) {
this.def = args.def;
this.baseUrl = util.stripSlashes(args.baseUrl);
this.status = args.status || 200;
}
static reset() {
URL_CACHE = {};
}
static async fromFile(args) {
const { baseUrl, loadBundleManifest } = args;
const path = fs.resolve(args.path);
if (!(await fs.pathExists(path))) {
throw new Error(`Manifest file does not exist: '${args.path}'`);
}
const yaml = await fs.readFile(path, 'utf-8');
const def = await Manifest.parse({ yaml, baseUrl, loadBundleManifest });
return Manifest.create({ def, baseUrl });
}
static async fromUrl(args) {
const { manifestUrl, loadBundleManifest } = args;
const errorResponse = (status, error) => {
return { ok: false, status, error: new Error(error) };
};
try {
const res = await http.get(args.manifestUrl);
if (!res.ok) {
const error = res.status === 403
? `The manifest YAML has not been made "public" on the internet.`
: `Failed while pulling manifest YAML from cloud.`;
return errorResponse(403, error);
}
const baseUrl = args.baseUrl || manifestUrl;
const manifest = await Manifest.fromYaml({ yaml: res.body, baseUrl, loadBundleManifest });
return {
ok: true,
status: 200,
manifest,
};
}
catch (error) {
return errorResponse(500, error);
}
}
static async fromYaml(args) {
const { yaml, baseUrl, loadBundleManifest } = args;
const def = await Manifest.parse({ yaml, baseUrl, loadBundleManifest });
return Manifest.create({ def, baseUrl });
}
static async parse(args) {
const { loadBundleManifest } = args;
const baseUrl = args.baseUrl.replace(/\/manifest.yml$/, '');
const yaml = util.parseYaml(args.yaml);
if (!yaml.ok || !yaml.data) {
const error = `Failed to parse manifest YAML. ${yaml.error.message}`;
throw new Error(error);
}
let sites = [];
const input = yaml.data.sites;
sites = await Site.formatMany({ input, baseUrl, loadBundleManifest });
const manifest = { sites };
return manifest;
}
static async get(args) {
const { ttl } = args;
const key = `${args.manifestUrl}:${args.loadBundleManifest || 'false'}`;
let cached = URL_CACHE[key];
if (!args.force && cached && !isCacheExpired({ key, ttl })) {
return cached.manifest;
}
const res = await Manifest.fromUrl(args);
if (res.manifest) {
const manifest = res.manifest;
cached = { manifest, time: time.now.timestamp };
URL_CACHE[key] = cached;
}
return URL_CACHE[key].manifest;
}
get ok() {
return this.status.toString().startsWith('2');
}
get sites() {
if (!this._sites) {
const manifest = this.def;
this._sites = this.def.sites.map((def, index) => Site.create({ index, manifest }));
}
return this._sites;
}
get site() {
return {
byName: (name) => {
name = (name || '').trim();
return this.sites.find(site => site.name.trim() === name);
},
byHost: (domain) => {
domain = util.stripHttp(domain || '');
return this.sites.find(site => site.isMatch(domain || ''));
},
};
}
get change() {
return {
site: (id) => {
const name = typeof id === 'string' ? id : id.name;
return {
bundle: async (args) => {
const site = this.site.byName(name);
if (!site) {
return undefined;
}
const bundle = args.value;
const def = Object.assign({}, this.def);
def.sites[site.index].bundle = bundle;
const manifest = Manifest.create({ def, baseUrl: this.baseUrl });
if (args.saveTo) {
await manifest.save(args.saveTo);
}
return manifest;
},
};
},
};
}
toObject() {
return Object.assign({ status: this.status }, this.def);
}
async save(path, options = {}) {
const def = Object.assign({}, this.def);
if (defaultValue(options.minimal, true)) {
def.sites.forEach(site => {
delete site.files;
delete site.entries;
delete site.baseUrl;
delete site.size;
delete site.bytes;
});
}
path = fs.resolve(path);
await fs.ensureDir(fs.dirname(path));
await fs.file.stringifyAndSave(path, def);
}
}
Manifest.create = (args) => new Manifest(args);
function isCacheExpired(args) {
const { key, ttl } = args;
const cached = URL_CACHE[key];
if (!cached || typeof ttl !== 'number') {
return false;
}
const age = time.now.timestamp - cached.time;
return age > ttl;
}