@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.
- package/dist/helpers/flattenJson.d.ts +1 -0
- package/dist/helpers/flattenJson.js +36 -0
- package/dist/lib/contentManagement.d.ts +1 -1
- package/dist/lib/contentManagement.js +2 -4
- package/dist/scripts/generate-types.js +98 -42
- package/dist/scripts/pull-content.js +3 -0
- package/dist/types.d.ts +11 -7
- package/package.json +1 -1
|
@@ -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):
|
|
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
|
|
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();
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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 ${
|
|
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');
|
|
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
|
-
//
|
|
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);
|
|
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
|
-
*
|
|
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.
|
|
31
|
-
* @template Prefix Internal accumulator for the current path prefix
|
|
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
|
|
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.
|
|
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",
|