meno-core 1.0.48 → 1.0.50
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/build-astro.ts +6 -2
- package/dist/build-static.js +7 -7
- package/dist/chunks/{chunk-D5E3OKSL.js → chunk-56EUSC6D.js} +5 -5
- package/dist/chunks/{chunk-3FHJUHAS.js → chunk-7NIC4I3V.js} +300 -43
- package/dist/chunks/chunk-7NIC4I3V.js.map +7 -0
- package/dist/chunks/{chunk-B2RTLDXY.js → chunk-AZQYF6KE.js} +132 -1
- package/dist/chunks/chunk-AZQYF6KE.js.map +7 -0
- package/dist/chunks/{chunk-TPQ7APVQ.js → chunk-CVLFID6V.js} +473 -73
- package/dist/chunks/chunk-CVLFID6V.js.map +7 -0
- package/dist/chunks/{chunk-NP76N4HQ.js → chunk-EDQSMAMP.js} +13 -2
- package/dist/chunks/{chunk-NP76N4HQ.js.map → chunk-EDQSMAMP.js.map} +2 -2
- package/dist/chunks/{chunk-RQSTH2BS.js → chunk-H4JSCDNW.js} +2 -2
- package/dist/chunks/{chunk-EK4KESLU.js → chunk-J23ZX5AP.js} +8 -2
- package/dist/chunks/{chunk-EK4KESLU.js.map → chunk-J23ZX5AP.js.map} +2 -2
- package/dist/chunks/{chunk-UUA5LEWF.js → chunk-LPVETICS.js} +156 -8
- package/dist/chunks/chunk-LPVETICS.js.map +7 -0
- package/dist/chunks/{chunk-BJRKEPMP.js → chunk-PQ2HRXDR.js} +5 -2
- package/dist/chunks/chunk-PQ2HRXDR.js.map +7 -0
- package/dist/chunks/{chunk-NKUV77SR.js → chunk-YWJJD5D6.js} +133 -37
- package/dist/chunks/chunk-YWJJD5D6.js.map +7 -0
- package/dist/chunks/{configService-IGJEC3MC.js → configService-VOY2MY2K.js} +3 -3
- package/dist/entries/server-router.js +9 -9
- package/dist/entries/server-router.js.map +2 -2
- package/dist/lib/client/index.js +92 -32
- package/dist/lib/client/index.js.map +3 -3
- package/dist/lib/server/index.js +14 -12
- package/dist/lib/server/index.js.map +2 -2
- package/dist/lib/shared/index.js +46 -10
- package/dist/lib/shared/index.js.map +3 -3
- package/entries/server-router.tsx +6 -2
- package/lib/client/core/ComponentBuilder.test.ts +34 -0
- package/lib/client/core/ComponentBuilder.ts +33 -4
- package/lib/client/core/builders/embedBuilder.ts +28 -7
- package/lib/client/core/builders/linkNodeBuilder.ts +28 -7
- package/lib/client/core/builders/localeListBuilder.ts +30 -11
- package/lib/client/styles/StyleInjector.ts +3 -2
- package/lib/client/templateEngine.ts +24 -0
- package/lib/client/theme.ts +4 -4
- package/lib/server/cssGenerator.test.ts +64 -1
- package/lib/server/cssGenerator.ts +48 -9
- package/lib/server/fileWatcher.test.ts +134 -0
- package/lib/server/fileWatcher.ts +100 -32
- package/lib/server/jsonLoader.ts +1 -0
- package/lib/server/providers/fileSystemCMSProvider.test.ts +163 -0
- package/lib/server/providers/fileSystemCMSProvider.ts +240 -19
- package/lib/server/routes/index.ts +1 -1
- package/lib/server/routes/pages.ts +23 -1
- package/lib/server/services/cmsService.test.ts +246 -0
- package/lib/server/services/cmsService.ts +122 -5
- package/lib/server/services/configService.ts +6 -0
- package/lib/server/services/fileWatcherService.ts +17 -0
- package/lib/server/ssr/attributeBuilder.ts +41 -0
- package/lib/server/ssr/htmlGenerator.test.ts +113 -0
- package/lib/server/ssr/htmlGenerator.ts +62 -7
- package/lib/server/ssr/liveReloadIntegration.test.ts +209 -0
- package/lib/server/ssr/ssrRenderer.test.ts +564 -0
- package/lib/server/ssr/ssrRenderer.ts +228 -49
- package/lib/server/webflow/buildWebflow.ts +1 -1
- package/lib/server/websocketManager.test.ts +61 -6
- package/lib/server/websocketManager.ts +25 -1
- package/lib/shared/cssGeneration.test.ts +267 -1
- package/lib/shared/cssGeneration.ts +240 -18
- package/lib/shared/cssProperties.test.ts +275 -1
- package/lib/shared/cssProperties.ts +223 -7
- package/lib/shared/interfaces/contentProvider.ts +39 -6
- package/lib/shared/pathSecurity.ts +16 -0
- package/lib/shared/responsiveScaling.test.ts +143 -0
- package/lib/shared/responsiveScaling.ts +253 -2
- package/lib/shared/themeDefaults.test.ts +3 -3
- package/lib/shared/themeDefaults.ts +3 -3
- package/lib/shared/types/api.ts +10 -1
- package/lib/shared/types/cms.ts +46 -12
- package/lib/shared/types/index.ts +1 -0
- package/lib/shared/utilityClassConfig.ts +3 -0
- package/lib/shared/utilityClassMapper.test.ts +123 -0
- package/lib/shared/utilityClassMapper.ts +179 -8
- package/lib/shared/validation/schemas.test.ts +93 -0
- package/lib/shared/validation/schemas.ts +71 -16
- package/lib/shared/validation/validators.ts +26 -1
- package/package.json +1 -1
- package/dist/chunks/chunk-3FHJUHAS.js.map +0 -7
- package/dist/chunks/chunk-B2RTLDXY.js.map +0 -7
- package/dist/chunks/chunk-BJRKEPMP.js.map +0 -7
- package/dist/chunks/chunk-NKUV77SR.js.map +0 -7
- package/dist/chunks/chunk-TPQ7APVQ.js.map +0 -7
- package/dist/chunks/chunk-UUA5LEWF.js.map +0 -7
- /package/dist/chunks/{chunk-D5E3OKSL.js.map → chunk-56EUSC6D.js.map} +0 -0
- /package/dist/chunks/{chunk-RQSTH2BS.js.map → chunk-H4JSCDNW.js.map} +0 -0
- /package/dist/chunks/{configService-IGJEC3MC.js.map → configService-VOY2MY2K.js.map} +0 -0
|
@@ -4,11 +4,49 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { watch, existsSync } from 'fs';
|
|
7
|
-
import { dirname } from 'path';
|
|
7
|
+
import { basename, dirname } from 'path';
|
|
8
8
|
import type { FSWatcher } from 'fs';
|
|
9
9
|
import { mapPageNameToPath } from './jsonLoader';
|
|
10
10
|
import { projectPaths, getProjectRoot } from './projectContext';
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Attach a recursive watcher to `dirPath`. If the directory doesn't exist
|
|
14
|
+
* yet, watch the parent directory and defer the attach until the target is
|
|
15
|
+
* created (e.g. a blank project that doesn't ship with `templates/` or `cms/`
|
|
16
|
+
* gets one added later by an external tool).
|
|
17
|
+
*
|
|
18
|
+
* Returns the currently active FSWatcher (which may be the deferred parent
|
|
19
|
+
* watcher until the target appears, then the proper recursive watcher).
|
|
20
|
+
*/
|
|
21
|
+
function attachWhenDirExists(
|
|
22
|
+
dirPath: string,
|
|
23
|
+
attach: () => FSWatcher | null,
|
|
24
|
+
setWatcher: (w: FSWatcher | null) => void
|
|
25
|
+
): void {
|
|
26
|
+
if (existsSync(dirPath)) {
|
|
27
|
+
setWatcher(attach());
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const parentDir = dirname(dirPath);
|
|
32
|
+
const targetName = basename(dirPath);
|
|
33
|
+
if (!existsSync(parentDir)) {
|
|
34
|
+
// Parent doesn't exist either — give up; nothing reasonable to watch.
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let parentWatcher: FSWatcher | null = null;
|
|
39
|
+
parentWatcher = watch(parentDir, (_event, filename) => {
|
|
40
|
+
if (filename !== targetName) return;
|
|
41
|
+
if (!existsSync(dirPath)) return;
|
|
42
|
+
// Target exists now — tear down the parent watcher and attach the real one.
|
|
43
|
+
parentWatcher?.close();
|
|
44
|
+
parentWatcher = null;
|
|
45
|
+
setWatcher(attach());
|
|
46
|
+
});
|
|
47
|
+
setWatcher(parentWatcher);
|
|
48
|
+
}
|
|
49
|
+
|
|
12
50
|
export interface FileWatchCallbacks {
|
|
13
51
|
onComponentChange?: () => Promise<void>;
|
|
14
52
|
onPageChange?: (pagePath: string) => Promise<void>;
|
|
@@ -18,6 +56,7 @@ export interface FileWatchCallbacks {
|
|
|
18
56
|
onCMSChange?: (collection: string) => Promise<void>;
|
|
19
57
|
onImageAdded?: (filename: string) => Promise<void>;
|
|
20
58
|
onLibraryChange?: () => Promise<void>;
|
|
59
|
+
onProjectConfigChange?: () => Promise<void>;
|
|
21
60
|
}
|
|
22
61
|
|
|
23
62
|
export class FileWatcher {
|
|
@@ -30,6 +69,7 @@ export class FileWatcher {
|
|
|
30
69
|
private cmsWatcher: FSWatcher | null = null;
|
|
31
70
|
private imagesWatcher: FSWatcher | null = null;
|
|
32
71
|
private librariesWatcher: FSWatcher | null = null;
|
|
72
|
+
private projectConfigWatcher: FSWatcher | null = null;
|
|
33
73
|
|
|
34
74
|
constructor(private callbacks: FileWatchCallbacks) {}
|
|
35
75
|
|
|
@@ -85,29 +125,31 @@ export class FileWatcher {
|
|
|
85
125
|
}
|
|
86
126
|
|
|
87
127
|
/**
|
|
88
|
-
* Start watching root templates directory for CMS template changes
|
|
128
|
+
* Start watching root templates directory for CMS template changes.
|
|
129
|
+
* Falls back to a deferred-attach watcher on the project root when
|
|
130
|
+
* `templates/` doesn't exist yet (blank projects, projects that have never
|
|
131
|
+
* had a CMS collection).
|
|
89
132
|
*/
|
|
90
133
|
watchTemplates(dirPath: string = projectPaths.templates()): void {
|
|
91
|
-
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
this.templatesWatcher = watch(
|
|
134
|
+
attachWhenDirExists(
|
|
96
135
|
dirPath,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
136
|
+
() => watch(
|
|
137
|
+
dirPath,
|
|
138
|
+
{ recursive: true },
|
|
139
|
+
async (event, filename) => {
|
|
140
|
+
if (filename && filename.endsWith('.json')) {
|
|
141
|
+
// Prefix with templates/ so the path is /templates/...
|
|
142
|
+
const pageName = `templates/${filename.replace('.json', '')}`;
|
|
143
|
+
const pagePath = mapPageNameToPath(pageName);
|
|
103
144
|
|
|
104
|
-
|
|
105
|
-
|
|
145
|
+
if (this.callbacks.onPageChange) {
|
|
146
|
+
await this.callbacks.onPageChange(pagePath);
|
|
147
|
+
}
|
|
106
148
|
}
|
|
107
149
|
}
|
|
108
|
-
|
|
150
|
+
),
|
|
151
|
+
(w) => { this.templatesWatcher = w; }
|
|
109
152
|
);
|
|
110
|
-
|
|
111
153
|
}
|
|
112
154
|
|
|
113
155
|
/**
|
|
@@ -164,26 +206,27 @@ export class FileWatcher {
|
|
|
164
206
|
}
|
|
165
207
|
|
|
166
208
|
/**
|
|
167
|
-
* Start watching CMS directory
|
|
168
|
-
* Watches for changes in CMS content files (cms/{collection}/*.json)
|
|
209
|
+
* Start watching CMS directory.
|
|
210
|
+
* Watches for changes in CMS content files (cms/{collection}/*.json).
|
|
211
|
+
* Falls back to a deferred-attach watcher when `cms/` doesn't exist yet.
|
|
169
212
|
*/
|
|
170
213
|
watchCMS(dirPath: string = projectPaths.cms()): void {
|
|
171
|
-
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
this.cmsWatcher = watch(
|
|
214
|
+
attachWhenDirExists(
|
|
176
215
|
dirPath,
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
216
|
+
() => watch(
|
|
217
|
+
dirPath,
|
|
218
|
+
{ recursive: true },
|
|
219
|
+
async (event, filename) => {
|
|
220
|
+
if (filename && filename.endsWith('.json')) {
|
|
221
|
+
// Extract collection from path: "blog/my-post.json" -> "blog"
|
|
222
|
+
const collection = filename.split('/')[0];
|
|
223
|
+
if (this.callbacks.onCMSChange) {
|
|
224
|
+
await this.callbacks.onCMSChange(collection);
|
|
225
|
+
}
|
|
184
226
|
}
|
|
185
227
|
}
|
|
186
|
-
|
|
228
|
+
),
|
|
229
|
+
(w) => { this.cmsWatcher = w; }
|
|
187
230
|
);
|
|
188
231
|
}
|
|
189
232
|
|
|
@@ -206,6 +249,25 @@ export class FileWatcher {
|
|
|
206
249
|
);
|
|
207
250
|
}
|
|
208
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Start watching project.config.json for changes.
|
|
254
|
+
* Picks up edits to i18n locales, breakpoints, libraries, icons, etc. so the
|
|
255
|
+
* studio reflects external writes (e.g. an AI tool adding a new locale) without
|
|
256
|
+
* a dev-server restart.
|
|
257
|
+
*/
|
|
258
|
+
watchProjectConfig(): void {
|
|
259
|
+
const dirPath = getProjectRoot();
|
|
260
|
+
this.projectConfigWatcher = watch(
|
|
261
|
+
dirPath,
|
|
262
|
+
{ recursive: false },
|
|
263
|
+
async (_event, filename) => {
|
|
264
|
+
if (filename === 'project.config.json' && this.callbacks.onProjectConfigChange) {
|
|
265
|
+
await this.callbacks.onProjectConfigChange();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
209
271
|
/**
|
|
210
272
|
* Start watching libraries directory for CSS/JS changes
|
|
211
273
|
*/
|
|
@@ -240,6 +302,7 @@ export class FileWatcher {
|
|
|
240
302
|
this.watchCMS();
|
|
241
303
|
this.watchImages();
|
|
242
304
|
this.watchLibraries();
|
|
305
|
+
this.watchProjectConfig();
|
|
243
306
|
}
|
|
244
307
|
|
|
245
308
|
/**
|
|
@@ -290,6 +353,11 @@ export class FileWatcher {
|
|
|
290
353
|
this.librariesWatcher.close();
|
|
291
354
|
this.librariesWatcher = null;
|
|
292
355
|
}
|
|
356
|
+
|
|
357
|
+
if (this.projectConfigWatcher) {
|
|
358
|
+
this.projectConfigWatcher.close();
|
|
359
|
+
this.projectConfigWatcher = null;
|
|
360
|
+
}
|
|
293
361
|
}
|
|
294
362
|
|
|
295
363
|
/**
|
package/lib/server/jsonLoader.ts
CHANGED
|
@@ -341,5 +341,168 @@ describe('FileSystemCMSProvider', () => {
|
|
|
341
341
|
const items = await provider.getItems('valid-collection');
|
|
342
342
|
expect(items).toEqual([]);
|
|
343
343
|
});
|
|
344
|
+
|
|
345
|
+
it('should reject filenames ending in reserved .draft suffix', async () => {
|
|
346
|
+
const item = { _id: '99', _filename: 'foo.draft', title: 'Sneaky', slug: 'foo' };
|
|
347
|
+
await expect(provider.saveItem('blog-posts', item))
|
|
348
|
+
.rejects.toThrow(/\.draft.*reserved/);
|
|
349
|
+
await expect(provider.deleteItem('blog-posts', 'foo.draft'))
|
|
350
|
+
.rejects.toThrow(/\.draft.*reserved/);
|
|
351
|
+
await expect(provider.getItemByFilename('blog-posts', 'foo.draft'))
|
|
352
|
+
.rejects.toThrow(/\.draft.*reserved/);
|
|
353
|
+
});
|
|
344
354
|
});
|
|
355
|
+
|
|
356
|
+
describe('drafts', () => {
|
|
357
|
+
it('saveDraft writes a sibling .draft.json file and getDraft reads it back', async () => {
|
|
358
|
+
const draft = {
|
|
359
|
+
_id: '1',
|
|
360
|
+
_filename: 'hello-world',
|
|
361
|
+
title: 'Hello World — WIP edits',
|
|
362
|
+
slug: 'hello-world',
|
|
363
|
+
content: 'Updated content (not yet published)',
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
await provider.saveDraft('blog-posts', draft);
|
|
367
|
+
|
|
368
|
+
expect(existsSync(join(CMS_DIR, 'blog-posts', 'hello-world.draft.json'))).toBe(true);
|
|
369
|
+
|
|
370
|
+
const read = await provider.getDraft('blog-posts', 'hello-world');
|
|
371
|
+
expect(read?.title).toBe('Hello World — WIP edits');
|
|
372
|
+
expect(read?._isDraft).toBe(true);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('drafts do not appear in published-only queries (getItems, getItemByFilename)', async () => {
|
|
376
|
+
// Pre-existing item: hello-world (published). Add a draft for it.
|
|
377
|
+
await provider.saveDraft('blog-posts', {
|
|
378
|
+
_id: '1',
|
|
379
|
+
_filename: 'hello-world',
|
|
380
|
+
title: 'Draft title',
|
|
381
|
+
slug: 'hello-world',
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Add a draft-only item (no published file)
|
|
385
|
+
await provider.saveDraft('blog-posts', {
|
|
386
|
+
_id: 'new-1',
|
|
387
|
+
_filename: 'brand-new',
|
|
388
|
+
title: 'Brand new draft',
|
|
389
|
+
slug: 'brand-new',
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const items = await provider.getItems('blog-posts');
|
|
393
|
+
// Still only the two original published items
|
|
394
|
+
expect(items).toHaveLength(2);
|
|
395
|
+
expect(items.find(i => i._filename === 'brand-new')).toBeUndefined();
|
|
396
|
+
// hello-world's published title is unchanged
|
|
397
|
+
expect(items.find(i => i._filename === 'hello-world')?.title).toBe('Hello World');
|
|
398
|
+
|
|
399
|
+
const single = await provider.getItemByFilename('blog-posts', 'brand-new');
|
|
400
|
+
expect(single).toBeNull();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('getAllDrafts returns drafts with _isDraft transient flag', async () => {
|
|
404
|
+
await provider.saveDraft('blog-posts', {
|
|
405
|
+
_id: '1', _filename: 'hello-world', title: 'WIP', slug: 'hello-world',
|
|
406
|
+
});
|
|
407
|
+
await provider.saveDraft('blog-posts', {
|
|
408
|
+
_id: 'new', _filename: 'brand-new', title: 'New', slug: 'brand-new',
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const drafts = await provider.getAllDrafts('blog-posts');
|
|
412
|
+
expect(drafts).toHaveLength(2);
|
|
413
|
+
expect(drafts.every(d => d._isDraft === true)).toBe(true);
|
|
414
|
+
expect(drafts.map(d => d._filename).sort()).toEqual(['brand-new', 'hello-world']);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('hasDraft reflects whether a draft file exists', async () => {
|
|
418
|
+
expect(await provider.hasDraft('blog-posts', 'hello-world')).toBe(false);
|
|
419
|
+
await provider.saveDraft('blog-posts', {
|
|
420
|
+
_id: '1', _filename: 'hello-world', title: 'WIP', slug: 'hello-world',
|
|
421
|
+
});
|
|
422
|
+
expect(await provider.hasDraft('blog-posts', 'hello-world')).toBe(true);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('discardDraft removes the draft file and is a no-op when none exists', async () => {
|
|
426
|
+
await expect(provider.discardDraft('blog-posts', 'hello-world')).resolves.toBeUndefined();
|
|
427
|
+
|
|
428
|
+
await provider.saveDraft('blog-posts', {
|
|
429
|
+
_id: '1', _filename: 'hello-world', title: 'WIP', slug: 'hello-world',
|
|
430
|
+
});
|
|
431
|
+
expect(existsSync(join(CMS_DIR, 'blog-posts', 'hello-world.draft.json'))).toBe(true);
|
|
432
|
+
|
|
433
|
+
await provider.discardDraft('blog-posts', 'hello-world');
|
|
434
|
+
expect(existsSync(join(CMS_DIR, 'blog-posts', 'hello-world.draft.json'))).toBe(false);
|
|
435
|
+
// Published file is untouched
|
|
436
|
+
expect(existsSync(join(CMS_DIR, 'blog-posts', 'hello-world.json'))).toBe(true);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('publishDraft promotes draft to published and removes the draft file', async () => {
|
|
440
|
+
await provider.saveDraft('blog-posts', {
|
|
441
|
+
_id: '1',
|
|
442
|
+
_filename: 'hello-world',
|
|
443
|
+
title: 'Updated Title',
|
|
444
|
+
slug: 'hello-world',
|
|
445
|
+
content: 'New content',
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const result = await provider.publishDraft('blog-posts', 'hello-world');
|
|
449
|
+
expect(result.title).toBe('Updated Title');
|
|
450
|
+
|
|
451
|
+
// Published file now has the new content
|
|
452
|
+
const item = await provider.getItemByFilename('blog-posts', 'hello-world');
|
|
453
|
+
expect(item?.title).toBe('Updated Title');
|
|
454
|
+
expect(item?.content).toBe('New content');
|
|
455
|
+
|
|
456
|
+
// Draft file is gone
|
|
457
|
+
expect(existsSync(join(CMS_DIR, 'blog-posts', 'hello-world.draft.json'))).toBe(false);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('publishDraft of a draft-only item creates the published file', async () => {
|
|
461
|
+
await provider.saveDraft('blog-posts', {
|
|
462
|
+
_id: 'new', _filename: 'brand-new', title: 'New post', slug: 'brand-new',
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const result = await provider.publishDraft('blog-posts', 'brand-new');
|
|
466
|
+
expect(result._filename).toBe('brand-new');
|
|
467
|
+
|
|
468
|
+
const items = await provider.getItems('blog-posts');
|
|
469
|
+
expect(items.find(i => i._filename === 'brand-new')?.title).toBe('New post');
|
|
470
|
+
expect(existsSync(join(CMS_DIR, 'blog-posts', 'brand-new.draft.json'))).toBe(false);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('publishDraft throws when no draft exists', async () => {
|
|
474
|
+
await expect(provider.publishDraft('blog-posts', 'hello-world'))
|
|
475
|
+
.rejects.toThrow(/No draft to publish/);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('deleteItem removes both published and draft files', async () => {
|
|
479
|
+
await provider.saveDraft('blog-posts', {
|
|
480
|
+
_id: '1', _filename: 'hello-world', title: 'WIP', slug: 'hello-world',
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
await provider.deleteItem('blog-posts', 'hello-world');
|
|
484
|
+
|
|
485
|
+
expect(existsSync(join(CMS_DIR, 'blog-posts', 'hello-world.json'))).toBe(false);
|
|
486
|
+
expect(existsSync(join(CMS_DIR, 'blog-posts', 'hello-world.draft.json'))).toBe(false);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('saveItem strips transient _isDraft / _hasDraft fields before writing', async () => {
|
|
490
|
+
const item = {
|
|
491
|
+
_id: '1',
|
|
492
|
+
_filename: 'hello-world',
|
|
493
|
+
title: 'Hello',
|
|
494
|
+
slug: 'hello-world',
|
|
495
|
+
_isDraft: true,
|
|
496
|
+
_hasDraft: true,
|
|
497
|
+
};
|
|
498
|
+
await provider.saveItem('blog-posts', item as any);
|
|
499
|
+
|
|
500
|
+
const { readFile } = await import('fs/promises');
|
|
501
|
+
const raw = await readFile(join(CMS_DIR, 'blog-posts', 'hello-world.json'), 'utf-8');
|
|
502
|
+
const parsed = JSON.parse(raw);
|
|
503
|
+
expect(parsed._isDraft).toBeUndefined();
|
|
504
|
+
expect(parsed._hasDraft).toBeUndefined();
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
345
508
|
});
|