@twin.org/ts-to-schema 0.0.1-next.8 → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,7 @@ var tsJsonSchemaGenerator = require('ts-json-schema-generator');
10
10
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
11
11
  // Copyright 2024 IOTA Stiftung.
12
12
  // SPDX-License-Identifier: Apache-2.0.
13
+ const SCHEMA_VERSION = "https://json-schema.org/draft/2020-12/schema";
13
14
  /**
14
15
  * Build the root command to be consumed by the CLI.
15
16
  * @param program The command to build on.
@@ -76,24 +77,38 @@ async function tsToSchema(config, outputFolder, workingDirectory) {
76
77
  await promises.writeFile(path.join(workingDirectory, "tsconfig.json"), JSON.stringify({
77
78
  compilerOptions: {}
78
79
  }, undefined, "\t"));
79
- cliCore.CLIDisplay.task(core.I18n.formatMessage("commands.ts-to-schema.progress.generatingSchemas"));
80
- const schemas = await generateSchemas(config.sources, config.types, workingDirectory);
81
80
  cliCore.CLIDisplay.break();
82
81
  cliCore.CLIDisplay.task(core.I18n.formatMessage("commands.ts-to-schema.progress.writingSchemas"));
83
- for (const type of config.types) {
84
- if (core.Is.empty(schemas[type])) {
85
- throw new core.GeneralError("commands", "commands.ts-to-schema.schemaNotFound", { type });
82
+ for (const typeSource of config.types) {
83
+ const typeSourceParts = typeSource.split("/");
84
+ const type = core.StringHelper.pascalCase(typeSourceParts[typeSourceParts.length - 1].replace(/(\.d)?\.ts$/, ""), false);
85
+ let schemaObject;
86
+ if (core.Is.object(config.overrides?.[type])) {
87
+ cliCore.CLIDisplay.task(core.I18n.formatMessage("commands.ts-to-schema.progress.overridingSchema"));
88
+ schemaObject = config.overrides?.[type];
86
89
  }
87
- let content = JSON.stringify(schemas[type], undefined, "\t");
90
+ else {
91
+ cliCore.CLIDisplay.task(core.I18n.formatMessage("commands.ts-to-schema.progress.generatingSchema"));
92
+ const schemas = await generateSchemas(typeSource, type, workingDirectory);
93
+ if (core.Is.empty(schemas[type])) {
94
+ throw new core.GeneralError("commands", "commands.ts-to-schema.schemaNotFound", { type });
95
+ }
96
+ schemaObject = schemas[type];
97
+ }
98
+ schemaObject = finaliseSchema(schemaObject, config.baseUrl, type);
99
+ let content = JSON.stringify(schemaObject, undefined, "\t");
88
100
  if (core.Is.objectValue(config.externalReferences)) {
89
101
  for (const external in config.externalReferences) {
90
102
  content = content.replace(new RegExp(`#/definitions/${external}`, "g"), config.externalReferences[external]);
91
103
  }
92
104
  }
93
- content = content.replace(/#\/definitions\/I?/g, config.baseUrl);
105
+ // First replace all types that start with II to a single I with the new base url
106
+ content = content.replace(/#\/definitions\/II(.*)/g, `${config.baseUrl}I$1`);
107
+ // Then other types starting with capitals (optionally interfaces starting with I)
108
+ content = content.replace(/#\/definitions\/I?([A-Z].*)/g, `${config.baseUrl}$1`);
94
109
  const filename = path.join(outputFolder, `${core.StringHelper.stripPrefix(type)}.json`);
95
110
  cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.ts-to-schema.progress.writingSchema"), filename, 1);
96
- await promises.writeFile(filename, content);
111
+ await promises.writeFile(filename, `${content}\n`);
97
112
  }
98
113
  }
99
114
  /**
@@ -104,48 +119,25 @@ async function tsToSchema(config, outputFolder, workingDirectory) {
104
119
  * @returns Nothing.
105
120
  * @internal
106
121
  */
