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.
@@ -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 readSchema(path) {
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
- return JSON.parse(readFileSync(path, 'utf-8'));
72
+ version = JSON.parse(readFileSync(packagePath, 'utf-8')).version;
26
73
  }
27
74
  catch {
28
- return null;
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: assetPath('content-templates', 'types-template.eta'),
218
+ path: templatePath(runtime, 'types-template.eta'),
106
219
  },
107
220
  {
108
221
  suffix: '.ts',
109
- path: assetPath('content-templates', 'hooks-template.eta'),
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 schemaFiles = getSchemaFiles(schemasDir);
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 jsonPath of schemaFiles) {
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
- const content = eta.renderString(template.source, {
146
- model,
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 generate ${tsPath}:`, error);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fimo",
3
- "version": "0.7.0",
3
+ "version": "0.8.0-staging.1",
4
4
  "description": "Fimo CLI - create, deploy, and manage Fimo projects",
5
5
  "bin": {
6
6
  "fimo": "dist/cli/index.js"
package/release.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "source": "npm",
3
- "apiUrl": "https://api.fimo.ai",
4
- "webUrl": "https://fimo.ai"
3
+ "apiUrl": "https://api.staging.fimo.team",
4
+ "webUrl": "https://staging.fimo.team"
5
5
  }
@@ -21,7 +21,7 @@
21
21
  "cmdk": "^1.1.1",
22
22
  "date-fns": "^4.1.0",
23
23
  "embla-carousel-react": "^8.6.0",
24
- "fimo": "0.7.0",
24
+ "fimo": "0.8.0-staging.1",
25
25
  "input-otp": "^1.4.2",
26
26
  "isbot": "^5",
27
27
  "lucide-react": "^0.577.0",