UNPKG

@knapsack/app

Version:

Build Design Systems with Knapsack

559 lines (557 loc) • 25.4 kB
"use strict"; var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var _Patterns_instances, _Patterns_db, _Patterns_registerTemplateInCodeSrcs; Object.defineProperty(exports, "__esModule", { value: true }); exports.Patterns = void 0; /** * Copyright (C) 2018 Basalt This file is part of Knapsack. Knapsack is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Knapsack is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Knapsack; if not, see <https://www.gnu.org/licenses>. */ const path_1 = require("path"); const globby_1 = __importDefault(require("globby")); const immer_1 = require("immer"); const chokidar_1 = __importDefault(require("chokidar")); const utils_1 = require("@knapsack/utils"); const types_1 = require("@knapsack/types"); const file_utils_1 = require("@knapsack/file-utils"); const server_utils_1 = require("../../server/server-utils"); const events_1 = require("../../server/events"); const file_db_1 = require("../../server/dbs/file-db"); const log_1 = require("../../cli/log"); const misc_1 = require("../../types/misc"); const wrap_html_render_result_1 = require("./renderers/wrap-html-render-result"); class Patterns { constructor({ dataDir, templateRenderers, assetSets, db, config, }) { _Patterns_instances.add(this); _Patterns_db.set(this, void 0); this.init = async ({ missingFileVerbosity, }) => { this.missingFileVerbosity = missingFileVerbosity; const { demos } = await __classPrivateFieldGet(this, _Patterns_db, "f").getData(); this.demosById = demos.byId; try { await this.updatePatternsData(); } catch (error) { log_1.log.error(error); process.exit(1); } }; this.hydrate = async ({ appClientData: { patternsState, db } }) => { if (!Object.keys(patternsState).includes('patterns')) { throw new Error(`patternsState.patterns is missing`); } this.byId = patternsState.patterns; this.demosById = db.demos.byId; }; this.watch = () => { log_1.log.verbose('Watch started for patterns'); this.watcher = chokidar_1.default.watch([], { ignoreInitial: true, }); this.watcher.on('change', async (path) => { if ((0, server_utils_1.getIsSavingLocally)()) { // if we're saving locally, the watch event is from us return; } const patternConfigFilePath = this.filePathsThatTriggerNewData.get(path); log_1.log.verbose(`changed file - path: ${path} patternConfigFilePath: ${patternConfigFilePath}`, 'pattern data'); await this.updatePatternData({ patternConfigPath: patternConfigFilePath, }); events_1.knapsackEvents.emitPatternsDataReady({ patterns: this.allPatterns, }); }); events_1.knapsackEvents.onShutdown(this.watcher.close); }; this.build = async () => { await Promise.all(Object.values(this.templateRenderers).map(async (renderer) => { await Promise.all([...renderer.getCodeSrcs()].map(async (path) => { const { exists } = await renderer.resolvePath(path); if (exists) return; const msg = `This file should exist but it doesn't: ${path} `; if (this.missingFileVerbosity === 'error') { throw new Error(msg); } else if (this.missingFileVerbosity === 'warn') { log_1.log.warn(msg); } })); })); }; this.getData = async () => { /** * originally this was checking if this.byId wasn't set which caused local changes to not show up * (https://linear.app/knapsack/issue/KSP-2231/local-changes-arent-triggering-rebuild) * * then we tried omitting this check which caused recompiling loops in some instances * (https://linear.app/knapsack/issue/KSP-2704/accutech-workspace-infinite-re-render-ks-version-3582-and-on) * * hence how we got to needing to set / check something else, this.hasUpdatePatternsDataRan * Run _at least_ once (to avoid caching issues) but also not too frequently (to avoid recompiling loops) */ if (this.hasUpdatePatternsDataRan === false) { this.hasUpdatePatternsDataRan = true; await this.updatePatternsData(); } const patternsConfig = await this.getPatternsConfig(); return { patterns: this.byId, renderers: (0, utils_1.entries)(this.templateRenderers).reduce((acc, [id, renderer]) => { const meta = renderer.getMeta(); acc[id] = { meta, }; return acc; }, {}), ...patternsConfig, }; }; this.clearCache = async () => { await this.updatePatternsData(); }; this.savePrep = async (data) => { const { patterns, renderers, ...rest } = data; const patternIdsToDelete = new Set(Object.keys(this.byId)); const changedPatternIds = []; const allFiles = []; await Promise.all(Object.keys(patterns).map(async (id) => { const pattern = patterns[id]; // if nothing has changed, let's not add it to the files to write list // const isDifferent = // JSON.stringify(pattern) !== JSON.stringify(prevPattern); // @todo restore this const isDifferent = true; patternIdsToDelete.delete(id); // delete from delete list - i.e. we keep it if (isDifferent) { // update the internal data store if the pattern has changed changedPatternIds.push(pattern.id); const patternData = (0, immer_1.produce)(pattern, (draftPattern) => { draftPattern.templates.forEach((template) => { var _a, _b; if ((_a = template === null || template === void 0 ? void 0 : template.spec) === null || _a === void 0 ? void 0 : _a.isInferred) { // if it's inferred, we don't want to save `spec.props` or `spec.slots` template.spec = { isInferred: (_b = template === null || template === void 0 ? void 0 : template.spec) === null || _b === void 0 ? void 0 : _b.isInferred, }; } }); }); // pattern.templates.forEach((template) => { // const prevTemplate = prevPattern?.templates?.find( // (t) => t.id === template.id, // ); // Object.values(template?.demosById ?? {}).forEach((demo) => { // const prevDemo = prevTemplate?.demosById?.[demo.id]; // const isDiff = !deepEqual(prevDemo, demo); // if (isDiff) { // // @todo consider emitting an event from this // log.verbose( // `savePrep(${id}) template ${template.id} demo ${demo.id} is different, clearing render cache...`, // ); // } // }); // }); const db = new file_db_1.FileDb({ filePath: (0, path_1.join)(this.dataDir, `knapsack.pattern.${id}.json`), type: 'json', writeFileIfAbsent: false, }); const files = await db.savePrep(patternData); files.forEach((file) => allFiles.push(file)); } })); patternIdsToDelete.forEach((id) => { allFiles.push({ isDeleted: true, contents: '', encoding: 'utf8', path: (0, path_1.join)(this.dataDir, `knapsack.pattern.${id}.json`), }); }); const files = await this.configDb.savePrep(rest); files.forEach((file) => allFiles.push(file)); log_1.log.verbose(`savePrep(${changedPatternIds.length}) ${changedPatternIds.join(', ')}`, null, 'pattern data'); return allFiles; }; __classPrivateFieldSet(this, _Patterns_db, db, "f"); this.userConfig = config; this.demosById = {}; this.hasUpdatePatternsDataRan = false; this.configDb = new file_db_1.FileDb({ filePath: (0, path_1.join)(dataDir, 'knapsack.patterns.json'), defaults: { statusSets: [ { id: 'main', title: 'Status', statuses: [ { id: 'draft', title: 'Draft', color: '#9b9b9b', }, { id: 'needsDesign', title: 'Needs Design', color: '#FC0', }, { id: 'needsDev', title: 'Needs Development', color: '#FC0', }, { id: 'needsReview', title: 'Needs Review', color: '#FC0', }, { id: 'ready', title: 'Ready', color: '#2ECC40', }, ], }, ], }, }); this.assetSets = assetSets; this.dataDir = dataDir; this.templateRenderers = {}; this.byId = {}; this.missingFileVerbosity = 'error'; // without this extra check + default value assignment in the constructor, this breaks when running ks:serve if ((0, misc_1.isValidVerbosityOption)(process.env.MISSING_FILE_VERBOSITY)) { this.missingFileVerbosity = process.env.MISSING_FILE_VERBOSITY; } this.filePathsThatTriggerNewData = new Map(); this.patternDataFiles = new Map(); templateRenderers.forEach((templateRenderer) => { this.templateRenderers[templateRenderer.id] = templateRenderer; }); } getRenderer(rendererId) { const renderer = this.templateRenderers[rendererId]; if (!renderer) { throw new Error(`Renderer "${rendererId}" does not exist, available renderers: ${Object.keys(this.templateRenderers).join(', ')}`); } return renderer; } get allPatterns() { return Object.values(this.byId); } async updatePatternData({ patternConfigPath, patternData, }) { const finish = (0, utils_1.timerInSeconds)(); let pattern; if (patternConfigPath) { pattern = await (0, file_utils_1.readJSON)(patternConfigPath); } else if (patternData) { pattern = patternData; } else { log_1.log.error(`Pattern data updates require a path pointing to the pattern's data file OR the data itself.`); return; } await Promise.all(pattern.templates.map(async (template) => { await __classPrivateFieldGet(this, _Patterns_instances, "m", _Patterns_registerTemplateInCodeSrcs).call(this, template); })); this.byId[pattern.id] = pattern; const duration = finish(); const isSlow = duration > 5; if (isSlow) { log_1.log.warn(`Slow: ${duration}s updatePatternData(${pattern.id})`, null, 'pattern data'); } } async updatePatternsData() { var _a, _b, _c; const s = (0, utils_1.timerInSeconds)(); (_b = (_a = this.watcher) === null || _a === void 0 ? void 0 : _a.unwatch) === null || _b === void 0 ? void 0 : _b.call(_a, [...this.filePathsThatTriggerNewData.values()]); this.filePathsThatTriggerNewData.clear(); const priorPatternIds = new Set(Object.keys(this.byId)); const { demos } = await __classPrivateFieldGet(this, _Patterns_db, "f").getData(); this.demosById = demos.byId; const patternDataFiles = await (0, globby_1.default)(`${(0, path_1.join)(this.dataDir, 'knapsack.pattern.*.json')}`, { expandDirectories: false, onlyFiles: true, }); // Initially creating the patterns `this.byId` object in alphabetical order so that everywhere else patterns are listed they are alphabetical patternDataFiles .map((file) => { // turns this: `data/knapsack.pattern.card-grid.json` // into this: `[ 'data/', 'card-grid.json' ]` const [, lastPart] = file.split('knapsack.pattern.'); // now we have `card-grid` const id = lastPart.replace('.json', ''); this.patternDataFiles.set(id, file); return id; }) .sort() .forEach((id) => { priorPatternIds.delete(id); this.byId[id] = { id, title: id, templates: [], }; }); await Promise.all(patternDataFiles.map(async (file) => { this.filePathsThatTriggerNewData.set(file, file); return this.updatePatternData({ patternConfigPath: file, }); })); (_c = this.watcher) === null || _c === void 0 ? void 0 : _c.add([...this.filePathsThatTriggerNewData.values()]); priorPatternIds.forEach((priorId) => { delete this.byId[priorId]; }); log_1.log.verbose(`updatePatternsData took: ${s()}`, null, 'pattern data'); events_1.knapsackEvents.emitPatternsDataReady({ patterns: this.allPatterns, }); } getPatterns() { return this.allPatterns; } async getPatternsConfig() { const config = await this.configDb.getData(); return config; } async getContentStateFromLocalJsonFiles() { const { getContentStateFromAppClientData } = await import('@knapsack/rendering-utils'); return getContentStateFromAppClientData({ patterns: this.byId, demosById: this.demosById, }); } async inspect(templateInfo) { const renderer = this.templateRenderers[templateInfo.rendererId]; if (!renderer) { return { type: 'renderer.notFound', }; } if (!renderer.inspect) { return { type: 'renderer.noInspectSupported', }; } try { return await renderer.inspect(templateInfo); } catch (e) { return { type: 'error.unknown', message: e.message, }; } } /** * Render template */ async render({ patternId, templateId, demo, state, isInIframe = false, websocketsPort, assetSetId, }) { var _a, _b, _c, _d; try { const pattern = state.patterns[patternId]; if (!pattern) { throw new Error(`Pattern not found: '${patternId}'`); } const template = (_a = pattern.templates) === null || _a === void 0 ? void 0 : _a.find((t) => t.id === templateId); if (!template) { throw new Error(`Could not find template "${templateId}" in pattern "${patternId}"`); } const renderer = this.getRenderer(template.templateLanguageId); if (!demo) { const msg = `No demo provided nor first demo to fallback on while trying to render pattern "${pattern.id}" template "${template.id}"`; throw new Error(msg); } const { bodyAttributes, inlineStyles } = demo; const assetSet = assetSetId && this.assetSets.getAssetSet(assetSetId); const assets = (_d = (_c = (_b = assetSet === null || assetSet === void 0 ? void 0 : assetSet.assets) === null || _b === void 0 ? void 0 : _b.filter) === null || _c === void 0 ? void 0 : _c.call(_b, (asset) => { if (Array.isArray(asset.includedRenderers)) { return asset.includedRenderers.includes(template.templateLanguageId); } if (Array.isArray(asset.excludedRenderers)) { return !asset.excludedRenderers.includes(template.templateLanguageId); } return true; })) !== null && _d !== void 0 ? _d : []; const { inlineJs = '', inlineCss = '', inlineFoot = '', inlineHead = '', } = assetSet !== null && assetSet !== void 0 ? assetSet : {}; const inlineFoots = [inlineFoot]; const inlineJSs = [inlineJs]; const inlineHeads = [ `<title>{K} Pattern: ${patternId} ~ Template: ${templateId}</title>`, inlineHead, ]; inlineHeads.push(` <script type="module" src="/renderer-client/renderer-client.mjs"></script> `); if (isInIframe) { inlineHeads.push(` <style> html, body { display: flex; align-items: safe center; justify-content: safe center; margin: 0; min-height: 100%; } .knapsack-pattern-direct-parent { max-width: 100vw; } </style> `); } const meta = { patternId, templateId, demoId: demo.id, assetSetId, isInIframe, websocketsPort, }; const metaScript = `<script id="${types_1.ksRendererClientMetaId}" type="application/json">${JSON.stringify(meta, null, ' ')}</script>`; const { enableDataDemos, enableTemplateDemos } = renderer.getMeta(); if (!enableDataDemos && (0, types_1.isDataDemo)(demo)) { throw new Error(`The template language renderer "${renderer.id}" does not support "Data Demos/Examples"`); } if (!enableTemplateDemos && (0, types_1.isTemplateDemo)(demo)) { throw new Error(`The template language renderer "${renderer.id}" does not support "Template Demos/Examples"`); } const renderedTemplate = await renderer .render({ state, demo, }) .catch((e) => { log_1.log.error('Error', e, 'pattern render'); const html = ` <h3>Error requesting render from ${renderer.id}</h3> <p>${e.message}</p>`; return { ok: false, html, wrappedHtml: html, usage: html, message: e.message, }; }); if (!renderedTemplate) { // filter out verbose demo data to clean up error message const { inlineStyles: _, bodyAttributes: __, ...restOfDemo } = demo; throw new Error(`Did not receive result from renderer ${renderer.id}, likely due to a missing demo - recieved '${JSON.stringify(restOfDemo)}'`); } const wrappedHtml = (0, wrap_html_render_result_1.wrapHtmlRenderResult)({ html: `${metaScript}${renderedTemplate.html}`, assets, inlineJs: inlineJSs.join('\n'), inlineCss: `${inlineCss}${inlineStyles}`, inlineHead: inlineHeads.join('\n'), inlineFoot: inlineFoots.join('\n'), isInIframe, bodyAttributes, }); const results = { ...renderedTemplate, usage: renderedTemplate.usage, html: renderedTemplate.html, wrappedHtml, }; return results; } catch (error) { const html = `<div style="max-width: 300px;"> <h4>Error in Pattern Render</h4> <pre><code>${error.toString()}</pre></code> </div>`; return { ok: false, html, message: error.message, wrappedHtml: html, }; } } async getTemplateSuggestions({ rendererId, state, newPath, }) { const renderer = this.getRenderer(rendererId); if (!renderer.getTemplateSuggestions) { return { suggestions: [], }; } const proto = renderer.getMeta().prototypingTemplate; const suggestions = await renderer.getTemplateSuggestions({ rendererId, state, newPath, }); return { ...suggestions, suggestions: suggestions.suggestions .filter(({ path, alias }) => { if (!proto) return true; // don't suggest the prototyping template if (proto.path) { return path !== proto.path; } if (proto.alias) { return alias !== proto.alias; } return true; }) .map((suggestion) => { return { ...suggestion, path: (0, path_1.isAbsolute)(suggestion.path) ? (0, path_1.relative)(this.dataDir, suggestion.path) : suggestion.path, }; }), }; } } exports.Patterns = Patterns; _Patterns_db = new WeakMap(), _Patterns_instances = new WeakSet(), _Patterns_registerTemplateInCodeSrcs = async function _Patterns_registerTemplateInCodeSrcs(template) { var _a, _b; const renderer = this.getRenderer(template.templateLanguageId); renderer.addCodeSrc({ path: template.path, }); (_b = (_a = template.demoIds) === null || _a === void 0 ? void 0 : _a.forEach) === null || _b === void 0 ? void 0 : _b.call(_a, (demoId) => { const demo = this.demosById[demoId]; if ((demo === null || demo === void 0 ? void 0 : demo.type) === 'template') { renderer.addCodeSrc({ path: demo.templateInfo.path }); } else if ((demo === null || demo === void 0 ? void 0 : demo.type) === 'data-w-template-info' && (0, types_1.isTemplateInfoWithCodeSrcPath)(demo.templateInfo)) { renderer.addCodeSrc({ path: demo.templateInfo.codeSrcPath }); } }); }; //# sourceMappingURL=patterns.js.map