/* * This file is part of the xPack project (http://xpack.github.io). * Copyright (c) 2021-2026 Liviu Ionescu. All rights reserved. * * Permission to use, copy, modify, and/or distribute this software * for any purpose is hereby granted, under the terms of the MIT license. * * If a copy of the license was not distributed with this file, it can * be obtained from https://opensource.org/license/mit. */ // ---------------------------------------------------------------------------- import assert from 'node:assert' import * as util from 'node:util' import * as readline from 'node:readline/promises' import * as path from 'node:path' import * as fs from 'node:fs/promises' // https://www.npmjs.com/package/liquidjs import { Liquid, LiquidOptions } from 'liquidjs' import { Logger } from '@xpack/logger' // ---------------------------------------------------------------------------- import { InitTemplateItemValue, InitTemplatePropertiesDefinitions, InitTemplateSubstitutionsVariables, } from '../types/xpm-init-template.js' import { Context } from '../types/xpm.js' import { isString, isObject, isBoolean, isNumber, } from '../functions/is-something.js' import { JsonSyntaxError, InputError, OutputError, ConfigurationError, } from './errors.js' import { LiquidEngine } from './liquid-engine.js' import { liquidSubstitutionsVariablesBase } from '../data/substitutions-variables.js' import type { Policies } from './policies.js' // ============================================================================ /** * Configuration parameters for constructing an `xpm init` template. * * @remarks * This interface defines the required configuration for creating an * instance of {@link InitTemplateBase} or its derived classes. All * properties are mandatory except for the optional `process` parameter, * parameter, which defaults to the global Node.js `process` object when * not specified. * * The parameters provide the template with access to the xpm * context, file system paths, property definitions, and the process * environment necessary for template operations. */ export interface InitTemplateConstructorParameters { /** * The xpm context containing configuration and logging utilities. */ context: Context /** * The absolute path to the module folder. */ __dirname: string /** * The absolute path to the templates folder. */ templatesPath: string /** * Definitions of all properties supported by this template. */ propertiesDefinitions: InitTemplatePropertiesDefinitions /** * The Node.js process object (defaults to the global `process`). * Intended for testing purposes to allow mocking of process properties * and methods. */ process?: NodeJS.Process /** * Optional configuration options for the Liquid templating engine. * * @remarks * These options customise the behaviour of the Liquid template engine * used for rendering template files. The options are merged with the * required `root` property (set to `templatesPath`) when initialising * the engine. Common options include `strictFilters`, * `strictVariables`, and `trimOutputLeft`/`trimOutputRight` for * controlling whitespace handling. * * Refer to the Liquid documentation for the complete list of available * configuration options: * \ */ options?: LiquidOptions /** * The policy flags instance that governs template behaviour. * * @remarks * The `Policies` instance encapsulates compatibility flags derived * from the minimum required xpm version declared by the * package being initialised. These flags control how the template * builds its substitution variables: * * */ policies: Policies } /** * Base class for `xpm init` templates. * * @remarks * This abstract class provides the foundation for template-based project * initialisation. It handles the complete workflow: property validation, * interactive user prompts for missing mandatory values, variable * substitution, and file generation using the Liquid templating engine. * * Template workflow: * *
    *
  1. Properties are validated against their definitions
  2. *
  3. Missing mandatory properties trigger interactive prompts (if TTY)
  4. *
  5. Substitution variables are prepared from properties
  6. *
  7. The InitTemplateBase.generate() method creates project * files
  8. *
