@407dev/cms-astro 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/README.md +69 -0
- package/dist/chunk-2TKIQIOE.js +118 -0
- package/dist/chunk-2TKIQIOE.js.map +1 -0
- package/dist/chunk-53OQ5RXJ.js +912 -0
- package/dist/chunk-53OQ5RXJ.js.map +1 -0
- package/dist/chunk-56OMNQLH.js +1013 -0
- package/dist/chunk-56OMNQLH.js.map +1 -0
- package/dist/chunk-5AOOMQ7K.js +1031 -0
- package/dist/chunk-5AOOMQ7K.js.map +1 -0
- package/dist/chunk-5XRUCFDC.js +743 -0
- package/dist/chunk-5XRUCFDC.js.map +1 -0
- package/dist/chunk-62ZHCOLZ.js +25 -0
- package/dist/chunk-62ZHCOLZ.js.map +1 -0
- package/dist/chunk-6AZLRSYA.js +126 -0
- package/dist/chunk-6AZLRSYA.js.map +1 -0
- package/dist/chunk-6L3ZTCFB.js +14 -0
- package/dist/chunk-6L3ZTCFB.js.map +1 -0
- package/dist/chunk-6O7ZNNDT.js +697 -0
- package/dist/chunk-6O7ZNNDT.js.map +1 -0
- package/dist/chunk-6PQIXTMK.js +697 -0
- package/dist/chunk-6PQIXTMK.js.map +1 -0
- package/dist/chunk-7AW6M7QW.js +1013 -0
- package/dist/chunk-7AW6M7QW.js.map +1 -0
- package/dist/chunk-7E3XJLKX.js +73 -0
- package/dist/chunk-7E3XJLKX.js.map +1 -0
- package/dist/chunk-7LM54OTA.js +1031 -0
- package/dist/chunk-7LM54OTA.js.map +1 -0
- package/dist/chunk-7LMUFCWX.js +126 -0
- package/dist/chunk-7LMUFCWX.js.map +1 -0
- package/dist/chunk-A3OGL4OX.js +706 -0
- package/dist/chunk-A3OGL4OX.js.map +1 -0
- package/dist/chunk-AO4UAWOV.js +1030 -0
- package/dist/chunk-AO4UAWOV.js.map +1 -0
- package/dist/chunk-B4YYLS7Y.js +14 -0
- package/dist/chunk-B4YYLS7Y.js.map +1 -0
- package/dist/chunk-BDFVGXM2.js +697 -0
- package/dist/chunk-BDFVGXM2.js.map +1 -0
- package/dist/chunk-CXM7NZVX.js +1031 -0
- package/dist/chunk-CXM7NZVX.js.map +1 -0
- package/dist/chunk-DMLOBKYE.js +96 -0
- package/dist/chunk-DMLOBKYE.js.map +1 -0
- package/dist/chunk-DXJSYGS6.js +743 -0
- package/dist/chunk-DXJSYGS6.js.map +1 -0
- package/dist/chunk-EFUSAQZM.js +72 -0
- package/dist/chunk-EFUSAQZM.js.map +1 -0
- package/dist/chunk-F7KDGPBN.js +1013 -0
- package/dist/chunk-F7KDGPBN.js.map +1 -0
- package/dist/chunk-FBSQT45X.js +126 -0
- package/dist/chunk-FBSQT45X.js.map +1 -0
- package/dist/chunk-FEPDOMVH.js +697 -0
- package/dist/chunk-FEPDOMVH.js.map +1 -0
- package/dist/chunk-FQDOL2QW.js +25 -0
- package/dist/chunk-FQDOL2QW.js.map +1 -0
- package/dist/chunk-GB3LGQJM.js +109 -0
- package/dist/chunk-GB3LGQJM.js.map +1 -0
- package/dist/chunk-HCNP6R3P.js +736 -0
- package/dist/chunk-HCNP6R3P.js.map +1 -0
- package/dist/chunk-J6WEJDRI.js +743 -0
- package/dist/chunk-J6WEJDRI.js.map +1 -0
- package/dist/chunk-JHEAPNJ2.js +117 -0
- package/dist/chunk-JHEAPNJ2.js.map +1 -0
- package/dist/chunk-JOJDJ6GW.js +25 -0
- package/dist/chunk-JOJDJ6GW.js.map +1 -0
- package/dist/chunk-KTDV6WYB.js +202 -0
- package/dist/chunk-KTDV6WYB.js.map +1 -0
- package/dist/chunk-KZWJBKPC.js +734 -0
- package/dist/chunk-KZWJBKPC.js.map +1 -0
- package/dist/chunk-LEAQBGL6.js +109 -0
- package/dist/chunk-LEAQBGL6.js.map +1 -0
- package/dist/chunk-LGZJL2Q7.js +697 -0
- package/dist/chunk-LGZJL2Q7.js.map +1 -0
- package/dist/chunk-LQPBU46R.js +109 -0
- package/dist/chunk-LQPBU46R.js.map +1 -0
- package/dist/chunk-M55NNUTY.js +1013 -0
- package/dist/chunk-M55NNUTY.js.map +1 -0
- package/dist/chunk-M6NCYCC3.js +1013 -0
- package/dist/chunk-M6NCYCC3.js.map +1 -0
- package/dist/chunk-MGN7OZAU.js +109 -0
- package/dist/chunk-MGN7OZAU.js.map +1 -0
- package/dist/chunk-MGYZOX4K.js +1013 -0
- package/dist/chunk-MGYZOX4K.js.map +1 -0
- package/dist/chunk-MY4IEKJS.js +1031 -0
- package/dist/chunk-MY4IEKJS.js.map +1 -0
- package/dist/chunk-NCQRGAB4.js +705 -0
- package/dist/chunk-NCQRGAB4.js.map +1 -0
- package/dist/chunk-NI4NHXI4.js +697 -0
- package/dist/chunk-NI4NHXI4.js.map +1 -0
- package/dist/chunk-NPI5N7AU.js +14 -0
- package/dist/chunk-NPI5N7AU.js.map +1 -0
- package/dist/chunk-QN4CPMJF.js +126 -0
- package/dist/chunk-QN4CPMJF.js.map +1 -0
- package/dist/chunk-QYUVAKMC.js +1013 -0
- package/dist/chunk-QYUVAKMC.js.map +1 -0
- package/dist/chunk-RAAT6PDT.js +25 -0
- package/dist/chunk-RAAT6PDT.js.map +1 -0
- package/dist/chunk-RDPO5VPS.js +1013 -0
- package/dist/chunk-RDPO5VPS.js.map +1 -0
- package/dist/chunk-RQHIW2PF.js +25 -0
- package/dist/chunk-RQHIW2PF.js.map +1 -0
- package/dist/chunk-RREVHA52.js +722 -0
- package/dist/chunk-RREVHA52.js.map +1 -0
- package/dist/chunk-S7PT4PZN.js +25 -0
- package/dist/chunk-S7PT4PZN.js.map +1 -0
- package/dist/chunk-SUXV5M32.js +109 -0
- package/dist/chunk-SUXV5M32.js.map +1 -0
- package/dist/chunk-SZEORUY6.js +696 -0
- package/dist/chunk-SZEORUY6.js.map +1 -0
- package/dist/chunk-T7NGGWGY.js +25 -0
- package/dist/chunk-T7NGGWGY.js.map +1 -0
- package/dist/chunk-UEVWP5VE.js +696 -0
- package/dist/chunk-UEVWP5VE.js.map +1 -0
- package/dist/chunk-UJNKNP7U.js +748 -0
- package/dist/chunk-UJNKNP7U.js.map +1 -0
- package/dist/chunk-V4XVFWFI.js +1013 -0
- package/dist/chunk-V4XVFWFI.js.map +1 -0
- package/dist/chunk-VJU2CIKZ.js +729 -0
- package/dist/chunk-VJU2CIKZ.js.map +1 -0
- package/dist/chunk-VRQUP5L3.js +202 -0
- package/dist/chunk-VRQUP5L3.js.map +1 -0
- package/dist/chunk-WBJ5E7CL.js +742 -0
- package/dist/chunk-WBJ5E7CL.js.map +1 -0
- package/dist/chunk-XJPVYMZM.js +24 -0
- package/dist/chunk-XJPVYMZM.js.map +1 -0
- package/dist/chunk-Y3Q65C7R.js +14 -0
- package/dist/chunk-Y3Q65C7R.js.map +1 -0
- package/dist/chunk-YHEGFPTR.js +1030 -0
- package/dist/chunk-YHEGFPTR.js.map +1 -0
- package/dist/chunk-ZDLBN46C.js +697 -0
- package/dist/chunk-ZDLBN46C.js.map +1 -0
- package/dist/dev/index.d.ts +122 -0
- package/dist/dev/index.js +662 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/fields/collections.d.ts +18 -0
- package/dist/fields/collections.js +37 -0
- package/dist/fields/collections.js.map +1 -0
- package/dist/index.d.ts +235 -0
- package/dist/index.js +956 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/index.d.ts +8 -0
- package/dist/middleware/index.js +13 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/preview-BEkLMD6N.d.ts +23 -0
- package/dist/preview-D9yewK8x.d.ts +24 -0
- package/dist/preview-DJEA4fj7.d.ts +23 -0
- package/dist/preview-DeD1Jn7U.d.ts +23 -0
- package/dist/toolbar/app.d.ts +28 -0
- package/dist/toolbar/app.js +1176 -0
- package/dist/toolbar/app.js.map +1 -0
- package/dist/types-15jfCQKb.d.ts +141 -0
- package/dist/types-BREdL9Gx.d.ts +140 -0
- package/dist/types-C6sUZNa2.d.ts +104 -0
- package/dist/types-CEYS5Fzc.d.ts +112 -0
- package/dist/types-DpeHpobR.d.ts +104 -0
- package/dist/types-NaKJe6Pg.d.ts +140 -0
- package/package.json +90 -0
- package/src/CmsHead.astro +25 -0
- package/src/CmsImage.astro +62 -0
- package/src/CmsVideo.astro +185 -0
- package/src/RichText.astro +32 -0
- package/src/client.test.ts +227 -0
- package/src/client.ts +164 -0
- package/src/dev/config.test.ts +258 -0
- package/src/dev/config.ts +319 -0
- package/src/dev/index.ts +5 -0
- package/src/dev/preview-token.ts +39 -0
- package/src/dev/routes.test.ts +354 -0
- package/src/dev/routes.ts +245 -0
- package/src/dev/token.test.ts +280 -0
- package/src/dev/token.ts +201 -0
- package/src/dev/verifier.test.ts +63 -0
- package/src/dev/verifier.ts +161 -0
- package/src/fields/attrs.ts +24 -0
- package/src/fields/collection.ts +72 -0
- package/src/fields/collections.ts +35 -0
- package/src/fields/entryScope.test.ts +62 -0
- package/src/fields/entryScope.ts +240 -0
- package/src/fields/group.ts +51 -0
- package/src/fields/helpers.test.ts +51 -0
- package/src/fields/helpers.ts +214 -0
- package/src/fields/index.ts +46 -0
- package/src/fields/scope.ts +21 -0
- package/src/fields/types.ts +119 -0
- package/src/head.test.ts +74 -0
- package/src/head.ts +123 -0
- package/src/image.test.ts +42 -0
- package/src/image.ts +64 -0
- package/src/index.test.ts +76 -0
- package/src/index.ts +13 -0
- package/src/integration.ts +218 -0
- package/src/jwt.ts +131 -0
- package/src/middleware/index.ts +4 -0
- package/src/middleware/preview.test.ts +184 -0
- package/src/middleware/preview.ts +206 -0
- package/src/preview/bridge-script.test.ts +683 -0
- package/src/preview/bridge-script.ts +763 -0
- package/src/preview/origins.ts +29 -0
- package/src/preview.ts +49 -0
- package/src/richtext/RichTextNode.astro +130 -0
- package/src/richtext/render.ts +248 -0
- package/src/richtext.test.ts +139 -0
- package/src/runtime/bundle.ts +28 -0
- package/src/runtime/store.ts +130 -0
- package/src/siteFiles.test.ts +93 -0
- package/src/siteFiles.ts +111 -0
- package/src/toolbar/app.test.ts +191 -0
- package/src/toolbar/app.ts +1091 -0
- package/src/toolbar/icon.ts +5 -0
- package/src/toolbar/widgets.test.ts +153 -0
- package/src/toolbar/widgets.ts +365 -0
- package/src/types.test.ts +154 -0
- package/src/types.ts +163 -0
- package/src/version.ts +3 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import type { ContentBundle } from '../types';
|
|
5
|
+
|
|
6
|
+
const globalWithCms = globalThis as typeof globalThis & {
|
|
7
|
+
__CMS_DRAFT_STORAGE__?: AsyncLocalStorage<ContentBundle>;
|
|
8
|
+
__CMS_ACTIVE_DRAFT_BUNDLE__?: ContentBundle | null;
|
|
9
|
+
__CMS_BUILD_BUNDLE__?: ContentBundle | null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const draftStorage: AsyncLocalStorage<ContentBundle> =
|
|
13
|
+
(typeof globalThis !== 'undefined' && globalWithCms.__CMS_DRAFT_STORAGE__) ||
|
|
14
|
+
(() => {
|
|
15
|
+
const storage = new AsyncLocalStorage<ContentBundle>();
|
|
16
|
+
if (typeof globalThis !== 'undefined') {
|
|
17
|
+
globalWithCms.__CMS_DRAFT_STORAGE__ = storage;
|
|
18
|
+
}
|
|
19
|
+
return storage;
|
|
20
|
+
})();
|
|
21
|
+
|
|
22
|
+
let buildBundle: ContentBundle | null = null;
|
|
23
|
+
let activeDraftBundle: ContentBundle | null = null;
|
|
24
|
+
|
|
25
|
+
export function setActiveDraftBundle(bundle: ContentBundle | null): void {
|
|
26
|
+
activeDraftBundle = bundle;
|
|
27
|
+
if (typeof globalThis !== 'undefined') {
|
|
28
|
+
globalWithCms.__CMS_ACTIVE_DRAFT_BUNDLE__ = bundle;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function setBuildBundle(bundle: ContentBundle | null): void {
|
|
33
|
+
buildBundle = bundle;
|
|
34
|
+
if (typeof globalThis !== 'undefined') {
|
|
35
|
+
globalWithCms.__CMS_BUILD_BUNDLE__ = bundle;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getBuildBundle(): ContentBundle | null {
|
|
40
|
+
return (
|
|
41
|
+
buildBundle || (typeof globalThis !== 'undefined' && globalWithCms.__CMS_BUILD_BUNDLE__) || null
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getActiveBundle(): ContentBundle | null {
|
|
46
|
+
const draft = draftStorage.getStore();
|
|
47
|
+
if (draft) return draft;
|
|
48
|
+
|
|
49
|
+
const fallbackDraft =
|
|
50
|
+
activeDraftBundle ||
|
|
51
|
+
(typeof globalThis !== 'undefined' && globalWithCms.__CMS_ACTIVE_DRAFT_BUNDLE__) ||
|
|
52
|
+
null;
|
|
53
|
+
if (fallbackDraft) return fallbackDraft;
|
|
54
|
+
|
|
55
|
+
const existingBuild =
|
|
56
|
+
buildBundle ||
|
|
57
|
+
(typeof globalThis !== 'undefined' && globalWithCms.__CMS_BUILD_BUNDLE__) ||
|
|
58
|
+
null;
|
|
59
|
+
if (existingBuild) return existingBuild;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const cmsDir = path.resolve(process.cwd(), '.cms');
|
|
63
|
+
const bundleFile = path.join(cmsDir, 'bundle.json');
|
|
64
|
+
if (fs.existsSync(bundleFile)) {
|
|
65
|
+
const parsed = JSON.parse(fs.readFileSync(bundleFile, 'utf-8'));
|
|
66
|
+
setBuildBundle(parsed);
|
|
67
|
+
return parsed;
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Ignore if file read fails
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function isDraftMode(): boolean {
|
|
77
|
+
return (
|
|
78
|
+
draftStorage.getStore() !== undefined ||
|
|
79
|
+
activeDraftBundle !== null ||
|
|
80
|
+
Boolean(typeof globalThis !== 'undefined' && globalWithCms.__CMS_ACTIVE_DRAFT_BUNDLE__)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function runWithDraftBundle<T>(bundle: ContentBundle, fn: () => T): T {
|
|
85
|
+
return draftStorage.run(bundle, fn);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const missingFieldWarnings = new Set<string>();
|
|
89
|
+
|
|
90
|
+
export function recordMissingField(key: string): void {
|
|
91
|
+
missingFieldWarnings.add(key);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getMissingFieldWarnings(): string[] {
|
|
95
|
+
return Array.from(missingFieldWarnings);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function clearMissingFieldWarnings(): void {
|
|
99
|
+
missingFieldWarnings.clear();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function updateBundleFieldValue(key: string, value: unknown): void {
|
|
103
|
+
const existing = getActiveBundle();
|
|
104
|
+
if (existing) {
|
|
105
|
+
if (!existing.fields) existing.fields = {};
|
|
106
|
+
existing.fields[key] = value;
|
|
107
|
+
setBuildBundle(existing);
|
|
108
|
+
setActiveDraftBundle(existing);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const cmsDir = path.resolve(process.cwd(), '.cms');
|
|
113
|
+
const bundleFile = path.join(cmsDir, 'bundle.json');
|
|
114
|
+
if (fs.existsSync(bundleFile)) {
|
|
115
|
+
const diskBundle = JSON.parse(fs.readFileSync(bundleFile, 'utf-8'));
|
|
116
|
+
if (!diskBundle.fields) diskBundle.fields = {};
|
|
117
|
+
diskBundle.fields[key] = value;
|
|
118
|
+
fs.writeFileSync(bundleFile, JSON.stringify(diskBundle, null, 2), 'utf-8');
|
|
119
|
+
if (!existing) {
|
|
120
|
+
setBuildBundle(diskBundle);
|
|
121
|
+
setActiveDraftBundle(diskBundle);
|
|
122
|
+
}
|
|
123
|
+
} else if (existing) {
|
|
124
|
+
if (!fs.existsSync(cmsDir)) fs.mkdirSync(cmsDir, { recursive: true });
|
|
125
|
+
fs.writeFileSync(bundleFile, JSON.stringify(existing, null, 2), 'utf-8');
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// Ignore file write errors
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
type SitemapRouteItem,
|
|
4
|
+
renderRedirectsFile,
|
|
5
|
+
renderRobotsTxt,
|
|
6
|
+
renderSitemapXml,
|
|
7
|
+
} from './siteFiles';
|
|
8
|
+
import type { CmsRedirect, CmsSiteConfig } from './types';
|
|
9
|
+
|
|
10
|
+
describe('siteFiles generators', () => {
|
|
11
|
+
it('renders _redirects file correctly', () => {
|
|
12
|
+
const redirects: CmsRedirect[] = [
|
|
13
|
+
{ from_path: '/old', to_path: '/new', status_code: 301, enabled: true },
|
|
14
|
+
{ from_path: '/temp', to_path: '/target', status_code: 302, enabled: true },
|
|
15
|
+
{ from_path: '/disabled', to_path: '/never', status_code: 301, enabled: false },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const result = renderRedirectsFile(redirects);
|
|
19
|
+
expect(result).toBe('/old /new 301\n/temp /target 302\n');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('renders robots.txt file with sitemap directive', () => {
|
|
23
|
+
const config: CmsSiteConfig = {
|
|
24
|
+
site_id: 'site-1',
|
|
25
|
+
redirects: [],
|
|
26
|
+
robots: {
|
|
27
|
+
rules: [
|
|
28
|
+
{ user_agent: '*', allow: ['/'], disallow: ['/admin', '/private'] },
|
|
29
|
+
{ user_agent: 'Googlebot', allow: ['/'] },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
default_seo: {},
|
|
33
|
+
sitemap: {},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const result = renderRobotsTxt(config, { siteUrl: 'https://example.com' });
|
|
37
|
+
expect(result).toContain('User-agent: *');
|
|
38
|
+
expect(result).toContain('Allow: /');
|
|
39
|
+
expect(result).toContain('Disallow: /admin');
|
|
40
|
+
expect(result).toContain('Disallow: /private');
|
|
41
|
+
expect(result).toContain('User-agent: Googlebot');
|
|
42
|
+
expect(result).toContain('Sitemap: https://example.com/sitemap.xml');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('renders sitemap.xml excluding 404 and noindex routes', () => {
|
|
46
|
+
const routes: SitemapRouteItem[] = [
|
|
47
|
+
{
|
|
48
|
+
id: 'r1',
|
|
49
|
+
path: '/',
|
|
50
|
+
title: 'Home',
|
|
51
|
+
published_at: '2026-08-20T12:00:00Z',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'r2',
|
|
55
|
+
path: '/about',
|
|
56
|
+
title: 'About',
|
|
57
|
+
published_at: '2026-08-21T12:00:00Z',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'r3',
|
|
61
|
+
path: '/404',
|
|
62
|
+
title: 'Not Found',
|
|
63
|
+
published_at: '2026-08-21T12:00:00Z',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'r4',
|
|
67
|
+
path: '/secret',
|
|
68
|
+
title: 'Secret Page',
|
|
69
|
+
seo: { noindex: true },
|
|
70
|
+
published_at: '2026-08-21T12:00:00Z',
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const config: CmsSiteConfig = {
|
|
75
|
+
site_id: 'site-1',
|
|
76
|
+
redirects: [],
|
|
77
|
+
robots: {},
|
|
78
|
+
default_seo: {},
|
|
79
|
+
sitemap: {},
|
|
80
|
+
not_found_path: '/404',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const result = renderSitemapXml(routes, {
|
|
84
|
+
siteUrl: 'https://example.com',
|
|
85
|
+
siteConfig: config,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(result).toContain('<loc>https://example.com/</loc>');
|
|
89
|
+
expect(result).toContain('<loc>https://example.com/about</loc>');
|
|
90
|
+
expect(result).not.toContain('<loc>https://example.com/404</loc>');
|
|
91
|
+
expect(result).not.toContain('<loc>https://example.com/secret</loc>');
|
|
92
|
+
});
|
|
93
|
+
});
|
package/src/siteFiles.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { CmsRedirect, CmsSiteConfig } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renders Cloudflare Pages _redirects file content from redirect definitions.
|
|
5
|
+
*/
|
|
6
|
+
export function renderRedirectsFile(redirects: CmsRedirect[] = []): string {
|
|
7
|
+
const lines = redirects
|
|
8
|
+
.filter((r) => r.enabled !== false && r.from_path && r.to_path)
|
|
9
|
+
.map((r) => `${r.from_path} ${r.to_path} ${r.status_code || 301}`);
|
|
10
|
+
|
|
11
|
+
return lines.length > 0 ? `${lines.join('\n')}\n` : '';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RenderRobotsOptions {
|
|
15
|
+
siteUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Renders robots.txt content from site settings.
|
|
20
|
+
*/
|
|
21
|
+
export function renderRobotsTxt(
|
|
22
|
+
config?: CmsSiteConfig | null,
|
|
23
|
+
options?: RenderRobotsOptions,
|
|
24
|
+
): string {
|
|
25
|
+
if (config?.robots?.raw) {
|
|
26
|
+
return `${config.robots.raw.trim()}\n`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const lines: string[] = [];
|
|
30
|
+
const rules = config?.robots?.rules || [{ user_agent: '*', allow: ['/'] }];
|
|
31
|
+
|
|
32
|
+
for (const rule of rules) {
|
|
33
|
+
lines.push(`User-agent: ${rule.user_agent || '*'}`);
|
|
34
|
+
if (rule.allow && rule.allow.length > 0) {
|
|
35
|
+
for (const path of rule.allow) {
|
|
36
|
+
lines.push(`Allow: ${path}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (rule.disallow && rule.disallow.length > 0) {
|
|
40
|
+
for (const path of rule.disallow) {
|
|
41
|
+
lines.push(`Disallow: ${path}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
lines.push('');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const siteUrl = options?.siteUrl ? options.siteUrl.replace(/\/+$/, '') : '';
|
|
48
|
+
if (siteUrl && config?.robots?.sitemap !== false) {
|
|
49
|
+
lines.push(`Sitemap: ${siteUrl}/sitemap.xml`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return `${lines.join('\n').trim()}\n`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RenderSitemapOptions {
|
|
56
|
+
siteUrl: string;
|
|
57
|
+
siteConfig?: CmsSiteConfig | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SitemapRouteItem {
|
|
61
|
+
id?: string;
|
|
62
|
+
path?: string;
|
|
63
|
+
pattern?: string;
|
|
64
|
+
published_at?: string;
|
|
65
|
+
updated_at?: string;
|
|
66
|
+
seo?: Record<string, unknown>;
|
|
67
|
+
[key: string]: unknown;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Renders sitemap.xml content from routes and site config.
|
|
72
|
+
* Excludes noindex routes and the 404 route.
|
|
73
|
+
*/
|
|
74
|
+
export function renderSitemapXml(
|
|
75
|
+
routes: SitemapRouteItem[],
|
|
76
|
+
options: RenderSitemapOptions,
|
|
77
|
+
): string {
|
|
78
|
+
const siteUrl = options.siteUrl.replace(/\/+$/, '');
|
|
79
|
+
const config = options.siteConfig;
|
|
80
|
+
const excludes = new Set(config?.sitemap?.exclude || []);
|
|
81
|
+
|
|
82
|
+
if (config?.not_found_path) {
|
|
83
|
+
excludes.add(config.not_found_path);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const filtered = routes.filter((r) => {
|
|
87
|
+
if ((r.path && excludes.has(r.path)) || (r.id && excludes.has(r.id))) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if ('seo' in r && r.seo && typeof r.seo === 'object') {
|
|
91
|
+
const seoObj = r.seo as { noindex?: boolean };
|
|
92
|
+
if (seoObj.noindex) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const urlEntries = filtered.map((r) => {
|
|
100
|
+
const rawPath = r.path || '/';
|
|
101
|
+
const normPath = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
|
|
102
|
+
const loc = `${siteUrl}${normPath}`;
|
|
103
|
+
const dateStr =
|
|
104
|
+
('published_at' in r && r.published_at) || r.updated_at || new Date().toISOString();
|
|
105
|
+
const lastmod = dateStr.split('T')[0];
|
|
106
|
+
|
|
107
|
+
return ` <url>\n <loc>${loc}</loc>\n <lastmod>${lastmod}</lastmod>\n </url>`;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urlEntries.join('\n')}\n</urlset>\n`;
|
|
111
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import toolbarApp from './app';
|
|
4
|
+
|
|
5
|
+
type InitParams = Parameters<NonNullable<typeof toolbarApp.init>>;
|
|
6
|
+
type ToolbarAppType = InitParams[1];
|
|
7
|
+
type ToolbarServerType = InitParams[2];
|
|
8
|
+
|
|
9
|
+
interface WindowWithCmsFields {
|
|
10
|
+
__CMS_FIELDS__?: Array<{
|
|
11
|
+
key: string;
|
|
12
|
+
kind?: string;
|
|
13
|
+
group?: string;
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('Astro Dev Toolbar App', () => {
|
|
18
|
+
let canvas: HTMLDivElement;
|
|
19
|
+
let mockApp: ToolbarAppType;
|
|
20
|
+
let mockServer: ToolbarServerType;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
document.body.innerHTML = '';
|
|
24
|
+
canvas = document.createElement('div');
|
|
25
|
+
document.body.appendChild(canvas);
|
|
26
|
+
|
|
27
|
+
mockApp = {
|
|
28
|
+
toggleNotification: vi.fn(),
|
|
29
|
+
} as unknown as ToolbarAppType;
|
|
30
|
+
|
|
31
|
+
mockServer = {
|
|
32
|
+
on: vi.fn(),
|
|
33
|
+
} as unknown as ToolbarServerType;
|
|
34
|
+
|
|
35
|
+
vi.stubGlobal(
|
|
36
|
+
'fetch',
|
|
37
|
+
vi.fn().mockImplementation((url: string) => {
|
|
38
|
+
if (url.includes('/_cms/dev/status')) {
|
|
39
|
+
return Promise.resolve({
|
|
40
|
+
ok: true,
|
|
41
|
+
status: 200,
|
|
42
|
+
json: async () => ({
|
|
43
|
+
status: 'ok',
|
|
44
|
+
siteId: '00000000-0000-0000-0000-000000000002',
|
|
45
|
+
apiUrl: 'http://localhost:8787',
|
|
46
|
+
manifest: { fresh: true, localHash: 'abc', pushedHash: 'abc' },
|
|
47
|
+
verifier: { available: true, errors: [], warnings: [] },
|
|
48
|
+
editorOrigins: ['http://localhost:5173'],
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (url.includes('/_cms/dev/bundle')) {
|
|
53
|
+
return Promise.resolve({
|
|
54
|
+
ok: true,
|
|
55
|
+
status: 200,
|
|
56
|
+
json: async () => ({
|
|
57
|
+
bundle: {
|
|
58
|
+
fields: {
|
|
59
|
+
'home.hero.heading': 'Hero Heading',
|
|
60
|
+
'home.hero.subheading': 'Hero Subheading',
|
|
61
|
+
'home.hero.image': 'hero.png',
|
|
62
|
+
'contact.heading': 'Contact Us',
|
|
63
|
+
'contact.intro': 'Send us a message',
|
|
64
|
+
'contact.emailLabel': 'Email Address',
|
|
65
|
+
'contact.messageLabel': 'Your Message',
|
|
66
|
+
'contact.submitLabel': 'Submit',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
fields: [
|
|
70
|
+
{ key: 'home.hero.heading', type: 'text' },
|
|
71
|
+
{ key: 'home.hero.subheading', type: 'text' },
|
|
72
|
+
{ key: 'home.hero.image', type: 'image' },
|
|
73
|
+
{ key: 'contact.heading', type: 'text' },
|
|
74
|
+
{ key: 'contact.intro', type: 'text' },
|
|
75
|
+
{ key: 'contact.emailLabel', type: 'text' },
|
|
76
|
+
{ key: 'contact.messageLabel', type: 'text' },
|
|
77
|
+
{ key: 'contact.submitLabel', type: 'text' },
|
|
78
|
+
],
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return Promise.reject(new Error(`Unknown url: ${url}`));
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('injects toolbar styles and mounts window and container with proper scrolling styles', () => {
|
|
88
|
+
toolbarApp.init?.(canvas as unknown as ShadowRoot, mockApp, mockServer);
|
|
89
|
+
|
|
90
|
+
const style = canvas.querySelector('style');
|
|
91
|
+
expect(style).not.toBeNull();
|
|
92
|
+
const css = style?.textContent || '';
|
|
93
|
+
|
|
94
|
+
// Verify container scroll rules
|
|
95
|
+
expect(css).toContain('overflow-y: auto');
|
|
96
|
+
expect(css).toContain('overflow-x: hidden');
|
|
97
|
+
expect(css).toContain('overscroll-behavior: contain');
|
|
98
|
+
expect(css).toContain('scrollbar-gutter: stable');
|
|
99
|
+
expect(css).toContain('flex: 1 1 auto');
|
|
100
|
+
expect(css).toContain('min-height: 0');
|
|
101
|
+
|
|
102
|
+
// Verify children do not shrink and cut off
|
|
103
|
+
expect(css).toContain('.toolbar-container > * {\n flex-shrink: 0;\n }');
|
|
104
|
+
expect(css).toContain('flex-shrink: 0');
|
|
105
|
+
|
|
106
|
+
// Verify window element and container are created
|
|
107
|
+
const windowEl = canvas.querySelector('astro-dev-toolbar-window');
|
|
108
|
+
expect(windowEl).not.toBeNull();
|
|
109
|
+
const container = windowEl?.querySelector('.toolbar-container') as HTMLElement;
|
|
110
|
+
expect(container).not.toBeNull();
|
|
111
|
+
expect(container.style.overflowY).toBe('auto');
|
|
112
|
+
expect(container.style.minHeight).toBe('0');
|
|
113
|
+
expect(container.style.flex).toBe('1 1 auto');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('renders all fields across multiple groups without cutting any off', async () => {
|
|
117
|
+
(window as unknown as WindowWithCmsFields).__CMS_FIELDS__ = [
|
|
118
|
+
{ key: 'home.hero.heading', kind: 'text', group: 'Hero' },
|
|
119
|
+
{ key: 'home.hero.subheading', kind: 'text', group: 'Hero' },
|
|
120
|
+
{ key: 'home.hero.image', kind: 'image', group: 'Hero' },
|
|
121
|
+
{ key: 'contact.heading', kind: 'text', group: 'ContactForm' },
|
|
122
|
+
{ key: 'contact.intro', kind: 'text', group: 'ContactForm' },
|
|
123
|
+
{ key: 'contact.emailLabel', kind: 'text', group: 'ContactForm' },
|
|
124
|
+
{ key: 'contact.messageLabel', kind: 'text', group: 'ContactForm' },
|
|
125
|
+
{ key: 'contact.submitLabel', kind: 'text', group: 'ContactForm' },
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
toolbarApp.init?.(canvas as unknown as ShadowRoot, mockApp, mockServer);
|
|
129
|
+
|
|
130
|
+
// Wait for fetch promises to resolve
|
|
131
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
132
|
+
|
|
133
|
+
const container = canvas.querySelector('.toolbar-container') as HTMLElement;
|
|
134
|
+
const groupCards = container.querySelectorAll('.group-card');
|
|
135
|
+
// Hero and ContactForm (all bundle fields matched, so no orphan card)
|
|
136
|
+
expect(groupCards.length).toBe(2);
|
|
137
|
+
|
|
138
|
+
// Group 1: Hero (3 fields)
|
|
139
|
+
const heroCard = groupCards[0];
|
|
140
|
+
const heroHeader = heroCard.querySelector('.group-header');
|
|
141
|
+
expect(heroHeader?.textContent).toBe('Hero');
|
|
142
|
+
const heroRows = heroCard.querySelectorAll('.field-row');
|
|
143
|
+
expect(heroRows.length).toBe(3);
|
|
144
|
+
expect(heroRows[0].getAttribute('data-field-key')).toBe('home.hero.heading');
|
|
145
|
+
expect(heroRows[1].getAttribute('data-field-key')).toBe('home.hero.subheading');
|
|
146
|
+
expect(heroRows[2].getAttribute('data-field-key')).toBe('home.hero.image');
|
|
147
|
+
|
|
148
|
+
// Group 2: ContactForm (5 fields - none cut off)
|
|
149
|
+
const contactCard = groupCards[1];
|
|
150
|
+
const contactHeader = contactCard.querySelector('.group-header');
|
|
151
|
+
expect(contactHeader?.textContent).toBe('ContactForm');
|
|
152
|
+
const contactRows = contactCard.querySelectorAll('.field-row');
|
|
153
|
+
expect(contactRows.length).toBe(5);
|
|
154
|
+
expect(contactRows[0].getAttribute('data-field-key')).toBe('contact.heading');
|
|
155
|
+
expect(contactRows[1].getAttribute('data-field-key')).toBe('contact.intro');
|
|
156
|
+
expect(contactRows[2].getAttribute('data-field-key')).toBe('contact.emailLabel');
|
|
157
|
+
expect(contactRows[3].getAttribute('data-field-key')).toBe('contact.messageLabel');
|
|
158
|
+
expect(contactRows[4].getAttribute('data-field-key')).toBe('contact.submitLabel');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('updates dynamically on cms:fields-updated event with all fields in group preserved', async () => {
|
|
162
|
+
(window as unknown as WindowWithCmsFields).__CMS_FIELDS__ = [];
|
|
163
|
+
|
|
164
|
+
toolbarApp.init?.(canvas as unknown as ShadowRoot, mockApp, mockServer);
|
|
165
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
166
|
+
|
|
167
|
+
const container = canvas.querySelector('.toolbar-container') as HTMLElement;
|
|
168
|
+
expect(container.textContent).toContain('No CMS fields detected');
|
|
169
|
+
|
|
170
|
+
// Simulate bridge-script scanning 4 fields in group
|
|
171
|
+
document.dispatchEvent(
|
|
172
|
+
new CustomEvent('cms:fields-updated', {
|
|
173
|
+
detail: [
|
|
174
|
+
{ key: 'section.f1', kind: 'text', group: 'Section' },
|
|
175
|
+
{ key: 'section.f2', kind: 'text', group: 'Section' },
|
|
176
|
+
{ key: 'section.f3', kind: 'text', group: 'Section' },
|
|
177
|
+
{ key: 'section.f4', kind: 'text', group: 'Section' },
|
|
178
|
+
],
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const groupCards = container.querySelectorAll('.group-card');
|
|
183
|
+
// First card is the Section rendered group; second card is the unrendered/orphaned bundle fields
|
|
184
|
+
expect(groupCards.length).toBe(2);
|
|
185
|
+
const sectionCard = groupCards[0];
|
|
186
|
+
const header = sectionCard.querySelector('.group-header');
|
|
187
|
+
expect(header?.textContent).toBe('Section');
|
|
188
|
+
const rows = sectionCard.querySelectorAll('.field-row');
|
|
189
|
+
expect(rows.length).toBe(4);
|
|
190
|
+
});
|
|
191
|
+
});
|