Loader (cms-client.js), submodule-aware editor source path, build:watch
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2e0bf1e88e
commit
cb7a7a1fab
8 files changed
+158
-7
No files matched your search
@@ -2,3 +2,4 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
.cms-editor/
|
||||||
@@ -146,9 +146,18 @@ schema contract: [docs/styling.md](docs/styling.md).
|
|||||||
|
|
||||||
## Deploying
|
## Deploying
|
||||||
|
|
||||||
Serve `dist/onpage-editor.js` at `/editor/onpage-editor.js` and let the existing loader do
|
Part of the ecommerce monorepo as the `cms-onpageeditor` submodule. Each instance image (`tornberg`, `bygglagret`,
|
||||||
`if (document.cookie.includes('cms_preview=1')) import('/editor/onpage-editor.js')`. The bundle
|
`k6n`) and `Dockerfile.nginx` build it in an `onpage` stage (named context `onpageeditor`) and ship two files in `/editor/`:
|
||||||
re-checks the cookie, so loading it for a visitor is a no-op.
|
|
||||||
|
- `cms-client.js` - the tiny loader (`src/loader-core.ts`, ~0.5 kB gz) that page templates already reference. It replaces the old
|
||||||
|
hand-written `cms-client` in every instance. With the `cms_preview=1` cookie it imports `onpage-editor.js` from the same directory;
|
||||||
|
otherwise it does nothing.
|
||||||
|
- `onpage-editor.js` - the editor.
|
||||||
|
|
||||||
|
Entering/leaving edit mode: open any page with `?cms_preview=1` (sets the cookie, strips the param, reloads so the edge serves the
|
||||||
|
live render) and `?cms_preview=0` to leave. Dev: `make dev` rebuilds both into the instance's `public/` (`npm run build:watch`,
|
||||||
|
`CMS_ONPAGE_OUT`). The shared `@k6n/cms-editor` sources are found at `../backoffice/web/packages/cms-editor/src` (submodule layout),
|
||||||
|
`../ecommerce/...` (standalone checkout), `.cms-editor/` (Docker) or `CMS_EDITOR_SRC`.
|
||||||
|
|
||||||
## Not done / next
|
## Not done / next
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -7,7 +7,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"dev:mock": "CMS_MODE=mock vite",
|
"dev:mock": "CMS_MODE=mock vite",
|
||||||
"build": "tsc --noEmit && vite build --mode lib",
|
"build": "tsc --noEmit && vite build --mode lib && vite build --mode loader",
|
||||||
|
"build:watch": "vite build --mode lib --watch & vite build --mode loader --watch & wait",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// The tiny script pages reference as /editor/cms-client.js (the URL the templates
|
||||||
|
// already use). Visitors pay for this file only; the editor bundle is fetched when
|
||||||
|
// the cms_preview cookie is set.
|
||||||
|
//
|
||||||
|
// ?cms_preview=1 sets the cookie, drops the param and reloads, so the edge
|
||||||
|
// renders the live (preview) page and the editor boots
|
||||||
|
// ?cms_preview=0 clears the cookie and reloads the published page
|
||||||
|
|
||||||
|
export const COOKIE = 'cms_preview'
|
||||||
|
export const DEFAULT_BUNDLE = '/editor/onpage-editor.js'
|
||||||
|
|
||||||
|
export interface LoaderEnv {
|
||||||
|
location: Pick<Location, 'href' | 'protocol' | 'replace'>
|
||||||
|
cookie: { get(): string; set(v: string): void }
|
||||||
|
/** URL of this script, used to find the editor bundle next to it. */
|
||||||
|
scriptSrc?: string
|
||||||
|
load(url: string): Promise<unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hasPreviewCookie = (cookie: string) => new RegExp(`(?:^|;\\s*)${COOKIE}=1`).test(cookie)
|
||||||
|
|
||||||
|
export function bundleUrl(scriptSrc?: string): string {
|
||||||
|
if (!scriptSrc) return DEFAULT_BUNDLE
|
||||||
|
try {
|
||||||
|
return new URL('onpage-editor.js', scriptSrc).href
|
||||||
|
} catch {
|
||||||
|
return DEFAULT_BUNDLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns what it did, for tests and the console. */
|
||||||
|
export function runLoader(env: LoaderEnv): 'redirected' | 'loaded' | 'idle' {
|
||||||
|
const url = new URL(env.location.href)
|
||||||
|
const flag = url.searchParams.get(COOKIE)
|
||||||
|
if (flag === '1' || flag === '0') {
|
||||||
|
const secure = env.location.protocol === 'https:' ? '; Secure' : ''
|
||||||
|
env.cookie.set(
|
||||||
|
flag === '1'
|
||||||
|
? `${COOKIE}=1; path=/; SameSite=Lax${secure}`
|
||||||
|
: `${COOKIE}=; path=/; max-age=0; SameSite=Lax${secure}`,
|
||||||
|
)
|
||||||
|
url.searchParams.delete(COOKIE)
|
||||||
|
env.location.replace(url.toString())
|
||||||
|
return 'redirected'
|
||||||
|
}
|
||||||
|
if (!hasPreviewCookie(env.cookie.get())) return 'idle'
|
||||||
|
void env.load(bundleUrl(env.scriptSrc)).catch((e) => console.error('[cms] could not load the on-page editor', e))
|
||||||
|
return 'loaded'
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { runLoader } from './loader-core'
|
||||||
|
|
||||||
|
runLoader({
|
||||||
|
location,
|
||||||
|
cookie: { get: () => document.cookie, set: (v) => void (document.cookie = v) },
|
||||||
|
scriptSrc: (document.currentScript as HTMLScriptElement | null)?.src,
|
||||||
|
load: (url) => import(/* @vite-ignore */ url),
|
||||||
|
})
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import { bundleUrl, hasPreviewCookie, runLoader, type LoaderEnv } from '../src/loader-core'
|
||||||
|
|
||||||
|
function env(href: string, cookie = '', scriptSrc?: string) {
|
||||||
|
const jar = { value: cookie }
|
||||||
|
const e = {
|
||||||
|
location: { href, protocol: new URL(href).protocol, replace: vi.fn() },
|
||||||
|
cookie: { get: () => jar.value, set: (v: string) => void (jar.value = v) },
|
||||||
|
scriptSrc,
|
||||||
|
load: vi.fn(() => Promise.resolve({})),
|
||||||
|
} satisfies LoaderEnv
|
||||||
|
return { e, jar }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('cms-client loader', () => {
|
||||||
|
it('does nothing for visitors', () => {
|
||||||
|
const { e } = env('https://k6n.net/')
|
||||||
|
expect(runLoader(e)).toBe('idle')
|
||||||
|
expect(e.load).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loads the editor bundle next to the script when the preview cookie is set', () => {
|
||||||
|
const { e } = env('https://k6n.net/', 'a=b; cms_preview=1', 'https://k6n.net/editor/cms-client.js')
|
||||||
|
expect(runLoader(e)).toBe('loaded')
|
||||||
|
expect(e.load).toHaveBeenCalledWith('https://k6n.net/editor/onpage-editor.js')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('?cms_preview=1 sets the cookie, strips the param and reloads', () => {
|
||||||
|
const { e, jar } = env('https://k6n.net/a?x=1&cms_preview=1')
|
||||||
|
expect(runLoader(e)).toBe('redirected')
|
||||||
|
expect(jar.value).toMatch(/^cms_preview=1; path=\/; SameSite=Lax; Secure$/)
|
||||||
|
expect(e.location.replace).toHaveBeenCalledWith('https://k6n.net/a?x=1')
|
||||||
|
expect(e.load).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('?cms_preview=0 clears the cookie (no Secure on http)', () => {
|
||||||
|
const { e, jar } = env('http://localhost/a?cms_preview=0', 'cms_preview=1')
|
||||||
|
runLoader(e)
|
||||||
|
expect(jar.value).toBe('cms_preview=; path=/; max-age=0; SameSite=Lax')
|
||||||
|
expect(e.location.replace).toHaveBeenCalledWith('http://localhost/a')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('helpers', () => {
|
||||||
|
expect(hasPreviewCookie('cms_preview=1')).toBe(true)
|
||||||
|
expect(hasPreviewCookie('xcms_preview=1')).toBe(false)
|
||||||
|
expect(hasPreviewCookie('cms_preview=0')).toBe(false)
|
||||||
|
expect(bundleUrl()).toBe('/editor/onpage-editor.js')
|
||||||
|
})
|
||||||
|
})
|
||||||
+7
-1
@@ -13,7 +13,13 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"types": ["vite/client", "node"],
|
"types": ["vite/client", "node"],
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": { "@k6n/cms-editor": ["../ecommerce/backoffice/web/packages/cms-editor/src/index.ts"] }
|
"paths": {
|
||||||
|
"@k6n/cms-editor": [
|
||||||
|
"../backoffice/web/packages/cms-editor/src/index.ts",
|
||||||
|
"../ecommerce/backoffice/web/packages/cms-editor/src/index.ts",
|
||||||
|
"./.cms-editor/src/index.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["src", "mock", "vite", "vite.config.ts", "test"]
|
"include": ["src", "mock", "vite", "vite.config.ts", "test"]
|
||||||
}
|
}
|
||||||
+30
-2
@@ -1,12 +1,24 @@
|
|||||||
import { defineConfig, loadEnv, type UserConfig } from 'vite'
|
import { defineConfig, loadEnv, type UserConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
import { existsSync } from 'node:fs'
|
||||||
import { cmsLivePlugin } from './vite/live'
|
import { cmsLivePlugin } from './vite/live'
|
||||||
import { cmsMockPlugin } from './vite/mock'
|
import { cmsMockPlugin } from './vite/mock'
|
||||||
|
|
||||||
export default defineConfig(({ command, mode }): UserConfig => {
|
export default defineConfig(({ command, mode }): UserConfig => {
|
||||||
const env = { ...loadEnv(mode, process.cwd(), ''), ...process.env } as Record<string, string>
|
const env = { ...loadEnv(mode, process.cwd(), ''), ...process.env } as Record<string, string>
|
||||||
const editorSrc = path.resolve(env.CMS_EDITOR_SRC || '../ecommerce/backoffice/web/packages/cms-editor/src')
|
// Shared @k6n/cms-editor sources. As a submodule of the ecommerce monorepo the
|
||||||
|
// backoffice is a sibling; a standalone checkout sits next to ../ecommerce.
|
||||||
|
const editorSrc = path.resolve(
|
||||||
|
env.CMS_EDITOR_SRC ||
|
||||||
|
['../backoffice/web/packages/cms-editor/src', '../ecommerce/backoffice/web/packages/cms-editor/src', './.cms-editor/src'].find((p) =>
|
||||||
|
existsSync(path.resolve(p)),
|
||||||
|
) ||
|
||||||
|
'../backoffice/web/packages/cms-editor/src',
|
||||||
|
)
|
||||||
|
// CMS_ONPAGE_OUT: build straight into an instance's public/ (dev launcher --watch).
|
||||||
|
const outDir = env.CMS_ONPAGE_OUT ? path.resolve(env.CMS_ONPAGE_OUT) : 'dist'
|
||||||
|
const emptyOutDir = !env.CMS_ONPAGE_OUT
|
||||||
const common: UserConfig = {
|
const common: UserConfig = {
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
resolve: {
|
resolve: {
|
||||||
@@ -23,6 +35,21 @@ export default defineConfig(({ command, mode }): UserConfig => {
|
|||||||
},
|
},
|
||||||
} as UserConfig
|
} as UserConfig
|
||||||
|
|
||||||
|
if (command === 'build' && mode === 'loader') {
|
||||||
|
// /editor/cms-client.js: the stable URL page templates reference. Classic
|
||||||
|
// script (IIFE), tiny; imports the editor bundle only for editors.
|
||||||
|
return {
|
||||||
|
...common,
|
||||||
|
define: { 'process.env.NODE_ENV': '"production"' },
|
||||||
|
build: {
|
||||||
|
outDir,
|
||||||
|
emptyOutDir: false,
|
||||||
|
lib: { entry: 'src/loader.ts', name: 'CMSClient', formats: ['iife'], fileName: () => 'cms-client.js' },
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (command === 'build') {
|
if (command === 'build') {
|
||||||
// The artifact served at /editor/onpage-editor.js. One self-contained file;
|
// The artifact served at /editor/onpage-editor.js. One self-contained file;
|
||||||
// only loaded when the preview cookie is present (see src/main.ts).
|
// only loaded when the preview cookie is present (see src/main.ts).
|
||||||
@@ -30,7 +57,8 @@ export default defineConfig(({ command, mode }): UserConfig => {
|
|||||||
...common,
|
...common,
|
||||||
define: { 'process.env.NODE_ENV': '"production"' },
|
define: { 'process.env.NODE_ENV': '"production"' },
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir,
|
||||||
|
emptyOutDir,
|
||||||
lib: { entry: 'src/main.ts', formats: ['es'], fileName: () => 'onpage-editor.js' },
|
lib: { entry: 'src/main.ts', formats: ['es'], fileName: () => 'onpage-editor.js' },
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
cssCodeSplit: false,
|
cssCodeSplit: false,
|
||||||
|
|||||||
Reference in new issue
Block a user