* * Derived classes must implement {@link InitTemplateBase.generate} * to define the specific files and folder structure to create. */ export abstract class InitTemplateBase { // -------------------------------------------------------------------------- // Public Members. // -------------------------------------------------------------------------- // Protected Members. /** * The xpm context containing configuration and logging utilities. */ readonly context: Context /** * The logger instance for output and diagnostics. */ readonly log: Logger /** * Definitions of all properties supported by this template. */ readonly propertiesDefinitions: InitTemplatePropertiesDefinitions = {} /** * The absolute path to the module folder. */ readonly __dirname: string /** * The absolute path to the templates folder. */ readonly templatesPath: string /** * The Liquid templating engine instance. */ readonly engine: Liquid /** * The variables to be used for template substitutions. */ substitutionsVariables?: InitTemplateSubstitutionsVariables /** * Flag indicating whether the template is running in interactive mode. * * @remarks * This flag determines whether the template execution involved user * interaction through terminal prompts for missing mandatory property * values. * * State management: * *
    *
  1. Initialised to false upon construction.
  2. *
  3. Set to true in {@link InitTemplateBase.run} if at least * one mandatory property was missing and required interactive * prompting.
  4. *
  5. Set to false if all mandatory properties were provided * via command-line options.
  6. *
* * When interactive mode is activated, the context start time is reset * after user input to exclude interactive time from performance metrics, * ensuring accurate measurement of the template processing duration. */ isInteractive = false /** * The Node.js process object for accessing runtime environment information. * * @remarks * This reference provides access to process properties including standard * I/O streams, platform information, and architecture details. It is * configurable via the constructor to support testing scenarios where * process properties need to be mocked or controlled. * * Usage within the template: * *
    *
  1. Platform detection via process.platform and * process.arch for * platform-specific property validation.
  2. *
  3. TTY detection via stdin.isTTY and * stdout.isTTY to determine * whether interactive prompting is possible.
  4. *
  5. Standard I/O access for interactive user prompts and diagnostic * output.
  6. *
* * Defaults to the global Node.js process object when not * explicitly provided in the constructor, enabling normal runtime * behaviour whilst allowing test environments to inject controlled * process implementations. */ readonly process: NodeJS.Process /** * The policy flags instance that governs template behaviour. * * @remarks * Stores the {@link Policies} instance supplied via the constructor. * It is consulted in {@link InitTemplateBase.run} to determine * which substitution variable layout to build: * * */ policies: Policies // -------------------------------------------------------------------------- // Constructor. /** * Constructs an `xpm init` template instance. * * @param context - The xpm context containing configuration and * logging. * @param __dirname - The absolute path to the module folder. * @param templatesPath - The absolute path to the templates folder. * @param propertiesDefinitions - The definitions of all supported properties. */ constructor({ context, __dirname, templatesPath, propertiesDefinitions, process: _process = process, options, policies, }: InitTemplateConstructorParameters) { assert(context, 'context is required') assert(context.log, 'context.log is required') assert(context.config, 'context.config is required') assert(context.config.projectName, 'context.config.projectName is required') assert(context.config.properties, 'context.config.properties is required') assert(__dirname, '__dirname is required') assert(templatesPath, 'templatesPath is required') assert(propertiesDefinitions, 'propertiesDefinitions is required') this.context = context this.log = context.log this.propertiesDefinitions = propertiesDefinitions this.__dirname = __dirname this.templatesPath = templatesPath this.process = _process this.policies = policies this._validatePropertiesDefinitions() // https://liquidjs.com this.engine = new LiquidEngine({ options: { ...options, root: this.templatesPath, }, }) } // -------------------------------------------------------------------------- // Public Methods. /** * Executes the template initialisation process. * * @remarks * This method orchestrates the complete template initialisation workflow. * It validates all provided properties, determines whether interactive * mode is required (when mandatory properties are missing), prompts for * missing values if in a TTY environment, prepares substitution variables * including the current year, and invokes the template-specific * {@link InitTemplateBase.generate} method to create project files. * * The method automatically applies default values to optional properties * that were not explicitly set. In interactive mode, the timer is reset * after user input to exclude interactive time from performance metrics. * * @returns A promise that resolves to 0 on success. * * @throws {@link JsonSyntaxError} * If property validation fails or interactive mode is required but not * available (non-TTY environment). */ async run(): Promise { const log = this.log log.trace(`${this.constructor.name}.run()`) log.info() const context = this.context const config = context.config assert(config.properties, 'config.properties is required') const validationErrors: string[] = [] for (const [key, val] of Object.entries(config.properties)) { try { config.properties[key] = this.validatePropertyValue(key, val as string) } catch (error) { if (error instanceof Error) { const errorMessage = `${key}: ${error.message}` log.error(errorMessage) validationErrors.push(errorMessage) } } } if (validationErrors.length > 0) { throw new JsonSyntaxError( validationErrors.length === 1 ? '1 invalid property' : `${String(validationErrors.length)} invalid properties` ) } // Properties set by `--property name=value` are in `config.properties`. // If there is at least one mandatory property without an explicit value, // enter the interactive mode and ask for the missing values. const mustAsk = Object.keys(this.propertiesDefinitions).some((key) => { return ( this.propertiesDefinitions[key].isMandatory && !config.properties?.[key] ) }) let isInteractive if (mustAsk) { // Need to ask for more values. if (!(this.process.stdin.isTTY && this.process.stdout.isTTY)) { throw new JsonSyntaxError( 'Interactive mode not possible without a TTY.' ) } await this._askForMoreValues() log.trace(util.inspect(config.properties)) // Reset start time to skip interactive time. context.startTime = Date.now() isInteractive = true } else { // Properties without explicit values get their defaults. Object.entries(this.propertiesDefinitions).forEach(([key, val]) => { assert(config.properties, 'config.properties is required') if (!config.properties[key] && val.default !== undefined) { config.properties[key] = val.default } }) isInteractive = false } this.isInteractive = isInteractive const currentTime = new Date() let substitutionsVariables: InitTemplateSubstitutionsVariables if (this.policies.topPropertiesXpmInitTemplate) { substitutionsVariables = { // Spread all config properties for easier access in templates. ...config.properties, // Also pass the properties grouped. properties: config.properties, // An array with the property names, for iteration in templates. propertiesNames: Object.keys(config.properties), // The project name, for convenience. projectName: config.projectName, // Current year, for copyright statements. year: currentTime.getFullYear().toString(), } } else { substitutionsVariables = { ...liquidSubstitutionsVariablesBase, // Pass the configuration properties grouped. matrix: { ...config.properties, }, // An array with the property names, for iteration in templates. propertiesNames: Object.keys(config.properties), // The project name, for convenience. projectName: config.projectName, // Current year, for copyright statements. year: currentTime.getFullYear().toString(), } } this.substitutionsVariables = substitutionsVariables await this.generate() return 0 // success } /** * Generates the project files from the template. * * @remarks * This abstract method must be implemented by derived classes to define * the specific files and folder structure to create for the project. * Implementations should use {@link InitTemplateBase.copyFile}, * {@link InitTemplateBase.copyFolder}, and * {@link InitTemplateBase.render} to create the project structure. * The substitution variables are available via the * {@link InitTemplateBase.substitutionsVariables} property. * * The implementation must be asynchronous to allow for file system * operations. * * @returns A promise that resolves when generation is complete. */ abstract /* async */ generate(): Promise /** * Determines whether the current platform is supported. * * @remarks * This method checks platform compatibility using a two-tier matching * strategy. First, it looks for an exact match with the current * platform-architecture combination (e.g., `darwin-arm64`). If not * found, it checks for a platform-only match (e.g., `darwin`). Returns * `false` if the platforms array is undefined, empty, or contains no * matches for the current execution environment. * * @param platforms - The array of supported platform identifiers, or * undefined if no platforms are specified. * @returns `true` if the current platform is supported, `false` * otherwise. */ isPlatformSupported(platforms: string[] | undefined): boolean { assert(platforms && platforms.length !== 0, 'platforms array is required') if (platforms.includes(`${this.process.platform}-${this.process.arch}`)) { return true } if (platforms.includes(this.process.platform)) { return true } return false } /** * Copies a single file from the templates folder to the destination. * * @remarks * This method resolves the source file path relative to the templates * folder and copies it to the destination, creating any necessary * parent directories. The file is copied without modifications, * preserving its content and structure. Use * {@link InitTemplateBase.render} instead if variable substitution * is needed. * * @param sourceFileRelativePath - The relative path to the source file * within the templates folder. * @param destinationFilePath - The destination file path (defaults to * the same relative path as the source). * @returns A promise that resolves when the file has been copied. */ async copyFile({ sourceFileRelativePath, destinationFilePath = sourceFileRelativePath, }: { sourceFileRelativePath: string destinationFilePath?: string }): Promise { assert(sourceFileRelativePath, 'sourceFileRelativePath is required') assert(destinationFilePath, 'destinationFilePath is required') const log = this.log await fs.mkdir(path.dirname(destinationFilePath), { recursive: true }) const sourceFileAbsolutePath = path.resolve( this.templatesPath, sourceFileRelativePath ) await fs.copyFile(sourceFileAbsolutePath, destinationFilePath) const destinationFileRelativePath = path.relative( this.context.config.cwd, destinationFilePath ) log.info(`File '${destinationFileRelativePath}' copied.`) } /** * Copies an entire folder from the templates folder to the destination. * * @remarks * This method recursively copies the complete folder structure, * including all files and subfolders, from the source to the * destination. The entire folder tree is replicated, preserving the * relative paths and structure. Files are copied without * modifications; use {@link InitTemplateBase.render} for * individual files that require variable substitution. * * @param sourceFolderRelativePath - The relative path to the source folder * within the templates folder. * @param destinationFolderPath - The destination folder path (defaults to the * same relative path as the source). * @returns A promise that resolves when the folder has been copied. */ async copyFolder({ sourceFolderRelativePath, destinationFolderPath = sourceFolderRelativePath, }: { sourceFolderRelativePath: string destinationFolderPath?: string }): Promise { assert(sourceFolderRelativePath, 'sourceFolderRelativePath is required') assert(destinationFolderPath, 'destinationFolderPath is required') const log = this.log const sourceFolderAbsolutePath = path.resolve( this.templatesPath, sourceFolderRelativePath ) await this._copyFolderRecursively({ sourceFolderPath: sourceFolderAbsolutePath, destinationFolderPath: path.resolve(destinationFolderPath), }) log.info(`Folder '${destinationFolderPath}' copied.`) } /** * Renders a template file using Liquid and writes the output. * * @remarks * This method processes a template file through the Liquid templating * engine with the provided substitution variables, generating the final * output file. Parent directories are created automatically if they do * not exist. The template file should be located in the templates * folder and use Liquid syntax for variable references (e.g., * `{{ variableName }}`). * * The substitution variables include all project properties plus * additional context like the current year. If substitutionsVariables * is not provided, the instance's substitutionsVariables property is * used. * * @param sourceFilePath - The absolute path to the template * file within the templates folder. * @param destinationFilePath - The destination path for the rendered * file. * @param substitutionsVariables - The variables to use for template * substitutions (defaults to the instance's substitutionsVariables). * @returns A promise that resolves when the file has been rendered and * written. * * @throws {@link OutputError} * If template rendering fails. */ async render({ sourceFilePath, destinationFilePath, substitutionsVariables = this.substitutionsVariables, }: { sourceFilePath: string destinationFilePath: string substitutionsVariables?: InitTemplateSubstitutionsVariables }): Promise { assert(sourceFilePath, 'sourceFilePath is required') assert(destinationFilePath, 'destinationFilePath is required') assert( substitutionsVariables !== undefined, 'substitutionsVariables is required for rendering templates. ' + 'Ensure that run() has been called to prepare the variables.' ) const log = this.log const context = this.context const config = context.config const cwd = config.cwd const sourceFileRelativePath = path.relative(cwd, sourceFilePath) const destinationFileRelativePath = path.relative(cwd, destinationFilePath) log.info( `Rendering template '${sourceFileRelativePath}' to ` + `'${destinationFileRelativePath}'` ) log.trace(`render(${sourceFilePath}, ${destinationFilePath})`) // const headerPath = path.resolve(codePath, `${pnam}.h`) try { const fileContent = (await this.engine.renderFile( sourceFilePath, substitutionsVariables )) as string await fs.mkdir(path.dirname(destinationFilePath), { recursive: true }) await fs.writeFile(destinationFilePath, fileContent, 'utf8') } catch (error) { if (error instanceof Error) { throw new OutputError(error.message) } } log.info(`File '${destinationFileRelativePath}' generated.`) } /** * Validates a property value against its definition. * * @remarks * This method checks whether the provided value is valid for the * specified property according to its type definition. It performs * type-specific validation and conversion: * *
    *
  • For select properties: validates against * allowed items andchecks platform compatibility if specified
  • *
  • For boolean properties: converts * 'true'/'false' * strings to booleans
  • *
  • For number properties: converts strings * to numbers
  • *
* * If the value is empty and a default is defined, the default value is * returned. For select properties with platform restrictions, only * platform-compatible items are considered valid. * * @param name - The property name to validate. * @param value - The property value to validate. * @returns The validated and potentially converted value (string, * boolean, or number). * * @throws {@link ConfigurationError} * If the property is unsupported or the value is invalid. */ validatePropertyValue( name: string, value: string ): string | boolean | number { const propDef = this.propertiesDefinitions[name] // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (propDef === undefined) { throw new ConfigurationError(`Unsupported property '${name}'`) } const trimmedValue = value.trim() if (trimmedValue === '') { if (propDef.default !== undefined) { return propDef.default } } else { switch (propDef.type) { case 'select': assert( propDef.items, `Property '${name}' of type 'select' has no items.` ) if (propDef.items[value]) { if (typeof propDef.items[value] === 'string') { return value } else if ( typeof propDef.items[value] === 'object' && this.isPlatformSupported(propDef.items[value].platforms) ) { return value } } break case 'boolean': if (trimmedValue === 'true') { return true } else if (trimmedValue === 'false') { return false } break case 'number': { const num = Number(trimmedValue) if (isFinite(num)) { return num } break } case 'string': return value // No default, the definition was already validated. } } throw new ConfigurationError( `Unsupported value '${value}' for property '${name}'` ) } // -------------------------------------------------------------------------- // Protected Methods. /** * Prompts the user interactively for missing property values. * * @remarks * This method creates a readline interface and iteratively prompts the * user to provide values for properties without explicit values. For * each property, the prompt displays: * *
    *
  • The property label
  • *
  • Valid options (for select and boolean types)
  • *
  • The default value in brackets, if available
  • *
* * If the user enters '?', help text is displayed showing the property * description and all valid options with their descriptions. Invalid * responses are rejected and the prompt is repeated until a valid value * is provided. Platform-incompatible options are excluded from select * properties. * * @returns A promise that resolves when all missing values have been * collected. */ protected async _askForMoreValues() { const context = this.context const config = context.config assert(config.properties, 'config.properties is required') const rl = readline.createInterface({ input: this.process.stdin, output: this.process.stdout, }) for (const name of Object.keys(this.propertiesDefinitions)) { if (config.properties[name]) { continue } const definition = this.propertiesDefinitions[name] let prompt = `${definition.label}?` switch (definition.type) { case 'select': { prompt += ' (' const validItems = [] assert(definition.items, 'definition.items is required') for (const [ikey, ival] of Object.entries(definition.items)) { if (isString(ival)) { validItems.push(ikey) } else if ( isObject(ival) && this.isPlatformSupported( (ival as InitTemplateItemValue).platforms ) ) { validItems.push(ikey) } } prompt += validItems.join(', ') prompt += ', ?)' break } case 'string': prompt += ' (string, ?)' break case 'number': prompt += ' (number, ?)' break case 'boolean': prompt += ' (true, false, ?)' break // No default, the definition was already validated. } if (definition.default !== undefined) { prompt += ` [${String(definition.default)}]` } prompt += ': ' const MAX_RETRIES = 42 let retryCount = 0 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition while (true) { /* c8 ignore start - Defensive check. */ if (++retryCount > MAX_RETRIES) { throw new InputError( `Too many invalid attempts for property '${name}' ` + `(limit: ${String(MAX_RETRIES)})` ) } /* c8 ignore stop */ const answer = (await rl.question(prompt)).trim() // console.log('{' + answer + '}') try { const value = this.validatePropertyValue(name, answer) // console.log('[' + value + ']') config.properties[name] = value break } catch (error) { if (error instanceof Error) { this.log.trace(error.message) } this.process.stdout.write(`${definition.description}\n`) if (definition.type === 'select') { assert(definition.items, 'definition.items is required') for (const [ikey, ival] of Object.entries(definition.items)) { if (isString(ival)) { this.process.stdout.write(`- ${ikey}: ${ival as string}\n`) } else if ( isObject(ival) && this.isPlatformSupported( (ival as InitTemplateItemValue).platforms ) ) { this.process.stdout.write( `- ${ikey}: ${(ival as InitTemplateItemValue).message}\n` ) } } } } } } } /** * Recursively copies all contents of a source folder to a destination folder. * * @remarks * This internal method traverses the source folder structure and replicates * it at the destination, copying all files and recursively processing * subfolders. * * @param sourceFolderPath - The absolute path to the source folder. * @param destinationFolderPath - The absolute path to the destination folder. * @returns A promise that resolves when all contents have been copied. */ protected async _copyFolderRecursively({ sourceFolderPath, destinationFolderPath, }: { sourceFolderPath: string destinationFolderPath: string }): Promise { // const log = this.log await fs.mkdir(destinationFolderPath, { recursive: true }) const dirents = await fs.readdir(sourceFolderPath, { withFileTypes: true, }) for (const dirent of dirents) { // log.trace(dirent.name) if (dirent.isDirectory()) { await this._copyFolderRecursively({ sourceFolderPath: path.join(sourceFolderPath, dirent.name), destinationFolderPath: path.join(destinationFolderPath, dirent.name), }) } else { await fs.copyFile( path.join(sourceFolderPath, dirent.name), path.join(destinationFolderPath, dirent.name) ) } } } /** * Validates the structure and content of property definitions. * * @remarks * This internal method performs comprehensive validation of the property * definitions object during template construction, ensuring all definitions * are well-formed and internally consistent before the template is used. * * Validation steps: * *
    *
  1. Overall structure: *
      *
    • Verifies that propertiesDefinitions is an object.
    • *
    • Ensures at least one property is defined (not empty).
    • *
    *
  2. *
  3. Common property fields: *
      *
    • label: Must be a non-empty string.
    • *
    • description: Must be a non-empty string.
    • *
    • isMandatory: Must be a boolean if present.
    • *
    • type: Must be defined and one of: select, * string, number, boolean.
    • *
    *
  4. *
  5. Type-specific validation: *
      *
    • Select properties: *
        *
      • Must have an items object with at least one * entry.
      • *
      • Each item must be either a string (description) or an object with * platforms array and message string.
      • *
      • Non-mandatory properties must have a default value.
      • *
      • Default values must be non-empty strings present in the items * list.
      • *
      *
    • *
    • String properties: Default value must be a non-empty string * if present.
    • *
    • Number properties: Default value must be a number if * present.
    • *
    • Boolean properties: Default value must be a boolean if * present.
    • *
    *
  6. *
* * This validation ensures that templates are correctly configured before * use, preventing runtime errors during property processing and interactive * prompting. Any validation failure triggers an assertion error with a * descriptive message indicating the specific problem. */ protected _validatePropertiesDefinitions(): void { assert( isObject(this.propertiesDefinitions), 'propertiesDefinitions is not an object.' ) assert( Object.keys(this.propertiesDefinitions).length > 0, 'propertiesDefinitions is an empty object.' ) for (const [key, val] of Object.entries(this.propertiesDefinitions)) { assert(isString(val.label), `Property '${key}' must have a string label`) assert(val.label.trim() !== '', `Property '${key}' has an empty label`) assert( isString(val.description), `Property '${key}' must have a string description` ) assert( val.description.trim() !== '', `Property '${key}' has an empty description` ) if (val.isMandatory !== undefined) { assert( isBoolean(val.isMandatory), `Property '${key}' has a non boolean isMandatory value.` ) } // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition assert(val.type !== undefined, `Property '${key}' has no type defined.`) switch (val.type) { case 'select': assert( val.items !== undefined, `Property '${key}' of type 'select' has no items.` ) assert( isObject(val.items), `Property '${key}' of type 'select' has invalid items.` ) assert( Object.keys(val.items).length !== 0, `Property '${key}' of type 'select' has no items.` ) for (const [ikey, ival] of Object.entries(val.items)) { assert( isString(ival) || (isObject(ival) && Array.isArray((ival as InitTemplateItemValue).platforms) && isString((ival as InitTemplateItemValue).message)), `Property '${key}' has invalid item '${ikey}'.` ) } if (!val.isMandatory) { assert( val.default !== undefined, `Property '${key}' of type 'select' ` + `must have a default value if not mandatory.` ) } if (val.default !== undefined) { assert( isString(val.default), `Property '${key}' has a non string default value.` ) assert( (val.default as string).trim() !== '', `Property '${key}' has an empty default value.` ) } if (val.default !== undefined) { assert( Object.keys(val.items).includes(String(val.default)), `Property '${key}' has a default value not in items list.` ) } break case 'string': if (val.default !== undefined) { assert( isString(val.default), `Property '${key}' has a non string default value.` ) assert( (val.default as string).trim() !== '', `Property '${key}' has an empty default value.` ) } break case 'number': if (val.default !== undefined) { assert( isNumber(val.default), `Property '${key}' has a non number default value.` ) } break case 'boolean': if (val.default !== undefined) { assert( isBoolean(val.default), `Property '${key}' has a non boolean default value.` ) } break default: assert( false, `Property '${key}' has unsupported type '${String(val.type)}'.` ) break } } } } // ----------------------------------------------------------------------------