@knapsack/app
Version:
Build Design Systems with Knapsack
404 lines • 15.9 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RendererBase = void 0;
// eslint-disable-next-line max-classes-per-file
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 json_schema_to_typescript_1 = require("json-schema-to-typescript");
const fs_extra_1 = __importDefault(require("fs-extra"));
const events_1 = require("../../../server/events");
const log_1 = require("../../../cli/log");
const cache_dir_1 = require("../../../lib/util/cache-dir");
const gitRoot = (0, file_utils_1.findGitRoot)();
/**
* Used in property accessors to indicate that the property is not yet set
*/
class TooEarlyError extends Error {
constructor(propName) {
super(`You cannot access ${propName} this early (likely trying in constructor) because it is not yet set from '.setConfig()'. Try in 'init', 'hyrdrate', 'build', or 'watch' methods`);
}
}
class RendererBase {
id;
language;
logPrefix;
log;
/**
* Aliases for package paths
* Example:
* ```js
* {
* 'my-pkg': '../path/to/my-pkg',
* }
* ```
*/
pkgPathAliases;
creators;
codeSrcs = new Map();
codeSrcsUserConfig = [];
// start all the lazy props that are set in `setConfig()`
#hydrateDataFilePath;
#publicPath;
#outputDir;
#userConfigDir;
#dataDir;
// end lazy props
constructor({ id, language, codeSrcsUserConfig = [], }) {
this.id = id;
this.language = language;
this.logPrefix = this.id;
this.pkgPathAliases = {};
this.codeSrcsUserConfig = codeSrcsUserConfig;
this.log = {
inspect: log_1.log.inspect,
info: (msg, extra, prefix) => {
log_1.log.info(msg, extra, prefix || this.logPrefix);
},
verbose: (msg, extra, prefix) => {
log_1.log.verbose(msg, extra, prefix || this.logPrefix);
},
warn: (msg, extra, prefix) => {
log_1.log.warn(msg, extra, prefix || this.logPrefix);
},
error: (msg, extra, prefix) => {
log_1.log.error(msg, extra, prefix || this.logPrefix);
},
};
}
/**
* This is ran after constructor and before any other methods
* We do this to pass in things that the users do no pass into the constructor in `knapsack.config.js`
*/
setConfig({ userConfigDir, dataDir, }) {
(0, file_utils_1.assertsIsAbsolutePath)(userConfigDir);
(0, file_utils_1.assertsIsAbsolutePath)(dataDir);
this.#userConfigDir = userConfigDir;
this.#dataDir = dataDir;
this.#hydrateDataFilePath = (0, file_utils_1.join)(cache_dir_1.ksCacheDir, `hydrate.renderer-${this.id}.json`);
this.#outputDir = (0, file_utils_1.join)(cache_dir_1.ksCacheDir, `knapsack-renderer-${this.id}`);
this.#publicPath = `/${(0, file_utils_1.relative)(cache_dir_1.ksCacheDir, this.outputDir)}/`;
fs_extra_1.default.ensureDirSync(this.outputDir);
}
async init(_) {
Object.entries(this.pkgPathAliases).forEach(([alias, path]) => {
const info = (0, file_utils_1.getPathType)(alias);
if (info.type !== 'package' && info.type !== 'package-sub-path') {
throw new Error(`Aliased path, "${alias}" must be a package or package sub-path, but was "${info.type}". Was used to alias to this path: "${path}"`);
}
this.addCodeSrc({ path });
});
if (this.codeSrcsUserConfig.length === 0)
return;
const allPathsResult = await Promise.all(this.codeSrcsUserConfig.map(async ({ path, filter = () => true }) => {
const allPaths = await (0, file_utils_1.globPkgsAndFiles)({
paths: [path],
cwd: this.#userConfigDir,
});
return allPaths.filter((p) => filter(p.path));
}));
const allPaths = allPathsResult.flat();
await Promise.all(allPaths.map(({ path }) => this.addCodeSrc({ path })));
}
async build() {
const codeSrcs = Object.fromEntries(this.codeSrcs.entries());
return {
codeSrcs,
};
}
async hydrate({ hydrateData, }) {
this.codeSrcs = new Map((0, utils_1.entries)(hydrateData.codeSrcs));
}
/**
* directory path where `knapsack.config.js` can be found
* non-absolute paths in `knapsack.config.js` will be relative from this
*/
get userConfigDir() {
const it = this.#userConfigDir;
if (!it)
throw new TooEarlyError('userConfigDir');
return it;
}
get dataDir() {
const it = this.#dataDir;
if (!it)
throw new TooEarlyError('dataDir');
return it;
}
get outputDir() {
const it = this.#outputDir;
if (!it)
throw new TooEarlyError('outputDir');
return it;
}
get publicPath() {
const it = this.#publicPath;
if (!it)
throw new TooEarlyError('publicPath');
return it;
}
get hydrateDataFilePath() {
const it = this.#hydrateDataFilePath;
if (!it)
throw new TooEarlyError('hydrateDataFilePath');
return it;
}
onChange() {
events_1.knapsackEvents.emitRequestRendererClientReload();
}
async watch() {
if ((0, types_1.isRendererIdForNativeMobile)(this.id))
return;
const templatePaths = [];
await Promise.all(this.getCodeSrcs().map(async ({ path }) => {
const { absolutePath } = await this.resolvePath({ path });
if (absolutePath)
templatePaths.push(absolutePath);
}));
if (templatePaths.length === 0)
return;
const watcher = chokidar_1.default.watch(templatePaths, {
ignoreInitial: true,
});
watcher
.on('change', (path) => {
events_1.knapsackEvents.emitPatternTemplateChanged({ path });
this.onChange();
})
.on('error', (error) => {
log_1.log.error(new Error(`Error watching: ${error.message}`, { cause: error }), '', `renderer:${this.id}`);
});
watcher.on('ready', () => {
log_1.log.verbose('Watching these files:', watcher.getWatched(), `renderer:${this.id}`);
});
events_1.knapsackEvents.onShutdown(() => watcher.close());
}
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
changeCase = (str) => (0, utils_1.pascalCase)(str);
normalizeTemplateInfo(opt) {
return (0, types_1.normalizeTemplateInfo)({ rendererId: this.id, ...opt });
}
normalizeDemo({ demo, state, }) {
switch (demo.type) {
case 'data': {
const { patternId, templateId } = demo;
const pattern = state.patterns[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}`);
}
if (template.path) {
const info = (0, file_utils_1.getPathType)(template.path);
if (info.type === 'absolute') {
throw new Error(`Absolute paths are not allowed to be stored in demo paths: "${template.path}"`);
}
return this.normalizeTemplateInfo({
path: info.path,
alias: template.alias,
});
}
return this.normalizeTemplateInfo({
alias: template.alias,
});
}
case 'data-w-template-info': {
const info = demo.templateInfo;
if ((0, types_1.isTemplateInfoWithCodeSrcPath)(info) &&
!this.codeSrcs.has(info.codeSrcPath)) {
throw new Error(`Could not find codeSrc: "${info.codeSrcPath}" for demo: ${JSON.stringify(demo)}`);
}
return info;
}
default: {
const _exhaustiveCheck = demo;
throw new Error(`Unhandled demo type at rendererBase.normalizeDemo`);
}
}
}
resolvePath = (path) => (0, file_utils_1.resolvePath)({
path: typeof path === 'string' ? path : path.path,
pkgPathAliases: this.pkgPathAliases,
resolveFromDir: typeof path !== 'string' && path.resolveFromDir
? path.resolveFromDir
: this.dataDir,
});
/** @deprecated use async `resolvePath` instead */
resolvePathSync = (path) => (0, file_utils_1.resolvePathSync)({
path: typeof path === 'string' ? path : path.path,
pkgPathAliases: this.pkgPathAliases,
resolveFromDir: typeof path !== 'string' && path.resolveFromDir
? path.resolveFromDir
: this.dataDir,
});
async addCodeSrc({ path, relativePathsFrom = 'data-dir', }) {
if (!path)
return;
// @todo handle how `this.pkgPathAliases` can create multiple redundant CodeSrcs and therefore templates - https://linear.app/knapsack/issue/KSP-6115/pkgpathaliases-can-cause-duplicate-codesrcs
// const aliasThisPathUses = Object.entries(this.pkgPathAliases).find(
// ([alias, pkgPath]) => {
// return path.startsWith(pkgPath);
// },
// );
if (this.codeSrcs.has(path))
return;
if ((0, types_1.isRendererIdForNativeMobile)(this.id)) {
// skipping the `resolvePath` step since it will fail since package paths are not on local filesystem
const pathPackage = path;
this.codeSrcs.set(pathPackage, {
type: 'package',
path: pathPackage,
pkgName: pathPackage,
pathFromOutputDir: pathPackage,
rendererId: this.id,
});
return;
}
const resolveFromDir = relativePathsFrom === 'data-dir' ? this.dataDir : this.userConfigDir;
const { absolutePath, exists } = await this.resolvePath({
path,
resolveFromDir,
});
if (!exists)
throw new Error(`File not found: "${path}"`);
const pathInfo = (0, file_utils_1.getPathType)(
// makes sure absolute paths don't get in b/c we turn them to relative paths
(0, file_utils_1.getPathType)(path).type === 'absolute'
? (0, file_utils_1.relative)(resolveFromDir, absolutePath)
: path);
switch (pathInfo.type) {
case 'package': {
this.codeSrcs.set(pathInfo.path, {
type: 'package',
path: pathInfo.path,
pathFromOutputDir: pathInfo.path,
pkgName: pathInfo.pkgName,
rendererId: this.id,
});
return;
}
case 'package-sub-path': {
this.codeSrcs.set(pathInfo.path, {
type: 'package-sub-path',
path: pathInfo.path,
pathFromOutputDir: pathInfo.path,
pkgName: pathInfo.pkgName,
subPath: pathInfo.subPath,
rendererId: this.id,
});
return;
}
case 'relative': {
this.codeSrcs.set(pathInfo.path, {
type: 'relative-from-data-dir',
path: pathInfo.path,
pathFromOutputDir: (0, file_utils_1.relative)(this.outputDir, absolutePath),
rendererId: this.id,
});
return;
}
case 'absolute': {
throw new Error(`Absolute paths are not allowed: ${path}`);
}
default: {
const _exhaustiveCheck = pathInfo;
throw new Error(`Unhandled path type: ${JSON.stringify(pathInfo)}`);
}
}
}
getCodeSrcs({ includePrototypePaths = false, } = {}) {
const proto = this.getMeta().prototypingTemplate;
const codeSrcs = [...this.codeSrcs.values()];
if (!proto)
return codeSrcs;
if (includePrototypePaths) {
return codeSrcs;
}
return codeSrcs.filter((codeSrc) => {
return codeSrc.path !== proto.path;
});
}
async getUnusedTemplatePaths({ globPaths, state, cwd = gitRoot || process.cwd(), }) {
const allPaths = await (0, file_utils_1.globby)(globPaths, {
absolute: true,
deep: 20,
ignore: ['**/node_modules/**'],
// respect .gitignore
gitignore: true,
cwd,
});
const paths = new Set();
Object.values(state.patterns).forEach((pattern) => {
pattern.templates.forEach(({ path, templateLanguageId }) => {
if (templateLanguageId === this.id) {
paths.add(path);
}
});
pattern.templateDemos.forEach(({ templateLanguageId, templateInfo: { path } }) => {
if (templateLanguageId === this.id) {
paths.add(path);
}
});
});
const usedAbsolutePaths = new Set();
await Promise.all(Array.from(paths).map(async (path) => {
const { exists, absolutePath } = await this.resolvePath(path);
if (exists) {
usedAbsolutePaths.add(absolutePath);
}
}));
const unusedPaths = allPaths.filter((path) => !usedAbsolutePaths.has(path));
return {
unusedPaths,
usedPaths: Array.from(usedAbsolutePaths),
};
}
static async convertSchemaToTypeScriptDefs({ schema, title, description = '', patternId, templateId, preBanner, postBanner, }) {
const theSchema = {
...schema,
additionalProperties: false,
description,
title,
};
const bannerComment = `
/**
* patternId: "${patternId}" templateId: "${templateId}"
* This file was automatically generated by Knapsack.
* DO NOT MODIFY IT BY HAND.
* Instead, adjust it's spec, by either:
* 1) go to "/patterns/${patternId}/${templateId}" and use the UI to edit the spec
* 2) OR edit the "knapsack.pattern.${patternId}.json" file's "spec.props".
* Run Knapsack again to regenerate this file.
*/`.trim();
const typeDefs = await (0, json_schema_to_typescript_1.compile)(theSchema, theSchema.title, {
bannerComment: [preBanner, bannerComment, postBanner]
.filter(Boolean)
.join('\n\n'),
style: {
singleQuote: true,
},
});
return typeDefs
.split('\n')
.map((line) => line.replace('export type', 'type'))
.join('\n');
}
writeHydrateData = async (data) => {
await (0, file_utils_1.writeJSON)({
path: this.hydrateDataFilePath,
contents: data,
minimize: true,
});
};
readHydrateData = async () => {
return (0, file_utils_1.readJSON)(this.hydrateDataFilePath);
};
}
exports.RendererBase = RendererBase;
//# sourceMappingURL=renderer-base.js.map