UNPKG

@knapsack/app

Version:

Build Design Systems on top of knapsack, by Basalt

648 lines (557 loc) • 21.6 kB
"use strict"; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard"); var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.Patterns = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _fsExtra = require("fs-extra"); var _path = require("path"); var _globby = _interopRequireDefault(require("globby")); var _immer = _interopRequireDefault(require("immer")); var _chokidar = _interopRequireDefault(require("chokidar")); var _schemaUtils = require("@knapsack/schema-utils"); var _md = _interopRequireDefault(require("md5")); var _serverUtils = require("./server-utils"); var _rendererBase = require("./renderer-base"); var _events = require("./events"); var _fileDb = require("./dbs/file-db"); var log = _interopRequireWildcard(require("../cli/log")); var _patterns = require("../schemas/patterns"); var _utils = require("../lib/utils"); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } // type Old = { // patterns: { // [id: string]: KnapsackPattern; // }; // templateStatuses: KnapsackTemplateStatus[]; // }; class Patterns { constructor({ dataDir, templateRenderers, assetSets }) { (0, _defineProperty2.default)(this, "configDb", void 0); (0, _defineProperty2.default)(this, "dataDir", void 0); (0, _defineProperty2.default)(this, "templateRenderers", void 0); (0, _defineProperty2.default)(this, "byId", void 0); (0, _defineProperty2.default)(this, "assetSets", void 0); (0, _defineProperty2.default)(this, "isReady", void 0); (0, _defineProperty2.default)(this, "filePathsThatTriggerNewData", void 0); (0, _defineProperty2.default)(this, "watcher", void 0); (0, _defineProperty2.default)(this, "cacheDir", void 0); this.configDb = new _fileDb.FileDb2({ filePath: (0, _path.join)(dataDir, 'knapsack.patterns.json'), defaults: { templateStatuses: [{ id: 'draft', title: 'Draft', color: '#9b9b9b' }, { id: 'inProgress', title: 'In Progress', color: '#FC0' }, { id: 'ready', title: 'Ready', color: '#2ECC40' }] } }); this.assetSets = assetSets; this.dataDir = dataDir; this.templateRenderers = {}; this.byId = {}; this.isReady = false; this.filePathsThatTriggerNewData = new Map(); templateRenderers.forEach(templateRenderer => { this.templateRenderers[templateRenderer.id] = templateRenderer; }); this.watcher = _chokidar.default.watch([], { ignoreInitial: true }); this.watcher.on('change', async path => { const patternConfigFilePath = this.filePathsThatTriggerNewData.get(path); log.verbose(`changed file - path: ${path} patternConfigFilePath: ${patternConfigFilePath}`, null, 'pattern data'); await this.updatePatternData(patternConfigFilePath); (0, _events.emitPatternsDataReady)(this.allPatterns); }); _events.knapsackEvents.on(_events.EVENTS.SHUTDOWN, () => this.watcher.close()); } async init({ cacheDir }) { this.cacheDir = cacheDir; try { await this.updatePatternsData(); } catch (error) { console.log(); console.log(error); log.error('Pattern Init failed', error.message); console.log(); log.verbose('', error); process.exit(1); } } get allPatterns() { return Object.values(this.byId); } getRendererMeta() { const results = {}; Object.entries(this.templateRenderers).forEach(([id, renderer]) => { const meta = renderer.getMeta(); results[id] = { meta }; }); return results; } async getData() { if (!this.byId) { await this.updatePatternsData(); } const templateStatuses = await this.getTemplateStatuses(); return { templateStatuses, patterns: this.byId, renderers: this.getRendererMeta() }; } async savePrep(data) { const patternIdsToDelete = new Set(Object.keys(this.byId)); this.byId = {}; const allFiles = []; await Promise.all(Object.keys(data.patterns).map(async id => { const pattern = data.patterns[id]; pattern.templates.forEach(template => { var _template$spec; if (template === null || template === void 0 ? void 0 : (_template$spec = template.spec) === null || _template$spec === void 0 ? void 0 : _template$spec.isInferred) { var _template$spec2; // if it's inferred, we don't want to save `spec.props` or `spec.slots` template.spec = { isInferred: template === null || template === void 0 ? void 0 : (_template$spec2 = template.spec) === null || _template$spec2 === void 0 ? void 0 : _template$spec2.isInferred }; } }); this.byId[id] = pattern; patternIdsToDelete.delete(id); const db = new _fileDb.FileDb2({ filePath: (0, _path.join)(this.dataDir, `knapsack.pattern.${id}.json`), type: 'json', watch: false, writeFileIfAbsent: false }); const files = await db.savePrep(pattern); files.forEach(file => allFiles.push(file)); })); patternIdsToDelete.forEach(id => { allFiles.push({ isDeleted: true, contents: '', encoding: 'utf8', path: (0, _path.join)(this.dataDir, `knapsack.pattern.${id}.json`) }); }); return allFiles; } async updatePatternData(patternConfigPath) { const finish = (0, _utils.timer)(); const pattern = await (0, _fsExtra.readJSON)(patternConfigPath); let { templates = [] } = pattern; // @todo validate: has template render that exists, using assetSets that exist templates = await Promise.all(templates.map(async template => { var _spec, _spec3; let { spec = {} } = template; // if we come across `{ typeof: 'function' }` in JSON Schema, the demo won't validate since we store as a string - i.e. `"() => alert('hi')"`, so we'll turn it into a string: const propsValidationSchema = (0, _immer.default)((_spec = spec) === null || _spec === void 0 ? void 0 : _spec.props, draft => { Object.values((draft === null || draft === void 0 ? void 0 : draft.properties) || {}).forEach(prop => { if ('typeof' in prop && prop.typeof === 'function') { delete prop.typeof; // eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore prop.type = 'string'; } }); }); if (template.demosById) { // validating data demos against spec Object.values(template.demosById).forEach((demo, i) => { var _spec2; if ((0, _patterns.isDataDemo)(demo) && ((_spec2 = spec) === null || _spec2 === void 0 ? void 0 : _spec2.props)) { const results = (0, _schemaUtils.validateDataAgainstSchema)(propsValidationSchema, demo.data.props); if (!results.ok) { log.inspect({ propsSpec: spec.props, demo, results }, 'invalid demo info'); log.warn(`invalid demo: patternId: "${pattern.id}", templateId: "${template.id}", demoId: "${demo.id}" ^^^`, 'pattern data'); } } if ((0, _patterns.isTemplateDemo)(demo)) { const { exists, absolutePath, relativePathFromCwd } = (0, _serverUtils.resolvePath)({ path: template.path, resolveFromDirs: [this.dataDir] }); if (!exists) { log.error('Template demo file does not exist!', { patternId: pattern.id, templateId: template.id, demoId: demo.id, path: template.path, resolvedAbsolutePath: absolutePath }); throw new Error(`Template demo file does not exist!`); } this.filePathsThatTriggerNewData.set(absolutePath, patternConfigPath); } }); } // inferring specs if ((_spec3 = spec) === null || _spec3 === void 0 ? void 0 : _spec3.isInferred) { const renderer = this.templateRenderers[template.templateLanguageId]; if (renderer === null || renderer === void 0 ? void 0 : renderer.inferSpec) { const pathToInferSpecFrom = typeof spec.isInferred === 'string' ? spec.isInferred : template.path; const { exists, absolutePath } = (0, _serverUtils.resolvePath)({ path: pathToInferSpecFrom, resolveFromDirs: [this.dataDir] }); if (!exists) { throw new Error(`File does not exist: "${pathToInferSpecFrom}"`); } this.filePathsThatTriggerNewData.set(absolutePath, patternConfigPath); try { const inferredSpec = await renderer.inferSpec({ templatePath: absolutePath, template }); if (inferredSpec === false) { log.warn(`Could not infer spec of pattern "${pattern.id}", template "${template.id}"`, { absolutePath }); } else { const { ok, message } = _rendererBase.KnapsackRendererBase.validateSpec(inferredSpec); if (!ok) { throw new Error(message); } log.silly(`Success inferring spec of pattern "${pattern.id}", template "${template.id}"`, inferredSpec); spec = _objectSpread({}, spec, {}, inferredSpec); } } catch (err) { console.log(err); console.log(); log.error(`Error inferring spec of pattern "${pattern.id}", template "${template.id}": ${err.message}`, { absolutePath }); process.exit(1); } } } const { ok, message } = _rendererBase.KnapsackRendererBase.validateSpec(spec); if (!ok) { const msg = [`Spec did not validate for pattern "${pattern.id}" template "${template.id}"`, message].join('\n'); log.error('Spec that failed', { spec }); throw new Error(msg); } return _objectSpread({}, template, { spec }); })); this.byId[pattern.id] = _objectSpread({}, pattern, { templates }); log.silly(`${finish()}s for ${pattern.id}`, null, 'pattern data'); } async updatePatternsData() { const s = (0, _utils.timer)(); this.watcher.unwatch([...this.filePathsThatTriggerNewData.values()]); this.filePathsThatTriggerNewData.clear(); const patternDataFiles = await (0, _globby.default)(`${(0, _path.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', ''); return id; }).sort().forEach(id => { this.byId[id] = { id, title: id, templates: [] }; }); await Promise.all(patternDataFiles.map(async file => { this.filePathsThatTriggerNewData.set(file, file); return this.updatePatternData(file); })); this.getAllTemplatePaths().forEach(path => { (0, _serverUtils.fileExistsOrExit)(path, `This file should exist but it doesn't: Resolved absolute path: ${path} `); }); this.watcher.add([...this.filePathsThatTriggerNewData.values()]); this.isReady = true; log.verbose(`updatePatternsData took: ${s()}`, null, 'pattern data'); (0, _events.emitPatternsDataReady)(this.allPatterns); } getPattern(id) { return this.byId[id]; } getPatterns() { return this.allPatterns; } /** * Get all the pattern's template file paths * @return - paths to all template files */ getAllTemplatePaths({ templateLanguageId = '', includeTemplateDemos = true } = {}) { const allTemplatePaths = []; this.allPatterns.forEach(pattern => { pattern.templates.filter(t => t.path) // some just use `alias` .forEach(template => { if (templateLanguageId === '' || template.templateLanguageId === templateLanguageId) { allTemplatePaths.push(this.getTemplateAbsolutePath({ patternId: pattern.id, templateId: template.id })); if (includeTemplateDemos) { Object.values((template === null || template === void 0 ? void 0 : template.demosById) || {}).filter(_patterns.isTemplateDemo).forEach(demo => { allTemplatePaths.push(this.getTemplateDemoAbsolutePath({ patternId: pattern.id, templateId: template.id, demoId: demo.id })); }); } } }); }); return allTemplatePaths; } getTemplateAbsolutePath({ patternId, templateId }) { const pattern = this.byId[patternId]; if (!pattern) throw new Error(`Could not find pattern "${patternId}"`); const template = pattern.templates.find(t => t.id === templateId); if (!template) { throw new Error(`Could not find template "${templateId}" in pattern "${patternId}"`); } const { exists, absolutePath } = (0, _serverUtils.resolvePath)({ path: template.path, resolveFromDirs: [this.dataDir] }); if (!exists) throw new Error(`File does not exist: "${template.path}"`); return absolutePath; } getTemplateDemoAbsolutePath({ patternId, templateId, demoId }) { var _demo$templateInfo; const pattern = this.byId[patternId]; if (!pattern) throw new Error(`Could not find pattern ${patternId}`); const template = pattern.templates.find(t => t.id === templateId); if (!template) throw new Error(`Could not find template "${templateId}" in pattern "${patternId}"`); const demo = template.demosById[demoId]; if (!demo) throw new Error(`Could not find demo "${demoId}" in template ${templateId} in pattern ${patternId}`); if (!(0, _patterns.isTemplateDemo)(demo)) { throw new Error(`Demo is not a "template" type of demo; cannot retrieve path for demo "${demoId}" in template "${templateId}" in pattern "${patternId}"`); } if (!((_demo$templateInfo = demo.templateInfo) === null || _demo$templateInfo === void 0 ? void 0 : _demo$templateInfo.path)) { throw new Error(`No "path" in demo "${demoId}" in template "${templateId}" in pattern "${patternId}"`); } const relPath = (0, _path.join)(this.dataDir, demo.templateInfo.path); const path = (0, _path.join)(process.cwd(), relPath); if (!(0, _serverUtils.fileExists)(path)) throw new Error(`File does not exist: "${path}"`); return path; } async getTemplateStatuses() { const config = await this.configDb.getData(); return config.templateStatuses; } /** * Render template */ async render({ patternId, templateId = '', demo, isInIframe = false, websocketsPort, assetSetId }) { try { var _assetSet; const pattern = this.getPattern(patternId); if (!pattern) { const message = `Pattern not found: '${patternId}'`; return { ok: false, html: `<p>${message}</p>`, wrappedHtml: `<p>${message}</p>`, message, dataId: '' }; } const template = pattern.templates.find(t => t.id === templateId); if (!template) { throw new Error(`Could not find template ${templateId} in pattern ${patternId}`); } const renderer = this.templateRenderers[template.templateLanguageId]; demo = typeof demo === 'string' ? template.demosById[demo] : demo; if (!demo) { var _template$demos; const [firstDemoId] = (_template$demos = template.demos) !== null && _template$demos !== void 0 ? _template$demos : []; if (!firstDemoId) { 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); } demo = template.demosById[template.demos[0]]; } const dataId = (0, _md.default)(JSON.stringify(demo)); const renderedTemplate = await renderer.render({ pattern, template, demo, patternManifest: this }).catch(e => { log.error('Error', e, 'pattern render'); const html = `<p>${e.message}</p>`; return { ok: false, html, wrappedHtml: html, usage: html, message: e.message }; }); if (!(renderedTemplate === null || renderedTemplate === void 0 ? void 0 : renderedTemplate.ok)) { return _objectSpread({}, renderedTemplate, { wrappedHtml: renderedTemplate.html, // many times error messages are in the html for users dataId }); } const globalAssetSets = this.assetSets.getGlobalAssetSets(); let assetSet = globalAssetSets ? globalAssetSets[0] : globalAssetSets[0]; if (assetSetId) { assetSet = this.assetSets.getAssetSet(assetSetId); } const { assets = [], inlineJs = '', inlineCss = '', inlineFoot = '', inlineHead = '' } = (_assetSet = assetSet) !== null && _assetSet !== void 0 ? _assetSet : {}; const inlineFoots = [inlineFoot]; const inlineJSs = [inlineJs]; const inlineHeads = [inlineHead]; inlineHeads.push(` <script type="module" src="/renderer-client/renderer-client.mjs"></script> <script nomodule> const systemJsLoaderTag = document.createElement('script'); systemJsLoaderTag.src = 'https://unpkg.com/systemjs@2.0.0/dist/s.min.js'; systemJsLoaderTag.addEventListener('load', function () { System.import('/renderer-client/renderer-client.js'); }); document.head.appendChild(systemJsLoaderTag); </script> `); if (isInIframe) { // Need just a little bit of space around the pattern inlineHeads.push(` <style> .knapsack-wrapper { padding: 5px; } </style> `); } const meta = { patternId, templateId, demoId: demo.id, assetSetId, isInIframe, websocketsPort }; inlineFoots.push(`<script id="ks-meta" type="application/json">${JSON.stringify(meta, null, ' ')}</script>`); const jsUrls = assets.filter(asset => asset.type === 'js').filter(asset => asset.tagLocation !== 'head').map(asset => this.assetSets.getAssetPublicPath(asset.src)); const headJsUrls = assets.filter(asset => asset.type === 'js').filter(asset => asset.tagLocation === 'head').map(asset => this.assetSets.getAssetPublicPath(asset.src)); const wrappedHtml = renderer.wrapHtml({ html: renderedTemplate.html, headJsUrls, cssUrls: assets.filter(asset => asset.type === 'css') // .map(asset => asset.publicPath), .map(asset => this.assetSets.getAssetPublicPath(asset.src)), jsUrls, inlineJs: inlineJSs.join('\n'), inlineCss, inlineHead: inlineHeads.join('\n'), inlineFoot: inlineFoots.join('\n'), isInIframe }); return _objectSpread({}, renderedTemplate, { usage: renderer.formatCode(renderedTemplate.usage), html: (0, _serverUtils.formatCode)({ code: renderedTemplate.html, language: 'html' }), wrappedHtml: (0, _serverUtils.formatCode)({ code: wrappedHtml, language: 'html' }), dataId }); } catch (error) { log.error(error.message, { patternId, templateId, demo, isInIframe, assetSetId, error }, 'pattern render'); const html = `<h1>Error in Pattern Render</h1> <pre><code>${error.toString()}</pre></code>`; return { ok: false, html, message: html, wrappedHtml: html, dataId: '' }; } } } exports.Patterns = Patterns;