fimo 0.7.0 → 0.8.0-staging.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.
- package/assets/agent-templates/seo-audit/GOAL.md +7 -3
- package/assets/content-templates/hooks-template.eta +42 -605
- package/assets/skills/fimo/references/content.md +1 -1
- package/assets/skills/fimo-studio/SKILL.md +5 -4
- package/assets/skills/fimo-studio/references/cms.md +30 -14
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +106 -100
- package/dist/scripts/sync-schemas.js +148 -62
- package/package.json +1 -1
- package/release.json +2 -2
- package/templates/react-router/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, join, resolve } from 'node:path';
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
3
|
/**
|
|
4
4
|
* Sync schema files — ensure all `.json` schemas have corresponding `.ts`
|
|
5
5
|
* hook files generated from the hooks template.
|
|
@@ -10,8 +10,17 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
10
10
|
*/
|
|
11
11
|
import { pascalCase } from 'change-case';
|
|
12
12
|
import { Eta } from 'eta';
|
|
13
|
+
import { z } from 'zod';
|
|
13
14
|
import { assetPath } from './utils/assets.js';
|
|
14
15
|
const SCHEMAS_REL = 'src/schemas';
|
|
16
|
+
const MINIMUM_THIN_CONTENT_CLIENT_RUNTIME_VERSION = '0.7.0';
|
|
17
|
+
const CONTENT_CLIENT_RUNTIME_UPGRADE_COMMAND = 'npm install fimo@latest';
|
|
18
|
+
const SCHEMA_DEFINITION_SCHEMA = z.object({
|
|
19
|
+
uid: z.string().min(1),
|
|
20
|
+
isSingleton: z.boolean().optional(),
|
|
21
|
+
i18n: z.object({ enabled: z.boolean().optional() }).optional(),
|
|
22
|
+
fields: z.record(z.string(), z.unknown()).optional(),
|
|
23
|
+
});
|
|
15
24
|
function getSchemaFiles(schemasDir) {
|
|
16
25
|
if (!existsSync(schemasDir)) {
|
|
17
26
|
return [];
|
|
@@ -20,13 +29,113 @@ function getSchemaFiles(schemasDir) {
|
|
|
20
29
|
.filter((f) => f.endsWith('.json'))
|
|
21
30
|
.map((f) => join(schemasDir, f));
|
|
22
31
|
}
|
|
23
|
-
function
|
|
32
|
+
function getGeneratedFiles(schemasDir) {
|
|
33
|
+
if (!existsSync(schemasDir)) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
return readdirSync(schemasDir)
|
|
37
|
+
.filter((f) => f.endsWith('.ts') && f !== 'index.ts')
|
|
38
|
+
.map((f) => join(schemasDir, f));
|
|
39
|
+
}
|
|
40
|
+
function parseVersion(version) {
|
|
41
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([^+]+))?(?:\+.*)?$/.exec(version);
|
|
42
|
+
if (!match) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
parts: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
47
|
+
prerelease: match[4] !== undefined,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function isVersionAtLeast(version, minimum) {
|
|
51
|
+
const parsed = parseVersion(version);
|
|
52
|
+
const parsedMinimum = parseVersion(minimum);
|
|
53
|
+
if (!parsed || !parsedMinimum) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
for (let index = 0; index < parsed.parts.length; index++) {
|
|
57
|
+
if (parsed.parts[index] !== parsedMinimum.parts[index]) {
|
|
58
|
+
return parsed.parts[index] > parsedMinimum.parts[index];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return !parsed.prerelease || parsedMinimum.prerelease;
|
|
62
|
+
}
|
|
63
|
+
function resolveProjectRuntime(cwd) {
|
|
64
|
+
const packagePath = join(cwd, 'node_modules', 'fimo', 'package.json');
|
|
65
|
+
if (!existsSync(packagePath)) {
|
|
66
|
+
throw new Error(`Cannot generate schema clients because the project-local fimo package could not be resolved from ${cwd}. ` +
|
|
67
|
+
`Run \`${CONTENT_CLIENT_RUNTIME_UPGRADE_COMMAND}\` in the project, then run \`npx fimo@latest validate\` again. ` +
|
|
68
|
+
'No generated files were changed.');
|
|
69
|
+
}
|
|
70
|
+
let version;
|
|
24
71
|
try {
|
|
25
|
-
|
|
72
|
+
version = JSON.parse(readFileSync(packagePath, 'utf-8')).version;
|
|
26
73
|
}
|
|
27
74
|
catch {
|
|
28
|
-
|
|
75
|
+
version = null;
|
|
76
|
+
}
|
|
77
|
+
if (typeof version !== 'string' || !parseVersion(version)) {
|
|
78
|
+
throw new Error(`Cannot generate schema clients with project-local fimo@${typeof version === 'string' ? version : 'unknown'}. ` +
|
|
79
|
+
`Run \`${CONTENT_CLIENT_RUNTIME_UPGRADE_COMMAND}\` in the project, then run \`npx fimo@latest validate\` again. ` +
|
|
80
|
+
'No generated files were changed.');
|
|
81
|
+
}
|
|
82
|
+
return { packageRoot: dirname(packagePath), version };
|
|
83
|
+
}
|
|
84
|
+
function templatePath(runtime, fileName) {
|
|
85
|
+
if (runtime && !isVersionAtLeast(runtime.version, MINIMUM_THIN_CONTENT_CLIENT_RUNTIME_VERSION)) {
|
|
86
|
+
return join(runtime.packageRoot, 'assets', 'content-templates', fileName);
|
|
87
|
+
}
|
|
88
|
+
return assetPath('content-templates', fileName);
|
|
89
|
+
}
|
|
90
|
+
function loadSchemas(schemaFiles) {
|
|
91
|
+
const errors = [];
|
|
92
|
+
const schemas = [];
|
|
93
|
+
const outputOwners = new Map();
|
|
94
|
+
for (const path of schemaFiles) {
|
|
95
|
+
const fileUid = basename(path, '.json');
|
|
96
|
+
const pascal = pascalCase(fileUid);
|
|
97
|
+
const existingOwner = outputOwners.get(pascal);
|
|
98
|
+
if (existingOwner && existingOwner !== fileUid) {
|
|
99
|
+
errors.push(`${basename(path)} and ${existingOwner}.json both generate ${pascal}.ts. Rename one schema to keep generated module names unique.`);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
outputOwners.set(pascal, fileUid);
|
|
103
|
+
}
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
errors.push(`${basename(path)} is invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
113
|
+
errors.push(`${basename(path)} must contain a JSON object.`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const candidate = parsed;
|
|
117
|
+
if (typeof candidate.uid !== 'string' || candidate.uid.length === 0) {
|
|
118
|
+
errors.push(`${basename(path)} is missing \"uid\". Set it to \"${fileUid}\".`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (candidate.uid !== fileUid) {
|
|
122
|
+
errors.push(`${basename(path)} declares uid \"${candidate.uid}\". It must match the file name \"${fileUid}\".`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const validation = SCHEMA_DEFINITION_SCHEMA.safeParse(candidate);
|
|
126
|
+
if (!validation.success) {
|
|
127
|
+
const details = validation.error.issues
|
|
128
|
+
.map((issue) => `${issue.path.join('.') || 'schema'}: ${issue.message}`)
|
|
129
|
+
.join('; ');
|
|
130
|
+
errors.push(`${basename(path)} has an invalid schema shape: ${details}`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
schemas.push({ pascal, schema: validation.data });
|
|
134
|
+
}
|
|
135
|
+
if (errors.length > 0) {
|
|
136
|
+
throw new Error(`Cannot generate schema clients:\n- ${errors.join('\n- ')}\nNo generated files were changed.`);
|
|
29
137
|
}
|
|
138
|
+
return schemas;
|
|
30
139
|
}
|
|
31
140
|
function normalizeFields(fields) {
|
|
32
141
|
return Object.fromEntries(Object.entries(fields).map(([key, value]) => {
|
|
@@ -99,87 +208,64 @@ export function syncSchemas(options = {}) {
|
|
|
99
208
|
}
|
|
100
209
|
};
|
|
101
210
|
const schemasDir = resolve(cwd, SCHEMAS_REL);
|
|
211
|
+
const schemaFiles = getSchemaFiles(schemasDir);
|
|
212
|
+
const schemas = loadSchemas(schemaFiles);
|
|
213
|
+
const generatedFilesOnDisk = getGeneratedFiles(schemasDir);
|
|
214
|
+
const runtime = schemas.length > 0 || generatedFilesOnDisk.length > 0 ? resolveProjectRuntime(cwd) : null;
|
|
102
215
|
const templates = [
|
|
103
216
|
{
|
|
104
217
|
suffix: '.types.ts',
|
|
105
|
-
path:
|
|
218
|
+
path: templatePath(runtime, 'types-template.eta'),
|
|
106
219
|
},
|
|
107
220
|
{
|
|
108
221
|
suffix: '.ts',
|
|
109
|
-
path:
|
|
222
|
+
path: templatePath(runtime, 'hooks-template.eta'),
|
|
110
223
|
},
|
|
111
224
|
].map((template) => ({
|
|
112
225
|
...template,
|
|
113
226
|
source: readFileSync(template.path, 'utf-8'),
|
|
114
227
|
}));
|
|
115
228
|
const eta = new Eta({ views: undefined, useWith: true, varName: 'it', autoEscape: false });
|
|
116
|
-
const
|
|
117
|
-
const schemasByPath = new Map(schemaFiles.flatMap((schemaPath) => {
|
|
118
|
-
const schema = readSchema(schemaPath);
|
|
119
|
-
return schema ? [[schemaPath, schema]] : [];
|
|
120
|
-
}));
|
|
121
|
-
const localizedByUid = new Map([...schemasByPath.values()].map((schema) => [schema.uid, schema.i18n?.enabled !== false]));
|
|
229
|
+
const localizedByUid = new Map(schemas.map(({ schema }) => [schema.uid, schema.i18n?.enabled !== false]));
|
|
122
230
|
const expectedTsFiles = new Set();
|
|
231
|
+
const generatedFiles = new Map();
|
|
123
232
|
let created = 0;
|
|
124
233
|
let updated = 0;
|
|
125
234
|
let deleted = 0;
|
|
126
|
-
for (const
|
|
127
|
-
const uid = jsonPath
|
|
128
|
-
.split('/')
|
|
129
|
-
.pop()
|
|
130
|
-
.replace(/\.json$/, '');
|
|
131
|
-
const pascal = pascalCase(uid);
|
|
132
|
-
const schema = schemasByPath.get(jsonPath);
|
|
133
|
-
if (!schema) {
|
|
134
|
-
console.error(`Failed to parse schema: ${jsonPath}`);
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (!schema.uid) {
|
|
138
|
-
schema.uid = uid;
|
|
139
|
-
}
|
|
235
|
+
for (const { pascal, schema } of schemas) {
|
|
140
236
|
const model = schemaToModel(schema, localizedByUid);
|
|
141
237
|
for (const template of templates) {
|
|
142
238
|
const tsPath = join(schemasDir, `${pascal}${template.suffix}`);
|
|
143
239
|
expectedTsFiles.add(tsPath);
|
|
240
|
+
generatedFiles.set(tsPath, eta.renderString(template.source, { model }));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
for (const [tsPath, content] of generatedFiles) {
|
|
244
|
+
const dir = dirname(tsPath);
|
|
245
|
+
if (!existsSync(dir)) {
|
|
246
|
+
mkdirSync(dir, { recursive: true });
|
|
247
|
+
}
|
|
248
|
+
if (!existsSync(tsPath)) {
|
|
249
|
+
writeFileSync(tsPath, content, 'utf-8');
|
|
250
|
+
created++;
|
|
251
|
+
log(`Created: ${tsPath}`);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (readFileSync(tsPath, 'utf-8') !== content) {
|
|
255
|
+
writeFileSync(tsPath, content, 'utf-8');
|
|
256
|
+
updated++;
|
|
257
|
+
log(`Updated: ${tsPath}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
for (const tsFile of generatedFilesOnDisk) {
|
|
261
|
+
if (!expectedTsFiles.has(tsFile)) {
|
|
144
262
|
try {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
});
|
|
148
|
-
const dir = dirname(tsPath);
|
|
149
|
-
if (!existsSync(dir)) {
|
|
150
|
-
mkdirSync(dir, { recursive: true });
|
|
151
|
-
}
|
|
152
|
-
if (!existsSync(tsPath)) {
|
|
153
|
-
writeFileSync(tsPath, content, 'utf-8');
|
|
154
|
-
created++;
|
|
155
|
-
log(`Created: ${tsPath}`);
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
if (readFileSync(tsPath, 'utf-8') !== content) {
|
|
159
|
-
writeFileSync(tsPath, content, 'utf-8');
|
|
160
|
-
updated++;
|
|
161
|
-
log(`Updated: ${tsPath}`);
|
|
162
|
-
}
|
|
263
|
+
unlinkSync(tsFile);
|
|
264
|
+
deleted++;
|
|
265
|
+
log(`Deleted orphaned: ${tsFile}`);
|
|
163
266
|
}
|
|
164
267
|
catch (error) {
|
|
165
|
-
console.error(`Failed to
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
if (existsSync(schemasDir)) {
|
|
170
|
-
const allTsFiles = readdirSync(schemasDir)
|
|
171
|
-
.filter((f) => f.endsWith('.ts') && f !== 'index.ts')
|
|
172
|
-
.map((f) => join(schemasDir, f));
|
|
173
|
-
for (const tsFile of allTsFiles) {
|
|
174
|
-
if (!expectedTsFiles.has(tsFile)) {
|
|
175
|
-
try {
|
|
176
|
-
unlinkSync(tsFile);
|
|
177
|
-
deleted++;
|
|
178
|
-
log(`Deleted orphaned: ${tsFile}`);
|
|
179
|
-
}
|
|
180
|
-
catch (error) {
|
|
181
|
-
console.error(`Failed to delete orphaned ${tsFile}:`, error);
|
|
182
|
-
}
|
|
268
|
+
console.error(`Failed to delete orphaned ${tsFile}:`, error);
|
|
183
269
|
}
|
|
184
270
|
}
|
|
185
271
|
}
|
package/package.json
CHANGED
package/release.json
CHANGED