@contentstorage/core 0.3.14 → 0.3.16

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 flattenJson(data: any, prefix?: string, result?: Record<string, any>): Record<string, any>;
@@ -0,0 +1,36 @@
1
+ export function flattenJson(data, prefix = '', result = {}) {
2
+ if (typeof data === 'object' && data !== null) {
3
+ if (Array.isArray(data)) {
4
+ if (data.length === 0 && prefix) {
5
+ // Handle empty arrays if prefix exists
6
+ result[prefix] = [];
7
+ }
8
+ data.forEach((item, index) => {
9
+ flattenJson(item, prefix ? `${prefix}.${index}` : `${index}`, result);
10
+ });
11
+ }
12
+ else {
13
+ // It's an object
14
+ let isEmptyObject = true;
15
+ for (const key in data) {
16
+ if (Object.prototype.hasOwnProperty.call(data, key)) {
17
+ isEmptyObject = false;
18
+ const newPrefix = prefix ? `${prefix}.${key}` : key;
19
+ flattenJson(data[key], newPrefix, result);
20
+ }
21
+ }
22
+ if (isEmptyObject && prefix) {
23
+ // Handle empty objects if prefix exists
24
+ result[prefix] = {};
25
+ }
26
+ }
27
+ }
28
+ else if (prefix) {
29
+ // Primitive value (string, number, boolean, null)
30
+ result[prefix] = data;
31
+ }
32
+ // If the initial data itself is a primitive and prefix is empty, it means we can't flatten it into key-value pairs.
33
+ // This function is designed to take a root object or array.
34
+ // If the root `data` is a primitive, `result` would remain empty, which is fine; `pullContent` should validate the root.
35
+ return result;
36
+ }
@@ -4,7 +4,7 @@ import { ContentStructure, DotNotationPaths } from '../types.js';
4
4
  * It will internally ensure the application configuration (for contentDir) is loaded.
5
5
  * @param languageCode The language code (e.g., 'EN', 'FR') for the JSON file to load.
6
6
  */
7
- export declare function setContentLanguage(contentJson: ContentStructure | null): Promise<void>;
7
+ export declare function setContentLanguage(contentJson: ContentStructure | null): void;
8
8
  /**
9
9
  * Retrieves the text string from the loaded JSON content for the given path.
10
10
  * Autocompletion for pathString is enabled via module augmentation of CustomContentStructure.
@@ -4,7 +4,7 @@ let activeContent = null;
4
4
  * It will internally ensure the application configuration (for contentDir) is loaded.
5
5
  * @param languageCode The language code (e.g., 'EN', 'FR') for the JSON file to load.
6
6
  */
