piral-cli
Version:
The standard CLI for creating and building a Piral instance or a Pilet.
324 lines • 12.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const url_1 = require("url");
const path_1 = require("path");
const fs_1 = require("fs");
const promises_1 = require("fs/promises");
const log_1 = require("../common/log");
const http_1 = require("../common/http");
const spec_1 = require("../common/spec");
const config_1 = require("../common/config");
const external_1 = require("../external");
async function getMetaOverride(root, metaFile) {
if (metaFile) {
const metaPath = (0, path_1.join)(root, metaFile);
const exists = await (0, promises_1.stat)(metaPath).then(() => true, () => false);
if (exists) {
const metaContent = await (0, promises_1.readFile)(metaPath, 'utf8');
return external_1.jju.parse(metaContent);
}
}
return undefined;
}
async function fillPiletMeta(pilet, metaFile, subPath) {
const { root, bundler } = pilet;
const packagePath = (0, path_1.join)(root, 'package.json');
const jsonContent = await (0, promises_1.readFile)(packagePath, 'utf8');
const def = external_1.jju.parse(jsonContent);
const file = bundler.bundle.name.replace(/^[\/\\]/, '');
const target = (0, path_1.join)(bundler.bundle.dir, file);
const metaOverride = await getMetaOverride(root, metaFile);
pilet.getMeta = (parentPath) => {
const basePath = `${parentPath}${subPath}`;
const url = new url_1.URL(file, basePath);
return {
custom: def.custom,
config: def.piletConfig,
...metaOverride,
name: def.name,
version: def.version,
link: `${url.href}?updated=${Date.now()}`,
...(0, spec_1.getPiletSpecMeta)(target, basePath),
};
};
}
async function loadFeed(feed, headers) {
try {
const response = await external_1.axios.get(feed, { headers });
if (Array.isArray(response.data)) {
return response.data;
}
else if (Array.isArray(response.data?.items)) {
return response.data.items;
}
else {
return [response.data];
}
}
catch (e) {
(0, log_1.log)('generalWarning_0001', `Couldn't load feed at ${feed}.`);
}
}
class PiletInjector {
constructor(config, serverConfig, core) {
this.cbs = {};
this.config = config;
this.serverConfig = serverConfig;
if (this.config.active) {
const { api, app, publicUrl, assetUrl } = config;
this.indexPath = `${publicUrl}index.html`;
// If we end with "/app" or "\app" we might have a proper emulator
if (app && (0, path_1.basename)(app) === 'app') {
const path = (0, path_1.resolve)(app, '..', 'package.json');
if ((0, fs_1.existsSync)(path)) {
try {
const packageJson = require(path);
if (typeof packageJson.piralCLI.source === 'string') {
this.proxyInfo = {
source: packageJson.piralCLI.source,
files: packageJson.files,
date: new Date(packageJson.piralCLI.timestamp),
};
}
}
catch {
// silently ignore errors - we just continue as-is
}
}
}
core.on('user-connected', (e) => {
const baseUrl = assetUrl || e.req.headers.origin;
if (e.target === '*' && e.url === api.substring(1)) {
this.cbs[e.id] = {
baseUrl,
notify: (msg) => e.ws.send(msg),
};
}
});
core.on('user-disconnected', (e) => {
delete this.cbs[e.id];
});
this.setupBundler();
}
}
setupBundler() {
const { pilets } = this.config;
pilets.forEach((p, i) => {
const handler = async () => {
await fillPiletMeta(p, this.config.meta, `/${i}/`);
for (const id of Object.keys(this.cbs)) {
const { baseUrl, notify } = this.cbs[id];
const meta = this.getPiletMeta(baseUrl, p);
notify(meta);
}
};
p.bundler.on(handler);
p.dispose = () => {
p.bundler.off(handler);
};
});
}
teardownBundler() {
const { pilets } = this.config;
pilets.forEach((p) => {
p.dispose();
});
}
get active() {
return this.config.active;
}
set active(value) {
this.config.active = value;
}
get name() {
return 'pilet-injector';
}
getOptions() {
return {};
}
setOptions(options) {
if ('pilets' in options) {
this.teardownBundler();
this.config.pilets = options.pilets;
this.setupBundler();
}
}
getPiletApi(baseUrl) {
const { api } = this.config;
if (/^https?:/.test(api)) {
return api;
}
else if (baseUrl) {
return `${baseUrl}${api}`;
}
else {
const { ssl, port } = this.serverConfig;
const { host } = config_1.config;
return `${ssl ? 'https' : 'http'}://${host}:${port}${api}`;
}
}
getPiletMeta(baseUrl, pilet) {
const basePath = this.getPiletApi(baseUrl);
return JSON.stringify(pilet.getMeta(basePath));
}
async getIndexMeta(baseUrl, headers) {
const { pilets, feed } = this.config;
const basePath = this.getPiletApi(baseUrl);
const localPilets = pilets.map((pilet) => pilet.getMeta?.(basePath)).filter(Boolean);
const mergedPilets = this.mergePilets(localPilets, await this.loadRemoteFeed(headers, feed));
return JSON.stringify(mergedPilets);
}
async loadRemoteFeed(rawHeaders, feed) {
if (feed) {
const feeds = Array.isArray(feed) ? feed : [feed];
const {
// We skip the standard client headers to focus on the custom ones
host: _0, ['user-agent']: _1, accept: _2, ['accept-language']: _3, ['accept-encoding']: _4, referer: _5, traceparent: _6, dnt: _7, connection: _8, ['sec-fetch-fest']: _9, ['sec-fetch-mode']: _10, ['sec-fetch-site']: _11, ...headers } = rawHeaders;
return await Promise.all(feeds.map((url) => loadFeed(url, headers)));
}
}
mergePilets(localPilets, remoteFeeds) {
if (!remoteFeeds || !Array.isArray(remoteFeeds)) {
return localPilets;
}
const { mergeConfig = false } = this.config;
const names = localPilets.map((pilet) => pilet.name);
const merged = [...localPilets];
for (const remotePilets of remoteFeeds) {
if (!Array.isArray(remotePilets)) {
continue;
}
const newPilets = remotePilets.filter((pilet) => {
if (!pilet || typeof pilet !== 'object') {
return false;
}
const name = pilet.name;
const isNew = name !== undefined && !names.includes(name);
if (!isNew && mergeConfig) {
const existing = merged.find((m) => m.name === name);
if (existing.config === undefined) {
existing.config = pilet.config;
}
else if (pilet.config !== undefined) {
Object.assign(existing.config, pilet.config);
}
}
return isNew;
});
names.push(...newPilets.map((p) => p.name));
merged.push(...newPilets);
}
return merged;
}
sendContent(content, type, url) {
const { headers } = this.config;
return {
injector: { name: this.name },
headers: {
...headers,
'content-type': type,
'cache-control': 'no-cache, no-store, must-revalidate',
pragma: 'no-cache',
expires: '0',
},
status: { code: 200 },
url,
content,
};
}
async sendFile(target, url) {
const content = await (0, promises_1.readFile)(target);
const type = external_1.mime.getType(target) ?? 'application/octet-stream';
return this.sendContent(content, type, url);
}
async sendResponse(path, req, baseUrl) {
const { url, headers } = req;
const { pilets } = this.config;
const [index, ...rest] = path.split('/');
const pilet = pilets[+index];
const bundler = pilet?.bundler;
await bundler?.ready();
if (!path) {
const content = await this.getIndexMeta(baseUrl, headers);
return this.sendContent(content, 'application/json', url);
}
else if (bundler?.bundle) {
const target = (0, path_1.join)(bundler.bundle.dir, rest.join('/'));
const info = await (0, promises_1.stat)(target).catch(() => undefined);
if (info && info.isFile()) {
return await this.sendFile(target, url);
}
}
return undefined;
}
async sendIndexFile(target, url, baseUrl) {
const indexHtml = await (0, promises_1.readFile)(target, 'utf8');
// mechanism to inject server side debug piletApi config into piral emulator
const windowInjectionScript = `window['dbg:pilet-api'] = '${this.getPiletApi(baseUrl)}';`;
const findStr = `<script`;
const replaceStr = `<script>/* Pilet Debugging Emulator Config Injection */${windowInjectionScript}</script><script`;
const content = indexHtml.replace(`${findStr}`, `${replaceStr}`);
return this.sendContent(content, external_1.mime.getType(target), url);
}
download(path) {
const manifestUrl = this.proxyInfo.source;
const url = new url_1.URL(path, manifestUrl);
const opts = (0, http_1.getAxiosOptions)(manifestUrl);
return external_1.axios.get(url.href, { ...opts, responseType: 'arraybuffer' });
}
async shouldLoad(target, path) {
if (this.proxyInfo) {
if (!this.proxyInfo.files.includes(path)) {
return false;
}
const fileInfo = await (0, promises_1.stat)(target).catch(() => undefined);
if (!fileInfo || fileInfo.mtime < this.proxyInfo.date) {
try {
const response = await this.download(path);
await (0, promises_1.writeFile)(target, response.data);
}
catch (ex) {
(0, log_1.log)('generalDebug_0003', `HTTP request for emulator asset retrieval failed: ${ex}`);
(0, log_1.log)(fileInfo ? 'optionalEmulatorAssetUpdateSkipped_0122' : 'requiredEmulatorAssetDownloadSkipped_0123', path);
return !!fileInfo;
}
}
return true;
}
else {
const fileInfo = await (0, promises_1.stat)(target).catch(() => undefined);
return fileInfo && fileInfo.isFile();
}
}
async handle(req) {
const { app, api, publicUrl, assetUrl } = this.config;
const baseUrl = assetUrl || (req.headers.host ? `${req.encrypted ? 'https' : 'http'}://${req.headers.host}` : undefined);
if (!req.target) {
if (req.url.startsWith(publicUrl)) {
const path = req.url.substring(publicUrl.length).split('?').shift();
if (app) {
const target = (0, path_1.join)(app, path);
if (await this.shouldLoad(target, path)) {
if (req.url === this.indexPath) {
return await this.sendIndexFile(target, req.url, baseUrl);
}
return await this.sendFile(target, req.url);
}
}
if (req.url !== this.indexPath) {
return await this.handle({
...req,
url: this.indexPath,
});
}
}
return undefined;
}
else if (req.target === api) {
const path = req.url.substring(1).split('?').shift();
return await this.sendResponse(path, req, baseUrl);
}
}
}
exports.default = PiletInjector;
//# sourceMappingURL=pilet-injector.js.map