From 9fb6da16a1549c2f3984c8de54cc0f0776a4751b Mon Sep 17 00:00:00 2001 From: Alexander Ivanov Date: Wed, 8 Nov 2023 18:43:24 +0300 Subject: [PATCH] v0.8.0 --- CHANGELOG.md | 8 ++-- README.md | 2 +- lib/forge.js | 5 +- modules/handyman/repairkit.js | 5 +- modules/types/README.md | 89 ++++++++++++++++++++++------------- modules/types/index.js | 31 ++++++------ modules/types/index.test.js | 64 +++++++++++++------------ modules/types/types.js | 22 +++++---- modules/types/utils.js | 11 ++++- package-lock.json | 4 +- package.json | 5 +- tests/custom.test.js | 9 +++- types/index.d.ts | 9 +++- 13 files changed, 161 insertions(+), 103 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73aed2c..cedd7f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,15 +6,15 @@ ## [1.0.0][] - 2023-11-00 - Release version +--> -## [0.9.0][] - 2023-11-00 + +- Code quality improvements --> -## [0.8.0][] - 2023-11-09 +## [0.8.0][] - 2023-11-08 - JSDOC generation for metatype module, [issue](https://github.com/astrohelm/metaforge/issues/11) - Metatest optional default export diff --git a/README.md b/README.md index 33efd14..db50a65 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

MetaForge v0.7.0 🕵️

+

MetaForge v0.8.0 🕵️

