sitedrift 0.3.2 → 0.3.5
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/AGENTS.md +3 -3
- package/README.md +3 -2
- package/assets/viewer.js +68 -1
- package/package.json +1 -1
- package/src/cloudflare-runtime.mjs +3 -6
- package/src/cloudflare.mjs +1 -1
package/AGENTS.md
CHANGED
|
@@ -107,6 +107,6 @@ LIVE render on separate origins. Never expose sitedrift through a public proxy.
|
|
|
107
107
|
|
|
108
108
|
The optional Cloudflare Pages addon is intentionally public-preview safe: it is
|
|
109
109
|
installed only on non-production builds, exposes only `/__sitedrift/*`, permits
|
|
110
|
-
only `GET` and `HEAD`, allowlists one configured live origin
|
|
111
|
-
|
|
112
|
-
Functions are unchanged.
|
|
110
|
+
only `GET` and `HEAD`, and allowlists one configured live origin. Hosted frames
|
|
111
|
+
execute the compared site's scripts and must be used only with trusted preview
|
|
112
|
+
code. Production output and existing API Functions are unchanged.
|
package/README.md
CHANGED
|
@@ -97,8 +97,9 @@ when `CF_PAGES=1` and `CF_PAGES_BRANCH` is not `main`. Production builds are
|
|
|
97
97
|
left unchanged. Use `--production-branch <name>` when production is another
|
|
98
98
|
branch.
|
|
99
99
|
|
|
100
|
-
Hosted proxies are read-only (`GET`/`HEAD`)
|
|
101
|
-
|
|
100
|
+
Hosted proxies are read-only (`GET`/`HEAD`) and fixed to the configured live
|
|
101
|
+
origin. Frames run the compared site's scripts so interactive previews behave
|
|
102
|
+
like the deployment; only enable the addon for preview code you trust. Review notes stay in that
|
|
102
103
|
browser's `localStorage`; they are not sent to an API, shared with agents, or
|
|
103
104
|
written to disk. Existing application Functions keep their original routes.
|
|
104
105
|
|
package/assets/viewer.js
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
config.dev = location.origin;
|
|
5
5
|
config.frameOrigins = { dev: location.origin, live: location.origin };
|
|
6
6
|
for (const iframe of document.querySelectorAll('iframe[data-side]')) {
|
|
7
|
-
|
|
7
|
+
// Safari requires same-origin for `style-src 'self'`; scripts are
|
|
8
|
+
// required for the preview to behave like the deployed application.
|
|
9
|
+
iframe.setAttribute('sandbox', 'allow-downloads allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts');
|
|
8
10
|
}
|
|
9
11
|
}
|
|
10
12
|
const root = document.documentElement;
|
|
@@ -85,12 +87,72 @@
|
|
|
85
87
|
: config[side] + normalizeRoute(route);
|
|
86
88
|
}
|
|
87
89
|
function framePost(side, type, data = {}) {
|
|
90
|
+
if (config.hosted) {
|
|
91
|
+
const win = frame(side).contentWindow;
|
|
92
|
+
const scrolling = frame(side).contentDocument?.scrollingElement;
|
|
93
|
+
if (type === 'scroll' && scrolling) scrolling.scrollTop = Number(data.y) || 0;
|
|
94
|
+
if (type === 'reload') win?.location.reload();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
88
97
|
frame(side).contentWindow?.postMessage(
|
|
89
98
|
{ source: 'sitedrift-parent', side, type, ...data },
|
|
90
99
|
config.hosted ? '*' : config.frameOrigins[side],
|
|
91
100
|
);
|
|
92
101
|
}
|
|
93
102
|
|
|
103
|
+
function hostedSnapshot(side) {
|
|
104
|
+
if (!config.hosted) return;
|
|
105
|
+
const iframe = frame(side);
|
|
106
|
+
const doc = iframe.contentDocument;
|
|
107
|
+
const win = iframe.contentWindow;
|
|
108
|
+
if (!doc || !win) return;
|
|
109
|
+
const q = (selector) => doc.querySelector(selector);
|
|
110
|
+
const images = [...doc.querySelectorAll('img')];
|
|
111
|
+
const title = (doc.title || '').trim();
|
|
112
|
+
const description = q('meta[name="description"]')?.content?.trim() || '';
|
|
113
|
+
const canonical = q('link[rel="canonical"]')?.href || '';
|
|
114
|
+
const checks = [
|
|
115
|
+
['Title present', !!title],
|
|
116
|
+
['Title 30–60 chars', title.length >= 30 && title.length <= 60, String(title.length)],
|
|
117
|
+
['Meta description', !!description],
|
|
118
|
+
['Description 70–160', description.length >= 70 && description.length <= 160, String(description.length)],
|
|
119
|
+
['Exactly one H1', doc.querySelectorAll('h1').length === 1, `${doc.querySelectorAll('h1').length} found`],
|
|
120
|
+
['Canonical link', !!q('link[rel="canonical"]')],
|
|
121
|
+
['Viewport meta', !!q('meta[name="viewport"]')],
|
|
122
|
+
['html lang', !!doc.documentElement.lang],
|
|
123
|
+
['Open Graph title', !!q('meta[property="og:title"]')],
|
|
124
|
+
['Open Graph image', !!q('meta[property="og:image"]')],
|
|
125
|
+
['Not noindex', !(q('meta[name="robots"]')?.content || '').toLowerCase().includes('noindex')],
|
|
126
|
+
['Favicon', !!q('link[rel~="icon"]')],
|
|
127
|
+
['Images have alt', images.every((image) => image.hasAttribute('alt')), `${images.filter((image) => !image.hasAttribute('alt')).length} missing`],
|
|
128
|
+
].map(([label, ok, note]) => ({ label, ok, note }));
|
|
129
|
+
const url = new URL(iframe.src);
|
|
130
|
+
const route = (url.pathname.slice(proxyPath(side).length) || '/') + url.search + url.hash;
|
|
131
|
+
renderMetadata(side, {
|
|
132
|
+
route,
|
|
133
|
+
meta: {
|
|
134
|
+
title,
|
|
135
|
+
description,
|
|
136
|
+
canonical,
|
|
137
|
+
heading: q('h1')?.textContent?.trim() || '',
|
|
138
|
+
siteName: q('meta[property="og:site_name"]')?.content?.trim() || '',
|
|
139
|
+
icon: q('link[rel~="icon"]')?.href || '',
|
|
140
|
+
checks,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
fetchStatus(side, route);
|
|
144
|
+
const reportScroll = () => {
|
|
145
|
+
const root = doc.scrollingElement || doc.documentElement;
|
|
146
|
+
frameState[side] = {
|
|
147
|
+
y: Number(win.scrollY) || 0,
|
|
148
|
+
max: Math.max(0, root.scrollHeight - win.innerHeight),
|
|
149
|
+
};
|
|
150
|
+
syncFrom(side);
|
|
151
|
+
};
|
|
152
|
+
win.addEventListener('scroll', reportScroll, { passive: true });
|
|
153
|
+
reportScroll();
|
|
154
|
+
}
|
|
155
|
+
|
|
94
156
|
function statusBadges(side) {
|
|
95
157
|
return [
|
|
96
158
|
document.querySelector('.label[data-label="' + side + '"] .status-badge'),
|
|
@@ -503,6 +565,11 @@
|
|
|
503
565
|
runFrameKey(message.key, side, message);
|
|
504
566
|
}
|
|
505
567
|
});
|
|
568
|
+
if (config.hosted) {
|
|
569
|
+
for (const side of ['dev', 'live']) {
|
|
570
|
+
frame(side).addEventListener('load', () => hostedSnapshot(side));
|
|
571
|
+
}
|
|
572
|
+
}
|
|
506
573
|
|
|
507
574
|
scrollButton.addEventListener('click', () => {
|
|
508
575
|
syncScroll = !syncScroll;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sitedrift",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Catch the drift between dev and live — frame your local site and production side-by-side on the same route, locked scroll, with a difference-blend overlay. Zero runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -35,9 +35,9 @@ async function devResponse(context, route) {
|
|
|
35
35
|
if (context.request.method === 'GET' && accept.includes('text/html')) {
|
|
36
36
|
const clean = pathname.replace(/^\/+/, '');
|
|
37
37
|
const candidates = pathname.endsWith('/')
|
|
38
|
-
? [`/
|
|
39
|
-
: [`/
|
|
40
|
-
if (pathname === '/') candidates.unshift('/
|
|
38
|
+
? [`/__sitedrift_source/${clean}index.html.txt`]
|
|
39
|
+
: [`/__sitedrift_source/${clean}.html.txt`, `/__sitedrift_source/${clean}/index.html.txt`];
|
|
40
|
+
if (pathname === '/') candidates.unshift('/__sitedrift_source/index.html.txt');
|
|
41
41
|
for (const pathname of candidates) {
|
|
42
42
|
const response = await context.env.ASSETS.fetch(new URL(pathname, requestUrl));
|
|
43
43
|
if (response.ok) {
|
|
@@ -72,9 +72,6 @@ async function liveResponse(context, route, live) {
|
|
|
72
72
|
|
|
73
73
|
export async function onRequest(context) {
|
|
74
74
|
const requestUrl = new URL(context.request.url);
|
|
75
|
-
if (requestUrl.pathname.startsWith('/__sitedrift/source')) {
|
|
76
|
-
return new Response('Not found.', { status: 404 });
|
|
77
|
-
}
|
|
78
75
|
const match = requestUrl.pathname.match(/^\/__sitedrift\/(dev|live)(\/.*)?$/);
|
|
79
76
|
if (!match) return context.env.ASSETS.fetch(context.request);
|
|
80
77
|
if (!['GET', 'HEAD'].includes(context.request.method)) {
|
package/src/cloudflare.mjs
CHANGED
|
@@ -53,7 +53,7 @@ export function installCloudflarePreview({
|
|
|
53
53
|
|
|
54
54
|
const liveUrl = secureLive(live);
|
|
55
55
|
const internal = path.join(output, '__sitedrift');
|
|
56
|
-
const source = path.join(
|
|
56
|
+
const source = path.join(output, '__sitedrift_source');
|
|
57
57
|
const assetDir = path.join(internal, 'assets');
|
|
58
58
|
fs.mkdirSync(source, { recursive: true });
|
|
59
59
|
fs.mkdirSync(assetDir, { recursive: true });
|