7
- export async function setContentLanguage(contentJson) {
7
+ export function setContentLanguage(contentJson) {
8
8
  if (!contentJson || typeof contentJson !== 'object') {
9
9
  throw new Error('[Contentstorage] Invalid contentUrl provided to setContentLanguage.');
10
10
  }
@@ -27,9 +27,7 @@ export async function setContentLanguage(contentJson) {
27
27
  * If not provided, and path is not found/value not string, undefined is returned.
28
28
  * @returns The text string from the JSON, or the fallbackValue, or undefined.
29
29
  */
30
- export function getText(
31
- // @ts-expect-error Is fine
32
- pathString, fallbackValue) {
30
+ export function getText(pathString, fallbackValue) {
33
31
  if (!activeContent) {
34
32
  const msg = `[Contentstorage] getText: Content not loaded (Path: "${String(pathString)}"). Ensure setContentLanguage() was called and completed successfully.`;
35
33
  console.warn(msg);
@@ -2,76 +2,132 @@
2
2
  // ^ Shebang ensures the script is executed with Node.js
3
3
  import fs from 'fs/promises';
4
4
  import path from 'path';
5
+ import axios from 'axios';
5
6
  import jsonToTS from 'json-to-ts'; // Import the library
6
7
  import chalk from 'chalk'; // Optional: for colored output
7
8
  import { loadConfig } from '../lib/configLoader.js';
9
+ import { flattenJson } from '../helpers/flattenJson.js';
8
10
  export async function generateTypes() {
9
11
  console.log(chalk.blue('Starting type generation...'));
10
- const config = await loadConfig(); // Ensure loadConfig provides languageCodes
11
- console.log(chalk.gray(`Content will be read from directory: ${config.contentDir}`));
12
- console.log(chalk.gray(`Saving TypeScript types to: ${config.typesOutputFile}`));
12
+ const config = await loadConfig();
13
+ if (!config.typesOutputFile) {
14
+ console.error(chalk.red.bold("Configuration error: 'typesOutputFile' is missing."));
15
+ process.exit(1);
16
+ }
17
+ if (!config.languageCodes ||
18
+ !Array.isArray(config.languageCodes) ||
19
+ config.languageCodes.length === 0) {
20
+ console.error(chalk.red.bold("Configuration error: 'languageCodes' must be a non-empty array."));
21
+ process.exit(1);
22
+ }
23
+ console.log(chalk.gray(`TypeScript types will be saved to: ${config.typesOutputFile}`));
24
+ let jsonObject; // To hold the JSON data from either local or remote source
25
+ let dataSourceDescription = ''; // For clearer logging
26
+ const firstLanguageCode = config.languageCodes[0];
13
27
  try {
14
- // Validate languageCodes from config
15
- if (!config.languageCodes ||
16
- !Array.isArray(config.languageCodes) ||
17
- config.languageCodes.length === 0) {
18
- throw new Error('config.languageCodes is missing, not an array, or empty. Cannot determine which JSON file to use for type generation.');
28
+ let attemptLocalLoad = false;
29
+ if (config.contentDir) {
30
+ try {
31
+ await fs.stat(config.contentDir); // Check if directory exists
32
+ attemptLocalLoad = true;
33
+ console.log(chalk.gray(`Local content directory found: ${config.contentDir}`));
34
+ }
35
+ catch (statError) {
36
+ if (statError.code === 'ENOENT') {
37
+ console.log(chalk.yellow(`Local content directory specified but not found: ${config.contentDir}. Will attempt to fetch from URL.`));
38
+ // attemptLocalLoad remains false
39
+ }
40
+ else {
41
+ // Other errors accessing contentDir (e.g., permissions)
42
+ throw new Error(`Error accessing content directory ${config.contentDir}: ${statError.message}`);
43
+ }
44
+ }
19
45
  }
20
- const firstLanguageCode = config.languageCodes[0];
21
- const targetFilename = `${firstLanguageCode}.json`;
22
- const jsonFilePath = path.join(config.contentDir, targetFilename);
23
- console.log(chalk.gray(`Attempting to generate types using the JSON file for the first configured language code ('${firstLanguageCode}').`));
24
- console.log(chalk.gray(`Target file: ${jsonFilePath}`));
25
- // Read the specific JSON file
26
- let jsonContent;
27
- try {
28
- jsonContent = await fs.readFile(jsonFilePath, 'utf-8');
46
+ else {
47
+ console.log(chalk.yellow(`Local content directory (config.contentDir) not specified. Attempting to fetch from URL.`));
29
48
  }
30
- catch (err) {
31
- if (err.code === 'ENOENT') {
32
- throw new Error(`Target JSON file not found: ${jsonFilePath}. Ensure content for language code '${firstLanguageCode}' has been pulled and exists at this location.`);
49
+ if (attemptLocalLoad && config.contentDir) {
50
+ // --- Load from local file system ---
51
+ const targetFilename = `${firstLanguageCode}.json`;
52
+ const jsonFilePath = path.join(config.contentDir, targetFilename);
53
+ dataSourceDescription = `local file (${jsonFilePath})`;
54
+ console.log(chalk.gray(`Attempting to read JSON from: ${jsonFilePath}`));
55
+ try {
56
+ const jsonContentString = await fs.readFile(jsonFilePath, 'utf-8');
57
+ console.log(chalk.gray('Parsing JSON'));
58
+ const parsendJsonObject = JSON.parse(jsonContentString);
59
+ console.log(chalk.gray('Flattening JSON for type generation'));
60
+ jsonObject = flattenJson(parsendJsonObject);
61
+ console.log(chalk.green(`Successfully read and parsed JSON from ${jsonFilePath}.`));
62
+ }
63
+ catch (fileError) {
64
+ if (fileError.code === 'ENOENT') {
65
+ throw new Error(`Target JSON file not found at ${jsonFilePath}. ` +
66
+ `Ensure content for language code '${firstLanguageCode}' has been pulled and exists locally, ` +
67
+ `or ensure 'contentDir' is not set if you intend to fetch from URL.`);
68
+ }
69
+ throw new Error(`Failed to read or parse JSON from ${jsonFilePath}: ${fileError.message}`);
33
70
  }
34
- // Re-throw other fs.readFile errors (e.g., permission issues)
35
- throw new Error(`Failed to read file ${jsonFilePath}: ${err.message}`);
36
71
  }
37
- // Parse the JSON content
38
- let jsonObject;
39
- try {
40
- jsonObject = JSON.parse(jsonContent);
72
+ else {
73
+ // --- Fetch from URL ---
74
+ if (!config.contentUrl) {
75
+ throw new Error("Cannot generate types: 'contentDir' is not accessible or not specified, and 'contentUrl' is also missing in configuration.");
76
+ }
77
+ const fileUrl = `${config.contentUrl}/${firstLanguageCode}.json`; // Adjust URL construction if necessary
78
+ dataSourceDescription = `remote URL (${fileUrl})`;
79
+ console.log(chalk.gray(`Attempting to fetch JSON from: ${fileUrl}`));
80
+ try {
81
+ const response = await axios.get(fileUrl, { responseType: 'json' });
82
+ const jsonResponse = response.data;
83
+ console.log(chalk.gray('Flattening JSON for type generation'));
84
+ jsonObject = flattenJson(jsonResponse);
85
+ if (typeof jsonObject !== 'object' || jsonObject === null) {
86
+ throw new Error(`Workspaceed data from ${fileUrl} is not a valid JSON object. Received type: ${typeof jsonObject}`);
87
+ }
88
+ console.log(chalk.green(`Successfully fetched and parsed JSON from ${fileUrl}. This content will not be saved locally.`));
89
+ }
90
+ catch (fetchError) {
91
+ let errorDetail = fetchError.message;
92
+ if (axios.isAxiosError(fetchError)) {
93
+ errorDetail = `Status: ${fetchError.response?.status}, Response: ${JSON.stringify(fetchError.response?.data)}`;
94
+ }
95
+ throw new Error(`Failed to fetch JSON from ${fileUrl}: ${errorDetail}`);
96
+ }
41
97
  }
42
- catch (parseError) {
43
- throw new Error(`Failed to parse JSON from file ${targetFilename}: ${parseError.message}`);
98
+ // Validate the obtained jsonObject (must be an object or array for json-to-ts)
99
+ if (typeof jsonObject !== 'object' || jsonObject === null) {
100
+ // jsonToTS can handle root arrays too, but if it's primitive it's an issue.
101
+ // Allowing arrays here explicitly based on jsonToTS capability.
102
+ if (!Array.isArray(jsonObject)) {
103
+ throw new Error(`The content obtained from ${dataSourceDescription} is not a JSON object or array (type: ${typeof jsonObject}). Cannot generate types.`);
104
+ }
44
105
  }
45
106
  // Generate TypeScript interfaces using json-to-ts
46
- // The root type name for the generated interface. You might want to make this configurable.
47
- const rootTypeName = 'ContentRoot';
107
+ const rootTypeName = 'ContentRoot'; // As per your previous update
108
+ console.log(chalk.gray(`Generating TypeScript types with root name '${rootTypeName}'...`));
48
109
  const typeDeclarations = jsonToTS.default(jsonObject, {
49
110
  rootName: rootTypeName,
50
111
  });
51
- // If your previous code `jsonToTS.default(...)` was correct for your setup,
52
- // please revert the line above to:
53
- // const typeDeclarations: string[] = jsonToTS.default(jsonObject, {
54
- // rootName: rootTypeName,
55
- // });
56
112
  if (!typeDeclarations || typeDeclarations.length === 0) {
57
- throw new Error(`Could not generate types from ${targetFilename}. The file might be empty, malformed, or not produce any types.`);
113
+ throw new Error(`Could not generate types from the content of ${dataSourceDescription}. 'json-to-ts' returned no declarations.`);
58
114
  }
59
- const outputContent = typeDeclarations.join('\n\n'); // Add extra newline between interfaces for readability
60
- // Ensure the output directory exists
115
+ const outputContent = typeDeclarations.join('\n\n');
116
+ // Ensure the output directory exists for the types file
61
117
  const outputDir = path.dirname(config.typesOutputFile);
62
118
  await fs.mkdir(outputDir, { recursive: true });
63
119
  // Write the generated types to the output file
64
120
  await fs.writeFile(config.typesOutputFile, outputContent, 'utf-8');
65
- console.log(chalk.green(`TypeScript types generated successfully at ${config.typesOutputFile}`));
121
+ console.log(chalk.green(`TypeScript types generated successfully at ${config.typesOutputFile} using data from ${dataSourceDescription}.`));
66
122
  }
67
123
  catch (error) {
68
124
  console.error(chalk.red.bold('\nError generating TypeScript types:'));
69
125
  console.error(chalk.red(error.message));
70
- // It's good practice to log the stack for unexpected errors if not already done by a higher-level handler
71
- // if (error.stack) {
126
+ // Optionally log stack for more details during development
127
+ // if (error.stack && process.env.NODE_ENV === 'development') {
72
128
  // console.error(chalk.gray(error.stack));
73
129
  // }
74
- process.exit(1); // Exit with error code
130
+ process.exit(1);
75
131
  }
76
132
  }
77
133
  generateTypes();
@@ -43,6 +43,9 @@ export async function pullContent() {
43
43
  Array.isArray(jsonData) /* jsonData === null is already covered */) {
44
44
  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.`);
45
45
  }
46
+ console.log(chalk.green(`Received and json for ${languageCode}. Saving to ${outputPath}`));
47
+ await fs.writeFile(outputPath, JSON.stringify(jsonData, null, 2));
48
+ console.log(chalk.green(`Successfully saved ${outputPath}`));
46
49
  console.log(chalk.green(`Received JSON for ${languageCode}. Saving to ${outputPath}`));
47
50
  // Write the JSON data to the file
48
51
  await fs.writeFile(outputPath, JSON.stringify(jsonData, null, 2));
package/dist/types.d.ts CHANGED
@@ -23,13 +23,17 @@ export type LanguageCode = 'SQ' | 'BE' | 'BS' | 'BG' | 'HR' | 'CS' | 'DA' | 'NL'
23
23
  export interface ContentStructure {
24
24
  [key: string]: any;
25
25
  }
26
+ type MaxPathDepth = [any, any, any, any, any, any];
26
27
  /**
27
- * Generates a union of all possible dot-notation path strings for a given object type `T`.
28
- * Defaults to using the `ContentStructure` interface, which consumers augment.
28
+ * Generates a union of all possible dot-notation path strings for a given object type `T`,
29
+ * with a limit on recursion depth to prevent TypeScript errors.
30
+ * Defaults to using the `CustomContentStructure` interface, which consumers augment.
29
31
  *
30
- * @template T The object type to generate paths from. Defaults to `ContentStructure`.
31
- * @template Prefix Internal accumulator for the current path prefix during recursion.
32
+ * @template T The object type to generate paths from.
33
+ * @template Prefix Internal accumulator for the current path prefix.
34
+ * @template CurrentDepth Internal tuple to track recursion depth.
32
35
  */
33
- export type DotNotationPaths<T = ContentStructure, Prefix extends string = ''> = T extends object ? {
34
- [K in keyof T]-?: K extends string | number ? T[K] extends object ? `${Prefix}${K}` | DotNotationPaths<T[K], `${Prefix}${K}.`> : `${Prefix}${K}` : never;
35
- }[keyof T] : '';
36
+ export type DotNotationPaths<T = ContentStructure, Prefix extends string = '', CurrentDepth extends any[] = []> = T extends object ? {
37
+ [K in keyof T & (string | number)]-?: CurrentDepth['length'] extends MaxPathDepth['length'] ? `${Prefix}${K}` : T[K] extends object ? `${Prefix}${K}` | DotNotationPaths<T[K], `${Prefix}${K}.`, [...CurrentDepth, any]> : `${Prefix}${K}`;
38
+ }[keyof T & (string | number)] : never;
39
+ export {};
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.14",
5
+ "version": "0.3.16",
6
6
  "type": "module",
7
7
  "description": "Fetch content from contentstorage and generate TypeScript types",
8
8
  "module": "dist/index.js",