107
- async function generateSchemas(modelDirWildcards, types, outputWorkingDir) {
122
+ async function generateSchemas(typeSource, type, outputWorkingDir) {
108
123
  const allSchemas = {};
109
- const arraySingularTypes = [];
110
- for (const type of types) {
111
- if (type.endsWith("[]")) {
112
- const singularType = type.slice(0, -2);
113
- arraySingularTypes.push(singularType);
114
- if (!types.includes(singularType)) {
115
- types.push(singularType);
116
- }
117
- }
118
- }
119
- for (const files of modelDirWildcards) {
120
- cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.ts-to-schema.progress.models"), files.replace(/\\/g, "/"), 1);
121
- const generator = tsJsonSchemaGenerator.createGenerator({
122
- path: files.replace(/\\/g, "/"),
123
- type: "*",
124
- tsconfig: path.join(outputWorkingDir, "tsconfig.json"),
125
- skipTypeCheck: true,
126
- expose: "all"
127
- });
128
- const schema = generator.createSchema("*");
129
- if (schema.definitions) {
130
- for (const def in schema.definitions) {
131
- // Remove the partial markers
132
- let defSub = def.replace(/^Partial<(.*?)>/g, "$1");
133
- // Cleanup the generic markers
134
- defSub = defSub.replace(/</g, "%3C").replace(/>/g, "%3E");
135
- allSchemas[defSub] = schema.definitions[def];
136
- }
124
+ cliCore.CLIDisplay.value(core.I18n.formatMessage("commands.ts-to-schema.progress.models"), typeSource, 1);
125
+ const generator = tsJsonSchemaGenerator.createGenerator({
126
+ path: typeSource,
127
+ type,
128
+ tsconfig: path.join(outputWorkingDir, "tsconfig.json"),
129
+ skipTypeCheck: true,
130
+ expose: "all"
131
+ });
132
+ const schema = generator.createSchema("*");
133
+ if (schema.definitions) {
134
+ for (const def in schema.definitions) {
135
+ const defSub = normaliseTypeName(def);
136
+ allSchemas[defSub] = schema.definitions[def];
137
137
  }
138
138
  }
139
139
  const referencedSchemas = {};
140
- extractTypes(allSchemas, types, referencedSchemas);
141
- for (const arraySingularType of arraySingularTypes) {
142
- referencedSchemas[`${arraySingularType}[]`] = {
143
- type: "array",
144
- items: {
145
- $ref: `#/components/schemas/${arraySingularType}`
146
- }
147
- };
148
- }
140
+ extractTypes(allSchemas, [type], referencedSchemas);
149
141
  return referencedSchemas;
150
142
  }
151
143
  /**
@@ -216,6 +208,96 @@ function extractTypesFromSchema(allTypes, schema, output) {
216
208
  extractTypes(allTypes, additionalTypes, output);
217
209
  }
218
210
  }
211
+ /**
212
+ * Process the schema object to ensure it has the correct properties.
213
+ * @param schemaObject The schema object to process.
214
+ * @param baseUrl The base URL for the schema references.
215
+ * @param type The type of the schema object.
216
+ * @returns The finalised schema object.
217
+ */
218
+ function finaliseSchema(schemaObject, baseUrl, type) {
219
+ processArrays(schemaObject);
220
+ const { description, ...rest } = schemaObject;
221
+ return {
222
+ $schema: SCHEMA_VERSION,
223
+ $id: `${baseUrl}${core.StringHelper.stripPrefix(type)}`,
224
+ description,
225
+ ...rest
226
+ };
227
+ }
228
+ /**
229
+ * Process arrays in the schema object.
230
+ * @param schemaObject The schema object to process.
231
+ */
232
+ function processArrays(schemaObject) {
233
+ if (core.Is.object(schemaObject)) {
234
+ // latest specs have singular items in `items` property
235
+ // and multiple items in prefixItems, so update the schema accordingly
236
+ // https://www.learnjsonschema.com/2020-12/applicator/items/
237
+ // https://www.learnjsonschema.com/2020-12/applicator/prefixitems/
238
+ const schemaItems = schemaObject.items;
239
+ if (core.Is.array(schemaItems) || core.Is.object(schemaItems)) {
240
+ schemaObject.prefixItems = core.ArrayHelper.fromObjectOrArray(schemaItems);
241
+ schemaObject.items = false;
242
+ }
243
+ const additionalItems = schemaObject.additionalItems;
244
+ if (core.Is.array(additionalItems) || core.Is.object(additionalItems)) {
245
+ schemaObject.items = core.ArrayHelper.fromObjectOrArray(additionalItems)[0];
246
+ delete schemaObject.additionalItems;
247
+ }
248
+ processSchemaDictionary(schemaObject.properties);
249
+ processArrays(schemaObject.additionalProperties);
250
+ processSchemaArray(schemaObject.allOf);
251
+ processSchemaArray(schemaObject.anyOf);
252
+ processSchemaArray(schemaObject.oneOf);
253
+ }
254
+ }
255
+ /**
256
+ * Process arrays in the schema object.
257
+ * @param schemaDictionary The schema object to process.
258
+ */
259
+ function processSchemaDictionary(schemaDictionary) {
260
+ if (core.Is.object(schemaDictionary)) {
261
+ for (const item of Object.values(schemaDictionary)) {
262
+ if (core.Is.object(item)) {
263
+ processArrays(item);
264
+ }
265
+ }
266
+ }
267
+ }
268
+ /**
269
+ * Process arrays in the schema object.
270
+ * @param schemaArray The schema object to process.
271
+ */
272
+ function processSchemaArray(schemaArray) {
273
+ if (core.Is.arrayValue(schemaArray)) {
274
+ for (const item of schemaArray) {
275
+ if (core.Is.object(item)) {
276
+ processArrays(item);
277
+ }
278
+ }
279
+ }
280
+ }
281
+ /**
282
+ * Cleanup TypeScript markers from the type name.
283
+ * @param typeName The definition string to clean up.
284
+ * @returns The cleaned up definition string.
285
+ */
286
+ function normaliseTypeName(typeName) {
287
+ // Remove the partial markers
288
+ let sTypeName = typeName.replace(/^Partial<(.*?)>/g, "$1");
289
+ sTypeName = sTypeName.replace(/Partial%3CI(.*?)%3E/g, "$1");
290
+ // Remove the omit markers
291
+ sTypeName = sTypeName.replace(/^Omit<(.*?),.*>/g, "$1");
292
+ sTypeName = sTypeName.replace(/Omit%3CI(.*?)%2C.*%3E/g, "$1");
293
+ // Remove the pick markers
294
+ sTypeName = sTypeName.replace(/^Pick<(.*?),.*>/g, "$1");
295
+ sTypeName = sTypeName.replace(/Pick%3CI(.*?)%2C.*%3E/g, "$1");
296
+ // Cleanup the generic markers
297
+ sTypeName = sTypeName.replace(/</g, "%3C").replace(/>/g, "%3E");
298
+ sTypeName = sTypeName.replace(/%3Cunknown%3E/g, "");
299
+ return sTypeName;
300
+ }
219
301
 
220
302
  // Copyright 2024 IOTA Stiftung.
221
303
  // SPDX-License-Identifier: Apache-2.0.
@@ -235,7 +317,7 @@ class CLI extends cliCore.CLIBase {
235
317
  return this.execute({
236
318
  title: "TWIN TypeScript To Schema",
237
319
  appName: "ts-to-schema",
238
- version: "0.0.1-next.8",
320
+ version: "0.0.1", // x-release-please-version
239
321
  icon: "⚙️ ",
240
322
  supportsEnvFiles: false,
241
323
  overrideOutputWidth: options?.overrideOutputWidth
@@ -2,11 +2,12 @@ import path from 'node:path';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import { CLIDisplay, CLIUtils, CLIBase } from '@twin.org/cli-core';
4
4
  import { mkdir, rm, writeFile } from 'node:fs/promises';
5
- import { I18n, GeneralError, Is, StringHelper } from '@twin.org/core';
5
+ import { I18n, GeneralError, Is, StringHelper, ArrayHelper } from '@twin.org/core';
6
6
  import { createGenerator } from 'ts-json-schema-generator';
7
7
 
8
8
  // Copyright 2024 IOTA Stiftung.
9
9
  // SPDX-License-Identifier: Apache-2.0.
10
+ const SCHEMA_VERSION = "https://json-schema.org/draft/2020-12/schema";
10
11
  /**
11
12
  * Build the root command to be consumed by the CLI.
12
13
  * @param program The command to build on.
@@ -73,24 +74,38 @@ async function tsToSchema(config, outputFolder, workingDirectory) {
73
74
  await writeFile(path.join(workingDirectory, "tsconfig.json"), JSON.stringify({
74
75
  compilerOptions: {}
75
76
  }, undefined, "\t"));
76
- CLIDisplay.task(I18n.formatMessage("commands.ts-to-schema.progress.generatingSchemas"));
77
- const schemas = await generateSchemas(config.sources, config.types, workingDirectory);
78
77
  CLIDisplay.break();
79
78
  CLIDisplay.task(I18n.formatMessage("commands.ts-to-schema.progress.writingSchemas"));
80
- for (const type of config.types) {
81
- if (Is.empty(schemas[type])) {
82
- throw new GeneralError("commands", "commands.ts-to-schema.schemaNotFound", { type });
79
+ for (const typeSource of config.types) {
80
+ const typeSourceParts = typeSource.split("/");
81
+ const type = StringHelper.pascalCase(typeSourceParts[typeSourceParts.length - 1].replace(/(\.d)?\.ts$/, ""), false);
82
+ let schemaObject;
83
+ if (Is.object(config.overrides?.[type])) {
84
+ CLIDisplay.task(I18n.formatMessage("commands.ts-to-schema.progress.overridingSchema"));
85
+ schemaObject = config.overrides?.[type];
83
86
  }
84
- let content = JSON.stringify(schemas[type], undefined, "\t");
87
+ else {
88
+ CLIDisplay.task(I18n.formatMessage("commands.ts-to-schema.progress.generatingSchema"));
89
+ const schemas = await generateSchemas(typeSource, type, workingDirectory);
90
+ if (Is.empty(schemas[type])) {
91
+ throw new GeneralError("commands", "commands.ts-to-schema.schemaNotFound", { type });
92
+ }
93
+ schemaObject = schemas[type];
94
+ }
95
+ schemaObject = finaliseSchema(schemaObject, config.baseUrl, type);
96
+ let content = JSON.stringify(schemaObject, undefined, "\t");
85
97
  if (Is.objectValue(config.externalReferences)) {
86
98
  for (const external in config.externalReferences) {
87
99
  content = content.replace(new RegExp(`#/definitions/${external}`, "g"), config.externalReferences[external]);
88
100
  }
89
101
  }
90
- content = content.replace(/#\/definitions\/I?/g, config.baseUrl);
102
+ // First replace all types that start with II to a single I with the new base url
103
+ content = content.replace(/#\/definitions\/II(.*)/g, `${config.baseUrl}I$1`);
104
+ // Then other types starting with capitals (optionally interfaces starting with I)
105
+ content = content.replace(/#\/definitions\/I?([A-Z].*)/g, `${config.baseUrl}$1`);
91
106
  const filename = path.join(outputFolder, `${StringHelper.stripPrefix(type)}.json`);
92
107
  CLIDisplay.value(I18n.formatMessage("commands.ts-to-schema.progress.writingSchema"), filename, 1);
93
- await writeFile(filename, content);
108
+ await writeFile(filename, `${content}\n`);
94
109
  }
95
110
  }
96
111
  /**
@@ -101,48 +116,25 @@ async function tsToSchema(config, outputFolder, workingDirectory) {
101
116
  * @returns Nothing.
102
117
  * @internal
103
118
  */
104
- async function generateSchemas(modelDirWildcards, types, outputWorkingDir) {
119
+ async function generateSchemas(typeSource, type, outputWorkingDir) {
105
120
  const allSchemas = {};
106
- const arraySingularTypes = [];
107
- for (const type of types) {
108
- if (type.endsWith("[]")) {
109
- const singularType = type.slice(0, -2);
110
- arraySingularTypes.push(singularType);
111
- if (!types.includes(singularType)) {
112
- types.push(singularType);
113
- }
114
- }
115
- }
116
- for (const files of modelDirWildcards) {
117
- CLIDisplay.value(I18n.formatMessage("commands.ts-to-schema.progress.models"), files.replace(/\\/g, "/"), 1);
118
- const generator = createGenerator({
119
- path: files.replace(/\\/g, "/"),
120
- type: "*",
121
- tsconfig: path.join(outputWorkingDir, "tsconfig.json"),
122
- skipTypeCheck: true,
123
- expose: "all"
124
- });
125
- const schema = generator.createSchema("*");
126
- if (schema.definitions) {
127
- for (const def in schema.definitions) {
128
- // Remove the partial markers
129
- let defSub = def.replace(/^Partial<(.*?)>/g, "$1");
130
- // Cleanup the generic markers
131
- defSub = defSub.replace(/</g, "%3C").replace(/>/g, "%3E");
132
- allSchemas[defSub] = schema.definitions[def];
133
- }
121
+ CLIDisplay.value(I18n.formatMessage("commands.ts-to-schema.progress.models"), typeSource, 1);
122
+ const generator = createGenerator({
123
+ path: typeSource,
124
+ type,
125
+ tsconfig: path.join(outputWorkingDir, "tsconfig.json"),
126
+ skipTypeCheck: true,
127
+ expose: "all"
128
+ });
129
+ const schema = generator.createSchema("*");
130
+ if (schema.definitions) {
131
+ for (const def in schema.definitions) {
132
+ const defSub = normaliseTypeName(def);
133
+ allSchemas[defSub] = schema.definitions[def];
134
134
  }
135
135
  }
136
136
  const referencedSchemas = {};
137
- extractTypes(allSchemas, types, referencedSchemas);
138
- for (const arraySingularType of arraySingularTypes) {
139
- referencedSchemas[`${arraySingularType}[]`] = {
140
- type: "array",
141
- items: {
142
- $ref: `#/components/schemas/${arraySingularType}`
143
- }
144
- };
145
- }
137
+ extractTypes(allSchemas, [type], referencedSchemas);
146
138
  return referencedSchemas;
147
139
  }
148
140
  /**
@@ -213,6 +205,96 @@ function extractTypesFromSchema(allTypes, schema, output) {
213
205
  extractTypes(allTypes, additionalTypes, output);
214
206
  }
215
207
  }
208
+ /**
209
+ * Process the schema object to ensure it has the correct properties.
210
+ * @param schemaObject The schema object to process.
211
+ * @param baseUrl The base URL for the schema references.
212
+ * @param type The type of the schema object.
213
+ * @returns The finalised schema object.
214
+ */
215
+ function finaliseSchema(schemaObject, baseUrl, type) {
216
+ processArrays(schemaObject);
217
+ const { description, ...rest } = schemaObject;
218
+ return {
219
+ $schema: SCHEMA_VERSION,
220
+ $id: `${baseUrl}${StringHelper.stripPrefix(type)}`,
221
+ description,
222
+ ...rest
223
+ };
224
+ }
225
+ /**
226
+ * Process arrays in the schema object.
227
+ * @param schemaObject The schema object to process.
228
+ */
229
+ function processArrays(schemaObject) {
230
+ if (Is.object(schemaObject)) {
231
+ // latest specs have singular items in `items` property
232
+ // and multiple items in prefixItems, so update the schema accordingly
233
+ // https://www.learnjsonschema.com/2020-12/applicator/items/
234
+ // https://www.learnjsonschema.com/2020-12/applicator/prefixitems/
235
+ const schemaItems = schemaObject.items;
236
+ if (Is.array(schemaItems) || Is.object(schemaItems)) {
237
+ schemaObject.prefixItems = ArrayHelper.fromObjectOrArray(schemaItems);
238
+ schemaObject.items = false;
239
+ }
240
+ const additionalItems = schemaObject.additionalItems;
241
+ if (Is.array(additionalItems) || Is.object(additionalItems)) {
242
+ schemaObject.items = ArrayHelper.fromObjectOrArray(additionalItems)[0];
243
+ delete schemaObject.additionalItems;
244
+ }
245
+ processSchemaDictionary(schemaObject.properties);
246
+ processArrays(schemaObject.additionalProperties);
247
+ processSchemaArray(schemaObject.allOf);
248
+ processSchemaArray(schemaObject.anyOf);
249
+ processSchemaArray(schemaObject.oneOf);
250
+ }
251
+ }
252
+ /**
253
+ * Process arrays in the schema object.
254
+ * @param schemaDictionary The schema object to process.
255
+ */
256
+ function processSchemaDictionary(schemaDictionary) {
257
+ if (Is.object(schemaDictionary)) {
258
+ for (const item of Object.values(schemaDictionary)) {
259
+ if (Is.object(item)) {
260
+ processArrays(item);
261
+ }
262
+ }
263
+ }
264
+ }
265
+ /**
266
+ * Process arrays in the schema object.
267
+ * @param schemaArray The schema object to process.
268
+ */
269
+ function processSchemaArray(schemaArray) {
270
+ if (Is.arrayValue(schemaArray)) {
271
+ for (const item of schemaArray) {
272
+ if (Is.object(item)) {
273
+ processArrays(item);
274
+ }
275
+ }
276
+ }
277
+ }
278
+ /**
279
+ * Cleanup TypeScript markers from the type name.
280
+ * @param typeName The definition string to clean up.
281
+ * @returns The cleaned up definition string.
282
+ */
283
+ function normaliseTypeName(typeName) {
284
+ // Remove the partial markers
285
+ let sTypeName = typeName.replace(/^Partial<(.*?)>/g, "$1");
286
+ sTypeName = sTypeName.replace(/Partial%3CI(.*?)%3E/g, "$1");
287
+ // Remove the omit markers
288
+ sTypeName = sTypeName.replace(/^Omit<(.*?),.*>/g, "$1");
289
+ sTypeName = sTypeName.replace(/Omit%3CI(.*?)%2C.*%3E/g, "$1");
290
+ // Remove the pick markers
291
+ sTypeName = sTypeName.replace(/^Pick<(.*?),.*>/g, "$1");
292
+ sTypeName = sTypeName.replace(/Pick%3CI(.*?)%2C.*%3E/g, "$1");
293
+ // Cleanup the generic markers
294
+ sTypeName = sTypeName.replace(/</g, "%3C").replace(/>/g, "%3E");
295
+ sTypeName = sTypeName.replace(/%3Cunknown%3E/g, "");
296
+ return sTypeName;
297
+ }
216
298
 
217
299
  // Copyright 2024 IOTA Stiftung.
218
300
  // SPDX-License-Identifier: Apache-2.0.
@@ -232,7 +314,7 @@ class CLI extends CLIBase {
232
314
  return this.execute({
233
315
  title: "TWIN TypeScript To Schema",
234
316
  appName: "ts-to-schema",
235
- version: "0.0.1-next.8",
317
+ version: "0.0.1", // x-release-please-version
236
318
  icon: "⚙️ ",
237
319
  supportsEnvFiles: false,
238
320
  overrideOutputWidth: options?.overrideOutputWidth