fimo 0.7.0-experimental.1 → 0.7.0-experimental.2

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.
Files changed (136) hide show
  1. package/README.md +1 -1
  2. package/assets/skills/fimo-cli/references/domains.md +1 -1
  3. package/assets/skills/fimo-studio/references/custom-domains.md +1 -1
  4. package/dist/cli/bundle.json +3 -2
  5. package/dist/cli/index.js +1203 -110671
  6. package/dist/runtime/content/client/createContentClient.d.ts +5 -0
  7. package/dist/runtime/content/client/createContentClient.js +300 -0
  8. package/dist/runtime/content/client/createSingletonClient.d.ts +5 -0
  9. package/dist/runtime/content/client/createSingletonClient.js +108 -0
  10. package/dist/runtime/content/client/internal.d.ts +44 -0
  11. package/dist/runtime/content/client/internal.js +236 -0
  12. package/dist/runtime/content/client/types.d.ts +136 -0
  13. package/dist/runtime/content/client/useQueryLocale.d.ts +2 -0
  14. package/dist/runtime/content/client/useQueryLocale.js +6 -0
  15. package/dist/runtime/iframe/runtime/overlay-hmr.js +3 -0
  16. package/dist/runtime/index.d.ts +3 -0
  17. package/dist/runtime/index.js +2 -0
  18. package/package.json +31 -13
  19. package/templates/react-router/package.json +1 -1
  20. package/templates/react-router/src/pages/Index.tsx +1 -1
  21. package/dist/build/vite/index.d.ts.map +0 -1
  22. package/dist/build/vite/plugins/content-sync.d.ts.map +0 -1
  23. package/dist/build/vite/plugins/content-sync.test.d.ts +0 -2
  24. package/dist/build/vite/plugins/content-sync.test.d.ts.map +0 -1
  25. package/dist/build/vite/plugins/content-sync.test.js +0 -64
  26. package/dist/build/vite/plugins/data-id.d.ts.map +0 -1
  27. package/dist/build/vite/plugins/fimo-config.d.ts.map +0 -1
  28. package/dist/build/vite/plugins/fimo-config.test.d.ts +0 -2
  29. package/dist/build/vite/plugins/fimo-config.test.d.ts.map +0 -1
  30. package/dist/build/vite/plugins/fimo-config.test.js +0 -46
  31. package/dist/build/vite/plugins/overlay.d.ts.map +0 -1
  32. package/dist/build/vite/plugins/robots.d.ts.map +0 -1
  33. package/dist/build/vite/plugins/seo.d.ts.map +0 -1
  34. package/dist/build/vite/plugins/server-config.d.ts.map +0 -1
  35. package/dist/build/vite/plugins/sitemap.d.ts.map +0 -1
  36. package/dist/build/vite/plugins/translations.d.ts.map +0 -1
  37. package/dist/build/vite/plugins/translations.test.d.ts +0 -2
  38. package/dist/build/vite/plugins/translations.test.d.ts.map +0 -1
  39. package/dist/build/vite/plugins/translations.test.js +0 -266
  40. package/dist/runtime/app/FimoProviders.d.ts.map +0 -1
  41. package/dist/runtime/app/FimoScripts.d.ts.map +0 -1
  42. package/dist/runtime/app/index.d.ts.map +0 -1
  43. package/dist/runtime/app/prefetch.d.ts.map +0 -1
  44. package/dist/runtime/content/DataSourcesProvider.d.ts.map +0 -1
  45. package/dist/runtime/content/RefreshContentProvider.d.ts.map +0 -1
  46. package/dist/runtime/content/RefreshContentProvider.test.d.ts +0 -2
  47. package/dist/runtime/content/RefreshContentProvider.test.d.ts.map +0 -1
  48. package/dist/runtime/content/RefreshContentProvider.test.js +0 -33
  49. package/dist/runtime/content/parseCollectionResponse.d.ts.map +0 -1
  50. package/dist/runtime/content/parseCollectionResponse.test.d.ts +0 -2
  51. package/dist/runtime/content/parseCollectionResponse.test.d.ts.map +0 -1
  52. package/dist/runtime/content/parseCollectionResponse.test.js +0 -20
  53. package/dist/runtime/content/utils/retrieve-data-sources.d.ts.map +0 -1
  54. package/dist/runtime/core/AppErrorBoundary.d.ts.map +0 -1
  55. package/dist/runtime/core/ViteLifecycles.d.ts.map +0 -1
  56. package/dist/runtime/i18n/FimoLocaleContext.d.ts.map +0 -1
  57. package/dist/runtime/i18n/locale.d.ts.map +0 -1
  58. package/dist/runtime/i18n/locale.test.d.ts +0 -2
  59. package/dist/runtime/i18n/locale.test.d.ts.map +0 -1
  60. package/dist/runtime/i18n/locale.test.js +0 -58
  61. package/dist/runtime/iframe/ConsoleErrorProvider.d.ts.map +0 -1
  62. package/dist/runtime/iframe/ScreenshotProvider.d.ts.map +0 -1
  63. package/dist/runtime/iframe/runtime/overlay-hmr.d.ts.map +0 -1
  64. package/dist/runtime/iframe/runtime/overlay-runtime.d.ts.map +0 -1
  65. package/dist/runtime/index.d.ts.map +0 -1
  66. package/dist/runtime/paths/get-fimo-paths.d.ts.map +0 -1
  67. package/dist/runtime/paths/get-fimo-paths.test.d.ts +0 -2
  68. package/dist/runtime/paths/get-fimo-paths.test.d.ts.map +0 -1
  69. package/dist/runtime/paths/get-fimo-paths.test.js +0 -69
  70. package/dist/runtime/paths/index.d.ts.map +0 -1
  71. package/dist/runtime/paths/types.d.ts.map +0 -1
  72. package/dist/runtime/primitives/components/Boolean.d.ts.map +0 -1
  73. package/dist/runtime/primitives/components/Date.d.ts.map +0 -1
  74. package/dist/runtime/primitives/components/Image.d.ts.map +0 -1
  75. package/dist/runtime/primitives/components/RichText.d.ts.map +0 -1
  76. package/dist/runtime/primitives/components/StaticImage.d.ts.map +0 -1
  77. package/dist/runtime/primitives/components/Text.d.ts.map +0 -1
  78. package/dist/runtime/primitives/components/Video.d.ts.map +0 -1
  79. package/dist/runtime/primitives/components/index.d.ts.map +0 -1
  80. package/dist/runtime/primitives/index.d.ts.map +0 -1
  81. package/dist/runtime/primitives/lib/image-optimization.d.ts.map +0 -1
  82. package/dist/runtime/primitives/lib/index.d.ts.map +0 -1
  83. package/dist/runtime/primitives/lib/primitives.d.ts.map +0 -1
  84. package/dist/runtime/primitives/lib/query.d.ts.map +0 -1
  85. package/dist/runtime/primitives/lib/template.d.ts.map +0 -1
  86. package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts.map +0 -1
  87. package/dist/runtime/primitives/public-surface.test.d.ts +0 -2
  88. package/dist/runtime/primitives/public-surface.test.d.ts.map +0 -1
  89. package/dist/runtime/primitives/public-surface.test.js +0 -49
  90. package/dist/runtime/primitives/translations.d.ts.map +0 -1
  91. package/dist/runtime/react-router/index.d.ts.map +0 -1
  92. package/dist/runtime/react-router/navigation-utils.d.ts.map +0 -1
  93. package/dist/runtime/react-router/navigation.d.ts.map +0 -1
  94. package/dist/runtime/react-router/navigation.test.d.ts +0 -2
  95. package/dist/runtime/react-router/navigation.test.d.ts.map +0 -1
  96. package/dist/runtime/react-router/navigation.test.js +0 -35
  97. package/dist/runtime/react-router/routes.d.ts.map +0 -1
  98. package/dist/runtime/react-router/routes.test.d.ts +0 -2
  99. package/dist/runtime/react-router/routes.test.d.ts.map +0 -1
  100. package/dist/runtime/react-router/routes.test.js +0 -67
  101. package/dist/runtime/routing/AppResetScrollOnNavigate.d.ts.map +0 -1
  102. package/dist/runtime/routing/AppRoutesProvider.d.ts.map +0 -1
  103. package/dist/runtime/seo/buildSeoForPath.d.ts.map +0 -1
  104. package/dist/runtime/seo/htmlProps.d.ts.map +0 -1
  105. package/dist/runtime/seo/index.d.ts.map +0 -1
  106. package/dist/runtime/seo/resolver.d.ts.map +0 -1
  107. package/dist/runtime/shared/fimo-config.server.d.ts.map +0 -1
  108. package/dist/runtime/shared/fimo-config.types.d.ts.map +0 -1
  109. package/dist/runtime/t/OptionalTracking.d.ts.map +0 -1
  110. package/dist/runtime/t/OptionalTracking.test.d.ts +0 -2
  111. package/dist/runtime/t/OptionalTracking.test.d.ts.map +0 -1
  112. package/dist/runtime/t/OptionalTracking.test.js +0 -11
  113. package/dist/runtime/t/Pageview.d.ts.map +0 -1
  114. package/dist/runtime/templates.d.ts.map +0 -1
  115. package/dist/scripts/generated-types.test.js +0 -212
  116. package/dist/scripts/lint-translation-keys.test.d.ts +0 -1
  117. package/dist/scripts/lint-translation-keys.test.js +0 -16
  118. package/dist/scripts/sync-schemas.test.d.ts +0 -1
  119. package/dist/scripts/sync-schemas.test.js +0 -144
  120. package/dist/scripts/validate-route-metadata.test.d.ts +0 -1
  121. package/dist/scripts/validate-route-metadata.test.js +0 -116
  122. package/dist/scripts/validate-schemas.test.d.ts +0 -1
  123. package/dist/scripts/validate-schemas.test.js +0 -263
  124. package/scripts/add-ext.mjs +0 -47
  125. package/scripts/bump-version.mjs +0 -58
  126. package/scripts/bundle-cli.mjs +0 -123
  127. package/scripts/install-cli.mjs +0 -36
  128. package/scripts/lib/channels.mjs +0 -70
  129. package/scripts/lib/dev-release-state.mjs +0 -44
  130. package/scripts/lib/dev-release-state.test.ts +0 -36
  131. package/scripts/lib/postinstall-onboarding.test.ts +0 -82
  132. package/scripts/lib/release-core.mjs +0 -225
  133. package/scripts/lib/release-core.test.ts +0 -147
  134. package/scripts/migration-qa-gate.test.ts +0 -155
  135. package/scripts/publish-npm.mjs +0 -180
  136. /package/dist/{scripts/generated-types.test.d.ts → runtime/content/client/types.js} +0 -0
