@contentstorage/core 0.3.29 → 0.3.31

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.
@@ -0,0 +1 @@
1
+ export declare function populateTextWithVariables(text: string, variables: Record<string, any>, textKey: string): string;
@@ -0,0 +1,10 @@
1
+ export function populateTextWithVariables(text, variables, textKey) {
2
+ return text.replace(/\{(\w+)}/g, (placeholder, variableName) => {
3
+ if (Object.prototype.hasOwnProperty.call(variables, variableName)) {
4
+ const value = variables[variableName];
5
+ return String(value);
6
+ }
7
+ console.warn(`[getText] Variable "${variableName}" for text id "${textKey}" not found in provided variables. Placeholder "${placeholder}" will be replaced with an empty string.`);
8
+ return placeholder;
9
+ });
10
+ }
@@ -1,5 +1,6 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs';
3
+ import chalk from 'chalk';
3
4
  const DEFAULT_CONFIG = {
4
5
  languageCodes: [],
5
6
  contentDir: path.join('src', 'content', 'json'),
@@ -13,19 +14,19 @@ export async function loadConfig() {
13
14
  // Use require for JS config file
14
15
  const loadedModule = await import(configPath);
15
16
  userConfig = loadedModule.default || loadedModule;
16
- console.log('Loaded config', JSON.stringify(userConfig));
17
- console.log(`Loaded configuration from ${configPath}`);
17
+ console.log(chalk.blue('Loaded config', JSON.stringify(userConfig)));
18
+ console.log(chalk.blue(`Loaded configuration from ${configPath}`));
18
19
  }
19
20
  catch (error) {
20
- console.error(`Error loading configuration from ${configPath}:`, error);
21
+ console.error(chalk.red(`Error loading configuration from ${configPath}:`, error));
21
22
  // Decide if you want to proceed with defaults or exit
22
23
  // For now, we'll proceed with defaults but warn
23
- console.warn('Proceeding with default configuration.');
24
+ console.warn(chalk.yellow('Proceeding with default configuration.'));
24
25
  userConfig = {}; // Reset in case of partial load failure
25
26
  }
26
27
  }
27
28
  else {
28
- console.log('No content.config.js found. Using default configuration.');
29
+ console.log(chalk.blue('No content.config.js found. Using default configuration.'));
29
30
  }
30
31
  const mergedConfig = {
31
32
  ...DEFAULT_CONFIG,
@@ -33,10 +34,9 @@ export async function loadConfig() {
33
34
  };
34
35
  // Validate required fields
35
36
  if (!mergedConfig.contentKey) {
36
- console.error('Error: Configuration is missing the required "contentUrl" property.');
37
- process.exit(1); // Exit if required URL is missing
37
+ console.error(chalk.red('Error: Configuration is missing the required "contentKey" property.'));
38
+ process.exit(1);
38
39
  }
39
- // Resolve paths relative to the user's project root (process.cwd())
40
40
  const finalConfig = {
41
41
  languageCodes: mergedConfig.languageCodes || [],
42
42
  contentKey: mergedConfig.contentKey,
@@ -11,12 +11,15 @@ export declare function setContentLanguage(contentJson: object): void;
11
11
  * `setContentLanguage()` must be called successfully before using this.
12
12
  *
13
13
  * @param pathString A dot-notation path string (e.g., 'HomePage.Login'). Autocompletion is provided.
14
+ * @param variables Variables help to render dynamic content inside text strings
14
15
  * @param fallbackValue Optional string to return if the path is not found or the value is not a string.
15
16
  * If not provided, and path is not found/value not string, undefined is returned.
16
17
  * @returns The text string from the JSON, or the fallbackValue, or undefined.
17
18
  */
18
- export declare function getText(pathString: keyof ContentStructure, fallbackValue?: string): string | undefined;
19
+ export declare function getText<Path extends keyof ContentStructure>(pathString: Path, variables?: ContentStructure[Path] extends {
20
+ variables: infer Vars;
21
+ } ? keyof Vars : Record<string, any>): string | undefined;
19
22
  export declare function getImage(pathString: keyof ContentStructure, fallbackValue?: ImageObject): ImageObject | undefined;
20
23
  export declare function getVariation<Path extends keyof ContentStructure>(pathString: Path, variationKey?: ContentStructure[Path] extends {
21
24
  data: infer D;
22
- } ? keyof D : string, fallbackString?: string): string | undefined;
25
+ } ? keyof D : string, variables?: Record<string, any>): string | undefined;
@@ -1,3 +1,4 @@
1
+ import { populateTextWithVariables } from '../helpers/populateTextWithVariables.js';
1
2
  let activeContent = null;
2
3
  /**
3
4
  * Loads and sets the content for a specific language.
@@ -6,7 +7,7 @@ let activeContent = null;
6
7
  */
