nebula-cms 0.1.0
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/.claude/settings.local.json +42 -0
- package/.github/workflows/ci.yml +31 -0
- package/.mcp.json +12 -0
- package/.prettierignore +5 -0
- package/.prettierrc.cjs +22 -0
- package/AGENTS.md +183 -0
- package/LICENSE +201 -0
- package/README.md +128 -0
- package/package.json +74 -0
- package/playground/.claude/settings.local.json +5 -0
- package/playground/astro.config.mjs +7 -0
- package/playground/node_modules/.bin/astro +21 -0
- package/playground/node_modules/.bin/rollup +21 -0
- package/playground/node_modules/.bin/tsc +21 -0
- package/playground/node_modules/.bin/tsserver +21 -0
- package/playground/node_modules/.bin/vite +21 -0
- package/playground/node_modules/.vite/_svelte_metadata.json +1 -0
- package/playground/node_modules/.vite/deps/@astrojs_svelte_client__js.js +80 -0
- package/playground/node_modules/.vite/deps/@astrojs_svelte_client__js.js.map +7 -0
- package/playground/node_modules/.vite/deps/_metadata.json +184 -0
- package/playground/node_modules/.vite/deps/astro___aria-query.js +6776 -0
- package/playground/node_modules/.vite/deps/astro___aria-query.js.map +7 -0
- package/playground/node_modules/.vite/deps/astro___axobject-query.js +3754 -0
- package/playground/node_modules/.vite/deps/astro___axobject-query.js.map +7 -0
- package/playground/node_modules/.vite/deps/astro___html-escaper.js +34 -0
- package/playground/node_modules/.vite/deps/astro___html-escaper.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-AJXJMYAF.js +0 -0
- package/playground/node_modules/.vite/deps/chunk-AJXJMYAF.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-ALJIOON6.js +1005 -0
- package/playground/node_modules/.vite/deps/chunk-ALJIOON6.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-BUSYA2B4.js +8 -0
- package/playground/node_modules/.vite/deps/chunk-BUSYA2B4.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-CNYJBM5F.js +21 -0
- package/playground/node_modules/.vite/deps/chunk-CNYJBM5F.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-DBPNBGEI.js +223 -0
- package/playground/node_modules/.vite/deps/chunk-DBPNBGEI.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-G3C2FXJT.js +204 -0
- package/playground/node_modules/.vite/deps/chunk-G3C2FXJT.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-GKDKFWC5.js +27 -0
- package/playground/node_modules/.vite/deps/chunk-GKDKFWC5.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-HNCLEOC5.js +4376 -0
- package/playground/node_modules/.vite/deps/chunk-HNCLEOC5.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-JICYXBFU.js +688 -0
- package/playground/node_modules/.vite/deps/chunk-JICYXBFU.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-KCUTL6DD.js +5099 -0
- package/playground/node_modules/.vite/deps/chunk-KCUTL6DD.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-ZP4UNCSN.js +23 -0
- package/playground/node_modules/.vite/deps/chunk-ZP4UNCSN.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-ZREFNRZZ.js +148 -0
- package/playground/node_modules/.vite/deps/chunk-ZREFNRZZ.js.map +7 -0
- package/playground/node_modules/.vite/deps/package.json +3 -0
- package/playground/node_modules/.vite/deps/smol-toml.js +843 -0
- package/playground/node_modules/.vite/deps/smol-toml.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte.js +55 -0
- package/playground/node_modules/.vite/deps/svelte.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte___clsx.js +9 -0
- package/playground/node_modules/.vite/deps/svelte___clsx.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_animate.js +57 -0
- package/playground/node_modules/.vite/deps/svelte_animate.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_attachments.js +15 -0
- package/playground/node_modules/.vite/deps/svelte_attachments.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_easing.js +67 -0
- package/playground/node_modules/.vite/deps/svelte_easing.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_events.js +11 -0
- package/playground/node_modules/.vite/deps/svelte_events.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal.js +5 -0
- package/playground/node_modules/.vite/deps/svelte_internal.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal_client.js +402 -0
- package/playground/node_modules/.vite/deps/svelte_internal_client.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal_disclose-version.js +10 -0
- package/playground/node_modules/.vite/deps/svelte_internal_disclose-version.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_async.js +8 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_async.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_legacy.js +8 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_legacy.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_tracing.js +8 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_tracing.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_legacy.js +35 -0
- package/playground/node_modules/.vite/deps/svelte_legacy.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_motion.js +545 -0
- package/playground/node_modules/.vite/deps/svelte_motion.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_reactivity.js +29 -0
- package/playground/node_modules/.vite/deps/svelte_reactivity.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_reactivity_window.js +127 -0
- package/playground/node_modules/.vite/deps/svelte_reactivity_window.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_store.js +103 -0
- package/playground/node_modules/.vite/deps/svelte_store.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_transition.js +208 -0
- package/playground/node_modules/.vite/deps/svelte_transition.js.map +7 -0
- package/playground/package.json +16 -0
- package/playground/pnpm-lock.yaml +3167 -0
- package/playground/src/content/authors/jane-doe.json +8 -0
- package/playground/src/content/config/build.toml +2 -0
- package/playground/src/content/courses/web-fundamentals.json +29 -0
- package/playground/src/content/docs/advanced.mdx +6 -0
- package/playground/src/content/docs/intro.md +6 -0
- package/playground/src/content/guides/getting-started.mdx +6 -0
- package/playground/src/content/posts/hello-world.md +7 -0
- package/playground/src/content/products/t-shirt.json +16 -0
- package/playground/src/content/recipes/pancakes.mdoc +8 -0
- package/playground/src/content/settings/site.yml +2 -0
- package/playground/src/content.config.ts +198 -0
- package/playground/src/env.d.ts +1 -0
- package/playground/src/pages/index.astro +11 -0
- package/playground/src/pages/nebula.astro +14 -0
- package/pnpm-workspace.yaml +2 -0
- package/scripts/subset-icons.mjs +178 -0
- package/src/astro/index.ts +295 -0
- package/src/client/Admin.svelte +283 -0
- package/src/client/components/BackendPicker.svelte +291 -0
- package/src/client/components/DraftChip.svelte +46 -0
- package/src/client/components/MetadataForm.svelte +56 -0
- package/src/client/components/ThemeToggle.svelte +18 -0
- package/src/client/components/dialogs/DeleteDraftDialog.svelte +51 -0
- package/src/client/components/dialogs/FilenameDialog.svelte +129 -0
- package/src/client/components/editor/EditorPane.svelte +227 -0
- package/src/client/components/editor/EditorTabs.svelte +81 -0
- package/src/client/components/editor/EditorToolbar.svelte +131 -0
- package/src/client/components/editor/FormatSelector.svelte +66 -0
- package/src/client/components/editor/Toolbar.svelte +17 -0
- package/src/client/components/fields/ArrayField.svelte +339 -0
- package/src/client/components/fields/ArrayItem.svelte +325 -0
- package/src/client/components/fields/BooleanField.svelte +114 -0
- package/src/client/components/fields/DateField.svelte +82 -0
- package/src/client/components/fields/EnumField.svelte +74 -0
- package/src/client/components/fields/FieldWrapper.svelte +96 -0
- package/src/client/components/fields/NumberField.svelte +99 -0
- package/src/client/components/fields/ObjectField.svelte +121 -0
- package/src/client/components/fields/SchemaField.svelte +107 -0
- package/src/client/components/fields/StringField.svelte +104 -0
- package/src/client/components/sidebar/AdminSidebar.svelte +339 -0
- package/src/client/components/sidebar/AdminSidebarSort.svelte +123 -0
- package/src/client/css/a11y.css +14 -0
- package/src/client/css/btn.css +113 -0
- package/src/client/css/dialog.css +29 -0
- package/src/client/css/field-input.css +39 -0
- package/src/client/css/reset.css +59 -0
- package/src/client/css/theme.css +77 -0
- package/src/client/index.ts +1 -0
- package/src/client/js/drafts/merge.svelte.ts +121 -0
- package/src/client/js/drafts/ops.svelte.ts +227 -0
- package/src/client/js/drafts/storage.ts +108 -0
- package/src/client/js/drafts/workers/diff.ts +40 -0
- package/src/client/js/editor/editor.svelte.ts +343 -0
- package/src/client/js/editor/languages.ts +98 -0
- package/src/client/js/editor/link-wrap.ts +45 -0
- package/src/client/js/editor/markdown-shortcuts.ts +261 -0
- package/src/client/js/handlers/admin.ts +246 -0
- package/src/client/js/state/dialogs.svelte.ts +35 -0
- package/src/client/js/state/router.svelte.ts +156 -0
- package/src/client/js/state/schema.svelte.ts +140 -0
- package/src/client/js/state/state.svelte.ts +334 -0
- package/src/client/js/state/theme.svelte.ts +173 -0
- package/src/client/js/storage/adapter.ts +102 -0
- package/src/client/js/storage/client.ts +150 -0
- package/src/client/js/storage/db.ts +36 -0
- package/src/client/js/storage/fsa.ts +110 -0
- package/src/client/js/storage/github.ts +297 -0
- package/src/client/js/storage/storage.ts +83 -0
- package/src/client/js/storage/workers/frontmatter.ts +320 -0
- package/src/client/js/storage/workers/storage.ts +177 -0
- package/src/client/js/storage/workers/toml-parser.ts +106 -0
- package/src/client/js/storage/workers/yaml-parser.ts +132 -0
- package/src/client/js/utils/file-types.ts +192 -0
- package/src/client/js/utils/format.ts +16 -0
- package/src/client/js/utils/frontmatter.ts +38 -0
- package/src/client/js/utils/schema-utils.ts +295 -0
- package/src/client/js/utils/slug.ts +18 -0
- package/src/client/js/utils/sort.ts +84 -0
- package/src/client/js/utils/stable-stringify.ts +27 -0
- package/src/client/js/utils/url-utils.ts +38 -0
- package/src/types.ts +25 -0
- package/src/virtual.d.ts +22 -0
- package/svelte.config.js +4 -0
- package/tests/astro/build.test.ts +63 -0
- package/tests/astro/index.test.ts +689 -0
- package/tests/client/components/Admin.test.ts +446 -0
- package/tests/client/components/BackendPicker.test.ts +239 -0
- package/tests/client/components/DraftChip.test.ts +53 -0
- package/tests/client/components/MetadataForm.test.ts +164 -0
- package/tests/client/components/dialogs/DeleteDraftDialog.test.ts +91 -0
- package/tests/client/components/dialogs/FilenameDialog.test.ts +209 -0
- package/tests/client/components/dialogs/dialog-stubs.ts +19 -0
- package/tests/client/components/editor/EditorPane.test.ts +100 -0
- package/tests/client/components/editor/EditorTabs.test.ts +253 -0
- package/tests/client/components/editor/EditorToolbar.test.ts +252 -0
- package/tests/client/components/editor/fixtures.ts +31 -0
- package/tests/client/components/fields/ArrayField.test.ts +197 -0
- package/tests/client/components/fields/BooleanField.test.ts +206 -0
- package/tests/client/components/fields/DateField.test.ts +210 -0
- package/tests/client/components/fields/EnumField.test.ts +246 -0
- package/tests/client/components/fields/NumberField.test.ts +240 -0
- package/tests/client/components/fields/ObjectField.test.ts +157 -0
- package/tests/client/components/fields/SchemaField.test.ts +190 -0
- package/tests/client/components/fields/StringField.test.ts +223 -0
- package/tests/client/components/sidebar/AdminSidebar.test.ts +285 -0
- package/tests/client/components/sidebar/AdminSidebarSort.test.ts +135 -0
- package/tests/client/components/sidebar/sort-mock.ts +23 -0
- package/tests/client/js/drafts/fixtures.ts +22 -0
- package/tests/client/js/drafts/merge.test.ts +282 -0
- package/tests/client/js/drafts/ops.test.ts +658 -0
- package/tests/client/js/drafts/storage.test.ts +200 -0
- package/tests/client/js/drafts/workers/diff.test.ts +165 -0
- package/tests/client/js/editor/editor.test.ts +616 -0
- package/tests/client/js/editor/link-wrap.test.ts +225 -0
- package/tests/client/js/editor/markdown-shortcuts.test.ts +370 -0
- package/tests/client/js/handlers/admin.test.ts +467 -0
- package/tests/client/js/state/router.test.ts +619 -0
- package/tests/client/js/state/schema.test.ts +266 -0
- package/tests/client/js/state/state.test.ts +328 -0
- package/tests/client/js/storage/adapter.test.ts +115 -0
- package/tests/client/js/storage/client.test.ts +250 -0
- package/tests/client/js/storage/db.test.ts +59 -0
- package/tests/client/js/storage/fsa.test.ts +284 -0
- package/tests/client/js/storage/github.test.ts +349 -0
- package/tests/client/js/storage/mock-port.ts +95 -0
- package/tests/client/js/storage/storage.test.ts +77 -0
- package/tests/client/js/storage/workers/frontmatter.test.ts +479 -0
- package/tests/client/js/storage/workers/storage.test.ts +299 -0
- package/tests/client/js/storage/workers/toml-parser.test.ts +169 -0
- package/tests/client/js/storage/workers/yaml-parser.test.ts +168 -0
- package/tests/client/js/utils/file-types.test.ts +268 -0
- package/tests/client/js/utils/frontmatter.test.ts +87 -0
- package/tests/client/js/utils/schema-utils.test.ts +318 -0
- package/tests/client/js/utils/slug.test.ts +58 -0
- package/tests/client/js/utils/sort.test.ts +276 -0
- package/tests/client/js/utils/stable-stringify.test.ts +68 -0
- package/tests/client/js/utils/url-utils.test.ts +70 -0
- package/tests/e2e/backend-connection.test.ts +301 -0
- package/tests/e2e/draft-lifecycle.test.ts +388 -0
- package/tests/e2e/editing.test.ts +355 -0
- package/tests/e2e/github-adapter.test.ts +330 -0
- package/tests/e2e/helpers/mock-adapter.ts +166 -0
- package/tests/e2e/helpers/test-app.ts +155 -0
- package/tests/e2e/navigation.test.ts +358 -0
- package/tests/e2e/publishing.test.ts +345 -0
- package/tests/e2e/unsaved-changes.test.ts +317 -0
- package/tests/setup.ts +2 -0
- package/tests/stubs/codemirror.ts +197 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +178 -0
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
mkdtempSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { resolve, join } from 'node:path';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { pathToFileURL } from 'node:url';
|
|
13
|
+
import NebulaCMS, { nebulaVitePlugin } from '../../src/astro/index.js';
|
|
14
|
+
import type { AstroIntegrationLogger } from 'astro';
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
//////////////////////////////
|
|
18
|
+
// Test Helpers
|
|
19
|
+
//////////////////////////////
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a minimal mock satisfying the AstroIntegrationLogger interface
|
|
24
|
+
* @return {AstroIntegrationLogger} Mock logger with a spied warn method
|
|
25
|
+
*/
|
|
26
|
+
function createMockLogger() {
|
|
27
|
+
return { warn: vi.fn() } as unknown as AstroIntegrationLogger;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
//////////////////////////////
|
|
32
|
+
// NebulaCMS integration object
|
|
33
|
+
//////////////////////////////
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
describe('NebulaCMS integration object', () => {
|
|
37
|
+
it('returns an integration with name "nebula-cms"', () => {
|
|
38
|
+
const integration = NebulaCMS();
|
|
39
|
+
expect(integration.name).toBe('nebula-cms');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('registers a Vite plugin via astro:config:setup', () => {
|
|
43
|
+
const integration = NebulaCMS();
|
|
44
|
+
const updateConfig = vi.fn();
|
|
45
|
+
const logger = createMockLogger();
|
|
46
|
+
|
|
47
|
+
const hook = integration.hooks['astro:config:setup'] as Function;
|
|
48
|
+
hook({ updateConfig, logger });
|
|
49
|
+
|
|
50
|
+
expect(updateConfig).toHaveBeenCalledWith({
|
|
51
|
+
vite: {
|
|
52
|
+
plugins: [expect.objectContaining({ name: 'vite-plugin-nebula-cms' })],
|
|
53
|
+
worker: { format: 'es' },
|
|
54
|
+
optimizeDeps: { include: ['smol-toml'] },
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('accepts a valid absolute basePath', () => {
|
|
60
|
+
const integration = NebulaCMS({ basePath: '/dashboard' });
|
|
61
|
+
expect(integration.name).toBe('nebula-cms');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('normalizes basePath without leading slash', () => {
|
|
65
|
+
// Should not throw — 'admin' is normalized to '/admin'
|
|
66
|
+
const integration = NebulaCMS({ basePath: 'admin' });
|
|
67
|
+
expect(integration.name).toBe('nebula-cms');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('normalizes basePath with trailing slash', () => {
|
|
71
|
+
// Should not throw — '/admin/' is normalized to '/admin'
|
|
72
|
+
const integration = NebulaCMS({ basePath: '/admin/' });
|
|
73
|
+
expect(integration.name).toBe('nebula-cms');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('throws on basePath with invalid characters', () => {
|
|
77
|
+
expect(() => NebulaCMS({ basePath: '/admin/<script>' })).toThrow(
|
|
78
|
+
'Invalid basePath',
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('accepts a valid absolute collectionsPath', () => {
|
|
83
|
+
const integration = NebulaCMS({ collectionsPath: '/schemas' });
|
|
84
|
+
expect(integration.name).toBe('nebula-cms');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('normalizes collectionsPath without leading slash', () => {
|
|
88
|
+
const integration = NebulaCMS({ collectionsPath: 'schemas' });
|
|
89
|
+
expect(integration.name).toBe('nebula-cms');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('normalizes collectionsPath with trailing slash', () => {
|
|
93
|
+
const integration = NebulaCMS({ collectionsPath: '/schemas/' });
|
|
94
|
+
expect(integration.name).toBe('nebula-cms');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('throws on collectionsPath with invalid characters', () => {
|
|
98
|
+
expect(() => NebulaCMS({ collectionsPath: '/my schemas' })).toThrow(
|
|
99
|
+
'Invalid collectionsPath',
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('throws on empty collectionsPath', () => {
|
|
104
|
+
expect(() => NebulaCMS({ collectionsPath: '' })).toThrow(
|
|
105
|
+
'Invalid collectionsPath',
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('accepts root basePath /', () => {
|
|
110
|
+
const integration = NebulaCMS({ basePath: '/' });
|
|
111
|
+
expect(integration.name).toBe('nebula-cms');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('collapses consecutive slashes in basePath', () => {
|
|
115
|
+
// '/admin//' is normalized to '/admin' via URL API
|
|
116
|
+
const integration = NebulaCMS({ basePath: '/admin//' });
|
|
117
|
+
expect(integration.name).toBe('nebula-cms');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('throws on empty basePath', () => {
|
|
121
|
+
expect(() => NebulaCMS({ basePath: '' })).toThrow('Invalid basePath ""');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('throws on collectionsPath set to /', () => {
|
|
125
|
+
expect(() => NebulaCMS({ collectionsPath: '/' })).toThrow(
|
|
126
|
+
'Collections require a path prefix',
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('throws on protocol-relative basePath', () => {
|
|
131
|
+
expect(() => NebulaCMS({ basePath: '//admin' })).toThrow(
|
|
132
|
+
'must not start with "//"',
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
/*
|
|
138
|
+
//////////////////////////////
|
|
139
|
+
// resolveId hook
|
|
140
|
+
//////////////////////////////
|
|
141
|
+
*/
|
|
142
|
+
|
|
143
|
+
describe('nebulaVitePlugin resolveId', () => {
|
|
144
|
+
it('resolves virtual:nebula/collections to the internal ID', () => {
|
|
145
|
+
const plugin = nebulaVitePlugin(createMockLogger(), '/fake', {
|
|
146
|
+
basePath: '/admin',
|
|
147
|
+
collectionsPath: '/collections',
|
|
148
|
+
});
|
|
149
|
+
expect(plugin.resolveId('virtual:nebula/collections')).toBe(
|
|
150
|
+
'\0virtual:nebula/collections',
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('resolves virtual:nebula/config to the internal ID', () => {
|
|
155
|
+
const plugin = nebulaVitePlugin(createMockLogger(), '/fake', {
|
|
156
|
+
basePath: '/admin',
|
|
157
|
+
collectionsPath: '/collections',
|
|
158
|
+
});
|
|
159
|
+
expect(plugin.resolveId('virtual:nebula/config')).toBe(
|
|
160
|
+
'\0virtual:nebula/config',
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('returns undefined for unrelated module IDs', () => {
|
|
165
|
+
const plugin = nebulaVitePlugin(createMockLogger(), '/fake', {
|
|
166
|
+
basePath: '/admin',
|
|
167
|
+
collectionsPath: '/collections',
|
|
168
|
+
});
|
|
169
|
+
expect(plugin.resolveId('some-other-module')).toBeUndefined();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('does not resolve old virtual:collections ID', () => {
|
|
173
|
+
const plugin = nebulaVitePlugin(createMockLogger(), '/fake', {
|
|
174
|
+
basePath: '/admin',
|
|
175
|
+
collectionsPath: '/collections',
|
|
176
|
+
});
|
|
177
|
+
expect(plugin.resolveId('virtual:collections')).toBeUndefined();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
/*
|
|
182
|
+
//////////////////////////////
|
|
183
|
+
// configureServer middleware
|
|
184
|
+
//////////////////////////////
|
|
185
|
+
*/
|
|
186
|
+
|
|
187
|
+
describe('nebulaVitePlugin configureServer', () => {
|
|
188
|
+
let tmpDir: string;
|
|
189
|
+
let logger: AstroIntegrationLogger;
|
|
190
|
+
|
|
191
|
+
beforeEach(() => {
|
|
192
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'nebula-test-'));
|
|
193
|
+
logger = createMockLogger();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
afterEach(() => {
|
|
197
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Creates a fake Connect-style middleware stack to capture registered handlers.
|
|
202
|
+
* @return {{ use: ReturnType<typeof vi.fn>, handlers: Function[] }}
|
|
203
|
+
*/
|
|
204
|
+
function createMiddlewareStub() {
|
|
205
|
+
const handlers: Function[] = [];
|
|
206
|
+
return {
|
|
207
|
+
use: vi.fn((fn: Function) => {
|
|
208
|
+
handlers.push(fn);
|
|
209
|
+
}),
|
|
210
|
+
get handlers() {
|
|
211
|
+
return handlers;
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Simulates a middleware request/response cycle for the schema middleware.
|
|
218
|
+
* @param {Function} handler - The middleware handler
|
|
219
|
+
* @param {string} url - The request URL
|
|
220
|
+
* @return {{ status: 'served' | 'skipped', body?: string, contentType?: string }}
|
|
221
|
+
*/
|
|
222
|
+
function callSchemaMiddleware(handler: Function, url: string) {
|
|
223
|
+
let result: { status: string; body?: string; contentType?: string } = {
|
|
224
|
+
status: 'skipped',
|
|
225
|
+
};
|
|
226
|
+
const req = { url };
|
|
227
|
+
const headers: Record<string, string> = {};
|
|
228
|
+
const res = {
|
|
229
|
+
setHeader: (k: string, v: string) => {
|
|
230
|
+
headers[k] = v;
|
|
231
|
+
},
|
|
232
|
+
end: (body: string) => {
|
|
233
|
+
result = {
|
|
234
|
+
status: 'served',
|
|
235
|
+
body,
|
|
236
|
+
contentType: headers['Content-Type'],
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
const next = () => {
|
|
241
|
+
result = { status: 'skipped' };
|
|
242
|
+
};
|
|
243
|
+
handler(req, res, next);
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Simulates a middleware request/response cycle for the SPA rewrite middleware.
|
|
249
|
+
* @param {Function} handler - The middleware handler
|
|
250
|
+
* @param {string} url - The request URL
|
|
251
|
+
* @param {string} accept - The Accept header value
|
|
252
|
+
* @return {{ rewritten: boolean, url: string, nextCalled: boolean }}
|
|
253
|
+
*/
|
|
254
|
+
function callRewriteMiddleware(
|
|
255
|
+
handler: Function,
|
|
256
|
+
url: string,
|
|
257
|
+
accept: string,
|
|
258
|
+
) {
|
|
259
|
+
const req = { url, headers: { accept } };
|
|
260
|
+
let nextCalled = false;
|
|
261
|
+
const next = () => {
|
|
262
|
+
nextCalled = true;
|
|
263
|
+
};
|
|
264
|
+
handler(req, {}, next);
|
|
265
|
+
return { rewritten: req.url !== url, url: req.url, nextCalled };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
it('serves schema files from .astro/collections', () => {
|
|
269
|
+
const dir = resolve(tmpDir, '.astro/collections');
|
|
270
|
+
mkdirSync(dir, { recursive: true });
|
|
271
|
+
writeFileSync(resolve(dir, 'posts.schema.json'), '{"type":"object"}');
|
|
272
|
+
|
|
273
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
274
|
+
basePath: '/admin',
|
|
275
|
+
collectionsPath: '/collections',
|
|
276
|
+
});
|
|
277
|
+
const mw = createMiddlewareStub();
|
|
278
|
+
plugin.configureServer({ middlewares: mw });
|
|
279
|
+
|
|
280
|
+
// First handler is the schema middleware
|
|
281
|
+
const result = callSchemaMiddleware(
|
|
282
|
+
mw.handlers[0],
|
|
283
|
+
'/collections/posts.schema.json',
|
|
284
|
+
);
|
|
285
|
+
expect(result.status).toBe('served');
|
|
286
|
+
expect(result.body).toBe('{"type":"object"}');
|
|
287
|
+
expect(result.contentType).toBe('application/json');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('calls next() for non-schema requests', () => {
|
|
291
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
292
|
+
basePath: '/admin',
|
|
293
|
+
collectionsPath: '/collections',
|
|
294
|
+
});
|
|
295
|
+
const mw = createMiddlewareStub();
|
|
296
|
+
plugin.configureServer({ middlewares: mw });
|
|
297
|
+
|
|
298
|
+
const result = callSchemaMiddleware(mw.handlers[0], '/some/other/path');
|
|
299
|
+
expect(result.status).toBe('skipped');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('calls next() when schema file does not exist', () => {
|
|
303
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
304
|
+
basePath: '/admin',
|
|
305
|
+
collectionsPath: '/collections',
|
|
306
|
+
});
|
|
307
|
+
const mw = createMiddlewareStub();
|
|
308
|
+
plugin.configureServer({ middlewares: mw });
|
|
309
|
+
|
|
310
|
+
const result = callSchemaMiddleware(
|
|
311
|
+
mw.handlers[0],
|
|
312
|
+
'/collections/missing.schema.json',
|
|
313
|
+
);
|
|
314
|
+
expect(result.status).toBe('skipped');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('uses custom collectionsPath prefix', () => {
|
|
318
|
+
const dir = resolve(tmpDir, '.astro/collections');
|
|
319
|
+
mkdirSync(dir, { recursive: true });
|
|
320
|
+
writeFileSync(resolve(dir, 'posts.schema.json'), '{}');
|
|
321
|
+
|
|
322
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
323
|
+
basePath: '/admin',
|
|
324
|
+
collectionsPath: '/schemas',
|
|
325
|
+
});
|
|
326
|
+
const mw = createMiddlewareStub();
|
|
327
|
+
plugin.configureServer({ middlewares: mw });
|
|
328
|
+
|
|
329
|
+
const miss = callSchemaMiddleware(
|
|
330
|
+
mw.handlers[0],
|
|
331
|
+
'/collections/posts.schema.json',
|
|
332
|
+
);
|
|
333
|
+
expect(miss.status).toBe('skipped');
|
|
334
|
+
|
|
335
|
+
const hit = callSchemaMiddleware(
|
|
336
|
+
mw.handlers[0],
|
|
337
|
+
'/schemas/posts.schema.json',
|
|
338
|
+
);
|
|
339
|
+
expect(hit.status).toBe('served');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('rewrites HTML requests under basePath to basePath', () => {
|
|
343
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
344
|
+
basePath: '/admin',
|
|
345
|
+
collectionsPath: '/collections',
|
|
346
|
+
});
|
|
347
|
+
const mw = createMiddlewareStub();
|
|
348
|
+
plugin.configureServer({ middlewares: mw });
|
|
349
|
+
|
|
350
|
+
// Second handler is the SPA rewrite middleware
|
|
351
|
+
const result = callRewriteMiddleware(
|
|
352
|
+
mw.handlers[1],
|
|
353
|
+
'/admin/posts/my-article',
|
|
354
|
+
'text/html',
|
|
355
|
+
);
|
|
356
|
+
expect(result.rewritten).toBe(true);
|
|
357
|
+
expect(result.url).toBe('/admin');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('does not rewrite exact basePath requests', () => {
|
|
361
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
362
|
+
basePath: '/admin',
|
|
363
|
+
collectionsPath: '/collections',
|
|
364
|
+
});
|
|
365
|
+
const mw = createMiddlewareStub();
|
|
366
|
+
plugin.configureServer({ middlewares: mw });
|
|
367
|
+
|
|
368
|
+
const result = callRewriteMiddleware(mw.handlers[1], '/admin', 'text/html');
|
|
369
|
+
expect(result.rewritten).toBe(false);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('does not rewrite non-HTML requests under basePath', () => {
|
|
373
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
374
|
+
basePath: '/admin',
|
|
375
|
+
collectionsPath: '/collections',
|
|
376
|
+
});
|
|
377
|
+
const mw = createMiddlewareStub();
|
|
378
|
+
plugin.configureServer({ middlewares: mw });
|
|
379
|
+
|
|
380
|
+
const result = callRewriteMiddleware(
|
|
381
|
+
mw.handlers[1],
|
|
382
|
+
'/admin/posts/my-article',
|
|
383
|
+
'application/json',
|
|
384
|
+
);
|
|
385
|
+
expect(result.rewritten).toBe(false);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('respects segment boundary — /administrator does not rewrite', () => {
|
|
389
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
390
|
+
basePath: '/admin',
|
|
391
|
+
collectionsPath: '/collections',
|
|
392
|
+
});
|
|
393
|
+
const mw = createMiddlewareStub();
|
|
394
|
+
plugin.configureServer({ middlewares: mw });
|
|
395
|
+
|
|
396
|
+
const result = callRewriteMiddleware(
|
|
397
|
+
mw.handlers[1],
|
|
398
|
+
'/administrator',
|
|
399
|
+
'text/html',
|
|
400
|
+
);
|
|
401
|
+
expect(result.rewritten).toBe(false);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('rewrites with custom basePath', () => {
|
|
405
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
406
|
+
basePath: '/nebula',
|
|
407
|
+
collectionsPath: '/collections',
|
|
408
|
+
});
|
|
409
|
+
const mw = createMiddlewareStub();
|
|
410
|
+
plugin.configureServer({ middlewares: mw });
|
|
411
|
+
|
|
412
|
+
const result = callRewriteMiddleware(
|
|
413
|
+
mw.handlers[1],
|
|
414
|
+
'/nebula/posts/draft-12345678',
|
|
415
|
+
'text/html',
|
|
416
|
+
);
|
|
417
|
+
expect(result.rewritten).toBe(true);
|
|
418
|
+
expect(result.url).toBe('/nebula');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('rewrites sub-routes when basePath is /', () => {
|
|
422
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
423
|
+
basePath: '/',
|
|
424
|
+
collectionsPath: '/collections',
|
|
425
|
+
});
|
|
426
|
+
const mw = createMiddlewareStub();
|
|
427
|
+
plugin.configureServer({ middlewares: mw });
|
|
428
|
+
|
|
429
|
+
const result = callRewriteMiddleware(
|
|
430
|
+
mw.handlers[1],
|
|
431
|
+
'/posts/my-article',
|
|
432
|
+
'text/html',
|
|
433
|
+
);
|
|
434
|
+
expect(result.rewritten).toBe(true);
|
|
435
|
+
expect(result.url).toBe('/');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('does not rewrite root path when basePath is /', () => {
|
|
439
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
440
|
+
basePath: '/',
|
|
441
|
+
collectionsPath: '/collections',
|
|
442
|
+
});
|
|
443
|
+
const mw = createMiddlewareStub();
|
|
444
|
+
plugin.configureServer({ middlewares: mw });
|
|
445
|
+
|
|
446
|
+
const result = callRewriteMiddleware(mw.handlers[1], '/', 'text/html');
|
|
447
|
+
expect(result.rewritten).toBe(false);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('rewrites HTML requests with query strings', () => {
|
|
451
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
452
|
+
basePath: '/admin',
|
|
453
|
+
collectionsPath: '/collections',
|
|
454
|
+
});
|
|
455
|
+
const mw = createMiddlewareStub();
|
|
456
|
+
plugin.configureServer({ middlewares: mw });
|
|
457
|
+
|
|
458
|
+
const result = callRewriteMiddleware(
|
|
459
|
+
mw.handlers[1],
|
|
460
|
+
'/admin/posts?draft=true',
|
|
461
|
+
'text/html',
|
|
462
|
+
);
|
|
463
|
+
expect(result.rewritten).toBe(true);
|
|
464
|
+
// Query string preserved during rewrite
|
|
465
|
+
expect(result.url).toBe('/admin?draft=true');
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('rejects path traversal in schema requests', () => {
|
|
469
|
+
const dir = resolve(tmpDir, '.astro/collections');
|
|
470
|
+
mkdirSync(dir, { recursive: true });
|
|
471
|
+
writeFileSync(resolve(dir, 'posts.schema.json'), '{}');
|
|
472
|
+
|
|
473
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
474
|
+
basePath: '/admin',
|
|
475
|
+
collectionsPath: '/collections',
|
|
476
|
+
});
|
|
477
|
+
const mw = createMiddlewareStub();
|
|
478
|
+
plugin.configureServer({ middlewares: mw });
|
|
479
|
+
|
|
480
|
+
const result = callSchemaMiddleware(
|
|
481
|
+
mw.handlers[0],
|
|
482
|
+
'/collections/../../../etc/passwd.schema.json',
|
|
483
|
+
);
|
|
484
|
+
expect(result.status).toBe('skipped');
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
/*
|
|
489
|
+
//////////////////////////////
|
|
490
|
+
// astro:build:done hook
|
|
491
|
+
//////////////////////////////
|
|
492
|
+
*/
|
|
493
|
+
|
|
494
|
+
describe('NebulaCMS astro:build:done', () => {
|
|
495
|
+
let tmpDir: string;
|
|
496
|
+
let logger: AstroIntegrationLogger;
|
|
497
|
+
|
|
498
|
+
beforeEach(() => {
|
|
499
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'nebula-test-'));
|
|
500
|
+
logger = createMockLogger();
|
|
501
|
+
vi.spyOn(process, 'cwd').mockReturnValue(tmpDir);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
afterEach(() => {
|
|
505
|
+
vi.restoreAllMocks();
|
|
506
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('copies schema files into the build output directory', () => {
|
|
510
|
+
const source = resolve(tmpDir, '.astro/collections');
|
|
511
|
+
mkdirSync(source, { recursive: true });
|
|
512
|
+
writeFileSync(resolve(source, 'posts.schema.json'), '{"type":"object"}');
|
|
513
|
+
writeFileSync(resolve(source, 'authors.schema.json'), '{"a":1}');
|
|
514
|
+
|
|
515
|
+
const outDir = resolve(tmpDir, 'dist');
|
|
516
|
+
mkdirSync(outDir, { recursive: true });
|
|
517
|
+
|
|
518
|
+
const integration = NebulaCMS();
|
|
519
|
+
const hook = integration.hooks['astro:build:done'] as Function;
|
|
520
|
+
hook({ dir: pathToFileURL(outDir + '/'), logger });
|
|
521
|
+
|
|
522
|
+
const target = resolve(outDir, 'collections');
|
|
523
|
+
expect(existsSync(resolve(target, 'posts.schema.json'))).toBe(true);
|
|
524
|
+
expect(existsSync(resolve(target, 'authors.schema.json'))).toBe(true);
|
|
525
|
+
expect(readFileSync(resolve(target, 'posts.schema.json'), 'utf-8')).toBe(
|
|
526
|
+
'{"type":"object"}',
|
|
527
|
+
);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it('warns when .astro/collections does not exist', () => {
|
|
531
|
+
const outDir = resolve(tmpDir, 'dist');
|
|
532
|
+
mkdirSync(outDir, { recursive: true });
|
|
533
|
+
|
|
534
|
+
const integration = NebulaCMS();
|
|
535
|
+
const hook = integration.hooks['astro:build:done'] as Function;
|
|
536
|
+
hook({ dir: pathToFileURL(outDir + '/'), logger });
|
|
537
|
+
|
|
538
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
539
|
+
expect.stringContaining('.astro/collections'),
|
|
540
|
+
);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('uses custom collectionsPath for the output directory', () => {
|
|
544
|
+
const source = resolve(tmpDir, '.astro/collections');
|
|
545
|
+
mkdirSync(source, { recursive: true });
|
|
546
|
+
writeFileSync(resolve(source, 'posts.schema.json'), '{}');
|
|
547
|
+
|
|
548
|
+
const outDir = resolve(tmpDir, 'dist');
|
|
549
|
+
mkdirSync(outDir, { recursive: true });
|
|
550
|
+
|
|
551
|
+
const integration = NebulaCMS({ collectionsPath: '/schemas' });
|
|
552
|
+
const hook = integration.hooks['astro:build:done'] as Function;
|
|
553
|
+
hook({ dir: pathToFileURL(outDir + '/'), logger });
|
|
554
|
+
|
|
555
|
+
expect(existsSync(resolve(outDir, 'schemas/posts.schema.json'))).toBe(true);
|
|
556
|
+
expect(existsSync(resolve(outDir, 'collections'))).toBe(false);
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
/*
|
|
561
|
+
//////////////////////////////
|
|
562
|
+
// load hook
|
|
563
|
+
//////////////////////////////
|
|
564
|
+
*/
|
|
565
|
+
|
|
566
|
+
describe('nebulaVitePlugin load', () => {
|
|
567
|
+
let tmpDir: string;
|
|
568
|
+
let logger: AstroIntegrationLogger;
|
|
569
|
+
|
|
570
|
+
beforeEach(() => {
|
|
571
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'nebula-test-'));
|
|
572
|
+
logger = createMockLogger();
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
afterEach(() => {
|
|
576
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it('returns undefined for unrelated module IDs', () => {
|
|
580
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
581
|
+
basePath: '/admin',
|
|
582
|
+
collectionsPath: '/collections',
|
|
583
|
+
});
|
|
584
|
+
expect(plugin.load('some-other-id')).toBeUndefined();
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('generates config module with basePath and collectionsPath', () => {
|
|
588
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
589
|
+
basePath: '/dashboard',
|
|
590
|
+
collectionsPath: '/schemas',
|
|
591
|
+
});
|
|
592
|
+
const result = plugin.load('\0virtual:nebula/config');
|
|
593
|
+
|
|
594
|
+
expect(result).toContain('"/dashboard"');
|
|
595
|
+
expect(result).toContain('"/schemas"');
|
|
596
|
+
expect(result).toContain('export default');
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('generates collections module mapping names to schema URLs', () => {
|
|
600
|
+
const dir = resolve(tmpDir, '.astro/collections');
|
|
601
|
+
mkdirSync(dir, { recursive: true });
|
|
602
|
+
writeFileSync(resolve(dir, 'posts.schema.json'), '{}');
|
|
603
|
+
|
|
604
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
605
|
+
basePath: '/admin',
|
|
606
|
+
collectionsPath: '/collections',
|
|
607
|
+
});
|
|
608
|
+
const result = plugin.load('\0virtual:nebula/collections');
|
|
609
|
+
|
|
610
|
+
expect(result).toContain('"posts"');
|
|
611
|
+
expect(result).toContain('"/collections/posts.schema.json"');
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('handles multiple schema files', () => {
|
|
615
|
+
const dir = resolve(tmpDir, '.astro/collections');
|
|
616
|
+
mkdirSync(dir, { recursive: true });
|
|
617
|
+
writeFileSync(resolve(dir, 'posts.schema.json'), '{}');
|
|
618
|
+
writeFileSync(resolve(dir, 'authors.schema.json'), '{}');
|
|
619
|
+
|
|
620
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
621
|
+
basePath: '/admin',
|
|
622
|
+
collectionsPath: '/collections',
|
|
623
|
+
});
|
|
624
|
+
const result = plugin.load('\0virtual:nebula/collections');
|
|
625
|
+
|
|
626
|
+
expect(result).toContain('"posts"');
|
|
627
|
+
expect(result).toContain('"authors"');
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it('ignores non-.schema.json files in the directory', () => {
|
|
631
|
+
const dir = resolve(tmpDir, '.astro/collections');
|
|
632
|
+
mkdirSync(dir, { recursive: true });
|
|
633
|
+
writeFileSync(resolve(dir, 'posts.schema.json'), '{}');
|
|
634
|
+
writeFileSync(resolve(dir, 'README.md'), '');
|
|
635
|
+
|
|
636
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
637
|
+
basePath: '/admin',
|
|
638
|
+
collectionsPath: '/collections',
|
|
639
|
+
});
|
|
640
|
+
const result = plugin.load('\0virtual:nebula/collections');
|
|
641
|
+
|
|
642
|
+
expect(result).toContain('"posts"');
|
|
643
|
+
expect(result).not.toContain('README');
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it('strips .schema.json suffix for collection names', () => {
|
|
647
|
+
const dir = resolve(tmpDir, '.astro/collections');
|
|
648
|
+
mkdirSync(dir, { recursive: true });
|
|
649
|
+
writeFileSync(resolve(dir, 'my-collection.schema.json'), '{}');
|
|
650
|
+
|
|
651
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
652
|
+
basePath: '/admin',
|
|
653
|
+
collectionsPath: '/collections',
|
|
654
|
+
});
|
|
655
|
+
const result = plugin.load('\0virtual:nebula/collections');
|
|
656
|
+
|
|
657
|
+
expect(result).toContain('"my-collection"');
|
|
658
|
+
expect(result).not.toContain('.schema.json":');
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it('returns empty default export and warns when directory is missing', () => {
|
|
662
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
663
|
+
basePath: '/admin',
|
|
664
|
+
collectionsPath: '/collections',
|
|
665
|
+
});
|
|
666
|
+
const result = plugin.load('\0virtual:nebula/collections');
|
|
667
|
+
|
|
668
|
+
expect(result).toBe('export default {};');
|
|
669
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
670
|
+
expect.stringContaining('.astro/collections'),
|
|
671
|
+
);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it('generates URLs with custom collectionsPath prefix', () => {
|
|
675
|
+
const dir = resolve(tmpDir, '.astro/collections');
|
|
676
|
+
mkdirSync(dir, { recursive: true });
|
|
677
|
+
writeFileSync(resolve(dir, 'posts.schema.json'), '{}');
|
|
678
|
+
|
|
679
|
+
const plugin = nebulaVitePlugin(logger, tmpDir, {
|
|
680
|
+
basePath: '/admin',
|
|
681
|
+
collectionsPath: '/schemas',
|
|
682
|
+
});
|
|
683
|
+
const result = plugin.load('\0virtual:nebula/collections');
|
|
684
|
+
|
|
685
|
+
expect(result).toContain('"posts"');
|
|
686
|
+
expect(result).toContain('"/schemas/posts.schema.json"');
|
|
687
|
+
expect(result).not.toContain('/collections/');
|
|
688
|
+
});
|
|
689
|
+
});
|