UNPKG

@knapsack/app

Version:

Build Design Systems with Knapsack

401 lines • 15.3 kB
"use strict"; 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