@knapsack/app
Version:
Build Design Systems on top of knapsack, by Basalt
648 lines (557 loc) • 21.6 kB
JavaScript
;
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;