@@ -1,144 +0,0 @@
1
- import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
4
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
- import { syncSchemas } from './sync-schemas.js';
6
- let scratch;
7
- beforeEach(() => {
8
- scratch = mkdtempSync(join(tmpdir(), 'fimo-sync-schemas-'));
9
- mkdirSync(join(scratch, 'src/schemas'), { recursive: true });
10
- });
11
- afterEach(() => {
12
- rmSync(scratch, { recursive: true, force: true });
13
- });
14
- function writeSchema(name, schema) {
15
- writeFileSync(join(scratch, 'src/schemas', `${name}.json`), JSON.stringify(schema, null, 2));
16
- }
17
- describe('syncSchemas', () => {
18
- it('generates raw references plus typed populated helpers', () => {
19
- writeSchema('Author', {
20
- uid: 'Author',
21
- name: 'Authors',
22
- fields: {
23
- name: { type: 'string' },
24
- },
25
- });
26
- writeSchema('Post', {
27
- uid: 'Post',
28
- name: 'Posts',
29
- fields: {
30
- title: { type: 'string' },
31
- author: { type: 'reference', target: 'Author' },
32
- contributors: { type: 'reference', target: 'Author', many: true },
33
- relatedPosts: { type: 'reference', target: 'Post', many: true },
34
- },
35
- });
36
- expect(syncSchemas({ cwd: scratch, verbose: false })).toEqual({ created: 4, updated: 0, deleted: 0 });
37
- const post = readFileSync(join(scratch, 'src/schemas/Post.ts'), 'utf-8');
38
- const postTypes = readFileSync(join(scratch, 'src/schemas/Post.types.ts'), 'utf-8');
39
- expect(post).toContain('import { wrapWithSource as wrapAuthorWithSource } from "./Author";');
40
- expect(post).toContain('isPopulatedEntry(value) ? wrapAuthorWithSource(value) : value');
41
- expect(post).toContain('function isPopulatedEntry(value: unknown): value is PopulatedEntryRecord');
42
- expect(post).toContain('import type { Post, PostInput, PostPopulate, PostResult, PostFields, PostQuery } from "./Post.types";');
43
- expect(post).toContain('from "./Post.types";');
44
- expect(post).not.toContain('import type { Author } from "./Author";');
45
- expect(post).not.toContain('export interface Post extends WithReferences<PostReferences>');
46
- expect(post).not.toContain('type FimoReference');
47
- expect(post).toContain('Promise<PostResult<undefined, TPopulate>>');
48
- expect(post).toContain('TPopulate extends PostPopulate | undefined');
49
- expect(post).toContain('wrapAuthorWithSource(value)');
50
- expect(post).toContain('wrapWithSource(value)');
51
- expect(post).toContain('export async function create(payload: PostInput): Promise<Post>');
52
- expect(post).toContain('export async function getById');
53
- expect(post).toContain('string | { id: string; locale?: string }');
54
- expect(post).toContain('typeof value === "string"');
55
- expect(post).toContain('/entries/Post/');
56
- expect(post).not.toContain('/v2/entries');
57
- expect(post).toContain('return wrapWithSource(data.data);');
58
- expect(post).toContain('mutationFn : (payload) => create(withMutationLocale(payload, activeLocale))');
59
- expect(post).toContain('mutationFn : ({ id, ...patch }) => update(id, withMutationLocale(patch, activeLocale))');
60
- expect(post).toContain('qk.byId(vars.id, { locale: vars.locale ?? activeLocale })');
61
- expect(post).toContain('{ ...value, locale: value.locale ?? activeLocale }');
62
- expect(postTypes).toContain('import type { Author } from "./Author.types";');
63
- expect(postTypes).toContain('export interface Post extends WithReferences<PostReferences>');
64
- expect(postTypes).toContain(' id: string;');
65
- expect(postTypes).toContain(' entryId: string;');
66
- expect(postTypes).toContain(' documentId: string;');
67
- expect(postTypes).toContain(' locale: string;');
68
- expect(postTypes).toContain('type FimoReference');
69
- expect(postTypes).toContain('author: FimoReference | null;');
70
- expect(postTypes).toContain('contributors: FimoReference[];');
71
- expect(postTypes).toContain('relatedPosts: FimoReference[];');
72
- expect(postTypes).toContain('export interface PostReferences');
73
- expect(postTypes).toContain('export interface PostInput');
74
- expect(postTypes).toContain('author?: FimoReference | null;');
75
- expect(postTypes).toContain('author: Author | null;');
76
- expect(postTypes).toContain('contributors: Author[];');
77
- expect(postTypes).toContain('relatedPosts: Post[];');
78
- expect(postTypes).toContain('export type PostResult<');
79
- expect(postTypes).not.toContain('PostPopulated');
80
- });
81
- it('generates effective-locale shared types and locale-sensitive populated target keys', () => {
82
- writeSchema('Homepage', {
83
- uid: 'Homepage',
84
- name: 'Homepages',
85
- fields: { title: { type: 'string' } },
86
- });
87
- writeSchema('Category', {
88
- uid: 'Category',
89
- name: 'Categories',
90
- i18n: { enabled: false },
91
- fields: { name: { type: 'string' } },
92
- });
93
- writeSchema('Settings', {
94
- uid: 'Settings',
95
- name: 'Settings',
96
- i18n: { enabled: false },
97
- fields: {
98
- homepage: { type: 'reference', target: 'Homepage' },
99
- category: { type: 'reference', target: 'Category' },
100
- },
101
- });
102
- syncSchemas({ cwd: scratch, verbose: false });
103
- const settings = readFileSync(join(scratch, 'src/schemas/Settings.ts'), 'utf-8');
104
- const settingsTypes = readFileSync(join(scratch, 'src/schemas/Settings.types.ts'), 'utf-8');
105
- const homepageTypes = readFileSync(join(scratch, 'src/schemas/Homepage.types.ts'), 'utf-8');
106
- expect(settingsTypes).toContain('locale: string;');
107
- expect(settingsTypes).not.toContain('locale?: string;');
108
- expect(homepageTypes).toContain('locale: string;');
109
- expect(settings).toContain('const LOCALIZED_CONTENT = false;');
110
- expect(settings).toContain('const LOCALIZED_REFERENCE_FIELDS = new Set<string>(["homepage"]);');
111
- expect(settings).toContain(' locale,');
112
- expect(settings).toContain('readLocale(params, LOCALIZED_CONTENT, useQueryLocale(params.locale))');
113
- expect(settings.match(/readLocale\(params, LOCALIZED_CONTENT, useQueryLocale\(params\.locale\)\)/g)?.length).toBe(4);
114
- expect(settings.match(/const locale = useQueryLocale\(options\.locale\);/g)?.length).toBe(2);
115
- });
116
- it('generates singleton document APIs without collection mutations', () => {
117
- writeSchema('Settings', {
118
- uid: 'Settings',
119
- name: 'Settings',
120
- isSingleton: true,
121
- i18n: { enabled: false },
122
- fields: { siteName: { type: 'string', required: true } },
123
- });
124
- syncSchemas({ cwd: scratch, verbose: false });
125
- const settings = readFileSync(join(scratch, 'src/schemas/Settings.ts'), 'utf-8');
126
- const settingsTypes = readFileSync(join(scratch, 'src/schemas/Settings.types.ts'), 'utf-8');
127
- expect(settings).toContain('fetch(`${base}${withReadOptions(`/documents/Settings`, options)}`');
128
- expect(settings).toContain('`Settings.id:${id}:${encodeURIComponent(locale)}`');
129
- expect(settingsTypes).toContain('id: string;');
130
- expect(settingsTypes).toContain('entryId: string;');
131
- expect(settingsTypes).toContain('locale?: string;');
132
- expect(settings).toContain('export async function update(payload: Partial<SettingsInput>)');
133
- expect(settings).toContain('mutationFn: (payload) => update(withMutationLocale(payload, activeLocale))');
134
- expect(settings).toContain('export function useSuspenseGet');
135
- expect(settings).not.toContain('export async function create(');
136
- expect(settings).not.toContain('export async function remove(');
137
- expect(settings).not.toContain('export function useCreate(');
138
- expect(settings).not.toContain('export function useList(');
139
- expect(settings).not.toContain('export function useInfiniteList(');
140
- expect(settings).not.toContain('useInfiniteQuery');
141
- expect(settingsTypes).toContain('siteName: string;');
142
- expect(settingsTypes).not.toContain('documentId?: string;');
143
- });
144
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,116 +0,0 @@
1
- import { mkdirSync, writeFileSync } from 'node:fs';
2
- import { mkdtempSync, rmSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import { afterEach, describe, expect, it } from 'vitest';
6
- import { validateRouteMetadata } from './validate-route-metadata';
7
- describe('validateRouteMetadata i18n checks', () => {
8
- let scratch = null;
9
- afterEach(() => {
10
- if (scratch) {
11
- rmSync(scratch, { recursive: true, force: true });
12
- scratch = null;
13
- }
14
- });
15
- it('accepts fimoRoutes() with canonical routes and multi-locale path prefixes', () => {
16
- const cwd = writeProject({
17
- config: {
18
- i18n: {
19
- defaultLocale: 'en',
20
- locales: ['en', 'es'],
21
- routing: { strategy: 'path-prefix' },
22
- },
23
- routes: {
24
- home: { path: '/', label: 'Home' },
25
- pricing: { path: '/pricing', label: 'Pricing' },
26
- },
27
- },
28
- routesSource: `import type { RouteConfig } from '@react-router/dev/routes';
29
- import { index, route } from '@react-router/dev/routes';
30
- import { fimoRoutes } from 'fimo/react-router/routes';
31
-
32
- const routes = [
33
- index('./pages/Index.tsx', { id: 'home' }),
34
- route('pricing', './pages/Pricing.tsx', { id: 'pricing' }),
35
- ] satisfies RouteConfig;
36
-
37
- export default fimoRoutes(routes) satisfies RouteConfig;
38
- `,
39
- files: ['src/pages/Index.tsx', 'src/pages/Pricing.tsx'],
40
- });
41
- expect(validateRouteMetadata({ cwd }).errors).toEqual([]);
42
- });
43
- it('rejects invalid locale config', () => {
44
- const cwd = writeProject({
45
- config: {
46
- i18n: {
47
- defaultLocale: 'en--US',
48
- locales: ['es'],
49
- routing: { strategy: 'path-prefix' },
50
- },
51
- routes: {
52
- home: { path: '/', label: 'Home' },
53
- },
54
- },
55
- routesSource: `import { index } from '@react-router/dev/routes';
56
- export default [index('./pages/Index.tsx')];
57
- `,
58
- files: ['src/pages/Index.tsx'],
59
- });
60
- const errors = validateRouteMetadata({ cwd }).errors.join('\n');
61
- expect(errors).toContain('Invalid i18n.defaultLocale "en--US"');
62
- expect(errors).toContain('i18n.locales must include i18n.defaultLocale');
63
- });
64
- it('rejects canonical routes that claim locale prefixes', () => {
65
- const cwd = writeProject({
66
- config: {
67
- i18n: {
68
- defaultLocale: 'en',
69
- locales: ['en', 'es'],
70
- routing: { strategy: 'path-prefix' },
71
- },
72
- routes: {
73
- spanish: { path: '/es', label: 'Spanish' },
74
- },
75
- },
76
- routesSource: `import { route } from '@react-router/dev/routes';
77
- export default [route('es', './pages/Spanish.tsx')];
78
- `,
79
- files: ['src/pages/Spanish.tsx'],
80
- });
81
- const errors = validateRouteMetadata({ cwd }).errors.join('\n');
82
- expect(errors).toContain('uses reserved locale prefix "es"');
83
- });
84
- it('warns as an error when top-level dynamic routes can capture locale URLs without fimoRoutes()', () => {
85
- const cwd = writeProject({
86
- config: {
87
- i18n: {
88
- defaultLocale: 'en',
89
- locales: ['en', 'es'],
90
- routing: { strategy: 'path-prefix' },
91
- },
92
- routes: {
93
- page: { path: '/:slug', label: 'Page', params: { slug: { schema: 'Page', queryField: 'slug' } } },
94
- },
95
- },
96
- routesSource: `import { route } from '@react-router/dev/routes';
97
- export default [route(':slug', './pages/Page.tsx')];
98
- `,
99
- files: ['src/pages/Page.tsx'],
100
- });
101
- const errors = validateRouteMetadata({ cwd }).errors.join('\n');
102
- expect(errors).toContain('top-level dynamic route');
103
- expect(errors).toContain('fimoRoutes(routes)');
104
- });
105
- function writeProject(args) {
106
- scratch = mkdtempSync(join(tmpdir(), 'fimo-route-metadata-'));
107
- mkdirSync(join(scratch, 'src'), { recursive: true });
108
- writeFileSync(join(scratch, 'fimo-config.json'), JSON.stringify(args.config, null, 2));
109
- writeFileSync(join(scratch, 'src', 'routes.ts'), args.routesSource);
110
- for (const file of args.files) {
111
- mkdirSync(join(scratch, file, '..'), { recursive: true });
112
- writeFileSync(join(scratch, file), 'export default function Page() { return null; }\n');
113
- }
114
- return scratch;
115
- }
116
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,263 +0,0 @@
1
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
4
- import { afterEach, describe, expect, it } from 'vitest';
5
- import { validateSchemas } from './validate-schemas';
6
- describe('validateSchemas', () => {
7
- let scratch = null;
8
- afterEach(() => {
9
- if (scratch) {
10
- rmSync(scratch, { recursive: true, force: true });
11
- scratch = null;
12
- }
13
- });
14
- function writeProject(schemas) {
15
- scratch = mkdtempSync(join(tmpdir(), 'validate-schemas-'));
16
- mkdirSync(join(scratch, 'src/schemas'), { recursive: true });
17
- for (const [fileName, schema] of Object.entries(schemas)) {
18
- writeFileSync(join(scratch, 'src/schemas', fileName), typeof schema === 'string' ? schema : JSON.stringify(schema, null, 2));
19
- }
20
- return scratch;
21
- }
22
- it('accepts a valid schema', () => {
23
- const cwd = writeProject({
24
- 'Author.json': {
25
- uid: 'Author',
26
- name: 'Authors',
27
- fields: {
28
- name: { type: 'string' },
29
- },
30
- },
31
- 'BlogPost.json': {
32
- uid: 'BlogPost',
33
- name: 'Blog Posts',
34
- fields: {
35
- title: { type: 'string' },
36
- body: { type: 'richtext' },
37
- coverImage: { type: 'media' },
38
- featured: { type: 'boolean' },
39
- author: { type: 'reference', target: 'Author' },
40
- },
41
- },
42
- });
43
- expect(validateSchemas({ cwd })).toEqual({ errors: [], schemasChecked: 2 });
44
- });
45
- it('accepts self-referential reference fields', () => {
46
- const cwd = writeProject({
47
- 'Item.json': {
48
- uid: 'Item',
49
- name: 'Items',
50
- fields: {
51
- title: { type: 'string' },
52
- parent: { type: 'reference', target: 'Item' },
53
- },
54
- },
55
- });
56
- expect(validateSchemas({ cwd })).toEqual({ errors: [], schemasChecked: 1 });
57
- });
58
- it('accepts a non-localized schema policy', () => {
59
- const cwd = writeProject({
60
- 'Author.json': {
61
- uid: 'Author',
62
- name: 'Authors',
63
- i18n: { enabled: false },
64
- fields: { name: { type: 'string' } },
65
- },
66
- });
67
- expect(validateSchemas({ cwd })).toEqual({ errors: [], schemasChecked: 1 });
68
- });
69
- it('rejects a non-boolean singleton policy', () => {
70
- const cwd = writeProject({
71
- 'Settings.json': {
72
- uid: 'Settings',
73
- name: 'Settings',
74
- isSingleton: 'yes',
75
- fields: {},
76
- },
77
- });
78
- const { errors } = validateSchemas({ cwd });
79
- expect(errors).toEqual([expect.objectContaining({ rule: 'schemas/singleton' })]);
80
- });
81
- it.each([true, { enabled: 'no' }, { enabled: false, fallback: 'en' }, { fallback: 'en' }])('rejects an invalid schema-level i18n policy %#', (i18n) => {
82
- const cwd = writeProject({
83
- 'Author.json': {
84
- uid: 'Author',
85
- name: 'Authors',
86
- i18n,
87
- fields: { name: { type: 'string' } },
88
- },
89
- });
90
- const { errors } = validateSchemas({ cwd });
91
- expect(errors).toHaveLength(1);
92
- expect(errors[0]).toMatchObject({ rule: 'schemas/i18n' });
93
- });
94
- it('returns no errors when the schemas directory does not exist', () => {
95
- scratch = mkdtempSync(join(tmpdir(), 'validate-schemas-'));
96
- expect(validateSchemas({ cwd: scratch })).toEqual({ errors: [], schemasChecked: 0 });
97
- });
98
- it('rejects reserved field names like slug', () => {
99
- const cwd = writeProject({
100
- 'TeamMember.json': {
101
- uid: 'TeamMember',
102
- name: 'Team Members',
103
- fields: {
104
- name: { type: 'string' },
105
- slug: { type: 'string' },
106
- },
107
- },
108
- });
109
- const { errors } = validateSchemas({ cwd });
110
- expect(errors).toHaveLength(1);
111
- expect(errors[0]).toMatchObject({ rule: 'schemas/reserved-field', file: 'src/schemas/TeamMember.json' });
112
- expect(errors[0].message).toContain('"slug" is reserved');
113
- expect(errors[0].message).toContain('entry data');
114
- });
115
- it('rejects every reserved metadata name', () => {
116
- const cwd = writeProject({
117
- 'Bad.json': {
118
- uid: 'Bad',
119
- name: 'Bad',
120
- fields: Object.fromEntries(['id', 'entryId', 'documentId', 'slug', 'locale', 'createdAt', 'updatedAt', '__fimo'].map((name) => [
121
- name,
122
- { type: 'string' },
123
- ])),
124
- },
125
- });
126
- const { errors } = validateSchemas({ cwd });
127
- expect(errors).toHaveLength(8);
128
- expect(errors.every((err) => err.rule === 'schemas/reserved-field')).toBe(true);
129
- });
130
- it('rejects unsupported field types', () => {
131
- const cwd = writeProject({
132
- 'Product.json': {
133
- uid: 'Product',
134
- name: 'Products',
135
- fields: { price: { type: 'decimal' } },
136
- },
137
- });
138
- const { errors } = validateSchemas({ cwd });
139
- expect(errors).toHaveLength(1);
140
- expect(errors[0]).toMatchObject({ rule: 'schemas/field-type' });
141
- expect(errors[0].message).toContain('"decimal"');
142
- expect(errors[0].message).toContain('string, richtext, media');
143
- });
144
- it('rejects reference fields without a valid target', () => {
145
- const cwd = writeProject({
146
- 'Post.json': {
147
- uid: 'Post',
148
- name: 'Posts',
149
- fields: { author: { type: 'reference' } },
150
- },
151
- });
152
- const { errors } = validateSchemas({ cwd });
153
- expect(errors).toHaveLength(1);
154
- expect(errors[0]).toMatchObject({ rule: 'schemas/reference-target' });
155
- });
156
- it('rejects reference fields targeting a missing local schema', () => {
157
- const cwd = writeProject({
158
- 'Post.json': {
159
- uid: 'Post',
160
- name: 'Posts',
161
- fields: { author: { type: 'reference', target: 'Author' } },
162
- },
163
- });
164
- const { errors } = validateSchemas({ cwd });
165
- expect(errors).toHaveLength(1);
166
- expect(errors[0]).toMatchObject({ rule: 'schemas/reference-target' });
167
- expect(errors[0].message).toContain('unknown schema "Author"');
168
- });
169
- it('rejects reference-only properties on scalar fields', () => {
170
- const cwd = writeProject({
171
- 'Post.json': {
172
- uid: 'Post',
173
- name: 'Posts',
174
- fields: { author: { type: 'string', target: 'Author' } },
175
- },
176
- });
177
- const { errors } = validateSchemas({ cwd });
178
- expect(errors).toHaveLength(1);
179
- expect(errors[0]).toMatchObject({ rule: 'schemas/reference-shape' });
180
- });
181
- it('rejects schema uid names that cannot be used as content type identifiers', () => {
182
- const cwd = writeProject({
183
- 'blog-post.json': {
184
- uid: 'blog-post',
185
- name: 'Blog Posts',
186
- fields: { title: { type: 'string' } },
187
- },
188
- });
189
- const { errors } = validateSchemas({ cwd });
190
- expect(errors).toHaveLength(1);
191
- expect(errors[0]).toMatchObject({ rule: 'schemas/type-name' });
192
- });
193
- it('rejects field names that cannot be used in generated source paths', () => {
194
- const cwd = writeProject({
195
- 'Page.json': {
196
- uid: 'Page',
197
- name: 'Pages',
198
- fields: { 'hero-title': { type: 'string' } },
199
- },
200
- });
201
- const { errors } = validateSchemas({ cwd });
202
- expect(errors).toHaveLength(1);
203
- expect(errors[0]).toMatchObject({ rule: 'schemas/field-name' });
204
- });
205
- it('rejects field-level i18n settings', () => {
206
- const cwd = writeProject({
207
- 'Page.json': {
208
- uid: 'Page',
209
- name: 'Pages',
210
- fields: { title: { type: 'string', i18n: true } },
211
- },
212
- });
213
- const { errors } = validateSchemas({ cwd });
214
- expect(errors).toHaveLength(1);
215
- expect(errors[0]).toMatchObject({ rule: 'schemas/field-i18n' });
216
- });
217
- it('rejects reference-level locale settings', () => {
218
- const cwd = writeProject({
219
- 'Page.json': {
220
- uid: 'Page',
221
- name: 'Pages',
222
- fields: { parent: { type: 'reference', target: 'Page', localized: true } },
223
- },
224
- });
225
- const { errors } = validateSchemas({ cwd });
226
- expect(errors).toHaveLength(1);
227
- expect(errors[0]).toMatchObject({ rule: 'schemas/field-i18n' });
228
- expect(errors[0].message).toContain('"localized"');
229
- });
230
- it('rejects a uid that does not match the file name', () => {
231
- const cwd = writeProject({
232
- 'TeamMember.json': {
233
- uid: 'Member',
234
- name: 'Team Members',
235
- fields: { name: { type: 'string' } },
236
- },
237
- });
238
- const { errors } = validateSchemas({ cwd });
239
- expect(errors).toHaveLength(1);
240
- expect(errors[0]).toMatchObject({ rule: 'schemas/uid' });
241
- });
242
- it('rejects missing uid, name, and fields', () => {
243
- const cwd = writeProject({ 'Empty.json': {} });
244
- const { errors } = validateSchemas({ cwd });
245
- expect(errors.map((err) => err.rule)).toEqual(['schemas/uid', 'schemas/name', 'schemas/fields']);
246
- });
247
- it('reports invalid JSON', () => {
248
- const cwd = writeProject({ 'Broken.json': '{ not json' });
249
- const { errors } = validateSchemas({ cwd });
250
- expect(errors).toHaveLength(1);
251
- expect(errors[0]).toMatchObject({ rule: 'schemas/parse' });
252
- });
253
- it('validates multiple files and counts them', () => {
254
- const cwd = writeProject({
255
- 'Good.json': { uid: 'Good', name: 'Good', fields: { title: { type: 'string' } } },
256
- 'Bad.json': { uid: 'Bad', name: 'Bad', fields: { slug: { type: 'string' } } },
257
- });
258
- const result = validateSchemas({ cwd });
259
- expect(result.schemasChecked).toBe(2);
260
- expect(result.errors).toHaveLength(1);
261
- expect(result.errors[0].file).toBe('src/schemas/Bad.json');
262
- });
263
- });
@@ -1,47 +0,0 @@
1
- #!/usr/bin/env node
2
- // Rewrite relative imports to add `.js` extensions (required by Node ESM).
3
- // Idempotent. Preserves all other code.
4
- import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
5
- import { extname, resolve } from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
-
8
- const HERE = resolve(fileURLToPath(import.meta.url), '..');
9
- const ROOTS = process.argv.slice(2).map((p) => resolve(p));
10
-
11
- const IMPORT_RE = /(from\s+|import\s*\(\s*)(['"])(\.\.?\/[^'"\s]+?)\2/g;
12
-
13
- function walk(dir, out) {
14
- for (const name of readdirSync(dir)) {
15
- if (['node_modules', 'dist', '.git'].includes(name)) continue;
16
- const p = `${dir}/${name}`;
17
- const st = statSync(p);
18
- if (st.isDirectory()) walk(p, out);
19
- else if (/\.(ts|tsx|mts|mjs)$/.test(name) && !name.endsWith('.d.ts')) out.push(p);
20
- }
21
- }
22
-
23
- function rewriteOne(filePath) {
24
- const src = readFileSync(filePath, 'utf-8');
25
- const dir = filePath.split('/').slice(0, -1).join('/');
26
- let mutated = false;
27
- const out = src.replace(IMPORT_RE, (match, lead, quote, spec) => {
28
- const ext = extname(spec);
29
- if (ext) return match;
30
- const candidate = resolve(dir, spec);
31
- if (existsSync(candidate) && statSync(candidate).isDirectory()) {
32
- mutated = true;
33
- return `${lead}${quote}${spec}/index.js${quote}`;
34
- }
35
- mutated = true;
36
- return `${lead}${quote}${spec}.js${quote}`;
37
- });
38
- if (mutated) {
39
- writeFileSync(filePath, out);
40
- console.log(`updated ${filePath.slice(HERE.length - 7)}`);
41
- }
42
- }
43
-
44
- const files = [];
45
- for (const r of ROOTS) walk(r, files);
46
- for (const f of files) rewriteOne(f);
47
- console.log(`[add-ext] processed ${files.length} files`);
@@ -1,58 +0,0 @@
1
- #!/usr/bin/env node
2
- // Bumps the Fimo CLI version in apps/cli/package.json — NO git tag, NO commit.
3
- //
4
- // Under the branch-driven release model the version lives in
5
- // apps/cli/package.json and you bump it in a PR. CI creates the
6
- // `cli@<version>` git tag when it publishes `@latest` on a `production`
7
- // merge, so this script must NOT create a tag — that would collide with
8
- // CI's tag-guard (the publish would skip, thinking the version already
9
- // shipped).
10
- //
11
- // Usage:
12
- // pnpm -F fimo release:bump patch
13
- // pnpm -F fimo release:bump minor
14
- // pnpm -F fimo release:bump major
15
- // pnpm -F fimo release:bump 1.2.3 # explicit version
16
- //
17
- // After bumping, commit the change in your PR. Merging it through to
18
- // `production` is what publishes `@latest` and creates the tag.
19
-
20
- import { spawnSync } from 'node:child_process';
21
- import { dirname, resolve } from 'node:path';
22
- import { fileURLToPath } from 'node:url';
23
-
24
- const HERE = dirname(fileURLToPath(import.meta.url));
25
- const CLI_ROOT = resolve(HERE, '..');
26
-
27
- const VALID_INCREMENTS = new Set(['major', 'minor', 'patch', 'premajor', 'preminor', 'prepatch', 'prerelease']);
28
-
29
- const args = process.argv.slice(2);
30
- if (args.length === 0) {
31
- console.error(
32
- '[cli/bump] usage: pnpm -F fimo release:bump <patch|minor|major|premajor|preminor|prepatch|prerelease|X.Y.Z> [--preid <id>]'
33
- );
34
- process.exit(1);
35
- }
36
-
37
- const increment = args[0];
38
- const isExplicitVersion = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/.test(increment);
39
- if (!VALID_INCREMENTS.has(increment) && !isExplicitVersion) {
40
- console.error(
41
- `[cli/bump] invalid increment "${increment}". expected one of ${[...VALID_INCREMENTS].join('|')} or an explicit X.Y.Z.`
42
- );
43
- process.exit(1);
44
- }
45
-
46
- // Bump package.json ONLY — `--no-git-tag-version` skips both the commit and
47
- // the tag. CI owns the `cli@<version>` tag (created on the @latest publish).
48
- // Use pnpm (not npm) — npm chokes on the `workspace:*` devDeps.
49
- const versionArgs = ['version', '--no-git-tag-version', ...args];
50
- console.log(`[cli/bump] $ pnpm ${versionArgs.join(' ')}`);
51
- const result = spawnSync('pnpm', versionArgs, { cwd: CLI_ROOT, stdio: 'inherit' });
52
- if (result.status !== 0) {
53
- console.error(`[cli/bump] pnpm version failed (exit ${result.status}).`);
54
- process.exit(result.status ?? 1);
55
- }
56
-
57
- console.log('[cli/bump] bumped apps/cli/package.json (no tag, no commit).');
58
- console.log('[cli/bump] commit it in your PR — merging to `production` publishes @latest + tags it.');