From 92426f9038c5b9e04fc6ea33fd34fbea7de6a29b Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues Date: Fri, 9 Jul 2021 18:57:13 +0200 Subject: [PATCH] feat(api): create typescript declaration for api --- .eslintrc.js | 4 +- README.md | 2 +- cli.js | 2 +- lib/formatInfo.js | 12 +- lib/formattingTools.js | 2 +- lib/index.js | 216 ++++++++++++++++--------- lib/keywords.js | 2 +- lib/locales/update.js | 2 +- lib/markdownBuilder.js | 6 +- lib/readmeBuilder.js | 2 +- lib/schemaProxy.js | 33 ++-- lib/symbols.js | 13 +- lib/traverseSchema.js | 2 +- lib/writeMarkdown.js | 125 ++++++++++----- lib/writeSchema.js | 61 ++++--- package-lock.json | 64 +++++++- package.json | 13 +- test/api.test.js | 14 +- test/cyclicSchemaIntegration.test.js | 2 +- test/formatting.test.js | 2 +- test/markdownBuilder.test.js | 36 ++--- test/readmeBuilder.test.js | 8 +- test/schemaProxy.test.js | 8 +- test/testUtils.js | 23 ++- test/testintegration.test.js | 4 +- test/traverseSchema.test.js | 2 +- test/zSchemaCoverage.test.js | 2 +- tsconfig.json | 12 ++ types/api.d.ts | 66 ++++++++ types/index.d.ts | 232 +++++++++++++++++++++++++++ 30 files changed, 764 insertions(+), 208 deletions(-) create mode 100644 tsconfig.json create mode 100644 types/api.d.ts create mode 100644 types/index.d.ts diff --git a/.eslintrc.js b/.eslintrc.js index 04f49c7c..e4019700 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -49,7 +49,7 @@ module.exports = { }], // enforce license header (todo: improve plugin to support patterns for multi-lines) - 'header/header': [2, 'block', ['', + 'header/header': [2, 'block', ['*', { pattern: ' * Copyright \\d{4} Adobe\\. All rights reserved\\.', template: ' * Copyright 2021 Adobe. All rights reserved.' }, ' * This file is licensed to you under the Apache License, Version 2.0 (the "License");', ' * you may not use this file except in compliance with the License. You may obtain a copy', diff --git a/README.md b/README.md index 85fe52ba..4fc9e77e 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ If you run `npm install` before running `npm run prepare`, `npm` will install th ### With API for Node.js ```javascript -const jsonschema2md = require('@adobe/jsonschema2md'); +const { jsonschema2md } = require('@adobe/jsonschema2md'); const schema = require('examples/schemas/example.schema.json'); const markdown = jsonschema2md(schema, { diff --git a/cli.js b/cli.js index 309c4874..c0e1e6d9 100755 --- a/cli.js +++ b/cli.js @@ -1,6 +1,6 @@ #! /usr/bin/env node /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/lib/formatInfo.js b/lib/formatInfo.js index d8dee485..d2635145 100644 --- a/lib/formatInfo.js +++ b/lib/formatInfo.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -49,13 +49,13 @@ function iscustom(schema) { function getdefined(schema) { if (schema[s.parent]) { return { - text: `${path.basename(schema[s.filename])}*`, - link: schema[s.filename], + text: `${schema[s.filename]}*`, + link: schema[s.inputpath], }; } return { - text: path.basename(schema[s.filename]), - link: schema[s.filename], + text: schema[s.filename], + link: schema[s.inputpath], }; } @@ -63,7 +63,7 @@ function plaindescription(schema) { try { if (schema[s.filename] && !schema[s.parent]) { const filename = path.resolve( - path.dirname(schema[s.filename]), + path.dirname(schema[s.inputpath]), schema[s.filename].replace(/\..*$/, '.description.md'), ); const longdesc = fs.readFileSync(filename); diff --git a/lib/formattingTools.js b/lib/formattingTools.js index 4806f58b..d2f76cc7 100644 --- a/lib/formattingTools.js +++ b/lib/formattingTools.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/lib/index.js b/lib/index.js index 82dfc2a8..74b44d4e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -9,31 +9,68 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ - +// @ts-check const yargs = require('yargs'); -const nodepath = require('path'); - +const path = require('path'); const fs = require('fs'); const readdirp = require('readdirp'); const logger = require('@adobe/helix-log'); const { - iter, pipe, filter, map, obj, + iter, pipe, filter, map, obj, pairs, } = require('ferrum'); -const npath = require('path'); const { i18nConfig } = require('es2015-i18n-tag'); const traverse = require('./traverseSchema'); const build = require('./markdownBuilder'); -const { writereadme, writemarkdown } = require('./writeMarkdown'); +const { writeReadme, writeMarkdown } = require('./writeMarkdown'); const readme = require('./readmeBuilder'); const { loader } = require('./schemaProxy'); const { writeSchema } = require('./writeSchema'); const { info, error, debug } = logger; +/** + * @typedef {import("../types/api").JsonSchema} JsonSchema + */ +/** + * @typedef {import("../types/api").SchemaFiles} SchemaFiles + */ +/** + * @typedef {import("../types/api").GeneratedOutput} GeneratedOutput + */ + +/** + * Public API for jsonschema2md that can be used to turn JSON Schema files + * into readable Markdown documentation. + * @param {JsonSchema | SchemaFiles} schema JSON Schema input to get data from + * @param {Object} options Additional options for generation + * @param {string} [options.schemaPath] - (optional) Path to directory containing all JSON Schemas + * or a single JSON Schema file. This will be considered as the baseURL. + * @param {string} [options.outDir] - (optional) Path to output directory. Generating files will + * be skipped if directory is not specified. + * @param {{ [key:string]: string }} [options.metadata] - (optional) Add metadata elements to + * .md files. + * @param {string} [options.schemaOut] - (optional) Output JSON Schema files including + * description and validated examples in the specified folder. + * @param {boolean} [options.includeReadme=true] - (optional) Generate a README.md file in the + * output directory. + * @param {{ [key:string]: string }[]} [options.links] - (optional) Add this file as a link + * explaining the specified attribute. + * @param {string} [options.i18n="locales/"] - (optional) Path to a locales folder with + * JSON files used for internationalization. + * @param {"en_US" | "de"} [options.language="en_US"] - (optional) Selected language. + * @param {"json" | "yaml"} [options.exampleFormat="json"] - (optional) How to format examples. + * @param {string[]} [options.includeProperties=[]] - (optional) Name of custom properties + * which should be also in the description of an element. + * @param {boolean} [options.header=true] - (optional) Whether or not to include the header + * in markdown. + * @param {string[]} [options.skipProperties=[]] - (optional) Name of a default property to + * skip in markdown. + * @returns {GeneratedOutput} List of raw markdown that were generated from input schema. + */ function jsonschema2md(schema, { schemaPath, - out, - meta, + outDir, + metadata, schemaOut, includeReadme, links, @@ -44,42 +81,66 @@ function jsonschema2md(schema, { header, skipProperties, }) { - const outOrDefault = out || nodepath.resolve(nodepath.join('.', 'out')); - const locales = i18n || nodepath.resolve(__dirname, 'locales'); + const locales = i18n || path.resolve(__dirname, 'locales'); // eslint-disable-next-line import/no-dynamic-require, global-require - i18nConfig(require(nodepath.resolve(locales, `${language || 'en_US'}.json`))); - - const schemas = [].concat(schema); - if (schemaOut) { - console.log('writing schemas'); - writeSchema({ - schemadir: schemaOut, - origindir: schemaPath, - })(schemas); + i18nConfig(require(path.resolve(locales, `${language || 'en_US'}.json`))); + + let normalized = { ...schema }; + if (normalized.$schema || normalized.$id || normalized.title) { + normalized = { + 'input.schema.json': normalized, + }; } + const schemaLoader = loader(); + + // collect data about the schemas and turn everything into a big object + const schemas = pipe( + normalized, + pairs, + map(([name, jsonData]) => { + let content = jsonData; + // Checking if data contains the file path or its contents (JSON schema) + if (typeof jsonData === 'string') { + // eslint-disable-next-line import/no-dynamic-require, global-require + content = require(jsonData); + } + return schemaLoader(name, content); + }), + traverse, + ); + + /** + * @type {GeneratedOutput} + */ + const output = {}; + + console.log('preparing schemas...'); + output.schema = writeSchema({ + schemaDir: schemaOut, + })(schemas); + if (includeReadme) { - console.log('writing README'); - pipe( + console.log('preparing README...'); + output.readme = pipe( schemas, // build readme readme({ - readme: includeReadme, + readme: true, }), - writereadme({ - readme: includeReadme, - out: outOrDefault, + writeReadme({ + out: outDir, info, error, debug, - meta, + meta: metadata, }), ); } - console.log('writing documentation'); - const markdown = pipe( + console.log('preparing documentation...'); + output.markdown = pipe( schemas, // generate Markdown ASTs build({ @@ -88,30 +149,33 @@ function jsonschema2md(schema, { includeProperties, exampleFormat, skipProperties, - rewritelinks: (origin) => { - const mddir = outOrDefault; - const srcdir = schemaPath; - const schemadir = schemaOut || schemaPath; - - const target = npath.relative( - mddir, - npath.resolve(schemadir, npath.relative(srcdir, origin)), - ).split(npath.sep).join(npath.posix.sep); + rewriteLinks: (origin) => { + const mdDir = outDir; + if (!mdDir) { + return origin; + } + const srcDir = schemaPath; + const schemaDir = schemaOut || schemaPath; + + const target = path.relative( + mdDir, + path.resolve(schemaDir, path.relative(srcDir, origin)), + ).split(path.sep).join(path.posix.sep); return target; }, }), // write to files - writemarkdown({ - out: outOrDefault, + writeMarkdown({ + out: outDir, info, error, debug, - meta, + meta: metadata, }), ); - return markdown; + return output; } async function main(args) { @@ -123,7 +187,7 @@ async function main(args) { .alias('d', 'input') .describe('d', 'path to directory containing all JSON Schemas or a single JSON Schema file. This will be considered as the baseURL. By default only files ending in .schema.json will be processed, unless the schema-extension is set with the -e flag.') .coerce('d', (d) => { - const resolved = nodepath.resolve(d); + const resolved = path.resolve(d); if (fs.existsSync(resolved) && fs.lstatSync(d).isDirectory()) { return resolved; } @@ -132,8 +196,8 @@ async function main(args) { .alias('o', 'out') .describe('o', 'path to output directory') - .default('o', nodepath.resolve(nodepath.join('.', 'out'))) - .coerce('o', (o) => nodepath.resolve(o)) + .default('o', path.resolve(path.join('.', 'out'))) + .coerce('o', (o) => path.resolve(o)) .option('m', { type: 'array', @@ -149,8 +213,8 @@ async function main(args) { .alias('x', 'schema-out') .describe('x', 'output JSON Schema files including description and validated examples in the specified folder, or suppress with -') - .default('x', nodepath.resolve(nodepath.join('.', 'out'))) - .coerce('x', (x) => (x === '-' ? '-' : nodepath.resolve(x))) + .default('x', path.resolve(path.join('.', 'out'))) + .coerce('x', (x) => (x === '-' ? '-' : path.resolve(x))) .alias('e', 'schema-extension') .describe('e', 'JSON Schema file extension eg. schema.json or json') @@ -163,8 +227,8 @@ async function main(args) { .alias('i', 'i18n') .describe('i', 'path to a locales folder with JSON files') - .default('i', nodepath.resolve(__dirname, 'locales')) - .coerce('i', (i) => nodepath.resolve(i)) + .default('i', path.resolve(__dirname, 'locales')) + .coerce('i', (i) => path.resolve(i)) .alias('l', 'language') .describe('l', 'the selected language') @@ -197,46 +261,46 @@ async function main(args) { obj, ); - const schemaPath = argv.d; - const out = argv.o; - const meta = argv.m; - const schemaOut = argv.x !== '-' ? argv.x : null; - const includeReadme = !argv.n; - const i18n = argv.i; - const language = argv.l; - const exampleFormat = argv.f; - const includeProperties = argv.p; - const header = argv.h; - const skipProperties = argv.s; + const resolved = await argv; - const schemaExtension = argv.e; + const schemaPath = resolved.d; + const outDir = resolved.o; + const metadata = resolved.m; + const schemaOut = resolved.x !== '-' ? resolved.x : null; + const includeReadme = !resolved.n; + const i18n = resolved.i; + const language = resolved.l; + const exampleFormat = resolved.f; + const includeProperties = resolved.p; + const header = resolved.h; + const skipProperties = resolved.s; - const schemaloader = loader(); + const schemaExtension = resolved.e; // list all schema files in the specified directory - const schemafiles = await readdirp.promise(schemaPath, { root: schemaPath, fileFilter: `*.${schemaExtension}` }); + const schemaFiles = await readdirp.promise(schemaPath, { root: schemaPath, fileFilter: `*.${schemaExtension}` }); - console.log(`loading ${schemafiles.length} schemas`); + console.log(`loading ${schemaFiles.length} schemas`); - // then collect data about the schemas and turn everything into a big object - const loadedschemas = pipe( - schemafiles, - map((schema) => schema.fullPath), - // eslint-disable-next-line import/no-dynamic-require, global-require - map((path) => schemaloader(require(path), path)), - // find contained schemas - traverse, - ); + /** + * @type {{ [name: string]: string }} + * */ + const schemas = {}; + schemaFiles.forEach((schema) => { + schemas[schema.basename] = schema.fullPath; + }); - jsonschema2md(loadedschemas, { + jsonschema2md(schemas, { schemaPath, - out, - meta, + outDir, + metadata, schemaOut, includeReadme, links, i18n, + // @ts-ignore language, + // @ts-ignore exampleFormat, includeProperties, header, diff --git a/lib/keywords.js b/lib/keywords.js index 42d60b4d..47d71ed1 100644 --- a/lib/keywords.js +++ b/lib/keywords.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/lib/locales/update.js b/lib/locales/update.js index 969fbfa5..7f38bde8 100644 --- a/lib/locales/update.js +++ b/lib/locales/update.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/lib/markdownBuilder.js b/lib/markdownBuilder.js index a8bde8c8..a3ecc008 100644 --- a/lib/markdownBuilder.js +++ b/lib/markdownBuilder.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -28,7 +28,7 @@ function build({ header, links = {}, includeProperties = [], - rewritelinks = (x) => x, + rewriteLinks = (x) => x, exampleFormat = 'json', skipProperties = [], } = {}) { @@ -268,7 +268,7 @@ function build({ && typeof schema[s.meta][prop.name] === 'object' && schema[s.meta][prop.name].link && schema[s.meta][prop.name].text) { - return tableCell(link(rewritelinks(schema[s.meta][prop.name].link), i18n`open original schema`, [text(schema[s.meta][prop.name].text)])); + return tableCell(link(rewriteLinks(schema[s.meta][prop.name].link), i18n`open original schema`, [text(schema[s.meta][prop.name].text)])); } const value = schema[s.meta] ? schema[s.meta][prop.name] : undefined; return tableCell(text(prop[`${String(value)}label`] || i18n`Unknown`)); diff --git a/lib/readmeBuilder.js b/lib/readmeBuilder.js index f609b56f..501986e2 100644 --- a/lib/readmeBuilder.js +++ b/lib/readmeBuilder.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/lib/schemaProxy.js b/lib/schemaProxy.js index 3c4d672a..98e110ef 100644 --- a/lib/schemaProxy.js +++ b/lib/schemaProxy.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -9,7 +9,8 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -const ghslugger = require('github-slugger'); +// @ts-check +const GhSlugger = require('github-slugger'); const path = require('path'); const { URL } = require('url'); const { formatmeta } = require('./formatInfo'); @@ -30,13 +31,14 @@ function loadExamples(file, num = 1) { } const handler = ({ - root = '', filename = '.', schemas, parent, slugger, + root = '', inputpath = '.', filename = '.', schemas, parent = null, slugger, }) => { const meta = {}; meta[symbols.parent] = () => parent; meta[symbols.pointer] = () => root; meta[symbols.filename] = () => filename; + meta[symbols.inputpath] = () => inputpath; meta[symbols.id] = (target) => { // if the schema has it's own ID, use it if (target[keyword`$id`]) { @@ -62,7 +64,7 @@ const handler = ({ }; meta[symbols.resolve] = (target, prop, receiver) => (proppath) => { - // console.log('trying to resolve', proppath, 'in', receiver[symbols.filename]); + // console.log('trying to resolve', proppath, 'in', receiver[symbols.fullpath]); if (proppath === undefined) { return receiver; } @@ -78,7 +80,7 @@ const handler = ({ meta[symbols.slug] = (target, prop, receiver) => { if (!receiver[myslug] && !parent && receiver[symbols.filename]) { // eslint-disable-next-line no-param-reassign - receiver[myslug] = slugger.slug(path.basename(receiver[symbols.filename]).replace(/\..*$/, '')); + receiver[myslug] = slugger.slug(receiver[symbols.filename].replace(/\..*$/, '')); } if (!receiver[myslug]) { const parentslug = parent[symbols.slug]; @@ -107,12 +109,12 @@ const handler = ({ const retval = Reflect.get(target, prop, receiver); if (retval === undefined && prop === keyword`examples` && !receiver[symbols.parent]) { - return loadExamples(receiver[symbols.filename], 1); + return loadExamples(receiver[symbols.inputpath], 1); } if (typeof retval === 'object' && retval !== null) { if (retval[keyword`$ref`]) { const [uri, pointer] = retval.$ref.split('#'); - // console.log('resolving ref', uri, pointer, 'from', receiver[symbols.filename]); + // console.log('resolving ref', uri, pointer, 'from', receiver[symbols.fullpath]); const basedoc = uri || receiver[symbols.id]; let referenced = null; @@ -121,7 +123,7 @@ const handler = ({ if (schemas.known[basedoc]) { referenced = schemas.known[basedoc][symbols.resolve](pointer); } else if (path.parse(basedoc)) { - const basepath = path.dirname(meta[symbols.filename]()); + const basepath = path.dirname(meta[symbols.inputpath]()); let reldoc = uri; // if uri is a URI then only try to resolve it locally if the scheme is 'file:' @@ -160,6 +162,7 @@ const handler = ({ const subschema = new Proxy(retval, handler({ root: `${root}/${prop}`, parent: receiver, + inputpath, filename, schemas, slugger, @@ -188,18 +191,22 @@ module.exports.loader = () => { files: {}, }; - const slugger = ghslugger(); + const slugger = new GhSlugger(); - return (filename, schema) => { - // console.log('loading', filename); - const proxied = new Proxy(schema, handler({ filename, schemas, slugger })); + return (name, schema) => { + // console.log('loading', name); + const filename = path.basename(name); + const inputpath = name === filename ? undefined : name; + const proxied = new Proxy(schema, handler({ + filename, inputpath, schemas, slugger, + })); schemas.loaded.push(proxied); if (proxied[keyword`$id`]) { // stow away the schema for lookup schemas.known[proxied[keyword`$id`]] = proxied; } - schemas.files[filename] = proxied; + schemas.files[inputpath || filename] = proxied; return proxied; }; diff --git a/lib/symbols.js b/lib/symbols.js index 522f5518..5da05723 100644 --- a/lib/symbols.js +++ b/lib/symbols.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -9,9 +9,20 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ + +/** + * @typedef {import("../types/api").UniqueSymbols} UniqueSymbols + */ + +/** + * @type {UniqueSymbols} + * */ const symbols = { pointer: Symbol('pointer'), filename: Symbol('filename'), + inputpath: Symbol('inputpath'), + outputcontent: Symbol('outputcontent'), + outputpath: Symbol('outputpath'), id: Symbol('id'), titles: Symbol('titles'), resolve: Symbol('resolve'), diff --git a/lib/traverseSchema.js b/lib/traverseSchema.js index 615a70e7..9f70404b 100644 --- a/lib/traverseSchema.js +++ b/lib/traverseSchema.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/lib/writeMarkdown.js b/lib/writeMarkdown.js index 212e4403..87908d8c 100644 --- a/lib/writeMarkdown.js +++ b/lib/writeMarkdown.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -9,6 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ +// @ts-check const { each, pairs } = require('ferrum'); const stringify = require('remark-stringify'); const unified = require('unified'); @@ -16,70 +17,122 @@ const gfm = require('remark-gfm'); const path = require('path'); const fs = require('fs-extra'); const yaml = require('js-yaml'); +const s = require('./symbols'); -function writemarkdown({ +/** + * @typedef {import("../types/api").JsonSchema} JsonSchema + */ +/** + * @typedef {import("../types/api").MarkdownAst} MarkdownAst + */ +/** + * @typedef {import("../types/api").ExtendedMarkdownAst} ExtendedMarkdownAst + */ +/** + * @typedef {import("../types/api").ReadmeMarkdownAst} ReadmeMarkdownAst + */ +/** + * @typedef {{ [name: string]: MarkdownAst }} MarkdownAstFiles + */ + +/** + * Write the Markdown to filesystem or an object + * @param {Object} options + * @param {string} [options.out] - (optional) Directory where the files will be saved + * @param {Function} [options.error] - (optional) Error logger + * @param {Function} [options.info] - (optional) Info logger + * @param {Function} [options.debug] - (optional) Debug logger + * @param {{ [key: string]: string }} [options.meta] - (optional) Metadata to be added to Markdown + * @returns {(astFiles: MarkdownAstFiles) => ExtendedMarkdownAst[]} + */ +function writeMarkdown({ out, error, meta, }) { const processor = unified() .use(gfm) .use(stringify); - fs.mkdirpSync(out); - - return (schemas) => { - each(pairs(schemas), ([name, markdown]) => { - const fileName = path.resolve(out, `${name}.md`); + return (astFiles) => { + /** @type {ExtendedMarkdownAst[]} */ + const output = []; + each(pairs(astFiles), (tuple) => { + /** @type {[ string, MarkdownAst ]} */ + const [name, markdownAst] = tuple; // add YAML frontmatter - const output = (!meta ? '' : '---\n') + const content = (!meta ? '' : '---\n') + (!meta ? '' : yaml.dump(meta)) + (!meta ? '' : '---\n\n') - + processor.stringify(markdown); + + processor.stringify(markdownAst); - fs.writeFile(fileName, output, (err) => { - if (err) { + const filename = `${name}.md`; + let outputPath; + if (out) { + outputPath = path.resolve(out, filename); + try { + fs.outputFileSync(outputPath, content); + } catch (err) { error(err); } - // info(`${fileName} created`); + // info(`${filename} created`); + } + output.push({ + ...markdownAst, + [s.filename]: filename, + [s.outputcontent]: content, + [s.outputpath]: outputPath, }); }); - return schemas; + return output; }; } -function writereadme({ - out, error, info, meta, readme, +/** + * Write the Readme Markdown to filesystem or an object + * @param {Object} options + * @param {string} [options.out] - (optional) Directory where the files will be saved + * @param {Function} [options.error] - (optional) Error logger + * @param {Function} [options.info] - (optional) Info logger + * @param {Function} [options.debug] - (optional) Debug logger + * @param {{ [key: string]: string }} [options.meta] - (optional) Metadata to be added to Markdown + * @returns {(markdownAst: MarkdownAst) => ReadmeMarkdownAst} + */ +function writeReadme({ + out, error, info, meta, }) { const processor = unified() .use(gfm) .use(stringify); - if (readme) { - fs.mkdirpSync(out); + return (markdownAst) => { + // add YAML frontmatter + const content = (!meta ? '' : '---\n') + + (!meta ? '' : yaml.dump(meta)) + + (!meta ? '' : '---\n\n') - return (readmeast) => { - const fileName = path.resolve(out, 'README.md'); - // add YAML frontmatter - const output = (!meta ? '' : '---\n') - + (!meta ? '' : yaml.dump(meta)) - + (!meta ? '' : '---\n\n') - - + processor.stringify(readmeast); + + processor.stringify(markdownAst); - fs.writeFile(fileName, output, (err) => { - if (err) { - error(err); - } - info(`${fileName} created`); - }); + const filename = 'README.md'; + let outputPath; + if (out) { + outputPath = path.resolve(out, filename); + try { + fs.outputFileSync(outputPath, content); + } catch (err) { + error(err); + } + info(`${filename} created`); + } - return fileName; + return { + ...markdownAst, + [s.outputcontent]: content, + [s.filename]: filename, + [s.outputpath]: outputPath, }; - } else { - return (args) => args; - } + }; } -module.exports = { writemarkdown, writereadme }; +module.exports = { writeMarkdown, writeReadme }; diff --git a/lib/writeSchema.js b/lib/writeSchema.js index 75a0e8be..3d9ade2c 100644 --- a/lib/writeSchema.js +++ b/lib/writeSchema.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -9,33 +9,58 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ +// @ts-check const fs = require('fs-extra'); const path = require('path'); const s = require('./symbols'); -function writeSchema({ schemadir, origindir }) { - const targetpath = (filename) => path.resolve(schemadir, path.relative(origindir, filename)); +/** + * @typedef {import("../types/api").JsonSchema} JsonSchema + */ +/** + * @typedef {import("../types/api").ExtendedJsonSchema} ExtendedJsonSchema + */ +/** + * Write the JSON Schemas to filesystem or an object + * @param {Object} options + * @param {string} [options.schemaDir] - (optional) Directory where the files will be saved + * @returns {(schemas: JsonSchema[]) => ExtendedJsonSchema[]} + */ +function writeSchema({ schemaDir }) { return (schemas) => { - console.log('writing schemas to', schemadir); + console.log('preparing schemas'); - const realschemas = Object.values(schemas).filter((schema) => !schema[s.parent]); - realschemas.forEach((schema) => { - // console.log('writing', schema[s.filename], 'to ', targetpath(schema[s.filename])); - const filename = targetpath(schema[s.filename]); - const dirname = path.dirname(filename); + const realSchemas = Object.values(schemas).filter((schema) => !schema[s.parent]); + return realSchemas.map((schema) => { + let content = { ...schema }; + const filename = content[s.filename]; + const inputPath = content[s.inputpath]; - const out = fs.readJSONSync(schema[s.filename]); - if (schema[s.meta] && schema[s.meta].description) { - // copy description from external file - out.description = schema[s.meta].description; + if (inputPath && inputPath !== '.') { + content = fs.readJsonSync(inputPath); + if (content[s.meta] && schema[s.meta].description) { + // copy description from external file + content.description = schema[s.meta].description; + } + if (schema.examples && Array.isArray(schema.examples) && schema.examples.length > 0) { + // copy examples from external files + content.examples = [...schema.examples]; + } } - if (schema.examples && Array.isArray(schema.examples) && schema.examples.length > 0) { - // copy examples from external files - out.examples = [...schema.examples]; + + let outputPath; + if (schemaDir) { + console.log('writing schemas to', schemaDir); + outputPath = path.resolve(schemaDir, filename); + fs.outputJsonSync(outputPath, content); } - fs.mkdirpSync(dirname); - fs.writeJsonSync(filename, out); + return { + ...content, + [s.filename]: filename, + [s.inputpath]: inputPath, + [s.outputpath]: outputPath, + }; }); }; } diff --git a/package-lock.json b/package-lock.json index 7469d23f..74564b01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1039,6 +1039,32 @@ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true }, + "@types/fs-extra": { + "version": "9.0.12", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.12.tgz", + "integrity": "sha512-I+bsBr67CurCGnSenZZ7v94gd3tc3+Aj2taxMT4yu4ABLuOgOjeFxX3dokG24ztSRg5tnT00sL8BszO7gSMoIw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/github-slugger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@types/github-slugger/-/github-slugger-1.3.0.tgz", + "integrity": "sha512-J/rMZa7RqiH/rT29TEVZO4nBoDP9XJOjnbbIofg7GQKs4JIduEO3WLpte+6WeUz/TcrXKlY+bM7FYrp8yFB+3g==", + "dev": true + }, + "@types/js-yaml": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.2.tgz", + "integrity": "sha512-KbeHS/Y4R+k+5sWXEYzAZKuB1yQlZtEghuhRxrVRLaqhtoG5+26JwQsa4HyS3AWX8v1Uwukma5HheduUDskasA==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.8.tgz", + "integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==" + }, "@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -1046,9 +1072,9 @@ "dev": true }, "@types/mdast": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.3.tgz", - "integrity": "sha512-SXPBMnFVQg1s00dlMCc/jCdvPqdE4mXaMMCeRlxLDmTAEoegHT53xKtkDnzDTOcmMHUfcjyf36/YYZ6SxRdnsw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.4.tgz", + "integrity": "sha512-gIdhbLDFlspL53xzol2hVzrXAbzt71erJHoOwQZWssjaiouOotf03lNtMmFm9VfFkvnLWccSVjUAZGQ5Kqw+jA==", "requires": { "@types/unist": "*" } @@ -1058,6 +1084,12 @@ "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz", "integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==" }, + "@types/node": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.0.1.tgz", + "integrity": "sha512-hBOx4SUlEPKwRi6PrXuTGw1z6lz0fjsibcWCM378YxsSu/6+C30L6CR49zIBKHiwNWCYIcOLjg4OHKZaFeLAug==", + "dev": true + }, "@types/normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -1080,6 +1112,21 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==" }, + "@types/yargs": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.2.tgz", + "integrity": "sha512-JhZ+pNdKMfB0rXauaDlrIvm+U7V4m03PPOSVoPS66z8gf+G4Z/UW8UlrVIj2MRQOBzuoEvYtjS0bqYwnpZaS9Q==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "20.2.1", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.1.tgz", + "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", + "dev": true + }, "@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", @@ -5967,6 +6014,11 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, + "json-schema": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.5.tgz", + "integrity": "sha512-gWJOWYFrhQ8j7pVm0EM8Slr+EPVq1Phf6lvzvD/WCeqkrx/f2xBI0xOsRRS9xCn3I4vKtP519dvs3TP09r24wQ==" + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -12643,6 +12695,12 @@ "is-typedarray": "^1.0.0" } }, + "typescript": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", + "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", + "dev": true + }, "uglify-js": { "version": "3.13.6", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.6.tgz", diff --git a/package.json b/package.json index 66dd5e75..a89087c4 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "description": "Validate and document complex JSON Schemas the easy way.", "version": "1.1.1", "main": "lib/index.js", + "typings": "types", "bin": { "jsonschema2md": "./cli.js" }, @@ -14,15 +15,19 @@ "start": "node cli.js", "examples": "node cli.js -d examples/schemas/ -o examples/docs/ -x examples/generated-schemas/", "test": " nyc --reporter=text --reporter=lcov --check-coverage --branches 80 --statements 95 --lines 95 mocha && node lib/locales/update.js", - "test-ci": "nyc --reporter=text --reporter=lcov --check-coverage --branches 80 --statements 95 --lines 95 mocha --reporter xunit --reporter-options output=./junit/test-results.xml; codecov" + "test-ci": "nyc --reporter=text --reporter=lcov --check-coverage --branches 80 --statements 95 --lines 95 mocha --reporter xunit --reporter-options output=./junit/test-results.xml; codecov", + "emit-types": "npx -p typescript tsc" }, "dependencies": { "@adobe/helix-log": "5.0.3", + "@types/json-schema": "^7.0.8", + "@types/mdast": "^3.0.4", "es2015-i18n-tag": "1.6.1", "ferrum": "1.9.2", "fs-extra": "10.0.0", "github-slugger": "1.3.0", "js-yaml": "4.1.0", + "json-schema": "^0.2.5", "mdast-builder": "1.1.1", "mdast-util-to-string": "2.0.0", "readdirp": "3.6.0", @@ -37,6 +42,11 @@ "@semantic-release/changelog": "5.0.1", "@semantic-release/git": "9.0.0", "@semantic-release/github": "7.2.3", + "@types/fs-extra": "^9.0.12", + "@types/github-slugger": "^1.3.0", + "@types/js-yaml": "^4.0.2", + "@types/node": "^16.0.1", + "@types/yargs": "^17.0.2", "codecov": "3.8.2", "commitizen": "4.2.4", "cz-conventional-changelog": "3.3.0", @@ -53,6 +63,7 @@ "mocha": "9.0.2", "nyc": "15.1.0", "semantic-release": "17.4.4", + "typescript": "^4.3.5", "unist-util-select": "3.0.4" }, "engines": { diff --git a/test/api.test.js b/test/api.test.js index fe5f2e8d..7080f7cf 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -14,7 +14,7 @@ const assert = require('assert'); const fs = require('fs-extra'); const path = require('path'); -const { loadschemas } = require('./testUtils'); +const { loadSchemas } = require('./testUtils'); const example = require('./fixtures/example/example.schema.json'); const { jsonschema2md } = require('../lib/index'); @@ -29,10 +29,10 @@ describe('Testing Main API', () => { }); it('Main API processes readme-1 directory', async () => { - const schemas = await loadschemas('readme-1'); + const schemas = await loadSchemas('readme-1'); const res = jsonschema2md(schemas, { schemaPath: path.resolve(__dirname, 'fixtures/readme-1'), - out: 'tmp', + outDir: 'tmp', schemaOut: 'tmp', includeReadme: true, }); @@ -43,13 +43,15 @@ describe('Testing Main API', () => { }); it('Main API processes example schema', async () => { - const res = jsonschema2md(example, { - out: 'tmp', + const res = await jsonschema2md(example, { + // schemaPath, + outDir: 'tmp', includeReadme: true, }); // console.log('done!', res); assert(res === Object(res)); - const readme = await fs.stat(path.resolve(__dirname, '..', 'tmp', 'README.md')); + const readme = await fs.stat(path.resolve(__dirname, '..', 'tmp', 'input.md')); assert.ok(readme.isFile()); + assert.notStrictEqual(readme.size, 0); }); }); diff --git a/test/cyclicSchemaIntegration.test.js b/test/cyclicSchemaIntegration.test.js index 3b85de4e..60f401df 100644 --- a/test/cyclicSchemaIntegration.test.js +++ b/test/cyclicSchemaIntegration.test.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/test/formatting.test.js b/test/formatting.test.js index 2377ff54..550122b3 100644 --- a/test/formatting.test.js +++ b/test/formatting.test.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/test/markdownBuilder.test.js b/test/markdownBuilder.test.js index 6f2b8687..c857327e 100644 --- a/test/markdownBuilder.test.js +++ b/test/markdownBuilder.test.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -11,7 +11,7 @@ */ /* eslint-env mocha */ /* eslint-disable no-unused-expressions */ -const { assertMarkdown, loadSchemas } = require('./testUtils'); +const { assertMarkdown, traverseSchemas } = require('./testUtils'); const build = require('../lib/markdownBuilder'); @@ -19,7 +19,7 @@ describe('Testing Markdown Builder: content', () => { let results; before(async () => { - const schemas = await loadSchemas('content'); + const schemas = await traverseSchemas('content'); const builder = build({ header: false }); results = builder(schemas); }); @@ -45,7 +45,7 @@ describe('Testing Markdown Builder: not', () => { let results; before(async () => { - const schemas = await loadSchemas('not'); + const schemas = await traverseSchemas('not'); const builder = build({ header: false }); results = builder(schemas); }); @@ -66,7 +66,7 @@ describe('Testing Markdown Builder: nullable', () => { let results; before(async () => { - const schemas = await loadSchemas('nullable'); + const schemas = await traverseSchemas('nullable'); const builder = build({ header: false }); results = builder(schemas); }); @@ -81,7 +81,7 @@ describe('Testing Markdown Builder: title', () => { let results; before(async () => { - const schemas = await loadSchemas('title'); + const schemas = await traverseSchemas('title'); const builder = build({ header: false }); results = builder(schemas); }); @@ -101,7 +101,7 @@ describe('Testing Markdown Builder: type', () => { let results; before(async () => { - const schemas = await loadSchemas('type'); + const schemas = await traverseSchemas('type'); const builder = build({ header: false }); results = builder(schemas); }); @@ -123,7 +123,7 @@ describe('Testing Markdown Builder: format', () => { let results; before(async () => { - const schemas = await loadSchemas('format'); + const schemas = await traverseSchemas('format'); const builder = build({ header: false }); results = builder(schemas); }); @@ -150,7 +150,7 @@ describe('Testing Markdown Builder: YAML examples', () => { let results; before(async () => { - const schemas = await loadSchemas('format'); + const schemas = await traverseSchemas('format'); const builder = build({ header: false, exampleFormat: 'yaml' }); results = builder(schemas); }); @@ -171,7 +171,7 @@ describe('Testing Markdown Builder: enums', () => { let results; before(async () => { - const schemas = await loadSchemas('enums'); + const schemas = await traverseSchemas('enums'); const builder = build({ header: true, includeProperties: ['foo', 'bar'] }); results = builder(schemas); }); @@ -195,7 +195,7 @@ describe('Testing Markdown Builder: null', () => { let results; before(async () => { - const schemas = await loadSchemas('null'); + const schemas = await traverseSchemas('null'); const builder = build({ header: true }); results = builder(schemas); }); @@ -212,7 +212,7 @@ describe('Testing Markdown Builder: additionalprops', () => { let results; before(async () => { - const schemas = await loadSchemas('additionalprops'); + const schemas = await traverseSchemas('additionalprops'); const builder = build({ header: true }); results = builder(schemas); }); @@ -236,7 +236,7 @@ describe('Testing Markdown Builder: types', () => { let results; before(async () => { - const schemas = await loadSchemas('types'); + const schemas = await traverseSchemas('types'); const builder = build({ header: true }); results = builder(schemas); }); @@ -271,7 +271,7 @@ describe('Testing Markdown Builder: identifiable', () => { let results; before(async () => { - const schemas = await loadSchemas('identifiable'); + const schemas = await traverseSchemas('identifiable'); const builder = build({ header: true }); results = builder(schemas); }); @@ -287,7 +287,7 @@ describe('Testing Markdown Builder: arrays', () => { let results; before(async () => { - const schemas = await loadSchemas('arrays'); + const schemas = await traverseSchemas('arrays'); const builder = build({ header: true }); results = builder(schemas); }); @@ -330,7 +330,7 @@ describe('Testing Markdown Builder: stringformats', () => { let results; before(async () => { - const schemas = await loadSchemas('stringformats'); + const schemas = await traverseSchemas('stringformats'); const builder = build({ header: true }); results = builder(schemas); }); @@ -356,7 +356,7 @@ describe('Testing Markdown Builder: readme-1', () => { let results; before(async () => { - const schemas = await loadSchemas('readme-1'); + const schemas = await traverseSchemas('readme-1'); const builder = build({ header: true, links: { abstract: 'fooabstract.html' } }); results = builder(schemas); }); @@ -416,7 +416,7 @@ describe('Testing Markdown Builder: Skip properties', () => { let schemas; before(async () => { - schemas = await loadSchemas('skipproperties'); + schemas = await traverseSchemas('skipproperties'); }); it('Skipped properties exist', () => { diff --git a/test/readmeBuilder.test.js b/test/readmeBuilder.test.js index dfa451d4..fd8ca9d2 100644 --- a/test/readmeBuilder.test.js +++ b/test/readmeBuilder.test.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -12,7 +12,7 @@ /* eslint-env mocha */ /* eslint-disable no-unused-expressions */ const assert = require('assert'); -const { assertMarkdown, loadSchemas } = require('./testUtils'); +const { assertMarkdown, traverseSchemas } = require('./testUtils'); const build = require('../lib/readmeBuilder'); const { loader } = require('../lib/schemaProxy'); @@ -28,7 +28,7 @@ describe('Testing Readme Builder', () => { }); it('Readme Builder builds a README for type', async () => { - const schemas = await loadSchemas('type'); + const schemas = await traverseSchemas('type'); const builder = build({ readme: true }); const result = builder(schemas); @@ -38,7 +38,7 @@ describe('Testing Readme Builder', () => { }); it('Readme Builder builds a medium README for multiple Schemas', async () => { - const schemas = await loadSchemas('readme-1'); + const schemas = await traverseSchemas('readme-1'); const builder = build({ readme: true }); const result = builder(schemas); diff --git a/test/schemaProxy.test.js b/test/schemaProxy.test.js index 7a7c9fb3..4cd96233 100644 --- a/test/schemaProxy.test.js +++ b/test/schemaProxy.test.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -113,9 +113,11 @@ describe('Testing Schema Proxy', () => { const myloader = loader(); const proxied1 = myloader('example.schema.json', example); const proxied2 = myloader('referencing.schema.json', referencing); - const proxied3 = myloader({ + const proxied3 = myloader('anotherreference.schema.json', { title: 'Referencing', - }, 'anotherreference.schema.json'); + }); + + console.log(proxied1[slug], JSON.stringify(proxied1)); assert.equal(proxied1[slug], 'example'); diff --git a/test/testUtils.js b/test/testUtils.js index e0b2e6ae..50b0b168 100644 --- a/test/testUtils.js +++ b/test/testUtils.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -74,14 +74,25 @@ ${inspect(node)}`); } async function loadSchemas(dir) { - const schemaloader = loader(); const schemaDir = path.resolve(__dirname, 'fixtures', dir); - const schemas = await readdirp.promise(schemaDir, { fileFilter: '*.schema.json' }); + const schemaFiles = await readdirp.promise(schemaDir, { fileFilter: '*.schema.json' }); + + const schemas = {}; + schemaFiles.forEach((schema) => { + schemas[schema.basename] = schema.fullPath; + }); + return schemas; +} + +async function traverseSchemas(dir) { + const schemas = await loadSchemas(dir); + const entries = Object.entries(schemas); + const schemaloader = loader(); - return traverse(schemas.map(({ fullPath }) => schemaloader( + return traverse(entries.map(([name, fullPath]) => schemaloader( // eslint-disable-next-line global-require, import/no-dynamic-require - fullPath, require(fullPath), + name, require(fullPath), ))); } -module.exports = { assertMarkdown, loadSchemas }; +module.exports = { assertMarkdown, loadSchemas, traverseSchemas }; diff --git a/test/testintegration.test.js b/test/testintegration.test.js index 42a82be9..09933b46 100644 --- a/test/testintegration.test.js +++ b/test/testintegration.test.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -54,6 +54,7 @@ describe('Integration Test', () => { console.log('done!', res); const readme = await fs.stat(path.resolve(__dirname, '..', 'tmp', 'README.md')); assert.ok(readme.isFile()); + assert.notStrictEqual(readme.size, 0); })); ['json-logic-js/schemas'].forEach((dir) => it(`CLI processes ${dir} directory`, async () => { @@ -61,5 +62,6 @@ describe('Integration Test', () => { console.log('done!', res); const readme = await fs.stat(path.resolve(__dirname, '..', 'tmp', 'README.md')); assert.ok(readme.isFile()); + assert.notStrictEqual(readme.size, 0); })); }); diff --git a/test/traverseSchema.test.js b/test/traverseSchema.test.js index 9b6021f7..07f6908e 100644 --- a/test/traverseSchema.test.js +++ b/test/traverseSchema.test.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/test/zSchemaCoverage.test.js b/test/zSchemaCoverage.test.js index f7de61cc..09101f25 100644 --- a/test/zSchemaCoverage.test.js +++ b/test/zSchemaCoverage.test.js @@ -1,5 +1,5 @@ /* - * Copyright 2019 Adobe. All rights reserved. + * Copyright 2021 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..37f0a9ad --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es6", + "moduleResolution": "node", + "allowJs": true, + "declaration": true, + "emitDeclarationOnly": true, + "outFile": "./types/index.d.ts" + }, + "include": ["lib/index.js"], + "exclude": ["**/locales/**/*"] +} diff --git a/types/api.d.ts b/types/api.d.ts new file mode 100644 index 00000000..65f72db6 --- /dev/null +++ b/types/api.d.ts @@ -0,0 +1,66 @@ +/* + * Copyright 2021 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { JSONSchema4, JSONSchema6, JSONSchema7 } from "json-schema"; +import { Root } from "mdast"; +import { filename, inputpath, outputcontent, outputpath } from "../lib/symbols"; + +export type UniqueSymbols = { + readonly pointer: unique symbol; + readonly filename: unique symbol; + readonly inputpath: unique symbol; + readonly outputcontent: unique symbol; + readonly outputpath: unique symbol; + readonly id: unique symbol; + readonly titles: unique symbol; + readonly resolve: unique symbol; + readonly slug: unique symbol; + readonly meta: unique symbol; + readonly parent: unique symbol; +}; + +export type JsonSchema = JSONSchema4 | JSONSchema6 | JSONSchema7; +export type SchemaFiles = { + [name: string]: string | JsonSchema; +}; +export type ExtendedJsonSchema = JsonSchema & { + [filename]: string; + [inputpath]?: string; + [outputpath]?: string; +}; + +export type MarkdownAst = Root; +export type ExtendedMarkdownAst = MarkdownAst & { + [filename]: string; + [outputcontent]: string; + [outputpath]?: string; +}; +export type ReadmeMarkdownAst = ExtendedMarkdownAst & { + [filename]: "README.md"; +}; + +/** + * GeneratedOutput + */ +export type GeneratedOutput = { + /** + * Input JSON schemas + */ + schema: ExtendedJsonSchema[]; + /** + * Ouput Readme file + */ + readme: ReadmeMarkdownAst; + /** + * Output markdown data + */ + markdown: ExtendedMarkdownAst[]; +}; diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 00000000..914618fa --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,232 @@ +declare module "symbols" { + export = symbols; + /** + * @typedef {import("../types/api").UniqueSymbols} UniqueSymbols + */ + /** + * @type {UniqueSymbols} + * */ + const symbols: UniqueSymbols; + namespace symbols { + export { UniqueSymbols }; + } + type UniqueSymbols = import("../types/api").UniqueSymbols; +} +declare module "traverseSchema" { + export = traverse; + /** + * Traverses a Schema node (containing a JSON Schema) to find sub-schemas, + * which are then emitted as asequence. + * @param {Schema} node + */ + function traverse(schemas: any): any[]; +} +declare module "formattingTools" { + export function gentitle(titles: any, type: any): any; + export function gendescription(schema: any): any; +} +declare module "keywords" { + export function keyword(str: any): any; + export function report(): Set; +} +declare module "markdownBuilder" { + export = build; + function build({ header, links, includeProperties, rewriteLinks, exampleFormat, skipProperties, }?: { + header: any; + links?: {}; + includeProperties?: any[]; + rewriteLinks?: (x: any) => any; + exampleFormat?: string; + skipProperties?: any[]; + }): (schemas: any) => any; +} +declare module "writeMarkdown" { + export type JsonSchema = import("../types/api").JsonSchema; + export type MarkdownAst = import("../types/api").MarkdownAst; + export type ExtendedMarkdownAst = import("../types/api").ExtendedMarkdownAst; + export type ReadmeMarkdownAst = import("../types/api").ReadmeMarkdownAst; + export type MarkdownAstFiles = { + [name: string]: import("mdast").Root; + }; + /** + * @typedef {import("../types/api").JsonSchema} JsonSchema + */ + /** + * @typedef {import("../types/api").MarkdownAst} MarkdownAst + */ + /** + * @typedef {import("../types/api").ExtendedMarkdownAst} ExtendedMarkdownAst + */ + /** + * @typedef {import("../types/api").ReadmeMarkdownAst} ReadmeMarkdownAst + */ + /** + * @typedef {{ [name: string]: MarkdownAst }} MarkdownAstFiles + */ + /** + * Write the Markdown to filesystem or an object + * @param {Object} options + * @param {string} [options.out] - (optional) Directory where the files will be saved + * @param {Function} [options.error] - (optional) Error logger + * @param {Function} [options.info] - (optional) Info logger + * @param {Function} [options.debug] - (optional) Debug logger + * @param {{ [key: string]: string }} [options.meta] - (optional) Metadata to be added to Markdown + * @returns {(astFiles: MarkdownAstFiles) => ExtendedMarkdownAst[]} + */ + export function writeMarkdown({ out, error, meta, }: { + out?: string; + error?: Function; + info?: Function; + debug?: Function; + meta?: { + [key: string]: string; + }; + }): (astFiles: MarkdownAstFiles) => ExtendedMarkdownAst[]; + /** + * Write the Readme Markdown to filesystem or an object + * @param {Object} options + * @param {string} [options.out] - (optional) Directory where the files will be saved + * @param {Function} [options.error] - (optional) Error logger + * @param {Function} [options.info] - (optional) Info logger + * @param {Function} [options.debug] - (optional) Debug logger + * @param {{ [key: string]: string }} [options.meta] - (optional) Metadata to be added to Markdown + * @returns {(markdownAst: MarkdownAst) => ReadmeMarkdownAst} + */ + export function writeReadme({ out, error, info, meta, }: { + out?: string; + error?: Function; + info?: Function; + debug?: Function; + meta?: { + [key: string]: string; + }; + }): (markdownAst: MarkdownAst) => ReadmeMarkdownAst; +} +declare module "readmeBuilder" { + export = build; + /** + * Generate the README.md + * @param {object} opts + */ + function build({ readme }: object): (schemas: any) => import("unist").Parent; +} +declare module "formatInfo" { + export function formatmeta(schema: any): { + longcomment: import("unist").Node; + shortcomment: any; + comment: string; + longdescription: import("unist").Node; + shortdescription: any; + description: any; + abstract: boolean; + extensible: boolean; + status: any; + identifiable: string; + custom: boolean; + additional: boolean; + definedin: { + text: any; + link: any; + }; + restrictions: string; + }; +} +declare module "schemaProxy" { + const _exports: { + pointer: unique symbol; + filename: unique symbol; + inputpath: unique symbol; + outputcontent: unique symbol; + outputpath: unique symbol; + id: unique symbol; + titles: unique symbol; + resolve: unique symbol; + slug: unique symbol; + meta: unique symbol; + parent: unique symbol; + loader: () => (name: any, schema: any) => any; + }; + export = _exports; + export function loader(): (name: any, schema: any) => any; +} +declare module "writeSchema" { + export type JsonSchema = import("../types/api").JsonSchema; + export type ExtendedJsonSchema = import("../types/api").ExtendedJsonSchema; + /** + * @typedef {import("../types/api").JsonSchema} JsonSchema + */ + /** + * @typedef {import("../types/api").ExtendedJsonSchema} ExtendedJsonSchema + */ + /** + * Write the JSON Schemas to filesystem or an object + * @param {Object} options + * @param {string} [options.schemaDir] - (optional) Directory where the files will be saved + * @returns {(schemas: JsonSchema[]) => ExtendedJsonSchema[]} + */ + export function writeSchema({ schemaDir }: { + schemaDir?: string; + }): (schemas: JsonSchema[]) => ExtendedJsonSchema[]; +} +declare module "index" { + export type JsonSchema = import("../types/api").JsonSchema; + export type SchemaFiles = import("../types/api").SchemaFiles; + export type GeneratedOutput = import("../types/api").GeneratedOutput; + /** + * @typedef {import("../types/api").JsonSchema} JsonSchema + */ + /** + * @typedef {import("../types/api").SchemaFiles} SchemaFiles + */ + /** + * @typedef {import("../types/api").GeneratedOutput} GeneratedOutput + */ + /** + * Public API for jsonschema2md that can be used to turn JSON Schema files + * into readable Markdown documentation. + * @param {JsonSchema | SchemaFiles} schema JSON Schema input to get data from + * @param {Object} options Additional options for generation + * @param {string} [options.schemaPath] - (optional) Path to directory containing all JSON Schemas + * or a single JSON Schema file. This will be considered as the baseURL. + * @param {string} [options.outDir] - (optional) Path to output directory. Generating files will + * be skipped if directory is not specified. + * @param {{ [key:string]: string }} [options.metadata] - (optional) Add metadata elements to + * .md files. + * @param {string} [options.schemaOut] - (optional) Output JSON Schema files including + * description and validated examples in the specified folder. + * @param {boolean} [options.includeReadme=true] - (optional) Generate a README.md file in the + * output directory. + * @param {{ [key:string]: string }[]} [options.links] - (optional) Add this file as a link + * explaining the specified attribute. + * @param {string} [options.i18n="locales/"] - (optional) Path to a locales folder with + * JSON files used for internationalization. + * @param {"en_US" | "de"} [options.language="en_US"] - (optional) Selected language. + * @param {"json" | "yaml"} [options.exampleFormat="json"] - (optional) How to format examples. + * @param {string[]} [options.includeProperties=[]] - (optional) Name of custom properties + * which should be also in the description of an element. + * @param {boolean} [options.header=true] - (optional) Whether or not to include the header + * in markdown. + * @param {string[]} [options.skipProperties=[]] - (optional) Name of a default property to + * skip in markdown. + * @returns {GeneratedOutput} List of raw markdown that were generated from input schema. + */ + export function jsonschema2md(schema: JsonSchema | SchemaFiles, { schemaPath, outDir, metadata, schemaOut, includeReadme, links, i18n, language, exampleFormat, includeProperties, header, skipProperties, }: { + schemaPath?: string; + outDir?: string; + metadata?: { + [key: string]: string; + }; + schemaOut?: string; + includeReadme?: boolean; + links?: { + [key: string]: string; + }[]; + i18n?: string; + language?: "en_US" | "de"; + exampleFormat?: "json" | "yaml"; + includeProperties?: string[]; + header?: boolean; + skipProperties?: string[]; + }): GeneratedOutput; + export function main(args: any): Promise; +}