@407dev/cms-astro 0.1.0 → 0.2.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 +4 -4
- package/dist/{chunk-7E3XJLKX.js → chunk-SGHAANOW.js} +30 -3
- package/dist/chunk-SGHAANOW.js.map +1 -0
- package/dist/{chunk-QYUVAKMC.js → chunk-YCQMH2T7.js} +51 -10
- package/dist/chunk-YCQMH2T7.js.map +1 -0
- package/dist/fields/collections.js +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +32 -31
- package/dist/index.js.map +1 -1
- package/dist/middleware/index.js +2 -2
- package/package.json +5 -5
- package/src/client.ts +0 -1
- package/src/fields/attrs.ts +4 -0
- package/src/fields/collection.ts +8 -0
- package/src/fields/entryScope.test.ts +22 -1
- package/src/fields/index.ts +10 -1
- package/src/index.test.ts +117 -1
- package/src/integration.ts +29 -7
- package/src/middleware/preview.test.ts +39 -7
- package/src/middleware/preview.ts +18 -4
- package/src/preview/bridge-script.test.ts +81 -2
- package/src/preview/bridge-script.ts +30 -1
- package/dist/chunk-53OQ5RXJ.js +0 -912
- package/dist/chunk-53OQ5RXJ.js.map +0 -1
- package/dist/chunk-56OMNQLH.js +0 -1013
- package/dist/chunk-56OMNQLH.js.map +0 -1
- package/dist/chunk-5AOOMQ7K.js +0 -1031
- package/dist/chunk-5AOOMQ7K.js.map +0 -1
- package/dist/chunk-5XRUCFDC.js +0 -743
- package/dist/chunk-5XRUCFDC.js.map +0 -1
- package/dist/chunk-62ZHCOLZ.js +0 -25
- package/dist/chunk-62ZHCOLZ.js.map +0 -1
- package/dist/chunk-6AZLRSYA.js +0 -126
- package/dist/chunk-6AZLRSYA.js.map +0 -1
- package/dist/chunk-6L3ZTCFB.js +0 -14
- package/dist/chunk-6L3ZTCFB.js.map +0 -1
- package/dist/chunk-6O7ZNNDT.js +0 -697
- package/dist/chunk-6O7ZNNDT.js.map +0 -1
- package/dist/chunk-6PQIXTMK.js +0 -697
- package/dist/chunk-6PQIXTMK.js.map +0 -1
- package/dist/chunk-7AW6M7QW.js +0 -1013
- package/dist/chunk-7AW6M7QW.js.map +0 -1
- package/dist/chunk-7E3XJLKX.js.map +0 -1
- package/dist/chunk-7LM54OTA.js +0 -1031
- package/dist/chunk-7LM54OTA.js.map +0 -1
- package/dist/chunk-A3OGL4OX.js +0 -706
- package/dist/chunk-A3OGL4OX.js.map +0 -1
- package/dist/chunk-AO4UAWOV.js +0 -1030
- package/dist/chunk-AO4UAWOV.js.map +0 -1
- package/dist/chunk-BDFVGXM2.js +0 -697
- package/dist/chunk-BDFVGXM2.js.map +0 -1
- package/dist/chunk-CXM7NZVX.js +0 -1031
- package/dist/chunk-CXM7NZVX.js.map +0 -1
- package/dist/chunk-DXJSYGS6.js +0 -743
- package/dist/chunk-DXJSYGS6.js.map +0 -1
- package/dist/chunk-EFUSAQZM.js +0 -72
- package/dist/chunk-EFUSAQZM.js.map +0 -1
- package/dist/chunk-F7KDGPBN.js +0 -1013
- package/dist/chunk-F7KDGPBN.js.map +0 -1
- package/dist/chunk-FBSQT45X.js +0 -126
- package/dist/chunk-FBSQT45X.js.map +0 -1
- package/dist/chunk-FEPDOMVH.js +0 -697
- package/dist/chunk-FEPDOMVH.js.map +0 -1
- package/dist/chunk-FQDOL2QW.js +0 -25
- package/dist/chunk-FQDOL2QW.js.map +0 -1
- package/dist/chunk-GB3LGQJM.js +0 -109
- package/dist/chunk-GB3LGQJM.js.map +0 -1
- package/dist/chunk-HCNP6R3P.js +0 -736
- package/dist/chunk-HCNP6R3P.js.map +0 -1
- package/dist/chunk-J6WEJDRI.js +0 -743
- package/dist/chunk-J6WEJDRI.js.map +0 -1
- package/dist/chunk-JHEAPNJ2.js +0 -117
- package/dist/chunk-JHEAPNJ2.js.map +0 -1
- package/dist/chunk-JOJDJ6GW.js +0 -25
- package/dist/chunk-JOJDJ6GW.js.map +0 -1
- package/dist/chunk-KTDV6WYB.js +0 -202
- package/dist/chunk-KTDV6WYB.js.map +0 -1
- package/dist/chunk-KZWJBKPC.js +0 -734
- package/dist/chunk-KZWJBKPC.js.map +0 -1
- package/dist/chunk-LEAQBGL6.js +0 -109
- package/dist/chunk-LEAQBGL6.js.map +0 -1
- package/dist/chunk-LGZJL2Q7.js +0 -697
- package/dist/chunk-LGZJL2Q7.js.map +0 -1
- package/dist/chunk-LQPBU46R.js +0 -109
- package/dist/chunk-LQPBU46R.js.map +0 -1
- package/dist/chunk-M55NNUTY.js +0 -1013
- package/dist/chunk-M55NNUTY.js.map +0 -1
- package/dist/chunk-M6NCYCC3.js +0 -1013
- package/dist/chunk-M6NCYCC3.js.map +0 -1
- package/dist/chunk-MGN7OZAU.js +0 -109
- package/dist/chunk-MGN7OZAU.js.map +0 -1
- package/dist/chunk-MGYZOX4K.js +0 -1013
- package/dist/chunk-MGYZOX4K.js.map +0 -1
- package/dist/chunk-MY4IEKJS.js +0 -1031
- package/dist/chunk-MY4IEKJS.js.map +0 -1
- package/dist/chunk-NCQRGAB4.js +0 -705
- package/dist/chunk-NCQRGAB4.js.map +0 -1
- package/dist/chunk-NI4NHXI4.js +0 -697
- package/dist/chunk-NI4NHXI4.js.map +0 -1
- package/dist/chunk-NPI5N7AU.js +0 -14
- package/dist/chunk-NPI5N7AU.js.map +0 -1
- package/dist/chunk-QN4CPMJF.js +0 -126
- package/dist/chunk-QN4CPMJF.js.map +0 -1
- package/dist/chunk-QYUVAKMC.js.map +0 -1
- package/dist/chunk-RAAT6PDT.js +0 -25
- package/dist/chunk-RAAT6PDT.js.map +0 -1
- package/dist/chunk-RDPO5VPS.js +0 -1013
- package/dist/chunk-RDPO5VPS.js.map +0 -1
- package/dist/chunk-RQHIW2PF.js +0 -25
- package/dist/chunk-RQHIW2PF.js.map +0 -1
- package/dist/chunk-RREVHA52.js +0 -722
- package/dist/chunk-RREVHA52.js.map +0 -1
- package/dist/chunk-S7PT4PZN.js +0 -25
- package/dist/chunk-S7PT4PZN.js.map +0 -1
- package/dist/chunk-SUXV5M32.js +0 -109
- package/dist/chunk-SUXV5M32.js.map +0 -1
- package/dist/chunk-SZEORUY6.js +0 -696
- package/dist/chunk-SZEORUY6.js.map +0 -1
- package/dist/chunk-T7NGGWGY.js +0 -25
- package/dist/chunk-T7NGGWGY.js.map +0 -1
- package/dist/chunk-UEVWP5VE.js +0 -696
- package/dist/chunk-UEVWP5VE.js.map +0 -1
- package/dist/chunk-UJNKNP7U.js +0 -748
- package/dist/chunk-UJNKNP7U.js.map +0 -1
- package/dist/chunk-V4XVFWFI.js +0 -1013
- package/dist/chunk-V4XVFWFI.js.map +0 -1
- package/dist/chunk-VJU2CIKZ.js +0 -729
- package/dist/chunk-VJU2CIKZ.js.map +0 -1
- package/dist/chunk-VRQUP5L3.js +0 -202
- package/dist/chunk-VRQUP5L3.js.map +0 -1
- package/dist/chunk-WBJ5E7CL.js +0 -742
- package/dist/chunk-WBJ5E7CL.js.map +0 -1
- package/dist/chunk-Y3Q65C7R.js +0 -14
- package/dist/chunk-Y3Q65C7R.js.map +0 -1
- package/dist/chunk-YHEGFPTR.js +0 -1030
- package/dist/chunk-YHEGFPTR.js.map +0 -1
- package/dist/chunk-ZDLBN46C.js +0 -697
- package/dist/chunk-ZDLBN46C.js.map +0 -1
- package/dist/preview-BEkLMD6N.d.ts +0 -23
- package/dist/preview-DJEA4fj7.d.ts +0 -23
- package/dist/preview-DeD1Jn7U.d.ts +0 -23
- package/dist/types-15jfCQKb.d.ts +0 -141
- package/dist/types-BREdL9Gx.d.ts +0 -140
- package/dist/types-C6sUZNa2.d.ts +0 -104
- package/dist/types-DpeHpobR.d.ts +0 -104
package/src/index.test.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
4
|
import {
|
|
3
5
|
cms,
|
|
4
6
|
cmsAstro,
|
|
@@ -12,6 +14,12 @@ import {
|
|
|
12
14
|
} from './index';
|
|
13
15
|
|
|
14
16
|
describe('cms-astro package entrypoint', () => {
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
const cmsDir = path.resolve(process.cwd(), '.cms');
|
|
19
|
+
if (fs.existsSync(cmsDir)) {
|
|
20
|
+
fs.rmSync(cmsDir, { recursive: true, force: true });
|
|
21
|
+
}
|
|
22
|
+
});
|
|
15
23
|
it('exports all expected SDK utilities and functions', () => {
|
|
16
24
|
expect(typeof createCmsClient).toBe('function');
|
|
17
25
|
expect(typeof cmsAstro).toBe('function');
|
|
@@ -33,6 +41,25 @@ describe('cms-astro package entrypoint', () => {
|
|
|
33
41
|
process.env.CMS_CONTENT_URL = 'https://content.example.com';
|
|
34
42
|
process.env.CMS_PREVIEW_SECRET = 'secret_env_456';
|
|
35
43
|
|
|
44
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
45
|
+
new Response(
|
|
46
|
+
JSON.stringify({
|
|
47
|
+
bundleVersion: 1,
|
|
48
|
+
site_id: 'site_env_123',
|
|
49
|
+
fields: {},
|
|
50
|
+
collections: {},
|
|
51
|
+
entries: {},
|
|
52
|
+
routes: [],
|
|
53
|
+
settings: {},
|
|
54
|
+
redirects: [],
|
|
55
|
+
}),
|
|
56
|
+
{
|
|
57
|
+
status: 200,
|
|
58
|
+
headers: { 'Content-Type': 'application/json' },
|
|
59
|
+
},
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
|
|
36
63
|
try {
|
|
37
64
|
const integration = cmsAstro();
|
|
38
65
|
expect(integration.name).toBe('@407dev/cms-astro');
|
|
@@ -68,9 +95,98 @@ describe('cms-astro package entrypoint', () => {
|
|
|
68
95
|
expect(define['import.meta.env.CMS_PREVIEW_SECRET']).toBeUndefined();
|
|
69
96
|
expect(warningLogged).toBe(true);
|
|
70
97
|
} finally {
|
|
98
|
+
fetchSpy.mockRestore();
|
|
71
99
|
process.env.CMS_SITE_ID = originalSiteId;
|
|
72
100
|
process.env.CMS_CONTENT_URL = originalContentUrl;
|
|
73
101
|
process.env.CMS_PREVIEW_SECRET = originalPreviewSecret;
|
|
74
102
|
}
|
|
75
103
|
});
|
|
104
|
+
|
|
105
|
+
const buildSetupArgs = () => ({
|
|
106
|
+
updateConfig: () => {},
|
|
107
|
+
command: 'build' as const,
|
|
108
|
+
injectScript: () => {},
|
|
109
|
+
addMiddleware: () => {},
|
|
110
|
+
addDevToolbarApp: () => {},
|
|
111
|
+
logger: { warn: () => {}, error: () => {}, info: () => {} },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('rethrows bundle fetch failures with URL and status on build', async () => {
|
|
115
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
116
|
+
new Response('Boom', {
|
|
117
|
+
status: 500,
|
|
118
|
+
statusText: 'Internal Server Error',
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
try {
|
|
122
|
+
const integration = cmsAstro({
|
|
123
|
+
siteId: 'site-build-fail',
|
|
124
|
+
contentUrl: 'https://content.example.com',
|
|
125
|
+
});
|
|
126
|
+
const setupHook = integration.hooks['astro:config:setup'] as (args: unknown) => Promise<void>;
|
|
127
|
+
|
|
128
|
+
await expect(setupHook(buildSetupArgs())).rejects.toThrow(
|
|
129
|
+
/Failed to fetch bundle from https:\/\/content\.example\.com\/content\/site-build-fail\/bundle.*500/,
|
|
130
|
+
);
|
|
131
|
+
} finally {
|
|
132
|
+
fetchSpy.mockRestore();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('wraps unreachable bundle fetches with the URL on build', async () => {
|
|
137
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('ECONNREFUSED'));
|
|
138
|
+
try {
|
|
139
|
+
const integration = cmsAstro({
|
|
140
|
+
siteId: 'site-build-unreachable',
|
|
141
|
+
contentUrl: 'https://content.example.com',
|
|
142
|
+
});
|
|
143
|
+
const setupHook = integration.hooks['astro:config:setup'] as (args: unknown) => Promise<void>;
|
|
144
|
+
|
|
145
|
+
await expect(setupHook(buildSetupArgs())).rejects.toThrow(
|
|
146
|
+
/Failed to fetch CMS bundle from https:\/\/content\.example\.com\/content\/site-build-unreachable\/bundle \(unreachable\).*ECONNREFUSED/,
|
|
147
|
+
);
|
|
148
|
+
} finally {
|
|
149
|
+
fetchSpy.mockRestore();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('tolerates a 404 bundle (never published) on build', async () => {
|
|
154
|
+
const fetchSpy = vi
|
|
155
|
+
.spyOn(globalThis, 'fetch')
|
|
156
|
+
.mockResolvedValue(new Response('Not Found', { status: 404, statusText: 'Not Found' }));
|
|
157
|
+
try {
|
|
158
|
+
const integration = cmsAstro({
|
|
159
|
+
siteId: 'site-never-published',
|
|
160
|
+
contentUrl: 'https://content.example.com',
|
|
161
|
+
});
|
|
162
|
+
const setupHook = integration.hooks['astro:config:setup'] as (args: unknown) => Promise<void>;
|
|
163
|
+
|
|
164
|
+
await expect(setupHook(buildSetupArgs())).resolves.toBeUndefined();
|
|
165
|
+
} finally {
|
|
166
|
+
fetchSpy.mockRestore();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('fails gracefully on bundle fetch failure in dev mode', async () => {
|
|
171
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('Network error'));
|
|
172
|
+
try {
|
|
173
|
+
const integration = cmsAstro({
|
|
174
|
+
siteId: 'site-dev-fail',
|
|
175
|
+
contentUrl: 'https://content.example.com',
|
|
176
|
+
});
|
|
177
|
+
const setupHook = integration.hooks['astro:config:setup'] as (args: unknown) => Promise<void>;
|
|
178
|
+
const fakeSetupArgs = {
|
|
179
|
+
updateConfig: () => {},
|
|
180
|
+
command: 'dev' as const,
|
|
181
|
+
injectScript: () => {},
|
|
182
|
+
addMiddleware: () => {},
|
|
183
|
+
addDevToolbarApp: () => {},
|
|
184
|
+
logger: { warn: () => {}, error: () => {}, info: () => {} },
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
await expect(setupHook(fakeSetupArgs)).resolves.not.toThrow();
|
|
188
|
+
} finally {
|
|
189
|
+
fetchSpy.mockRestore();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
76
192
|
});
|
package/src/integration.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { buildBridgeScript } from './preview/bridge-script';
|
|
|
8
8
|
import { markBundleReady } from './runtime/bundle';
|
|
9
9
|
import { clearMissingFieldWarnings, getMissingFieldWarnings } from './runtime/store';
|
|
10
10
|
import { CMS_TOOLBAR_ICON } from './toolbar/icon';
|
|
11
|
-
import type { CmsAstroOptions } from './types';
|
|
11
|
+
import type { CmsAstroOptions, ContentBundle } from './types';
|
|
12
12
|
|
|
13
13
|
function deriveRouteFromComponent(componentPath: string): string {
|
|
14
14
|
let route = componentPath
|
|
@@ -133,6 +133,7 @@ export function cmsAstro(options: CmsAstroOptions = {}): AstroIntegration {
|
|
|
133
133
|
// Fetch bundle once on setup/build and write to .cms/bundle.json
|
|
134
134
|
const activeClient = getClient();
|
|
135
135
|
if (activeClient) {
|
|
136
|
+
let bundle: ContentBundle | undefined;
|
|
136
137
|
try {
|
|
137
138
|
// Dev serves draft content; self-mint a preview token from the shared secret
|
|
138
139
|
// since the content-worker's draft endpoint still requires one (token rules unchanged).
|
|
@@ -141,22 +142,43 @@ export function cmsAstro(options: CmsAstroOptions = {}): AstroIntegration {
|
|
|
141
142
|
? await mintDevPreviewToken(activeClient.siteId, effectivePreviewSecret)
|
|
142
143
|
: null;
|
|
143
144
|
|
|
144
|
-
|
|
145
|
+
bundle = await activeClient.getBundle(
|
|
145
146
|
devPreviewToken ? { previewToken: devPreviewToken } : undefined,
|
|
146
147
|
);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
if (command === 'build') {
|
|
150
|
+
const status = (err as { status?: number })?.status;
|
|
151
|
+
// 404 = nothing published yet; the first build/preview-twin build must still succeed.
|
|
152
|
+
if (status === 404) {
|
|
153
|
+
logger.warn(
|
|
154
|
+
'CMS bundle not found (no published version yet); building without it.',
|
|
155
|
+
);
|
|
156
|
+
} else if (status !== undefined) {
|
|
157
|
+
throw err;
|
|
158
|
+
} else {
|
|
159
|
+
const url = `${activeClient.contentUrl.replace(/\/+$/, '')}/content/${activeClient.siteId}/bundle`;
|
|
160
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Failed to fetch CMS bundle from ${url} (unreachable): ${message}`,
|
|
163
|
+
{
|
|
164
|
+
cause: err,
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Fail gracefully if fetch fails in dev
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (bundle) {
|
|
147
173
|
markBundleReady(bundle);
|
|
148
174
|
|
|
149
175
|
const cmsDir = path.resolve(process.cwd(), '.cms');
|
|
150
|
-
|
|
151
|
-
fs.mkdirSync(cmsDir, { recursive: true });
|
|
152
|
-
}
|
|
176
|
+
fs.mkdirSync(cmsDir, { recursive: true });
|
|
153
177
|
fs.writeFileSync(
|
|
154
178
|
path.join(cmsDir, 'bundle.json'),
|
|
155
179
|
JSON.stringify(bundle, null, 2),
|
|
156
180
|
'utf-8',
|
|
157
181
|
);
|
|
158
|
-
} catch {
|
|
159
|
-
// Fail gracefully if fetch fails
|
|
160
182
|
}
|
|
161
183
|
}
|
|
162
184
|
},
|
|
@@ -104,29 +104,61 @@ describe('createPreviewMiddleware', () => {
|
|
|
104
104
|
expect(html).toContain('<h1>Hello</h1>');
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
-
it('returns 404 when bundle fetch returns 401/403 (e.g. wrong site or invalid token)', async () => {
|
|
107
|
+
it('returns 404 when bundle fetch returns 401/403/404 (e.g. wrong site or invalid token)', async () => {
|
|
108
|
+
for (const status of [401, 403, 404]) {
|
|
109
|
+
const failingClient: CmsClient = {
|
|
110
|
+
...mockClient,
|
|
111
|
+
async getBundle() {
|
|
112
|
+
const error = new Error(`CMS Client: Failed to fetch bundle (status ${status})`);
|
|
113
|
+
(error as unknown as { status: number }).status = status;
|
|
114
|
+
throw error;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const middleware = createPreviewMiddleware({ client: failingClient });
|
|
119
|
+
const context = {
|
|
120
|
+
request: new Request('http://localhost:4321/?preview_token=invalid-token'),
|
|
121
|
+
url: new URL('http://localhost:4321/?preview_token=invalid-token'),
|
|
122
|
+
};
|
|
123
|
+
const next = vi.fn();
|
|
124
|
+
|
|
125
|
+
const res = await middleware(context, next);
|
|
126
|
+
expect(res.status).toBe(404);
|
|
127
|
+
expect(await res.text()).toBe('Preview token is not valid for this site');
|
|
128
|
+
expect(res.headers.get('Cache-Control')).toBe('private, no-store');
|
|
129
|
+
expect(res.headers.get('X-Robots-Tag')).toBe('noindex, nofollow');
|
|
130
|
+
expect(next).not.toHaveBeenCalled();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('returns 502 and console.errors when bundle fetch fails with other errors (e.g. 500 or network error)', async () => {
|
|
135
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
136
|
+
|
|
108
137
|
const failingClient: CmsClient = {
|
|
109
138
|
...mockClient,
|
|
110
139
|
async getBundle() {
|
|
111
|
-
const error = new Error('
|
|
112
|
-
(error as unknown as { status: number }).status =
|
|
140
|
+
const error = new Error('Upstream content-worker down');
|
|
141
|
+
(error as unknown as { status: number }).status = 500;
|
|
113
142
|
throw error;
|
|
114
143
|
},
|
|
115
144
|
};
|
|
116
145
|
|
|
117
146
|
const middleware = createPreviewMiddleware({ client: failingClient });
|
|
118
147
|
const context = {
|
|
119
|
-
request: new Request('http://localhost:4321/?preview_token=
|
|
120
|
-
url: new URL('http://localhost:4321/?preview_token=
|
|
148
|
+
request: new Request('http://localhost:4321/?preview_token=valid-token'),
|
|
149
|
+
url: new URL('http://localhost:4321/?preview_token=valid-token'),
|
|
121
150
|
};
|
|
122
151
|
const next = vi.fn();
|
|
123
152
|
|
|
124
153
|
const res = await middleware(context, next);
|
|
125
|
-
expect(res.status).toBe(
|
|
126
|
-
expect(await res.text()).toBe('
|
|
154
|
+
expect(res.status).toBe(502);
|
|
155
|
+
expect(await res.text()).toBe('Failed to load preview content');
|
|
127
156
|
expect(res.headers.get('Cache-Control')).toBe('private, no-store');
|
|
128
157
|
expect(res.headers.get('X-Robots-Tag')).toBe('noindex, nofollow');
|
|
158
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
129
159
|
expect(next).not.toHaveBeenCalled();
|
|
160
|
+
|
|
161
|
+
consoleErrorSpy.mockRestore();
|
|
130
162
|
});
|
|
131
163
|
|
|
132
164
|
it('resolves singleton client via getClient() if not passed directly in options', async () => {
|
|
@@ -111,9 +111,21 @@ export function createPreviewMiddleware(options: PreviewMiddlewareOptions = {})
|
|
|
111
111
|
let draftBundle: ContentBundle;
|
|
112
112
|
try {
|
|
113
113
|
draftBundle = await client.getBundle({ previewToken });
|
|
114
|
-
} catch (
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
} catch (err) {
|
|
115
|
+
const status = (err as { status?: number })?.status;
|
|
116
|
+
if (status === 401 || status === 403 || status === 404) {
|
|
117
|
+
return new Response('Preview token is not valid for this site', {
|
|
118
|
+
status: 404,
|
|
119
|
+
headers: {
|
|
120
|
+
'Cache-Control': 'private, no-store',
|
|
121
|
+
'X-Robots-Tag': 'noindex, nofollow',
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.error(err);
|
|
127
|
+
return new Response('Failed to load preview content', {
|
|
128
|
+
status: 502,
|
|
117
129
|
headers: {
|
|
118
130
|
'Cache-Control': 'private, no-store',
|
|
119
131
|
'X-Robots-Tag': 'noindex, nofollow',
|
|
@@ -167,7 +179,9 @@ export function createPreviewMiddleware(options: PreviewMiddlewareOptions = {})
|
|
|
167
179
|
|
|
168
180
|
if (isHtml) {
|
|
169
181
|
const html = await response.text();
|
|
170
|
-
const bridgeScript = buildBridgeScript({
|
|
182
|
+
const bridgeScript = buildBridgeScript({
|
|
183
|
+
editorOrigins: options.editorOrigins,
|
|
184
|
+
});
|
|
171
185
|
const scriptTag = `<script>${bridgeScript}</script>`;
|
|
172
186
|
const modifiedHtml = html.includes('</body>')
|
|
173
187
|
? html.replace('</body>', `${scriptTag}</body>`)
|
|
@@ -27,6 +27,7 @@ describe('bridge-script protocol parity', () => {
|
|
|
27
27
|
expect(script).toContain("type: 'ready'");
|
|
28
28
|
expect(script).toContain("type: 'field-click'");
|
|
29
29
|
expect(script).toContain("type: 'group-click'");
|
|
30
|
+
expect(script).toContain("type: 'collection-click'");
|
|
30
31
|
expect(script).toContain("type: 'rects-update'");
|
|
31
32
|
expect(script).toContain("type: 'rendered-fields'");
|
|
32
33
|
|
|
@@ -204,6 +205,84 @@ describe('bridge-script protocol parity', () => {
|
|
|
204
205
|
expect(customEvent.detail.key).toBe('home.hero.heading');
|
|
205
206
|
});
|
|
206
207
|
|
|
208
|
+
it('emits collection-click with or without slug when clicking collection in edit mode', () => {
|
|
209
|
+
document.body.innerHTML = `
|
|
210
|
+
<ul id="collection-list" data-cms-collection="blog">
|
|
211
|
+
<li id="item-1" data-cms-collection="blog" data-cms-entry="post-1">
|
|
212
|
+
<span id="title-1" data-cms-entry-field="title">Post 1</span>
|
|
213
|
+
<span id="gap-1">Unmarked item text</span>
|
|
214
|
+
</li>
|
|
215
|
+
</ul>
|
|
216
|
+
`;
|
|
217
|
+
document.documentElement.setAttribute('data-cms-edit-mode', 'true');
|
|
218
|
+
|
|
219
|
+
(window as unknown as { __CMS_BRIDGE_INITIALIZED__?: boolean }).__CMS_BRIDGE_INITIALIZED__ =
|
|
220
|
+
false;
|
|
221
|
+
|
|
222
|
+
const scriptCode = buildBridgeScript({ editorOrigins: ['http://localhost:5173'] });
|
|
223
|
+
const runScript = new Function(scriptCode);
|
|
224
|
+
runScript();
|
|
225
|
+
|
|
226
|
+
const messages: unknown[] = [];
|
|
227
|
+
const mockParent = {
|
|
228
|
+
postMessage: (msg: unknown) => messages.push(msg),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
window.dispatchEvent(
|
|
232
|
+
new MessageEvent('message', {
|
|
233
|
+
data: {
|
|
234
|
+
source: 'cms-editor',
|
|
235
|
+
v: PREVIEW_BRIDGE_PROTOCOL_VERSION,
|
|
236
|
+
type: 'hello',
|
|
237
|
+
protocolVersion: PREVIEW_BRIDGE_PROTOCOL_VERSION,
|
|
238
|
+
},
|
|
239
|
+
origin: 'http://localhost:5173',
|
|
240
|
+
source: mockParent as unknown as MessageEventSource,
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// 1. Clicking on the wrapper ul (without slug) emits collection-click with slug undefined
|
|
245
|
+
const ul = document.getElementById('collection-list');
|
|
246
|
+
ul?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
247
|
+
|
|
248
|
+
const ulClickMsg = messages.find(
|
|
249
|
+
(m) => (m as { type?: string }).type === 'collection-click' && !(m as { slug?: string }).slug,
|
|
250
|
+
) as { type: string; collection: string; slug?: string; rect: unknown } | undefined;
|
|
251
|
+
expect(ulClickMsg).toBeDefined();
|
|
252
|
+
expect(ulClickMsg?.collection).toBe('blog');
|
|
253
|
+
expect(ulClickMsg?.slug).toBeUndefined();
|
|
254
|
+
|
|
255
|
+
// 2. Clicking on an item (with slug) emits collection-click with slug
|
|
256
|
+
const gap = document.getElementById('gap-1');
|
|
257
|
+
gap?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
258
|
+
|
|
259
|
+
const itemClickMsg = messages.find(
|
|
260
|
+
(m) =>
|
|
261
|
+
(m as { type?: string }).type === 'collection-click' &&
|
|
262
|
+
(m as { slug?: string }).slug === 'post-1',
|
|
263
|
+
) as { type: string; collection: string; slug?: string; rect: unknown } | undefined;
|
|
264
|
+
expect(itemClickMsg).toBeDefined();
|
|
265
|
+
expect(itemClickMsg?.collection).toBe('blog');
|
|
266
|
+
expect(itemClickMsg?.slug).toBe('post-1');
|
|
267
|
+
|
|
268
|
+
// 3. Clicking on a field inside the item emits field-click, NOT collection-click
|
|
269
|
+
const title = document.getElementById('title-1');
|
|
270
|
+
title?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
271
|
+
|
|
272
|
+
const fieldClickMsg = messages.find((m) => (m as { type?: string }).type === 'field-click') as
|
|
273
|
+
| { type: string; key: string; entryRef?: { collection: string; slug: string; path: string } }
|
|
274
|
+
| undefined;
|
|
275
|
+
expect(fieldClickMsg).toBeDefined();
|
|
276
|
+
expect(fieldClickMsg?.key).toBe('title');
|
|
277
|
+
expect(fieldClickMsg?.entryRef).toEqual({
|
|
278
|
+
collection: 'blog',
|
|
279
|
+
slug: 'post-1',
|
|
280
|
+
path: 'title',
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
document.documentElement.removeAttribute('data-cms-edit-mode');
|
|
284
|
+
});
|
|
285
|
+
|
|
207
286
|
it('skips textContent replacement when patching structured non-image object values', () => {
|
|
208
287
|
document.body.innerHTML = `
|
|
209
288
|
<div data-cms-field="home.videoFeed.videos">
|
|
@@ -326,9 +405,9 @@ describe('bridge-script protocol parity', () => {
|
|
|
326
405
|
expect(injectedStyle?.textContent).not.toContain('astro-dev-toolbar');
|
|
327
406
|
});
|
|
328
407
|
|
|
329
|
-
it('includes comments
|
|
408
|
+
it('includes comments and collection-click capabilities and handles comment-mode messages', () => {
|
|
330
409
|
const script = buildBridgeScript();
|
|
331
|
-
expect(script).toContain("capabilities: ['comments']");
|
|
410
|
+
expect(script).toContain("capabilities: ['comments', 'collection-click']");
|
|
332
411
|
expect(script).toContain("data.type === 'comment-mode'");
|
|
333
412
|
expect(script).toContain("data.type === 'watch-anchors'");
|
|
334
413
|
expect(script).toContain("data.type === 'scroll-to-anchor'");
|
|
@@ -97,6 +97,13 @@ export function buildBridgeScript(options: BridgeScriptOptions = {}): string {
|
|
|
97
97
|
'html[data-cms-edit-mode] [data-cms-group]:hover {',
|
|
98
98
|
' outline: 2px solid ${chromeTokens.success} !important;',
|
|
99
99
|
' outline-offset: 3px !important;',
|
|
100
|
+
'}',
|
|
101
|
+
'html[data-cms-edit-mode] [data-cms-collection] {',
|
|
102
|
+
' cursor: pointer !important;',
|
|
103
|
+
'}',
|
|
104
|
+
'html[data-cms-edit-mode] [data-cms-collection]:hover {',
|
|
105
|
+
' outline: 2px dashed ${chromeTokens.warning} !important;',
|
|
106
|
+
' outline-offset: 4px !important;',
|
|
100
107
|
'}'
|
|
101
108
|
].filter(Boolean).join('\\n');
|
|
102
109
|
|
|
@@ -589,6 +596,28 @@ export function buildBridgeScript(options: BridgeScriptOptions = {}): string {
|
|
|
589
596
|
rect: gRect
|
|
590
597
|
});
|
|
591
598
|
}
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
var collectionEl = target.closest('[data-cms-collection]');
|
|
603
|
+
if (collectionEl) {
|
|
604
|
+
e.preventDefault();
|
|
605
|
+
e.stopPropagation();
|
|
606
|
+
|
|
607
|
+
var collKey = collectionEl.getAttribute('data-cms-collection') || '';
|
|
608
|
+
var entryEl = target.closest('[data-cms-entry]');
|
|
609
|
+
var slug = collectionEl.getAttribute('data-cms-entry') || (entryEl ? entryEl.getAttribute('data-cms-entry') : null) || undefined;
|
|
610
|
+
var cRect = getElementRect(collectionEl);
|
|
611
|
+
|
|
612
|
+
postToEditor({
|
|
613
|
+
source: 'cms-preview',
|
|
614
|
+
v: PROTOCOL_VERSION,
|
|
615
|
+
type: 'collection-click',
|
|
616
|
+
collection: collKey,
|
|
617
|
+
slug: slug,
|
|
618
|
+
rect: cRect
|
|
619
|
+
});
|
|
620
|
+
return;
|
|
592
621
|
}
|
|
593
622
|
}, true);
|
|
594
623
|
|
|
@@ -608,7 +637,7 @@ export function buildBridgeScript(options: BridgeScriptOptions = {}): string {
|
|
|
608
637
|
v: PROTOCOL_VERSION,
|
|
609
638
|
type: 'ready',
|
|
610
639
|
protocolVersion: PROTOCOL_VERSION,
|
|
611
|
-
capabilities: ['comments']
|
|
640
|
+
capabilities: ['comments', 'collection-click']
|
|
612
641
|
});
|
|
613
642
|
|
|
614
643
|
scanRenderedFields();
|