Merge branch 'main' of git.k6n.net:mats/cms-onpageeditor
This commit is contained in:
commit
2d35df4c7f
15 files changed
+295
-16
No files matched your search
@@ -2,3 +2,4 @@ node_modules
|
||||
dist
|
||||
.env
|
||||
.env.local
|
||||
.cms-editor/
|
||||
@@ -146,9 +146,19 @@ schema contract: [docs/styling.md](docs/styling.md).
|
||||
|
||||
## Deploying
|
||||
|
||||
Serve `dist/onpage-editor.js` at `/editor/onpage-editor.js` and let the existing loader do
|
||||
`if (document.cookie.includes('cms_preview=1')) import('/editor/onpage-editor.js')`. The bundle
|
||||
re-checks the cookie, so loading it for a visitor is a no-op.
|
||||
Part of the ecommerce monorepo as the `cms-onpageeditor` submodule. Each instance image (`tornberg`, `bygglagret`,
|
||||
`k6n`) and `Dockerfile.nginx` build it in an `onpage` stage (named context `onpageeditor`) and ship two files in `/editor/`:
|
||||
|
||||
- `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 edit mode on a deployed site: open **`/edit`** or **`/edit/<path>`** (the edge, `deploy/nginx.conf`, sets the cookie and redirects
|
||||
to the page). 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
|
||||
|
||||
|
||||
+6
-2
@@ -64,8 +64,12 @@ and every block with a numeric `columns`.
|
||||
|
||||
## Open points / suggestions for the CMS side
|
||||
|
||||
1. **Responsive spacing** is not possible today: no `gap/padding/margin` classes per breakpoint. If wanted: add `x-style` spacing
|
||||
fields to `$defs/breakpoint` and emit e.g. `k6n-gap-md@t` classes from `layout.gohtml`; the editor picks them up unchanged.
|
||||
1. **Responsive spacing** (done): `$defs/breakpoint` declares `gap`, `padding(Top|Bottom)`, `margin(Top|Bottom)`, `align`, `justify`; the k6n
|
||||
layout template emits `k6n-t-gap-md` / `k6n-m-pt-sm` classes (tablet ≤900px, mobile ≤620px). Columns are resolved in the template into
|
||||
`--k6n-cols` / `--k6n-cols-t` / `--k6n-cols-m` (defaults: tablet ≤2, mobile 1); cards/quotes/cases/projects/people read the same variables.
|
||||
**Device preview**: the Style panel's *Show … preview* opens the page in an iframe at 390/820/1280px (real viewport, so media queries apply),
|
||||
reloaded after each edit; the editor does not boot inside it (`window.name`). Only the k6n content root has this so far; the shared
|
||||
`data/templates/layout.gohtml` still uses the old class scheme.
|
||||
2. **Left/right padding and margin** have no fields (`paddingTop/Bottom` only), so those grips edit the all-sides field. Adding
|
||||
`paddingX/marginX` (`side: "x"`) gives them their own handles with no editor change.
|
||||
3. `InsertOp` in `@k6n/cms-editor` lacks the optional `id` the server accepts (the editor casts for the wrap batch).
|
||||
|
||||
+10
-3
@@ -53,6 +53,9 @@ export const schemas: Record<string, any> = {
|
||||
properties: {
|
||||
flow: { 'x-style': { kind: 'flow' }, type: 'string', enum: ['row', 'stack', 'actions'] },
|
||||
columns: { 'x-style': { kind: 'columns' }, type: 'integer', minimum: 1, maximum: 12 },
|
||||
gap: { 'x-style': { kind: 'gap', token: '--k6n-sp-{value}' }, type: 'string', enum: ['none', 'xs', 'sm', 'md', 'lg', 'xl'] },
|
||||
paddingTop: { 'x-style': { kind: 'spacing', box: 'padding', side: 'top', overrides: 'padding', token: '--k6n-sp-{value}' }, type: 'string', enum: ['none', 'xs', 'sm', 'md', 'lg', 'xl'] },
|
||||
paddingBottom: { 'x-style': { kind: 'spacing', box: 'padding', side: 'bottom', overrides: 'padding', token: '--k6n-sp-{value}' }, type: 'string', enum: ['none', 'xs', 'sm', 'md', 'lg', 'xl'] },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
@@ -193,9 +196,13 @@ export function render(g: Graph, id: string): string {
|
||||
.map(([k, c]) => ` k6n-${c}-${d[k]}`)
|
||||
.join('')
|
||||
const r = d.responsive ?? {}
|
||||
const resp = [['mobile', 'm'], ['tablet', 't'], ['desktop', 'd']]
|
||||
.flatMap(([bp, c]) => [r[bp]?.flow ? ` k6n-layout--${c}-${r[bp].flow}` : '', r[bp]?.columns ? ` k6n-layout--${c}c-${r[bp].columns}` : ''])
|
||||
.join('')
|
||||
// Mirrors data/k6n/templates/layout.gohtml: tablet flow class only when it differs; spacing as k6n-<t|m>-<prop>-<size>.
|
||||
const norm = (f: string) => ({ vertical: 'stack', horizontal: 'row', grid: 'row' } as Record<string, string>)[f] ?? f
|
||||
const tf = r.tablet?.flow ? norm(r.tablet.flow) : flow
|
||||
const resp = (tf !== flow ? ` k6n-layout--t-${tf}` : '') +
|
||||
[['tablet', 't'], ['mobile', 'm']]
|
||||
.flatMap(([bp, c]) => [['gap', 'gap'], ['padding', 'p'], ['paddingTop', 'pt'], ['paddingBottom', 'pb']].filter(([k]) => r[bp]?.[k]).map(([k, n]) => ` k6n-${c}-${n}-${r[bp][k]}`))
|
||||
.join('')
|
||||
const tone = d.tone && d.tone !== 'none' ? ` k6n-${d.tone}` : ''
|
||||
const cells = g.children(id).map((e) => `<div class="k6n-layout__cell">${render(g, e.to)}</div>`).join('\n')
|
||||
return `<div class="k6n-layout k6n-layout--${flow} k6n-gap-${d.gap ?? 'md'}${sp}${tone}${resp}" style="--k6n-cols:${d.columns ?? 2}" ${attrs} data-cms-container="true">\n${cells}\n</div>`
|
||||
|
||||
+2
-1
@@ -7,7 +7,8 @@
|
||||
"scripts": {
|
||||
"dev": "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",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"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),
|
||||
})
|
||||
+3
-1
@@ -2,10 +2,12 @@
|
||||
// own loader only when the preview cookie is set (docs/onpage-editor.md §2).
|
||||
// It re-checks, so loading it by mistake never shows editor UI to visitors.
|
||||
import { boot } from './boot'
|
||||
import { PREVIEW_FRAME_NAME } from './style/DevicePreview'
|
||||
|
||||
export const previewEnabled = () => /(?:^|;\s*)cms_preview=1/.test(document.cookie)
|
||||
|
||||
if (previewEnabled()) boot()
|
||||
// The device-preview iframe shows the page only; it must not boot a second editor.
|
||||
if (previewEnabled() && window.name !== PREVIEW_FRAME_NAME) boot()
|
||||
|
||||
export { boot }
|
||||
export * from './registry'
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEditorState } from '../ui/hooks'
|
||||
import type { Breakpoint } from './model'
|
||||
|
||||
/** Real viewport widths: media queries only answer to an actual window, hence an iframe. */
|
||||
export const DEVICE_WIDTH: Record<Breakpoint | 'base', number> = { mobile: 390, tablet: 820, desktop: 1280, base: 1280 }
|
||||
/** Marks the preview frame so the editor does not boot inside it (see main.ts). */
|
||||
export const PREVIEW_FRAME_NAME = 'cms-device-preview'
|
||||
|
||||
const SHOWN = 380 // px the frame occupies on screen at most
|
||||
const HEIGHT = 520
|
||||
|
||||
/**
|
||||
* The page at the width of the selected breakpoint, floating bottom-left. It is the same URL, so it
|
||||
* renders the live store (cms_preview cookie) and reloads shortly after every edit. Because the
|
||||
* viewport is real, `@media` rules - including the layout's tablet/mobile column variables - apply.
|
||||
*/
|
||||
export function DevicePreview({ bp, onClose }: { bp?: Breakpoint; onClose(): void }) {
|
||||
const ref = useRef<HTMLIFrameElement>(null)
|
||||
const rev = useEditorState((s) => s.editRev)
|
||||
const first = useRef(true)
|
||||
const width = DEVICE_WIDTH[bp ?? 'base']
|
||||
const scale = Math.min(1, SHOWN / width)
|
||||
|
||||
useEffect(() => {
|
||||
if (first.current) {
|
||||
first.current = false
|
||||
return
|
||||
}
|
||||
const t = setTimeout(() => {
|
||||
try {
|
||||
ref.current?.contentWindow?.location.reload()
|
||||
} catch {
|
||||
/* cross-origin: nothing to reload */
|
||||
}
|
||||
}, 450)
|
||||
return () => clearTimeout(t)
|
||||
}, [rev])
|
||||
|
||||
const url = location.pathname + location.search
|
||||
return (
|
||||
<div
|
||||
className="cms-devicepreview"
|
||||
style={{ position: 'fixed', left: 16, bottom: 16, zIndex: 2147483000, background: '#161821', border: '1px solid #2a2d3a', borderRadius: 10, boxShadow: '0 8px 30px #0008', padding: 6, color: '#c3c6d4', font: '12px system-ui' }}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '0 4px 6px' }}>
|
||||
<span>
|
||||
{bp ?? 'desktop'} preview · {width}px
|
||||
</span>
|
||||
<button type="button" aria-label="Close preview" onClick={onClose} style={{ background: 'none', border: 0, color: 'inherit', cursor: 'pointer' }}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ width: width * scale, height: HEIGHT, overflow: 'hidden', borderRadius: 6, background: '#fff' }}>
|
||||
<iframe
|
||||
ref={ref}
|
||||
name={PREVIEW_FRAME_NAME}
|
||||
title="Device preview"
|
||||
src={url}
|
||||
style={{ width, height: HEIGHT / scale, border: 0, transform: `scale(${scale})`, transformOrigin: 'top left' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { pxOf } from './tokens'
|
||||
import type { Scale } from './tokens'
|
||||
import { Icon } from '../ui/Icon'
|
||||
import { describeStyle, fieldForSide } from './model'
|
||||
import { DevicePreview } from './DevicePreview'
|
||||
|
||||
type Data = Record<string, any>
|
||||
|
||||
@@ -35,6 +36,7 @@ export function StylePanel({ node, schema, ctrl }: EditorProps) {
|
||||
const fields = useMemo(() => describeStyle(schema), [schema])
|
||||
const [data, setData] = useState<Data>(node.data ?? {})
|
||||
const [bp, setBp] = useState<Breakpoint | undefined>()
|
||||
const [preview, setPreview] = useState(false)
|
||||
const rev = useEditorState((s) => s.editRev)
|
||||
useEffect(() => {
|
||||
if (rev) void ctrl.node(node.id, true).then((n) => setData(n.data ?? {})).catch(() => {})
|
||||
@@ -64,7 +66,19 @@ export function StylePanel({ node, schema, ctrl }: EditorProps) {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{bp && <p className="cms-muted">Overrides for {bp === 'mobile' ? 'small screens' : bp === 'tablet' ? 'tablets' : 'desktops'}; unset values inherit from “All sizes”.</p>}
|
||||
<div className="cms-style__preview">
|
||||
<button type="button" className={`cms-chip${preview ? ' is-on' : ''}`} aria-pressed={preview} onClick={() => setPreview((p) => !p)}>
|
||||
{preview ? 'Hide' : 'Show'} {bp ?? 'desktop'} preview
|
||||
</button>
|
||||
</div>
|
||||
{preview && <DevicePreview bp={bp} onClose={() => setPreview(false)} />}
|
||||
{bp && (
|
||||
<p className="cms-muted">
|
||||
Overrides for {bp === 'mobile' ? 'small screens (≤620px)' : bp === 'tablet' ? 'tablets (≤900px)' : 'desktops'}; unset values inherit from “All sizes”.
|
||||
{bp === 'tablet' && ' Default: at most 2 columns.'}
|
||||
{bp === 'mobile' && ' Default: one column.'}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{of('flow', 'columns', 'align', 'justify').length > 0 && (
|
||||
<Section title="Layout">
|
||||
@@ -85,6 +99,14 @@ export function StylePanel({ node, schema, ctrl }: EditorProps) {
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{bp && of('gap', 'spacing').length > 0 && (
|
||||
<Section title="Spacing">
|
||||
{of('gap', 'spacing').map((f) => (
|
||||
<Control key={f.name} f={f} value={get(f)} onChange={(v) => set(f, v)} />
|
||||
))}
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{of('tone', 'border').length > 0 && (
|
||||
<Section title="Surface">
|
||||
{of('tone', 'border').map((f) => (
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -84,7 +84,6 @@ describe('Style panel (schema-driven)', () => {
|
||||
await select('l1')
|
||||
await h.press('Style')
|
||||
await act(async () => void [...h.shadow.querySelectorAll<HTMLElement>('[role=tab]')].find((t) => t.textContent === 'Tablet')!.click())
|
||||
expect(h.shadow.querySelector('[data-style-field="gap"]')).toBeNull() // spacing has no per-breakpoint form
|
||||
await act(async () => void chip('flow', 'Stack').click())
|
||||
await h.settle()
|
||||
expect(data('l1').responsive).toEqual({ tablet: { flow: 'stack' } })
|
||||
@@ -95,6 +94,33 @@ describe('Style panel (schema-driven)', () => {
|
||||
expect('responsive' in data('l1')).toBe(false)
|
||||
})
|
||||
|
||||
it('breakpoints can override gap, which renders as a k6n-<bp>-gap class', async () => {
|
||||
h = await setup()
|
||||
await h.settle()
|
||||
await select('l1')
|
||||
await h.press('Style')
|
||||
await act(async () => void [...h.shadow.querySelectorAll<HTMLElement>('[role=tab]')].find((t) => t.textContent === 'Mobile')!.click())
|
||||
await act(async () => void chip('gap', 'xs').click())
|
||||
await h.settle()
|
||||
expect(data('l1').responsive).toEqual({ mobile: { gap: 'xs' } })
|
||||
expect(data('l1').gap).not.toBe('xs')
|
||||
expect(byId('l1').className).toContain('k6n-m-gap-xs')
|
||||
})
|
||||
|
||||
it('the device preview is an iframe named so the editor does not boot inside it', async () => {
|
||||
h = await setup()
|
||||
await h.settle()
|
||||
await select('l1')
|
||||
await h.press('Style')
|
||||
await act(async () => void [...h.shadow.querySelectorAll<HTMLElement>('[role=tab]')].find((t) => t.textContent === 'Mobile')!.click())
|
||||
await act(async () => void [...h.shadow.querySelectorAll<HTMLElement>('.cms-style__preview .cms-chip')][0].click())
|
||||
const f = h.shadow.querySelector('iframe')!
|
||||
expect(f.name).toBe('cms-device-preview')
|
||||
expect(f.style.width).toBe('390px')
|
||||
await act(async () => void h.shadow.querySelector<HTMLElement>('[aria-label="Close preview"]')!.click())
|
||||
expect(h.shadow.querySelector('iframe')).toBeNull()
|
||||
})
|
||||
|
||||
it('rapid clicks on one field coalesce into a single undo step', async () => {
|
||||
h = await setup()
|
||||
await h.settle()
|
||||
|
||||
+2
-1
@@ -25,7 +25,8 @@ describe('describeStyle', () => {
|
||||
expect(byName('paddingTop')).toMatchObject({ kind: 'spacing', box: 'padding', side: 'top', overrides: 'padding', token: '--k6n-sp-{value}', inferred: false })
|
||||
expect(byName('gap')).toMatchObject({ kind: 'gap', inferred: false })
|
||||
expect(byName('flow').responsive).toEqual(['mobile', 'tablet', 'desktop'])
|
||||
expect(byName('gap').responsive).toEqual([]) // spacing is not a breakpoint override (the template has no classes for it)
|
||||
expect(byName('gap').responsive).toEqual(['mobile', 'tablet', 'desktop'])
|
||||
expect(byName('padding').responsive).toEqual([]) // only declared per-side in the breakpoint def
|
||||
expect(isWrapperSchema(layout)).toBe(true)
|
||||
})
|
||||
|
||||
|
||||
+7
-1
@@ -13,7 +13,13 @@
|
||||
"noEmit": true,
|
||||
"types": ["vite/client", "node"],
|
||||
"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"]
|
||||
}
|
||||
+30
-2
@@ -1,12 +1,24 @@
|
||||
import { defineConfig, loadEnv, type UserConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'node:path'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { cmsLivePlugin } from './vite/live'
|
||||
import { cmsMockPlugin } from './vite/mock'
|
||||
|
||||
export default defineConfig(({ command, mode }): UserConfig => {
|
||||
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 = {
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
@@ -23,6 +35,21 @@ export default defineConfig(({ command, mode }): 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') {
|
||||
// The artifact served at /editor/onpage-editor.js. One self-contained file;
|
||||
// only loaded when the preview cookie is present (see src/main.ts).
|
||||
@@ -30,7 +57,8 @@ export default defineConfig(({ command, mode }): UserConfig => {
|
||||
...common,
|
||||
define: { 'process.env.NODE_ENV': '"production"' },
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
outDir,
|
||||
emptyOutDir,
|
||||
lib: { entry: 'src/main.ts', formats: ['es'], fileName: () => 'onpage-editor.js' },
|
||||
sourcemap: true,
|
||||
cssCodeSplit: false,
|
||||
|
||||
Reference in new issue
Block a user