@knapsack/app
Version:
Build Design Systems with Knapsack
401 lines • 15.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getApiRoutes = getApiRoutes;
const utils_1 = require("@knapsack/utils");
const file_utils_1 = require("@knapsack/file-utils");
const types_1 = require("@knapsack/types");
const json_stable_stringify_1 = __importDefault(require("json-stable-stringify"));
const path_1 = require("path");
const object_hash_1 = __importDefault(require("object-hash"));
const log_1 = require("../cli/log");
const utils_2 = require("../lib/utils");
const api_client_1 = require("../lib/api-client");
const cache_dir_1 = require("../lib/util/cache-dir");
/** May be empty string when deployed in situations w/o git, like a Docker container */
const gitRoot = (0, file_utils_1.findGitRoot)();
function getApiRoutes({ ksBrain, isLocalDev, collectionsParentKey, discovery: { metaState: { meta }, }, }) {
const { patterns: patternManifest, config } = ksBrain;
/** We will do Ava unit testing on some endpoints but only on `ks-sandbox` and in our monorepo */
const isOkToRunLocallyForTesting = config.cloud?.siteId === 'ks-sandbox' && utils_2.isInMonorepo;
const shouldCache = (0, utils_2.getCliCommand)() === 'serve';
const siteId = meta.knapsackCloudSiteId;
const appClientVersion = meta.ksVersions.app;
const rootDir = gitRoot || ksBrain.config.data;
const createCachedFn = (fn) => {
return shouldCache
? (0, utils_1.memoize)(fn, {
max: 100,
promise: true,
normalizer: (args) => (0, json_stable_stringify_1.default)(args) || JSON.stringify(args),
})
: fn;
};
async function renderUncached({ websocketsPort, stateId, dataId, patternId, templateId, assetSetId, isInIframe, }) {
const errorPrefix = `Rendering error: `;
if (!stateId) {
throw new Error(`${errorPrefix}Missing required "stateId" param`);
}
if (!dataId) {
throw new Error(`${errorPrefix}Missing required "dataId" param`);
}
const [state, demo] = await Promise.all([
(0, api_client_1.getContentStateFromId)({ stateId, siteId, appClientVersion }),
(0, api_client_1.getDemoFromId)({ dataId, siteId, appClientVersion }),
]);
if (!state) {
throw new Error(`${errorPrefix}Cannot find state using stateId param: "${stateId}"`);
}
if (!demo) {
throw new Error(`${errorPrefix}Cannot find demo using dataId param: "${dataId}"`);
}
const results = await patternManifest.render({
patternId,
templateId,
demo,
isInIframe,
websocketsPort,
assetSetId,
state,
});
if (results.ok) {
return results;
}
const pattern = state?.patterns[patternId];
const template = pattern?.templates.find((t) => t.id === templateId);
const templateInfo = [template?.alias, template?.path]
.filter(Boolean)
.join(' ');
log_1.log.error(results.message, {
templateInfo,
patternId,
templateId,
hasPattern: !!pattern,
hasTemplate: !!template,
}, 'render');
return results;
}
const render = createCachedFn(renderUncached);
const inspect = createCachedFn(patternManifest.inspect.bind(patternManifest));
const { buildTime, buildId } = cache_dir_1.buildMetaFileHelper.exists()
? cache_dir_1.buildMetaFileHelper.readSync()
: { buildTime: '', buildId: '' };
const baseEndpoint = {
path: '/api/v1',
method: 'GET',
handle: async (req) => {
const { host } = req.headers;
const { knapsackCloudSiteId, ksVersions } = meta;
const data = {
host,
siteId: knapsackCloudSiteId,
ksVersions,
buildTime,
buildId,
};
return {
ok: true,
message: 'Welcome to the API!',
data,
};
},
};
const pluginsEndpoint = {
method: 'POST',
path: `/api/v1/plugins`,
validateBody: (body) => {
if (!body?.pluginId) {
throw new Error(`Missing required "pluginId" param`);
}
},
handle: async ({ body: { pluginId, type } }) => {
if (type !== 'getContent') {
throw new Error(`Unsupported plugin action: ${type}`);
}
const plugin = config.plugins?.find((p) => p.id === pluginId);
if (!plugin) {
return {
type: 'getContent',
ok: false,
message: `Plugin not found for id: "${pluginId}"`,
payload: {},
};
}
const content = await plugin.loadContent();
return {
type: 'getContent',
ok: true,
payload: content,
};
},
};
const renderEndpointPost = {
method: 'POST',
path: '/api/v1/render',
handle: async (req) => {
const { patternId, templateId, isInIframe, assetSetId, dataId, stateId } = req.body;
return render({
patternId,
templateId,
isInIframe,
assetSetId,
dataId,
stateId,
websocketsPort: meta.websocketsPort,
});
},
};
const renderEndpointGet = {
method: 'GET',
path: '/api/v1/render',
handle: async (req) => {
const { query } = req;
const wrapHtml = typeof query.wrapHtml === 'boolean'
? query.wrapHtml
: query.wrapHtml === 'true';
const isInIframe = typeof query.isInIframe === 'boolean'
? query.isInIframe
: query.isInIframe === 'true';
const { patternId, templateId, assetSetId, dataId, stateId } = query;
const results = await render({
patternId,
templateId,
assetSetId,
isInIframe,
dataId,
stateId,
websocketsPort: meta.websocketsPort,
});
return wrapHtml ? results.wrappedHtml : results.html;
},
};
const renderDemoIdEndpoint = {
method: 'GET',
path: '/api/v1/render-demo-id',
handle: async ({ query }) => {
if (!isLocalDev) {
throw new Error(`This endpoint only available while running "knapsack start"`);
}
const { patternId, templateId, assetSetId, demoId } = query;
const { inlineDemoData, getContentStateFromAppClientData } = await import('@knapsack/rendering-utils');
let demo = patternManifest.demosById[demoId];
if (!demo) {
throw new Error(`Cannot find demo "${demoId}"`);
}
if ((0, types_1.isDemoWithData)(demo)) {
demo = inlineDemoData({
demosById: patternManifest.demosById,
demo,
collectionsParentKey,
});
}
const state = getContentStateFromAppClientData({
patterns: patternManifest.byId,
demosById: patternManifest.demosById,
});
const results = await patternManifest.render({
patternId,
templateId,
demo,
isInIframe: false,
assetSetId,
state,
});
return results.wrappedHtml;
},
};
const filesEndpoint = {
path: '/api/v1/files',
method: 'POST',
validateBody: (body) => {
if (!body.payload) {
throw new Error(`Missing required "payload" on request`);
}
},
handle: async ({ body }) => {
const { data: dataDir } = config;
if (!isLocalDev && !isOkToRunLocallyForTesting) {
throw new Error(`This endpoint only available to local developers`);
}
const { path, rendererId } = body.payload;
const { exists, absolutePath, type } = await (0, file_utils_1.resolvePath)({
path,
resolveFromDir: dataDir,
pkgPathAliases: rendererId
? patternManifest.templateRenderers[rendererId]?.pkgPathAliases
: undefined,
});
return {
type: 'verify',
payload: {
exists,
relativePath: exists
? (0, file_utils_1.relative)(dataDir, absolutePath)
: '',
absolutePath,
type,
},
};
},
};
const inspectEndpoint = {
path: '/api/v2/inspect',
method: 'GET',
authLevel: 'VIEWER',
handle: async (req) => {
if (!shouldCache) {
return inspect(req.query);
}
// Try to read from cache first
const helper = (0, cache_dir_1.getTemplateInspectFileHelper)(req.query);
const { data: cachedSpecData } = await (0, utils_1.tryCatch)(helper.read());
if (cachedSpecData) {
return cachedSpecData;
}
// if there is no cached spec data, we proceed to inspect the template
log_1.log.info(`Spec cache miss; running...`, req.query);
const { data: inspectData, error: inspectError } = await (0, utils_1.tryCatch)(inspect(req.query));
// if there is an error inspecting the template, we cache the error result
// to prevent future requests from trying to inspect again
const finalResult = inspectError
? {
type: 'error.unknown',
message: inspectError.message,
}
: inspectData;
// capture any errors from writing to the cache
const { error: writerError } = await (0, utils_1.tryCatch)(helper.write(finalResult));
if (writerError) {
const error = new Error(`Error writing inspect data to cache for "${(0, types_1.createTemplateInfoId)(req.query)}"`, { cause: writerError });
log_1.log.warn(error.message, req.query);
(0, log_1.reportToDatadog)({
status: 'error',
message: error.message,
error,
metadata: req.query,
http: {
query: req.query,
url: '/api/v2/inspect',
},
});
}
return finalResult;
},
};
const rendererDiscoveryEndpoint = {
path: '/api/v2/renderer-discovery',
method: 'GET',
authLevel: 'VIEWER',
handle: async (req) => {
const { rendererId } = req.query;
if (!rendererId) {
throw new Error(`Missing required "rendererId" param`);
}
if (shouldCache) {
try {
return await (0, cache_dir_1.getRendererDiscoveryFileHelper)(rendererId).read();
}
catch (e) {
throw new Error(`Renderer discovery file not found for ${rendererId}`, { cause: e });
}
}
const renderer = patternManifest.getRenderer(rendererId);
return renderer.getDiscovery();
},
};
const pkgInfoEndpoint = {
path: '/api/v2/pkg-info',
method: 'GET',
authLevel: 'VIEWER',
handle: async (req) => {
const { pkgName } = req.query;
const { pkgDir, getPkgJson } = await (0, file_utils_1.resolvePkg)(pkgName);
const pkgJson = await getPkgJson();
return {
pkgName,
pkgJson,
isFromNodeModules: pkgDir.split(path_1.sep).includes('node_modules'),
relativePath: (0, file_utils_1.relative)(rootDir, pkgDir),
};
},
};
const getTemplateSuggestionsEndpoint = {
path: '/api/v1/template-suggestions',
method: 'GET',
handle: async (req) => {
if (!isLocalDev) {
throw new Error(`This endpoint only available to local developers`);
}
try {
const { rendererId, stateId } = req.query;
if (!rendererId) {
return {
type: 'error.invalidParams',
message: `Missing query param "rendererId"`,
};
}
if (!stateId) {
return {
type: 'error.invalidParams',
message: `Missing query param "stateId"`,
};
}
const state = await (0, api_client_1.getContentStateFromId)({
stateId,
siteId,
appClientVersion,
});
const suggestions = await patternManifest.getTemplateSuggestions({
rendererId,
state,
});
return {
type: 'success',
data: suggestions,
};
}
catch (error) {
log_1.log.verbose(`getTemplateSuggestions endpoint error: ${error.message}`, error);
if (error.message.startsWith('Cannot find path to import')) {
return {
type: 'error.pathNotFound',
message: error.message,
};
}
return {
type: 'error.couldNotParse',
message: error.message,
};
}
},
};
const saveDataEndpoint = {
path: '/api/v1/save-data',
method: 'POST',
handle: async (req) => {
const data = req.body;
const dataId = (0, object_hash_1.default)(data, { unorderedObjects: false });
api_client_1.localDevSaveDataCache.set(dataId, data);
return {
ok: true,
data: { dataId },
};
},
};
return [
baseEndpoint,
pluginsEndpoint,
renderEndpointGet,
renderEndpointPost,
renderDemoIdEndpoint,
filesEndpoint,
inspectEndpoint,
rendererDiscoveryEndpoint,
getTemplateSuggestionsEndpoint,
pkgInfoEndpoint,
saveDataEndpoint,
];
}
//# sourceMappingURL=rest-api.js.map