UNPKG

@knapsack/app

Version:

Build Design Systems with Knapsack

404 lines • 15.9 kB
"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