## Describe your data structures by subset of JavaScript and: diff --git a/lib/forge.js b/lib/forge.js index 5eba5d2..67b1649 100644 --- a/lib/forge.js +++ b/lib/forge.js @@ -22,7 +22,10 @@ module.exports = function Forge(schema, custom = {}) { const meta = plan.$meta; if (plan.$id) this.$id = plan.$id; [this.$required, this.$type] = [plan.$required ?? true, name]; - if (meta && typeof meta === 'object' && !Array.isArray(meta)) Object.assign(this, meta); + if (meta && typeof meta === 'object' && !Array.isArray(meta)) { + this.$meta = meta; + Object.assign(this, meta); + } [...before, ...chain, ...after].forEach(proto => proto.call(this, plan, schema.tools)); if (!this.$kind) this.$kind = 'unknown'; }; diff --git a/modules/handyman/repairkit.js b/modules/handyman/repairkit.js index aa7be53..0cbdc40 100644 --- a/modules/handyman/repairkit.js +++ b/modules/handyman/repairkit.js @@ -46,7 +46,7 @@ module.exports = function RepairKit(schema, namespace) { function object(plan, warn) { if (typeof plan.$calc === 'function') return func(plan, warn); - const { $required = true, $meta, $id, ...fields } = plan; //? Schema wrapper #2 + const { $required = true, $id, ...fields } = plan; //? Schema wrapper #2 if (plan.constructor.name === 'Schema') return { $type: 'schema', schema: plan, $required }; if (!plan || plan.constructor.name !== 'Object') return unknown; if ($id) return { $type: 'schema', $id, schema: child(fields), $required }; @@ -55,7 +55,8 @@ module.exports = function RepairKit(schema, namespace) { warn({ cause: TYPE_NOT_FOUND + plan.$type, sample: plan.$type, plan }); return unknown; } - const result = { $type: 'object', properties: { ...fields }, $required }; + const { $meta, ...other } = fields; + const result = { $type: 'object', properties: { ...other }, $required }; if ($meta) result.$meta = $meta; return result; } diff --git a/modules/types/README.md b/modules/types/README.md index cf02ebb..b4a59a3 100644 --- a/modules/types/README.md +++ b/modules/types/README.md @@ -1,25 +1,28 @@ # Metatype module -Generate type annotation from schema; - -> Warning: You will receive compressed version; +Generate type annotation & jsdoc from schema; ## Usage -By default module runs in mjs mode, that means that: - -- It will export all schemas with $id field & root schema -- it will export as default root schema - -> In cjs mode, it will export only root schema - ```js const plan = 'string'; const schema = new Schema(plan); -schema.dts('Example', { mode: 'mjs' }); +schema.dts('Example'); // Equal to: +schema.dts('Example', { export: { type: 'mjs', mode: 'all' } }); +// type Example = string; +// export type = { Example }; +// export default Example; +schema.dts('Example', { export: { type: 'mjs', mode: 'no' } }); +// type Example = string; +schema.dts('Example', { export: { type: 'mjs', mode: 'exports-only' } }); // type Example = string; // export type = { Example }; +schema.dts('Example', { export: { type: 'mjs', mode: 'default-only' } }); +// type Example = string; // export default Example; +schema.dts('Example', { export: { type: 'cjs' } }); +// type Example = string; +// export = Example; ``` ## Example @@ -28,24 +31,39 @@ schema.dts('Example', { mode: 'mjs' }); ```js { + $meta: { '@name': 'User', '@description': 'User data' } "firstName": 'string', - "lastName": 'string', + "lastName": { $type: '?string', $meta: { '@description': 'optional' } }, "label": ["member", "guest", "vip"] "age": '?number', - settings: { alertLevel: 'string', $id: 'Setting' } + settings: { + $id: 'Setting', + alertLevel: 'string', + $meta: { '@description': 'User settings' } + } } ``` -### Output (mjs mode): +### Output: ```ts +/** + * @description User settings + */ interface Settings { alertLevel: string; } +/** + * @name User + * @description User data + */ interface Example { firstName: string; - lastName: string; + /** + * @description optional + */ + lastName?: string; label: 'member' | 'guest' | 'vip'; age?: number; settings: Settings; @@ -55,29 +73,13 @@ export type { Example }; export default Example; ``` -### Output (cjs mode): - -```ts -interface Settings { - alertLevel: string; -} - -interface Example { - firstName: string; - lastName: string; - label: 'member' | 'guest' | 'vip'; - settings: Settings; - age?: number; -} - -export = Example; -``` - ## Writing custom prototypes with Metatype By default all custom types will recieve unknown type; If you want to have custom type, you may create custom prototype with toTypescript field; +> If your prototype has children prototypes, it not be handled with jsdoc comments; + ```js function Date(plan, tools) { this.toTypescript = (name, namespace) => { @@ -89,6 +91,27 @@ function Date(plan, tools) { //? You can return only name or value that can be assigned to type return name; // Equal to: return 'Date'; + //? Returned value will be assigned to other type or will be exported if it was on top }; } ``` + +## JSDOC + +To have JSDOC comments in your type annotations you need to add \$meta field to your +schema; Also, your \$meta properties should start with @; + +### Example + +```js +({ + reciever: 'number', + money: 'number', + $meta: { + '@version': 'v1', + '@name': 'Pay check', + '@description': 'Cash settlement', + '@example': 'Check example\n{ money: 100, reciever: 2 }', + }, +}); +``` diff --git a/modules/types/index.js b/modules/types/index.js index a1f841a..0854ced 100644 --- a/modules/types/index.js +++ b/modules/types/index.js @@ -1,6 +1,6 @@ 'use strict'; -const { nameFix } = require('./utils'); +const { nameFix, jsdoc } = require('./utils'); const types = require('./types'); module.exports = schema => { @@ -11,26 +11,25 @@ module.exports = schema => { this.toTypescript = (name, namespace) => compile(nameFix(name), namespace); }); - // TODO: Default export removable - // TODO: JSDOC documentation schema.dts = (name = 'MetaForge', options = {}) => { - const mode = options.mode ?? 'mjs'; - // const defaultExport = options.defaultExport ?? true; + const [exportMode, exportType] = [options.export?.mode ?? 'all', options.export?.type ?? 'mjs']; if (name !== nameFix(name)) throw new Error('Invalid name format'); const namespace = { definitions: new Set(), exports: new Set() }; const type = schema.toTypescript(name, namespace); + namespace.exports.add(name); if (type !== name) { - if (namespace.exports.size === 1) { - const definitions = Array.from(namespace.definitions).join(''); - if (mode === 'cjs') return definitions + `export = ${type}`; - return definitions + `export type ${name}=${type};export default ${name};`; - } - namespace.definitions.add(`type ${name}=${type};`); + const meta = schema.$meta; + namespace.definitions.add(`${meta ? jsdoc(meta) : ''}type ${name} = ${type};`); } - namespace.exports.add(name); - const definitions = Array.from(namespace.definitions).join(''); - if (mode === 'cjs') return definitions + `export = ${name};`; - const exports = `export type{${Array.from(namespace.exports).join(',')}};`; - return definitions + exports + `export default ${name};`; + let result = Array.from(namespace.definitions).join('\n\n'); + if (exportMode === 'no') return result; + if (exportMode !== 'default-only' && exportType === 'mjs') { + result += `\nexport type { ${Array.from(namespace.exports).join(', ')} };`; + } + if (exportMode !== 'exports-only') { + if (exportType === 'mjs') return result + `\nexport default ${name};`; + return result + `\nexport = ${name};`; + } + return result; }; }; diff --git a/modules/types/index.test.js b/modules/types/index.test.js index 40b56a0..8045cd2 100644 --- a/modules/types/index.test.js +++ b/modules/types/index.test.js @@ -5,67 +5,66 @@ const [test, assert] = [require('node:test'), require('node:assert')]; const Schema = require('../../'); const generate = (type, name) => new Schema(type).dts(name); -const base = 'type MetaForge='; -const exp = 'export type{MetaForge};export default MetaForge;'; +const base = 'type MetaForge = '; +const exp = '\nexport type { MetaForge };\nexport default MetaForge;'; test('[DTS] Basic', () => { assert.strictEqual(generate({ $type: 'string' }), base + 'string;' + exp); assert.strictEqual(generate('number'), base + 'number;' + exp); assert.strictEqual(generate('bigint'), base + 'bigint;' + exp); assert.strictEqual(generate('boolean'), base + 'boolean;' + exp); assert.strictEqual(generate('unknown'), base + 'unknown;' + exp); - assert.strictEqual(generate('?any'), base + '(any|null|undefined);' + exp); + assert.strictEqual(generate('?any'), base + '(any|undefined);' + exp); }); test('[DTS] Enumerable', () => { assert.strictEqual(generate(['hello', 'world']), base + "('hello'|'world');" + exp); const data = ['hello', 'there', 'my', 'dear', 'world']; - const result = `type MetaForge='hello'|'there'|'my'|'dear'|'world';`; + const result = `type MetaForge = 'hello'|'there'|'my'|'dear'|'world';`; assert.strictEqual(generate(data), result + exp); }); test('[DTS] Union', () => { assert.strictEqual( generate({ $type: 'union', types: ['string', '?number'] }), - 'type MetaForge=(string|(number|null|undefined));' + exp, + 'type MetaForge = (string|(number|undefined));' + exp, ); assert.strictEqual( generate({ $type: 'union', types: [{ $type: 'union', types: ['string', '?number'] }] }), - 'type MetaForge=((string|(number|null|undefined)));' + exp, + 'type MetaForge = ((string|(number|undefined)));' + exp, ); }); test('[DTS] Array', () => { assert.strictEqual( generate(['string', '?number']), - 'type MetaForge=[string,(number|null|undefined)];' + exp, + 'type MetaForge = [string,(number|undefined)];' + exp, ); assert.strictEqual( generate({ $type: 'set', items: ['string', '?number'] }), - 'type MetaForge=Set;' + exp, + 'type MetaForge = Set;' + exp, ); assert.strictEqual( generate({ $type: 'array', items: { $type: 'union', types: ['string', '?number'] } }), - 'type MetaForge=((string|(number|null|undefined)))[];' + exp, + 'type MetaForge = ((string|(number|undefined)))[];' + exp, ); assert.strictEqual( generate({ $type: 'tuple', items: { $type: 'union', types: ['string', '?number'] } }), - 'type MetaForge=[(string|(number|null|undefined))];' + exp, + 'type MetaForge = [(string|(number|undefined))];' + exp, ); const enumerable = ['hello', 'there', 'my', 'dear', 'world']; const complex = ['?number', enumerable, { a: 'string', b: enumerable }]; - let result = "type MetaForge_1='hello'|'there'|'my'|'dear'|'world';"; - result += "type MetaForge_2_b='hello'|'there'|'my'|'dear'|'world';"; - result += 'interface MetaForge_2{a:string;b:MetaForge_2_b;};'; - result += 'type MetaForge=[(number|null|undefined),MetaForge_1,MetaForge_2];' + exp; + let result = "type MetaForge_1 = 'hello'|'there'|'my'|'dear'|'world';\n\n"; + result += "type MetaForge_2_b = 'hello'|'there'|'my'|'dear'|'world';\n\n"; + result += 'interface MetaForge_2 {\n a: string;\n b: MetaForge_2_b;\n};\n\n'; + result += 'type MetaForge = [(number|undefined),MetaForge_1,MetaForge_2];' + exp; assert.strictEqual(generate(complex), result); }); test('[DTS] Struct', () => { const schema = { "'": 'string', '"': 'string', b: '?number', 'c+': { d: ['hello', 'world'] } }; - let result = "interface MetaForge_c{d:('hello'|'world');};"; - result += 'interface MetaForge{"\'":string;\'"\':string;'; - result += "b?:(number|null|undefined);'c+':MetaForge_c;};"; - result += exp; + let result = "interface MetaForge_c {\n d: ('hello'|'world');\n};\n\n"; + result += 'interface MetaForge {\n "\'": string;\n \'"\': string;'; + result += "\n b?: (number|undefined);\n 'c+': MetaForge_c;\n};" + exp; assert.strictEqual(generate(schema), result); }); @@ -78,18 +77,25 @@ test('[DTS] Schema', () => { d: { $type: 'schema', schema: new Schema('number'), $id: 'MySubSchema2' }, e: { $type: 'schema', schema: new Schema({ $type: 'number', $id: 'MySubSchema3' }) }, }; - let r = 'interface MySubSchema{c:number;};type MySubSchema2=number;type MySubSchema3=number;'; - r += `interface MetaForge{a:string;b:MySubSchema;c?:(string|null|undefined);`; - r += 'd:MySubSchema2;e:MySubSchema3;};'; - r += 'export type{MySubSchema,MySubSchema2,MySubSchema3,MetaForge};export default MetaForge;'; + let r = 'interface MySubSchema {\n c: number;\n};\n\ntype MySubSchema2 = number;\n\n'; + r += `type MySubSchema3 = number;\n\n`; + r += 'interface MetaForge {\n a: string;\n b: MySubSchema;'; + r += `\n c?: (string|undefined);\n d: MySubSchema2;\n e: MySubSchema3;\n};\n`; + r += 'export type { MySubSchema, MySubSchema2, MySubSchema3, MetaForge };'; + r += '\nexport default MetaForge;'; assert.strictEqual(generate(schema), r); }); -test('[DTS] Modes', () => { - const schema = new Schema({ a: { $id: 'MySubSchema', c: 'number' } }); - const result = 'interface MySubSchema{c:number;};interface MetaForge{a:MySubSchema;};'; - const mjs = result + 'export type{MySubSchema,MetaForge};export default MetaForge;'; - const cjs = result + 'export = MetaForge;'; - assert.strictEqual(schema.dts('MetaForge'), mjs); - assert.strictEqual(schema.dts('MetaForge', { mode: 'cjs' }), cjs); +test('[DTS] JSDoc', () => { + const schema = new Schema({ + $id: 'User', + $meta: { '@name': 'user', '@description': 'About user' }, + name: { $type: 'string', $meta: { '@description': 'User name' } }, + age: '?number', + }); + let result = '/**\n * @name user\n * @description About user\n */\n'; + result += 'interface MetaForge {\n /**\n * @description User name\n */\n'; + result += ' name: string;\n age?: (number|undefined);\n};\n\n'; + result += 'export type { MetaForge };\nexport default MetaForge;'; + assert.strictEqual(schema.dts(), result); }); diff --git a/modules/types/types.js b/modules/types/types.js index cb49a66..df4b1f9 100644 --- a/modules/types/types.js +++ b/modules/types/types.js @@ -20,9 +20,9 @@ module.exports = new Map( }), ); -const { brackets, MAX_ITEMS } = require('./utils'); +const { brackets, MAX_ITEMS, jsdoc } = require('./utils'); function Scalar() { - this.toTypescript = () => (this.$required ? this.$type : `(${this.$type}|null|undefined)`); + this.toTypescript = () => (this.$required ? this.$type : `(${this.$type}|undefined)`); } function Enumerable() { @@ -30,7 +30,7 @@ function Enumerable() { const or = i => (this.$enum.length - 1 === i ? '' : '|'); const type = this.$enum.reduce((acc, s, i) => acc + brackets(s, false) + or(i), ''); if (this.$enum.length < MAX_ITEMS) return '(' + type + ')'; - namespace.definitions.add(`type ${name}=${type};`); + namespace.definitions.add(`type ${name} = ${type};`); return name; }; } @@ -43,19 +43,23 @@ function Iterable() { else if (this.$isTuple) type = `[${builded.join(',')}]`; else type = `(${builded.join('|')})[]`; if (builded.length < MAX_ITEMS) return type; - namespace.definitions.add(`type ${name}=${type};`); + namespace.definitions.add(`type ${name} = ${type};`); return name; }; } +const SPACING = ' '; function Struct() { this.toTypescript = (name, namespace) => { - let result = `interface ${name}{`; + const rootMeta = this.$meta; + let result = `interface ${name} {\n`; + if (rootMeta) result = jsdoc(rootMeta) + result; for (const [key, proto] of this.$properties.entries()) { const type = proto.toTypescript(`${name}_${key}`, namespace); - result += `${brackets(key, true) + (proto.$required ? '' : '?')}:${type};`; + if (proto.$meta) result += jsdoc(proto.$meta, SPACING); + result += `${SPACING + brackets(key, true) + (proto.$required ? '' : '?')}: ${type};\n`; } - namespace.definitions.add(result + '};'); + namespace.definitions.add(result + '};' + (rootMeta ? '\n' : '')); return name; }; } @@ -65,7 +69,7 @@ function Union() { const types = this.$types.map((type, i) => type.toTypescript(`${name}_${i}`, namespace)); const type = types.join(this.$condition === 'allof' ? '&' : '|'); if (types.length < MAX_ITEMS) return '(' + type + ')'; - namespace.definitions.add(`type ${name}=${type};`); + namespace.definitions.add(`type ${name} = ${type};`); return name; }; } @@ -76,7 +80,7 @@ function Schema() { const id = this.$id ?? name; const type = compile(id, namespace); if (!this.$id) return type; - if (type !== id) namespace.definitions.add(`type ${id}=${type};`); + if (type !== id) namespace.definitions.add(`type ${id} = ${type};`); namespace.exports.add(id); return id; }; diff --git a/modules/types/utils.js b/modules/types/utils.js index 6922ef3..073ecc8 100644 --- a/modules/types/utils.js +++ b/modules/types/utils.js @@ -15,4 +15,13 @@ const brackets = (sample, allowSkip) => { return sep + sample + sep; }; -module.exports = { nameFix, brackets, MAX_ITEMS }; +const jsdoc = (meta, spacing = '') => { + let result = spacing + '/**\n'; + for (const key in meta) { + if (key[0] !== '@') continue; + result += spacing + ` * ${key} ${meta[key]}\n`; + } + return result + spacing + ' */\n'; +}; + +module.exports = { nameFix, brackets, MAX_ITEMS, jsdoc }; diff --git a/package-lock.json b/package-lock.json index f730a68..0fa9d14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "metaforge", - "version": "0.6.0", + "version": "0.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "metaforge", - "version": "0.6.0", + "version": "0.8.0", "license": "MIT", "dependencies": { "astropack": "^0.4.2" diff --git a/package.json b/package.json index f1dfd40..c5b0769 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,10 @@ "node": ">= 18" }, "browser": {}, - "files": ["/lib", "/types"], + "files": [ + "/lib", + "/types" + ], "scripts": { "test": "node --test", "dev": "node index.js", diff --git a/tests/custom.test.js b/tests/custom.test.js index 6ac497c..cca081a 100644 --- a/tests/custom.test.js +++ b/tests/custom.test.js @@ -48,12 +48,13 @@ test('Custom modules', () => { test('Example test', () => { const userSchema = new Schema({ $id: 'userSchema', - $meta: { name: 'user', description: 'schema for users testing' }, + $meta: { '@name': 'user', '@description': 'schema for users testing' }, phone: { $type: 'union', types: ['number', 'string'] }, //? number or string name: { $type: 'set', items: ['string', '?string'] }, //? set tuple phrase: (_, parent) => 'Hello ' + [...parent.name].join(' ') + ' !', mask: { $type: 'array', items: 'string' }, //? array ip: { + $meta: { '@description': 'User ip adress' }, $type: 'array', $required: false, $rules: [ip => ip[0] === '192'], //? custom rules @@ -66,7 +67,11 @@ test('Example test', () => { options: { notifications: 'boolean', lvls: ['number', 'string'] }, }); - const systemSchema = new Schema({ $type: 'array', items: userSchema }); + const systemSchema = new Schema({ + $meta: { '@name': 'Users', '@description': 'Array of users' }, + $type: 'array', + items: userSchema, + }); const sample = [ { diff --git a/types/index.d.ts b/types/index.d.ts index 2b129cf..e5c4abb 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -84,11 +84,16 @@ class ForgePrototype { * @description Generates type declaration file for schema * @example * const schema = new Schema('string').dts('MyType'); - * // type MyType = (unknown | null | undefined); + * // type MyType = (unknown | undefined); * // export type { MyType }; * // export default MyType; */ - dts?: (name?: string, options?: { mode?: 'cjs' | 'mjs' }) => string; + dts?: ( + name?: string, + options?: { + export: { mode?: 'cjs' | 'mjs'; type: 'all' | 'no' | 'exports-only' | 'default-only' }; + }, + ) => string; /** * Handyman module required * @description Calculated fields