UNPKG

quasar-json-api

Version:

Normalizes and validates JSON API for 3rd-party Quasar Components, Directives, Mixins and Plugins using the Quasar UI Kit

451 lines (381 loc) 14.3 kB
const fs = require('fs') const path = require('path') const { logError, writeFile } = require('./build.utils') const rootDir = global.rootDir const typeRoot = path.resolve(rootDir, './types') const distRoot = path.resolve(rootDir, './dist/types') const resolvePath = (file) => path.resolve(distRoot, file) const extraInterfaces = {} // eslint-disable-next-line no-useless-escape const toCamelCase = (str) => str.replace(/(-\w)/g, (m) => m[1].toUpperCase()) // if destination folder does not exist, create it now if (fs.existsSync(distRoot) === false) { fs.mkdirSync(distRoot) } const writeJson = function (file, json) { return fs.writeFileSync(file, JSON.stringify(json, null, 2) + '\n', 'utf-8') } function writeLine(fileContent, line = '', indent = 0) { fileContent.push(`${line.padStart(line.length + indent * 4, ' ')}\n`) } function writeLines(fileContent, lines = '', indent = 0) { lines.split('\n').forEach((line) => writeLine(fileContent, line, indent)) } function write(fileContent, text = '') { fileContent.push(`${text}`) } const typeMap = new Map([ ['Any', 'any'], ['Component', 'Component'], ['String', 'string'], ['Boolean', 'boolean'], ['Number', 'number'], ]) const fallbackComplexTypeMap = new Map([ ['Array', 'any[]'], ['Object', 'LooseDictionary'], ]) const dontNarrowValues = ['(Boolean) true', '(Boolean) false', '(CSS selector)', '(DOM Element)'] function convertTypeVal(key, type, def, required) { const t = type.trim() if (def.values && t === 'String') { const narrowedValues = def.values .filter((v) => !dontNarrowValues.includes(v) && typeof v === 'string') .map((v) => `'${v}'`) if (narrowedValues.length) { return narrowedValues.join(' | ') } } if (typeMap.has(t)) { return typeMap.get(t) } if (fallbackComplexTypeMap.has(t)) { console.error(`warning: '${key}' missing 'tsType' for complex "type": "${t}"...`) if (def.definition) { const propDefinitions = getPropDefinitions(def.definition, required, true) const lines = [] propDefinitions.forEach((p) => lines.push(...p.split('\n'))) return propDefinitions && propDefinitions.length > 0 ? `{\n ${lines.join('\n ')} }${t === 'Array' ? '[]' : ''}` : fallbackComplexTypeMap.get(t) } return fallbackComplexTypeMap.get(t) } return t } function getTypeVal(key, def, required) { if (def.tsType !== void 0) { return Array.isArray(def.tsType) ? def.tsType.map((type) => type).join(' | ') : def.tsType } return Array.isArray(def.type) ? def.type.map((type) => convertTypeVal(key, type, def, required)).join(' | ') : convertTypeVal(key, def.type, def, required) } function getPropDefinition(key, propDef, required, docs = false, isMethodParam = false) { const propName = toCamelCase(key) if (propName.startsWith('...')) { return isMethodParam ? `${propName}: any[]` : '[index: string]: any' } else { const propType = getTypeVal(key, propDef, required) addToExtraInterfaces(propDef) return `${docs ? `/**\n * ${propDef.desc}\n */\n` : ''}${propName}${ !propDef.required && !required ? '?' : '' } : ${propType}` } } function getPropDefinitions(propDefs, required, docs = false, areMethodParams = false) { const defs = [] for (const key in propDefs) { if (propDefs[key].removedIn === void 0) { const def = getPropDefinition(key, propDefs[key], required, docs, areMethodParams) def && defs.push(def) } } return defs } function getMethodDefinition(key, methodDef, required, isUtil = false) { let def = `/**\n * ${methodDef.desc}\n` if (methodDef.params) { def += `${Object.entries(methodDef.params) .map(([name, paramDef]) => ` * @param ${name} ${paramDef.desc}`) .join('\n')}\n` } const returns = methodDef.returns if (returns) { def += ` * @returns ${returns.desc}\n` } def += ` */\n${isUtil ? 'export function ' + key : key}` if (methodDef.tsType !== void 0) { def += `: ${methodDef.tsType}` addToExtraInterfaces(methodDef) } else { def += ' (' if (methodDef.params) { // TODO: Verify if this should be optional even for plugins const params = getPropDefinitions(methodDef.params, false, false, true) def += params.join(', ') } def += `): ${returns ? getTypeVal(key, returns, required) : 'void'}` addToExtraInterfaces(returns, true) } return def } function getObjectParamDefinition(def, required) { const res = [] Object.keys(def).forEach((propName) => { const propDef = def[propName] if (propDef.type && propDef.type === 'Function') { res.push(getMethodDefinition(propName, propDef, required)) } else { res.push(getPropDefinition(propName, propDef, required, true)) } }) return res } function getInjectionDefinition(injectionName, typeDef) { // Get property injection point for (const propKey in typeDef.props) { const propDef = typeDef.props[propKey] if (propDef.tsInjectionPoint) { return getPropDefinition(injectionName, propDef, true, true) } } // Get method injection point for (const methodKey in typeDef.methods) { const methodDef = typeDef.methods[methodKey] if (methodDef.tsInjectionPoint) { return getMethodDefinition(injectionName, methodDef, true) } } } function copyHelpers(name) { const tsHelpers = path.resolve(__dirname, `./helpers/${name}`), destPath = resolvePath(`./${name}`) if (fs.existsSync(tsHelpers) === true) { writeFile(resolvePath(destPath), fs.readFileSync(tsHelpers)) } } function UpdatePackageJson() { const data = 'dist/types/index.d.ts' const packagePath = path.resolve(rootDir, './package.json') const packageJson = require(packagePath) if (packageJson.typings !== void 0 && packageJson.typings === data) return packageJson.typings = data writeJson(packagePath, packageJson) } function copyPredefinedTypes(dir, parentDir) { fs.readdirSync(dir) .filter((file) => path.basename(file).startsWith('.') !== true) .forEach((file) => { const fullPath = path.resolve(dir, file) const stats = fs.lstatSync(fullPath) if (stats.isFile()) { writeFile(resolvePath(parentDir ? parentDir + file : file), fs.readFileSync(fullPath)) } else if (stats.isDirectory()) { const p = resolvePath(parentDir ? parentDir + file : file) if (!fs.existsSync(p)) { fs.mkdirSync(p) } copyPredefinedTypes(fullPath, parentDir ? parentDir + file : file + '/') } }) } function addToExtraInterfaces(def, required) { if (def !== void 0 && def.tsType !== void 0) { // When a type name is found and it has a definition, // it's added for later usage if a previous definition isn't already there. // When the new interface doesn't have a definition, we initialize its key anyway // to mark its existence, but with an undefined value. // In this way it can be overwritten if a definition is found later on. // Interfaces without definition at the end of the build script // are considered external custom types and imported as such if (extraInterfaces[def.tsType] === void 0 && def.definition !== void 0) { extraInterfaces[def.tsType] = getObjectParamDefinition(def.definition, required) } else if (Array.isArray(def.tsType)) { def.tsType.forEach((type) => { // eslint-disable-next-line no-prototype-builtins if (!extraInterfaces.hasOwnProperty(type)) { extraInterfaces[type] = void 0 } }) } // eslint-disable-next-line no-prototype-builtins else if (!extraInterfaces.hasOwnProperty(def.tsType)) { extraInterfaces[def.tsType] = void 0 } } } function writeIndexDTS(apis, forcedTypes) { const contents = [] const quasarTypeContents = [] const components = [] const directives = [] const plugins = [] const utils = [] // addQuasarLangCodes(quasarTypeContents) // This line must be BEFORE ANY TS INSTRUCTION, // or it won't be interpreted as a TS compiler directive // but as a normal comment // On Vue CLI projects `@quasar/app` isn't available, // we ignore the "missing package" error because it's the intended behaviour // writeLine(contents, `// @ts-ignore`) // writeLine(contents, `/// <reference types="@quasar/app" />`) writeLine(contents, "import type { ComponentPublicInstance, ComponentOptions } from 'vue'") // writeLine(contents, "import type { LooseDictionary } from './ts-helpers'") // We expose `ts-helpers` because they are needed, if they exist if (fs.existsSync(path.resolve(typeRoot, './types.d.ts'))) { writeLine(quasarTypeContents, `export * from './types'`) } writeLine(contents) apis.forEach((api) => { if (api.api.type !== 'util') { writeLine(quasarTypeContents, `export as namespace ${api.name}`) } }) const injections = {} apis.forEach((data) => { const content = data.api const typeName = data.name const isUtil = content.type === 'util' if (isUtil !== true) { const extendsVue = content.type === 'component' || content.type === 'mixin' const typeValue = `${extendsVue ? 'ComponentOptions' : typeName}` // Add Type to the appropriate section of types const propTypeDef = `${typeName}?: ${typeValue}` if (content.type === 'component') { write(components, propTypeDef) } else if (content.type === 'directive') { write(directives, propTypeDef) } else if (content.type === 'plugin') { write(plugins, propTypeDef) } // Declare class writeLine( quasarTypeContents, `export const ${typeName}: ${extendsVue ? 'ComponentOptions' : typeName}` ) writeLine( contents, `export interface ${typeName} ${extendsVue ? 'extends ComponentPublicInstance ' : ''}{` ) // Write Props const props = getPropDefinitions(content.props, content.type === 'plugin', true) props.forEach((prop) => writeLines(contents, prop, 1)) } // Write Methods for (const methodKey in content.methods) { const method = content.methods[methodKey] const methodDefinition = getMethodDefinition( methodKey, method, content.type === 'plugin', isUtil ) writeLines(contents, methodDefinition, isUtil !== true ? 1 : 0) } // Close class declaration if (isUtil !== true) { writeLine(contents, '}') } writeLine(contents) // Copy Injections for type declaration if (content.type === 'plugin') { if (content.injection) { const injectionParts = content.injection.split('.') if (!injections[injectionParts[0]]) { injections[injectionParts[0]] = [] } let def = getInjectionDefinition(injectionParts[1], content) if (!def) { def = `${injectionParts[1]}: ${typeName}` } injections[injectionParts[0]].push(def) } } }) const importName = [] if (forcedTypes && Array.isArray(forcedTypes) && forcedTypes.length > 0) { importName.push(...forcedTypes) } Object.keys(extraInterfaces).forEach((name) => { if (extraInterfaces[name] === void 0) { // If we find the symbol as part of the generated Quasar API, // we don't need to import it from custom TS API patches if (apis.some((definition) => definition.name === name)) { return } importName.push(name) } else { writeLine(contents, `export interface ${name} {`) extraInterfaces[name].forEach((def) => { writeLines(contents, def, 1) }) writeLine(contents, '}\n') } }) if (importName.length > 0) { const tsTypes = resolvePath('./types.d.ts') if (fs.existsSync(tsTypes)) { const data = `import { ${importName.join(', ')} } from './types'` writeLine(contents, data) } else { console.error( `warning: 'types.d.ts' missing for import declarions needed for "${importName.join(', ')}"` ) } } // Write injection types for (const key in injections) { const injectionDefs = injections[key] if (injectionDefs) { const injectionName = `${key.toUpperCase().replace('$', '')}VueGlobals` writeLine(contents, `import { ${injectionName}, QSingletonGlobals } from "./globals";`) writeLine(contents, 'declare module "./globals" {') writeLine(contents, `export interface ${injectionName} {`) for (const defKey in injectionDefs) { writeLines(contents, injectionDefs[defKey], 1) } writeLine(contents, '}') writeLine(contents, '}') } } writeLine(contents) // Extend Vue instance with injections if (injections) { writeLine(contents, "declare module 'vue' {") writeLine(contents, 'interface ComponentCustomProperties {', 1) for (const key3 in injections) { writeLine(contents, `${key3}: ${key3.toUpperCase().replace('$', '')}VueGlobals`, 2) } writeLine(contents, '}', 1) writeLine(contents, '}') } // addQuasarPluginOptions(contents, components, directives, plugins) quasarTypeContents.forEach((line) => write(contents, line)) // writeLine(contents, 'export const Quasar: { install: (app: App, options: Partial<QuasarPluginOptions>) => any } & QSingletonGlobals') // writeLine(contents, 'export default Quasar') writeLine(contents) // These imports force TS compiler to evaluate contained declarations // which by defaults would be ignored because inside node_modules // and not directly referenced by any file // writeLine(contents, 'import \'./shim-icon-set\'') // writeLine(contents, 'import \'./shim-lang\'') writeFile(resolvePath('index.d.ts'), contents.join('')) } module.exports.generate = function (data, forcedTypes) { const apis = data.plugins.concat(data.directives).concat(data.components).concat(data.utils) try { copyHelpers('ts-helpers.d.ts') copyHelpers('tsconfig.json') copyHelpers('vue.d.ts') copyPredefinedTypes(typeRoot) writeIndexDTS(apis, forcedTypes) UpdatePackageJson() } catch (err) { logError('build.types.js: something went wrong...') console.log() console.error(err) console.log() process.exit(1) } }