7
8
  export function setContentLanguage(contentJson) {
8
9
  if (!contentJson || typeof contentJson !== 'object') {
9
- throw new Error('[Contentstorage] Invalid contentUrl provided to setContentLanguage.');
10
+ throw new Error('[Contentstorage] Invalid contentKey might be provided which caused setContentLanguage to fail.');
10
11
  }
11
12
  try {
12
13
  activeContent = contentJson; // Relies on augmentation
@@ -23,15 +24,16 @@ export function setContentLanguage(contentJson) {
23
24
  * `setContentLanguage()` must be called successfully before using this.
24
25
  *
25
26
  * @param pathString A dot-notation path string (e.g., 'HomePage.Login'). Autocompletion is provided.
27
+ * @param variables Variables help to render dynamic content inside text strings
26
28
  * @param fallbackValue Optional string to return if the path is not found or the value is not a string.
27
29
  * If not provided, and path is not found/value not string, undefined is returned.
28
30
  * @returns The text string from the JSON, or the fallbackValue, or undefined.
29
31
  */
30
- export function getText(pathString, fallbackValue) {
32
+ export function getText(pathString, variables) {
31
33
  if (!activeContent) {
32
34
  const msg = `[Contentstorage] getText: Content not loaded (Key: "${String(pathString)}"). Ensure setContentLanguage() was called and completed successfully.`;
33
35
  console.warn(msg);
34
- return fallbackValue;
36
+ return '';
35
37
  }
36
38
  const keys = pathString.split('.');
37
39
  let current = activeContent;
@@ -42,16 +44,19 @@ export function getText(pathString, fallbackValue) {
42
44
  else {
43
45
  const msg = `[Contentstorage] getText: Path "${String(pathString)}" not found in loaded content.`;
44
46
  console.warn(msg);
45
- return fallbackValue;
47
+ return '';
46
48
  }
47
49
  }
48
50
  if (typeof current === 'string') {
49
- return current;
51
+ if (!variables || Object.keys(variables).length === 0) {
52
+ return current;
53
+ }
54
+ return populateTextWithVariables(current, variables, pathString);
50
55
  }
51
56
  else {
52
57
  const msg = `[Contentstorage] getText: Value at path "${String(pathString)}" is not a string (actual type: ${typeof current}).`;
53
58
  console.warn(msg);
54
- return fallbackValue;
59
+ return '';
55
60
  }
56
61
  }
57
62
  export function getImage(pathString, fallbackValue) {
@@ -85,11 +90,11 @@ export function getImage(pathString, fallbackValue) {
85
90
  return fallbackValue;
86
91
  }
87
92
  }
88
- export function getVariation(pathString, variationKey, fallbackString) {
93
+ export function getVariation(pathString, variationKey, variables) {
89
94
  if (!activeContent) {
90
95
  const msg = `[Contentstorage] getVariation: Content not loaded (Key: "${pathString}", Variation: "${variationKey?.toString()}"). Ensure setContentLanguage() was called and completed successfully.`;
91
96
  console.warn(msg);
92
- return fallbackString;
97
+ return '';
93
98
  }
94
99
  const keys = pathString.split('.');
95
100
  let current = activeContent;
@@ -100,7 +105,7 @@ export function getVariation(pathString, variationKey, fallbackString) {
100
105
  else {
101
106
  const msg = `[Contentstorage] getVariation: Path "${pathString}" for variation object not found in loaded content.`;
102
107
  console.warn(msg);
103
- return fallbackString;
108
+ return '';
104
109
  }
105
110
  }
106
111
  if (current &&
@@ -113,7 +118,11 @@ export function getVariation(pathString, variationKey, fallbackString) {
113
118
  typeof variationKey === 'string' &&
114
119
  variationKey in variationObject.data) {
115
120
  if (typeof variationObject.data[variationKey] === 'string') {
116
- return variationObject.data[variationKey];
121
+ const current = variationObject.data[variationKey];
122
+ if (!variables || Object.keys(variables).length === 0) {
123
+ return current;
124
+ }
125
+ return populateTextWithVariables(current, variables, pathString);
117
126
  }
118
127
  else {
119
128
  const msg = `[Contentstorage] getVariation: Variation value for key "${variationKey}" at path "${pathString}" is not a string (actual type: ${typeof variationObject.data[variationKey]}).`;
@@ -124,25 +133,20 @@ export function getVariation(pathString, variationKey, fallbackString) {
124
133
  if ('default' in variationObject.data && typeof variationKey === 'string') {
125
134
  if (typeof variationObject.data.default === 'string') {
126
135
  if (variationKey && variationKey !== 'default') {
127
- // Warn if specific key was requested but default is being returned
128
- const msg = `[Contentstorage] getVariation: Variation key "${variationKey}" not found at path "${pathString}". Returning 'default' variation.`;
129
- console.warn(msg);
136
+ console.warn(`[Contentstorage] getVariation: Variation key "${variationKey}" not found at path "${pathString}". Returning 'default' variation.`);
130
137
  }
131
138
  return variationObject.data.default;
132
139
  }
133
140
  else {
134
- const msg = `[Contentstorage] getVariation: 'default' variation value at path "${pathString}" is not a string (actual type: ${typeof variationObject.data.default}).`;
135
- console.warn(msg);
141
+ console.warn(`[Contentstorage] getVariation: 'default' variation value at path "${pathString}" is not a string (actual type: ${typeof variationObject.data.default}).`);
136
142
  }
137
143
  }
138
144
  // If neither specific key nor 'default' is found or valid
139
- const msg = `[Contentstorage] getVariation: Neither variation key "${variationKey?.toString()}" nor 'default' variation found or valid at path "${pathString}".`;
140
- console.warn(msg);
141
- return fallbackString;
145
+ console.warn(`[Contentstorage] getVariation: Neither variation key "${variationKey?.toString()}" nor 'default' variation found or valid at path "${pathString}".`);
146
+ return '';
142
147
  }
143
148
  else {
144
- const msg = `[Contentstorage] getVariation: Value at path "${pathString}" is not a valid variation object (actual value: ${JSON.stringify(current)}).`;
145
- console.warn(msg);
146
- return fallbackString;
149
+ console.warn(`[Contentstorage] getVariation: Value at path "${pathString}" is not a valid variation object (actual value: ${JSON.stringify(current)}).`);
150
+ return '';
147
151
  }
148
152
  }
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
- // ^ Shebang ensures the script is executed with Node.js
2
+ // ^ Ensures the script is executed with Node.js
3
3
  import fs from 'fs/promises';
4
4
  import path from 'path';
5
5
  import axios from 'axios';
6
- import jsonToTS from 'json-to-ts'; // Import the library
7
6
  import chalk from 'chalk'; // Optional: for colored output
8
7
  import { loadConfig } from '../lib/configLoader.js';
9
8
  import { flattenJson } from '../helpers/flattenJson.js';
10
9
  import { CONTENTSTORAGE_CONFIG } from '../contentstorage-config.js';
10
+ import { jsonToTS } from '../type-generation/index.js';
11
11
  export async function generateTypes() {
12
12
  console.log(chalk.blue('Starting type generation...'));
13
13
  const config = await loadConfig();
@@ -21,7 +21,7 @@ export async function generateTypes() {
21
21
  console.error(chalk.red.bold("Configuration error: 'languageCodes' must be a non-empty array."));
22
22
  process.exit(1);
23
23
  }
24
- console.log(chalk.gray(`TypeScript types will be saved to: ${config.typesOutputFile}`));
24
+ console.log(chalk.blue(`TypeScript types will be saved to: ${config.typesOutputFile}`));
25
25
  let jsonObject; // To hold the JSON data from either local or remote source
26
26
  let dataSourceDescription = ''; // For clearer logging
27
27
  const firstLanguageCode = config.languageCodes[0];
@@ -31,7 +31,7 @@ export async function generateTypes() {
31
31
  try {
32
32
  await fs.stat(config.contentDir); // Check if directory exists
33
33
  attemptLocalLoad = true;
34
- console.log(chalk.gray(`Local content directory found: ${config.contentDir}`));
34
+ console.log(chalk.blue(`Local content directory found: ${config.contentDir}`));
35
35
  }
36
36
  catch (statError) {
37
37
  if (statError.code === 'ENOENT') {
@@ -52,12 +52,12 @@ export async function generateTypes() {
52
52
  const targetFilename = `${firstLanguageCode}.json`;
53
53
  const jsonFilePath = path.join(config.contentDir, targetFilename);
54
54
  dataSourceDescription = `local file (${jsonFilePath})`;
55
- console.log(chalk.gray(`Attempting to read JSON from: ${jsonFilePath}`));
55
+ console.log(chalk.blue(`Attempting to read JSON from: ${jsonFilePath}`));
56
56
  try {
57
57
  const jsonContentString = await fs.readFile(jsonFilePath, 'utf-8');
58
- console.log(chalk.gray('Parsing JSON'));
58
+ console.log(chalk.blue('Parsing JSON'));
59
59
  const parsendJsonObject = JSON.parse(jsonContentString);
60
- console.log(chalk.gray('Flattening JSON for type generation'));
60
+ console.log(chalk.blue('Flattening JSON for type generation'));
61
61
  jsonObject = flattenJson(parsendJsonObject);
62
62
  console.log(chalk.green(`Successfully read and parsed JSON from ${jsonFilePath}.`));
63
63
  }
@@ -76,11 +76,11 @@ export async function generateTypes() {
76
76
  }
77
77
  const fileUrl = `${CONTENTSTORAGE_CONFIG.BASE_URL}/${config.contentKey}/content/${firstLanguageCode}.json`; // Adjust URL construction if necessary
78
78
  dataSourceDescription = `remote URL (${fileUrl})`;
79
- console.log(chalk.gray(`Attempting to fetch JSON from: ${fileUrl}`));
79
+ console.log(chalk.blue(`Attempting to fetch JSON from: ${fileUrl}`));
80
80
  try {
81
81
  const response = await axios.get(fileUrl, { responseType: 'json' });
82
82
  const jsonResponse = response.data;
83
- console.log(chalk.gray('Flattening JSON for type generation'));
83
+ console.log(chalk.blue('Flattening JSON for type generation'));
84
84
  jsonObject = flattenJson(jsonResponse);
85
85
  if (typeof jsonObject !== 'object' || jsonObject === null) {
86
86
  throw new Error(`Workspaceed data from ${fileUrl} is not a valid JSON object. Received type: ${typeof jsonObject}`);
@@ -104,9 +104,9 @@ export async function generateTypes() {
104
104
  }
105
105
  }
106
106
  // Generate TypeScript interfaces using json-to-ts
107
- const rootTypeName = 'ContentRoot'; // As per your previous update
108
- console.log(chalk.gray(`Generating TypeScript types with root name '${rootTypeName}'...`));
109
- const typeDeclarations = jsonToTS.default(jsonObject, {
107
+ const rootTypeName = 'ContentRoot';
108
+ console.log(chalk.blue(`Generating TypeScript types with root name '${rootTypeName}'...`));
109
+ const typeDeclarations = jsonToTS(jsonObject, {
110
110
  rootName: rootTypeName,
111
111
  });
112
112
  if (!typeDeclarations || typeDeclarations.length === 0) {
@@ -9,8 +9,8 @@ export async function pullContent() {
9
9
  console.log(chalk.blue('Starting content pull...'));
10
10
  // Load configuration (assuming this function is defined elsewhere and works)
11
11
  const config = await loadConfig();
12
- console.log(chalk.gray(`Content key: ${config.contentKey}`));
13
- console.log(chalk.gray(`Saving content to: ${config.contentDir}`));
12
+ console.log(chalk.blue(`Content key: ${config.contentKey}`));
13
+ console.log(chalk.blue(`Saving content to: ${config.contentDir}`));
14
14
  try {
15
15
  // Validate languageCodes array
16
16
  if (!Array.isArray(config.languageCodes)) {
@@ -29,7 +29,7 @@ export async function pullContent() {
29
29
  const filename = `${languageCode}.json`;
30
30
  const outputPath = path.join(config.contentDir, filename);
31
31
  console.log(chalk.blue(`\nProcessing language: ${languageCode}`));
32
- console.log(chalk.gray(`Workspaceing from: ${fileUrl}`));
32
+ console.log(chalk.blue(`Using following contentKey to fetch json: ${config.contentKey}`));
33
33
  try {
34
34
  // Fetch data for the current language
35
35
  const response = await axios.get(fileUrl);
@@ -44,11 +44,7 @@ export async function pullContent() {
44
44
  Array.isArray(jsonData) /* jsonData === null is already covered */) {
45
45
  throw new Error(`Expected a single JSON object from ${fileUrl} for language ${languageCode}, but received type ${Array.isArray(jsonData) ? 'array' : typeof jsonData}. Cannot save the file.`);
46
46
  }
47
- console.log(chalk.green(`Received and json for ${languageCode}. Saving to ${outputPath}`));
48
- await fs.writeFile(outputPath, JSON.stringify(jsonData, null, 2));
49
- console.log(chalk.green(`Successfully saved ${outputPath}`));
50
47
  console.log(chalk.green(`Received JSON for ${languageCode}. Saving to ${outputPath}`));
51
- // Write the JSON data to the file
52
48
  await fs.writeFile(outputPath, JSON.stringify(jsonData, null, 2));
53
49
  console.log(chalk.green(`Successfully saved ${outputPath}`));
54
50
  }
@@ -0,0 +1,5 @@
1
+ import { InterfaceDescription, NameEntry, TypeStructure } from './model.js';
2
+ export declare function getInterfaceStringFromDescription({ name, typeMap, isRoot, }: InterfaceDescription & {
3
+ isRoot?: boolean;
4
+ }): string;
5
+ export declare function getInterfaceDescriptions(typeStructure: TypeStructure, names: NameEntry[]): InterfaceDescription[];
@@ -0,0 +1,97 @@
1
+ import { findTypeById, isHash, isNonArrayUnion } from './util.js';
2
+ function isKeyNameValid(keyName) {
3
+ const regex = /^[a-zA-Z_][a-zA-Z\d_]*$/;
4
+ return regex.test(keyName);
5
+ }
6
+ function parseKeyMetaData(key) {
7
+ const isOptional = key.endsWith('--?');
8
+ if (isOptional) {
9
+ return {
10
+ isOptional,
11
+ keyValue: key.slice(0, -3),
12
+ };
13
+ }
14
+ else {
15
+ return {
16
+ isOptional,
17
+ keyValue: key,
18
+ };
19
+ }
20
+ }
21
+ function findNameById(id, names) {
22
+ // @ts-expect-error
23
+ return names.find((_) => _.id === id).name;
24
+ }
25
+ function removeUndefinedFromUnion(unionTypeName) {
26
+ const typeNames = unionTypeName.split(' | ');
27
+ const undefinedIndex = typeNames.indexOf('undefined');
28
+ typeNames.splice(undefinedIndex, 1);
29
+ return typeNames.join(' | ');
30
+ }
31
+ function replaceTypeObjIdsWithNames(typeObj, names) {
32
+ return (Object.entries(typeObj)
33
+ // quote key if is invalid and question mark if optional from array merging
34
+ .map(([key, type]) => {
35
+ const { isOptional, keyValue } = parseKeyMetaData(key);
36
+ const isValid = isKeyNameValid(keyValue);
37
+ const validName = isValid ? keyValue : `'${keyValue}'`;
38
+ return isOptional
39
+ ? [`${validName}?`, type, isOptional]
40
+ : [validName, type, isOptional];
41
+ })
42
+ // replace hashes with names referencing the hashes
43
+ .map(([key, type, isOptional]) => {
44
+ if (!isHash(type)) {
45
+ return [key, type, isOptional];
46
+ }
47
+ const newType = findNameById(type, names);
48
+ return [key, newType, isOptional];
49
+ })
50
+ // if union has undefined, remove undefined and make type optional
51
+ .map(([key, type, isOptional]) => {
52
+ if (!(isNonArrayUnion(type) && type.includes('undefined'))) {
53
+ return [key, type, isOptional];
54
+ }
55
+ const newType = removeUndefinedFromUnion(type);
56
+ const newKey = isOptional ? key : `${key}?`; // if already optional dont add question mark
57
+ return [newKey, newType, isOptional];
58
+ })
59
+ // make undefined optional and set type as any
60
+ .map(([key, type, isOptional]) => {
61
+ if (type !== 'undefined') {
62
+ return [key, type, isOptional];
63
+ }
64
+ const newType = 'any';
65
+ const newKey = isOptional ? key : `${key}?`; // if already optional dont add question mark
66
+ return [newKey, newType, isOptional];
67
+ })
68
+ .reduce((agg, [key, value]) => {
69
+ // @ts-expect-error
70
+ agg[key] = value;
71
+ return agg;
72
+ }, {}));
73
+ }
74
+ export function getInterfaceStringFromDescription({ name, typeMap, isRoot, }) {
75
+ const stringTypeMap = Object.entries(typeMap)
76
+ .map(([key, name]) => ` ${key}: ${name};\n`)
77
+ .reduce((a, b) => (a += b), '');
78
+ const declarationKeyWord = 'interface';
79
+ let interfaceString = `${isRoot ? 'export ' : ''}${declarationKeyWord} ${name} {\n`;
80
+ interfaceString += stringTypeMap;
81
+ interfaceString += '}';
82
+ return interfaceString;
83
+ }
84
+ export function getInterfaceDescriptions(typeStructure, names) {
85
+ return names
86
+ .map(({ id, name }) => {
87
+ const typeDescription = findTypeById(id, typeStructure.types);
88
+ if (typeDescription.typeObj) {
89
+ const typeMap = replaceTypeObjIdsWithNames(typeDescription.typeObj, names);
90
+ return { name, typeMap };
91
+ }
92
+ else {
93
+ return null;
94
+ }
95
+ })
96
+ .filter((_) => _ !== null);
97
+ }
@@ -0,0 +1,2 @@
1
+ import { NameEntry, TypeStructure } from './model.js';
2
+ export declare function getNames(typeStructure: TypeStructure, rootName?: string): NameEntry[];
@@ -0,0 +1,142 @@
1
+ // @ts-expect-error
2
+ import * as pluralize from "pluralize";
3
+ import { TypeGroup } from './model.js';
4
+ import { findTypeById, getTypeDescriptionGroup, isHash, parseKeyMetaData } from './util.js';
5
+ function getName({ rootTypeId, types }, keyName, names, isInsideArray
6
+ // @ts-ignore
7
+ ) {
8
+ const typeDesc = types.find((_) => _.id === rootTypeId);
9
+ switch (getTypeDescriptionGroup(typeDesc)) {
10
+ case TypeGroup.Array:
11
+ // @ts-expect-error
12
+ typeDesc.arrayOfTypes.forEach((typeIdOrPrimitive, i) => {
13
+ getName({ rootTypeId: typeIdOrPrimitive, types },
14
+ // to differenttiate array types
15
+ i === 0 ? keyName : `${keyName}${i + 1}`, names, true);
16
+ });
17
+ return {
18
+ rootName: getNameById(typeDesc.id, keyName, isInsideArray, types, names),
19
+ names,
20
+ };
21
+ case TypeGroup.Object:
22
+ Object.entries(typeDesc.typeObj).forEach(([key, value]) => {
23
+ getName({ rootTypeId: value, types }, key, names, false);
24
+ });
25
+ return {
26
+ rootName: getNameById(typeDesc.id, keyName, isInsideArray, types, names),
27
+ names,
28
+ };
29
+ case TypeGroup.Primitive:
30
+ // in this case rootTypeId is primitive type string (string, null, number, boolean)
31
+ return {
32
+ rootName: rootTypeId,
33
+ names,
34
+ };
35
+ }
36
+ }
37
+ export function getNames(typeStructure, rootName = "RootObject") {
38
+ return getName(typeStructure, rootName, [], false).names.reverse();
39
+ }
40
+ function getNameById(id, keyName, isInsideArray, types, nameMap) {
41
+ const nameEntry = nameMap.find((_) => _.id === id);
42
+ if (nameEntry) {
43
+ return nameEntry.name;
44
+ }
45
+ const typeDesc = findTypeById(id, types);
46
+ const group = getTypeDescriptionGroup(typeDesc);
47
+ let name;
48
+ switch (group) {
49
+ case TypeGroup.Array:
50
+ name = typeDesc.isUnion ? getArrayName(typeDesc, types, nameMap) : formatArrayName(typeDesc, types, nameMap);
51
+ break;
52
+ case TypeGroup.Object:
53
+ /**
54
+ * picking name for type in array requires to singularize that type name,
55
+ * and if not then no need to singularize
56
+ */
57
+ name = [keyName]
58
+ .map((key) => parseKeyMetaData(key).keyValue)
59
+ .map((name) => (isInsideArray ? pluralize.singular(name) : name))
60
+ .map(pascalCase)
61
+ .map(normalizeInvalidTypeName)
62
+ .map(pascalCase) // needed because removed symbols might leave first character uncapitalized
63
+ .map((name) => uniqueByIncrement(name, nameMap.map(({ name }) => name)))
64
+ .pop();
65
+ break;
66
+ }
67
+ // @ts-ignore
68
+ nameMap.push({ id, name });
69
+ // @ts-ignore
70
+ return name;
71
+ }
72
+ function pascalCase(name) {
73
+ return name
74
+ .split(/\s+/g)
75
+ .filter((_) => _ !== "")
76
+ .map(capitalize)
77
+ .reduce((a, b) => a + b, "");
78
+ }
79
+ function capitalize(name) {
80
+ return name.charAt(0).toUpperCase() + name.slice(1);
81
+ }
82
+ function normalizeInvalidTypeName(name) {
83
+ if (/^[a-zA-Z][a-zA-Z0-9]*$/.test(name)) {
84
+ return name;
85
+ }
86
+ else {
87
+ const noSymbolsName = name.replace(/[^a-zA-Z0-9]/g, "");
88
+ const startsWithWordCharacter = /^[a-zA-Z]/.test(noSymbolsName);
89
+ return startsWithWordCharacter ? noSymbolsName : `_${noSymbolsName}`;
90
+ }
91
+ }
92
+ function uniqueByIncrement(name, names) {
93
+ for (let i = 0; i < 1000; i++) {
94
+ const nameProposal = i === 0 ? name : `${name}${i + 1}`;
95
+ if (!names.includes(nameProposal)) {
96
+ return nameProposal;
97
+ }
98
+ }
99
+ }
100
+ function getArrayName(typeDesc, types, nameMap) {
101
+ // @ts-ignore
102
+ if (typeDesc.arrayOfTypes.length === 0) {
103
+ return "any";
104
+ }
105
+ else { // @ts-ignore
106
+ if (typeDesc.arrayOfTypes.length === 1) {
107
+ // @ts-ignore
108
+ const [idOrPrimitive] = typeDesc.arrayOfTypes;
109
+ return convertToReadableType(idOrPrimitive, types, nameMap);
110
+ }
111
+ else {
112
+ return unionToString(typeDesc, types, nameMap);
113
+ }
114
+ }
115
+ }
116
+ function convertToReadableType(idOrPrimitive, types, nameMap) {
117
+ return isHash(idOrPrimitive)
118
+ ? // array keyName makes no difference in picking name for type
119
+ // @ts-ignore
120
+ getNameById(idOrPrimitive, null, true, types, nameMap)
121
+ : idOrPrimitive;
122
+ }
123
+ function unionToString(typeDesc, types, nameMap) {
124
+ // @ts-ignore
125
+ return typeDesc.arrayOfTypes.reduce((acc, type, i) => {
126
+ const readableTypeName = convertToReadableType(type, types, nameMap);
127
+ return i === 0 ? readableTypeName : `${acc} | ${readableTypeName}`;
128
+ }, "");
129
+ }
130
+ function formatArrayName(typeDesc, types, nameMap) {
131
+ // @ts-ignore
132
+ const innerTypeId = typeDesc.arrayOfTypes[0];
133
+ // const isMultipleTypeArray = findTypeById(innerTypeId, types).arrayOfTypes.length > 1
134
+ const isMultipleTypeArray = isHash(innerTypeId) &&
135
+ findTypeById(innerTypeId, types).isUnion &&
136
+ // @ts-ignore
137
+ findTypeById(innerTypeId, types).arrayOfTypes.length > 1;
138
+ const readableInnerType = getArrayName(typeDesc, types, nameMap);
139
+ return isMultipleTypeArray
140
+ ? `(${readableInnerType})[]` // add semicolons for union type
141
+ : `${readableInnerType}[]`;
142
+ }
@@ -0,0 +1,4 @@
1
+ import { TypeDescription, TypeStructure } from './model.js';
2
+ export declare function getTypeStructure(targetObj: any, // object that we want to create types for
3
+ types?: TypeDescription[]): TypeStructure;
4
+ export declare function optimizeTypeStructure(typeStructure: TypeStructure): void;
@@ -0,0 +1,258 @@
1
+ import * as hash from "hash.js";
2
+ import { TypeGroup } from './model.js';
3
+ import { findTypeById, getTypeDescriptionGroup, isArray, isDate, isHash, isObject, onlyUnique } from './util.js';
4
+ function createTypeDescription(typeObj, isUnion) {
5
+ if (isArray(typeObj)) {
6
+ return {
7
+ id: Hash(JSON.stringify([...typeObj, isUnion])),
8
+ arrayOfTypes: typeObj,
9
+ isUnion,
10
+ };
11
+ }
12
+ else {
13
+ return {
14
+ id: Hash(JSON.stringify(typeObj)),
15
+ typeObj,
16
+ };
17
+ }
18
+ }
19
+ function getIdByType(typeObj, types, isUnion = false) {
20
+ let typeDesc = types.find((el) => {
21
+ return typeObjectMatchesTypeDesc(typeObj, el, isUnion);
22
+ });
23
+ if (!typeDesc) {
24
+ typeDesc = createTypeDescription(typeObj, isUnion);
25
+ types.push(typeDesc);
26
+ }
27
+ return typeDesc.id;
28
+ }
29
+ function Hash(content) {
30
+ return hash.sha1().update(content).digest("hex");
31
+ }
32
+ function typeObjectMatchesTypeDesc(typeObj, typeDesc, isUnion) {
33
+ if (isArray(typeObj)) {
34
+ // @ts-expect-error
35
+ return arraysContainSameElements(typeObj, typeDesc.arrayOfTypes) && typeDesc.isUnion === isUnion;
36
+ }
37
+ else {
38
+ return objectsHaveSameEntries(typeObj, typeDesc.typeObj);
39
+ }
40
+ }
41
+ function arraysContainSameElements(arr1, arr2) {
42
+ if (arr1 === undefined || arr2 === undefined)
43
+ return false;
44
+ return arr1.sort().join("") === arr2.sort().join("");
45
+ }
46
+ function objectsHaveSameEntries(obj1, obj2) {
47
+ if (obj1 === undefined || obj2 === undefined)
48
+ return false;
49
+ const entries1 = Object.entries(obj1);
50
+ const entries2 = Object.entries(obj2);
51
+ const sameLength = entries1.length === entries2.length;
52
+ const sameTypes = entries1.every(([key, value]) => {
53
+ return obj2[key] === value;
54
+ });
55
+ return sameLength && sameTypes;
56
+ }
57
+ function getSimpleTypeName(value) {
58
+ if (value === null) {
59
+ return "null";
60
+ }
61
+ else if (value instanceof Date) {
62
+ return "Date";
63
+ }
64
+ else {
65
+ return typeof value;
66
+ }
67
+ }
68
+ function getTypeGroup(value) {
69
+ if (isDate(value)) {
70
+ return TypeGroup.Date;
71
+ }
72
+ else if (isArray(value)) {
73
+ return TypeGroup.Array;
74
+ }
75
+ else if (isObject(value)) {
76
+ return TypeGroup.Object;
77
+ }
78
+ else {
79
+ return TypeGroup.Primitive;
80
+ }
81
+ }
82
+ function createTypeObject(obj, types) {
83
+ return Object.entries(obj).reduce((typeObj, [key, value]) => {
84
+ const { rootTypeId } = getTypeStructure(value, types);
85
+ return {
86
+ ...typeObj,
87
+ [key]: rootTypeId,
88
+ };
89
+ }, {});
90
+ }
91
+ function getMergedObjects(typesOfArray, types) {
92
+ const typeObjects = typesOfArray.map((typeDesc) => typeDesc.typeObj);
93
+ const allKeys = typeObjects
94
+ // @ts-expect-error
95
+ .map((typeObj) => Object.keys(typeObj))
96
+ .reduce((a, b) => [...a, ...b], [])
97
+ .filter(onlyUnique);
98
+ const commonKeys = typeObjects.reduce((commonKeys, typeObj) => {
99
+ // @ts-expect-error
100
+ const keys = Object.keys(typeObj);
101
+ return commonKeys.filter((key) => keys.includes(key));
102
+ }, allKeys);
103
+ const getKeyType = (key) => {
104
+ const typesOfKey = typeObjects
105
+ .filter((typeObj) => {
106
+ return Object.keys(typeObj).includes(key);
107
+ })
108
+ .map((typeObj) => typeObj[key])
109
+ .filter(onlyUnique);
110
+ if (typesOfKey.length === 1) {
111
+ return typesOfKey.pop();
112
+ }
113
+ else {
114
+ return getInnerArrayType(typesOfKey, types);
115
+ }
116
+ };
117
+ const typeObj = allKeys.reduce((obj, key) => {
118
+ const isMandatory = commonKeys.includes(key);
119
+ const type = getKeyType(key);
120
+ const keyValue = isMandatory ? key : toOptionalKey(key);
121
+ return {
122
+ ...obj,
123
+ [keyValue]: type,
124
+ };
125
+ }, {});
126
+ return getIdByType(typeObj, types, true);
127
+ }
128
+ function toOptionalKey(key) {
129
+ return key.endsWith("--?") ? key : `${key}--?`;
130
+ }
131
+ function getMergedArrays(typesOfArray, types) {
132
+ // @ts-expect-error
133
+ const idsOfArrayTypes = typesOfArray?.map((typeDesc) => typeDesc.arrayOfTypes)
134
+ // @ts-expect-error
135
+ .reduce((a, b) => [...a, ...b], [])
136
+ .filter(onlyUnique);
137
+ if (idsOfArrayTypes.length === 1) {
138
+ return getIdByType([idsOfArrayTypes.pop()], types);
139
+ }
140
+ else {
141
+ return getIdByType([getInnerArrayType(idsOfArrayTypes, types)], types);
142
+ }
143
+ }
144
+ // we merge union types example: (number | string), null -> (number | string | null)
145
+ function getMergedUnion(typesOfArray, types) {
146
+ const innerUnionsTypes = typesOfArray
147
+ .map((id) => {
148
+ return findTypeById(id, types);
149
+ })
150
+ .filter((_) => !!_ && _.isUnion)
151
+ .map((_) => _.arrayOfTypes)
152
+ .reduce((a, b) => [...a, ...b], []);
153
+ const primitiveTypes = typesOfArray.filter((id) => !findTypeById(id, types) || !findTypeById(id, types).isUnion); // primitives or not union
154
+ return getIdByType([...innerUnionsTypes, ...primitiveTypes], types, true);
155
+ }
156
+ function getInnerArrayType(typesOfArray, types) {
157
+ // return inner array type
158
+ const containsUndefined = typesOfArray.includes("undefined");
159
+ const arrayTypesDescriptions = typesOfArray.map((id) => findTypeById(id, types)).filter((_) => !!_);
160
+ const allArrayType = arrayTypesDescriptions.filter((typeDesc) => getTypeDescriptionGroup(typeDesc) === TypeGroup.Array).length ===
161
+ typesOfArray.length;
162
+ const allArrayTypeWithUndefined = arrayTypesDescriptions.filter((typeDesc) => getTypeDescriptionGroup(typeDesc) === TypeGroup.Array).length + 1 ===
163
+ typesOfArray.length && containsUndefined;
164
+ const allObjectTypeWithUndefined = arrayTypesDescriptions.filter((typeDesc) => getTypeDescriptionGroup(typeDesc) === TypeGroup.Object).length + 1 ===
165
+ typesOfArray.length && containsUndefined;
166
+ const allObjectType = arrayTypesDescriptions.filter((typeDesc) => getTypeDescriptionGroup(typeDesc) === TypeGroup.Object).length ===
167
+ typesOfArray.length;
168
+ if (typesOfArray.length === 0) {
169
+ // no types in array -> empty union type
170
+ return getIdByType([], types, true);
171
+ }
172
+ if (typesOfArray.length === 1) {
173
+ // one type in array -> that will be our inner type
174
+ return typesOfArray.pop();
175
+ }
176
+ if (typesOfArray.length > 1) {
177
+ // multiple types in merge array
178
+ // if all are object we can merge them and return merged object as inner type
179
+ if (allObjectType)
180
+ return getMergedObjects(arrayTypesDescriptions, types);
181
+ // if all are array we can merge them and return merged array as inner type
182
+ if (allArrayType)
183
+ return getMergedArrays(arrayTypesDescriptions, types);
184
+ // all array types with posibble undefined, result type = undefined | (*mergedArray*)[]
185
+ if (allArrayTypeWithUndefined) {
186
+ return getMergedUnion([getMergedArrays(arrayTypesDescriptions, types), "undefined"], types);
187
+ }
188
+ // all object types with posibble undefined, result type = undefined | *mergedObject*
189
+ if (allObjectTypeWithUndefined) {
190
+ return getMergedUnion([getMergedObjects(arrayTypesDescriptions, types), "undefined"], types);
191
+ }
192
+ // if they are mixed or all primitive we cant merge them so we return as mixed union type
193
+ return getMergedUnion(typesOfArray, types);
194
+ }
195
+ }
196
+ export function getTypeStructure(targetObj, // object that we want to create types for
197
+ types = []) {
198
+ switch (getTypeGroup(targetObj)) {
199
+ case TypeGroup.Array:
200
+ const typesOfArray = targetObj.map((_) => getTypeStructure(_, types).rootTypeId).filter(onlyUnique);
201
+ const arrayInnerTypeId = getInnerArrayType(typesOfArray, types); // create "union type of array types"
202
+ const typeId = getIdByType([arrayInnerTypeId], types); // create type "array of union type"
203
+ return {
204
+ rootTypeId: typeId,
205
+ types,
206
+ };
207
+ case TypeGroup.Object:
208
+ const typeObj = createTypeObject(targetObj, types);
209
+ const objType = getIdByType(typeObj, types);
210
+ return {
211
+ rootTypeId: objType,
212
+ types,
213
+ };
214
+ case TypeGroup.Primitive:
215
+ return {
216
+ rootTypeId: getSimpleTypeName(targetObj),
217
+ types,
218
+ };
219
+ case TypeGroup.Date:
220
+ const dateType = getSimpleTypeName(targetObj);
221
+ return {
222
+ rootTypeId: dateType,
223
+ types,
224
+ };
225
+ }
226
+ }
227
+ function getAllUsedTypeIds({ rootTypeId, types }) {
228
+ const typeDesc = types.find((_) => _.id === rootTypeId);
229
+ const subTypes = (typeDesc) => {
230
+ switch (getTypeDescriptionGroup(typeDesc)) {
231
+ case TypeGroup.Array:
232
+ const arrSubTypes = typeDesc.arrayOfTypes
233
+ .filter(isHash)
234
+ .map((typeId) => {
235
+ const typeDesc = types.find((_) => _.id === typeId);
236
+ return subTypes(typeDesc);
237
+ })
238
+ .reduce((a, b) => [...a, ...b], []);
239
+ return [typeDesc.id, ...arrSubTypes];
240
+ case TypeGroup.Object:
241
+ const objSubTypes = Object.values(typeDesc.typeObj)
242
+ // @ts-expect-error
243
+ .filter(isHash)
244
+ .map((typeId) => {
245
+ const typeDesc = types.find((_) => _.id === typeId);
246
+ return subTypes(typeDesc);
247
+ })
248
+ .reduce((a, b) => [...a, ...b], []);
249
+ return [typeDesc.id, ...objSubTypes];
250
+ }
251
+ };
252
+ return subTypes(typeDesc);
253
+ }
254
+ export function optimizeTypeStructure(typeStructure) {
255
+ const usedTypeIds = getAllUsedTypeIds(typeStructure);
256
+ const optimizedTypes = typeStructure.types.filter((typeDesc) => usedTypeIds.includes(typeDesc.id));
257
+ typeStructure.types = optimizedTypes;
258
+ }
@@ -0,0 +1,2 @@
1
+ import { Options } from './model.js';
2
+ export declare function jsonToTS(json: any, userOptions?: Options): string[];
@@ -0,0 +1,34 @@
1
+ import { isArray, isObject } from './util.js';
2
+ import { getTypeStructure, optimizeTypeStructure, } from './get-type-structure.js';
3
+ import { getNames } from './get-names.js';
4
+ import { getInterfaceDescriptions, getInterfaceStringFromDescription, } from './get-interfaces.js';
5
+ export function jsonToTS(json, userOptions) {
6
+ const defaultOptions = {
7
+ rootName: 'RootObject',
8
+ };
9
+ const options = {
10
+ ...defaultOptions,
11
+ ...userOptions,
12
+ };
13
+ /**
14
+ * Parsing currently works with (Objects) and (Array of Objects) not and primitive types and mixed arrays etc..
15
+ * so we shall validate, so we dont start parsing non Object type
16
+ */
17
+ const isArrayOfObjects = isArray(json) &&
18
+ json.length > 0 &&
19
+ json.reduce((a, b) => a && isObject(b), true);
20
+ if (!(isObject(json) || isArrayOfObjects)) {
21
+ throw new Error('Only (Object) and (Array of Object) are supported');
22
+ }
23
+ const typeStructure = getTypeStructure(json);
24
+ /**
25
+ * due to merging array types some types are switched out for merged ones
26
+ * so we delete the unused ones here
27
+ */
28
+ optimizeTypeStructure(typeStructure);
29
+ const names = getNames(typeStructure, options.rootName);
30
+ return getInterfaceDescriptions(typeStructure, names).map((description) => getInterfaceStringFromDescription({
31
+ ...description,
32
+ isRoot: options.rootName === description.name,
33
+ }));
34
+ }
@@ -0,0 +1,39 @@
1
+ export declare enum TypeGroup {
2
+ Primitive = 0,
3
+ Array = 1,
4
+ Object = 2,
5
+ Date = 3
6
+ }
7
+ export interface TypeDescription {
8
+ id: string;
9
+ isUnion?: boolean;
10
+ typeObj?: {
11
+ [index: string]: string;
12
+ };
13
+ arrayOfTypes?: string[];
14
+ }
15
+ export interface TypeStructure {
16
+ rootTypeId: string;
17
+ types: TypeDescription[];
18
+ }
19
+ export interface NameEntry {
20
+ id: string;
21
+ name: string;
22
+ }
23
+ export interface NameStructure {
24
+ rootName: string;
25
+ names: NameEntry[];
26
+ }
27
+ export interface InterfaceDescription {
28
+ name: string;
29
+ typeMap: object;
30
+ }
31
+ export interface Options {
32
+ rootName?: string;
33
+ /** To generate using type alias instead of interface */
34
+ useTypeAlias?: boolean;
35
+ }
36
+ export interface KeyMetaData {
37
+ keyValue: string;
38
+ isOptional: boolean;
39
+ }
@@ -0,0 +1,7 @@
1
+ export var TypeGroup;
2
+ (function (TypeGroup) {
3
+ TypeGroup[TypeGroup["Primitive"] = 0] = "Primitive";
4
+ TypeGroup[TypeGroup["Array"] = 1] = "Array";
5
+ TypeGroup[TypeGroup["Object"] = 2] = "Object";
6
+ TypeGroup[TypeGroup["Date"] = 3] = "Date";
7
+ })(TypeGroup || (TypeGroup = {}));
@@ -0,0 +1,10 @@
1
+ import { KeyMetaData, TypeDescription, TypeGroup } from './model.js';
2
+ export declare function isHash(str: string): boolean;
3
+ export declare function onlyUnique(value: any, index: any, self: any): boolean;
4
+ export declare function isArray(x: any): boolean;
5
+ export declare function isNonArrayUnion(typeName: string): boolean;
6
+ export declare function isObject(x: any): boolean;
7
+ export declare function isDate(x: any): x is Date;
8
+ export declare function parseKeyMetaData(key: string): KeyMetaData;
9
+ export declare function getTypeDescriptionGroup(desc: TypeDescription): TypeGroup;
10
+ export declare function findTypeById(id: string, types: TypeDescription[]): TypeDescription;
@@ -0,0 +1,49 @@
1
+ import { TypeGroup } from './model.js';
2
+ export function isHash(str) {
3
+ return str.length === 40;
4
+ }
5
+ export function onlyUnique(value, index, self) {
6
+ return self.indexOf(value) === index;
7
+ }
8
+ export function isArray(x) {
9
+ return Object.prototype.toString.call(x) === "[object Array]";
10
+ }
11
+ export function isNonArrayUnion(typeName) {
12
+ const arrayUnionRegex = /^\(.*\)\[\]$/;
13
+ return typeName.includes(" | ") && !arrayUnionRegex.test(typeName);
14
+ }
15
+ export function isObject(x) {
16
+ return Object.prototype.toString.call(x) === "[object Object]" && x !== null;
17
+ }
18
+ export function isDate(x) {
19
+ return x instanceof Date;
20
+ }
21
+ export function parseKeyMetaData(key) {
22
+ const isOptional = key.endsWith("--?");
23
+ if (isOptional) {
24
+ return {
25
+ isOptional,
26
+ keyValue: key.slice(0, -3)
27
+ };
28
+ }
29
+ else {
30
+ return {
31
+ isOptional,
32
+ keyValue: key
33
+ };
34
+ }
35
+ }
36
+ export function getTypeDescriptionGroup(desc) {
37
+ if (desc === undefined) {
38
+ return TypeGroup.Primitive;
39
+ }
40
+ else if (desc.arrayOfTypes !== undefined) {
41
+ return TypeGroup.Array;
42
+ }
43
+ else {
44
+ return TypeGroup.Object;
45
+ }
46
+ }
47
+ export function findTypeById(id, types) {
48
+ return types.find(_ => _.id === id);
49
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@contentstorage/core",
3
3
  "author": "Kaido Hussar <kaidohus@gmail.com>",
4
4
  "homepage": "https://contentstorage.app",
5
- "version": "0.3.29",
5
+ "version": "0.3.31",
6
6
  "type": "module",
7
7
  "description": "Fetch content from contentstorage and generate TypeScript types",
8
8
  "module": "dist/index.js",
@@ -27,7 +27,9 @@
27
27
  "dependencies": {
28
28
  "axios": "^1.7.2",
29
29
  "chalk": "^4.1.2",
30
- "json-to-ts": "^2.0.1"
30
+ "es7-shim": "^6.0.0",
31
+ "hash.js": "^1.0.3",
32
+ "pluralize": "^3.1.0"
31
33
  },
32
34
  "devDependencies": {
33
35
  "@eslint/js": "^9.26.0",
@@ -1,6 +0,0 @@
1
- import { AppConfig } from '../types.js';
2
- /**
3
- * Helper function to define your application configuration.
4
- * Provides autocompletion and type-checking for contentstorage.config.js files.
5
- */
6
- export declare function defineConfig(config: AppConfig): AppConfig;
@@ -1,19 +0,0 @@
1
- /**
2
- * Helper function to define your application configuration.
3
- * Provides autocompletion and type-checking for contentstorage.config.js files.
4
- */
5
- export function defineConfig(config) {
6
- // You can add basic runtime validation here if desired,
7
- // e.g., check if contentUrl is a valid URL format,
8
- // or if languageCodes is not empty.
9
- if (!config.languageCodes || config.languageCodes.length === 0) {
10
- console.warn('Warning: languageCodes array is empty or missing in the configuration.');
11
- }
12
- if (!config.contentDir) {
13
- // This would typically be a hard error, but defineConfig is more for type safety at edit time.
14
- // Runtime validation (see point 3) is better for hard errors.
15
- console.warn('Warning: contentDir is missing in the configuration.');
16
- }
17
- // ... other checks
18
- return config;
19
- }