From e16b6e825a7fbde8adf23ac343bed4890d07c88c Mon Sep 17 00:00:00 2001 From: Christian Georgi Date: Fri, 7 Aug 2026 10:38:14 +0200 Subject: [PATCH 1/4] Name models --- cds/cql.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/cds/cql.md b/cds/cql.md index 0c6558ea1..374566958 100644 --- a/cds/cql.md +++ b/cds/cql.md @@ -190,7 +190,7 @@ SELECT from Books { * } excluding { author } The effect is about **late materialization** of signatures and staying open to late extensions. For example, assume the following definitions: -```cds +```cds [FooBar] entity Foo { foo : String; bar : String; car : String; } entity Bar as select from Foo excluding { bar }; entity Boo as select from Foo { foo, car }; @@ -198,21 +198,25 @@ entity Boo as select from Foo { foo, car }; A `SELECT * from Bar` would result into the same as a query of `Boo`: -```cql +```cds live [FooBar] SELECT * from Bar //> { foo, car } +``` +```cds live [FooBar] SELECT * from Boo //> { foo, car } ``` Now, assume a consumer of that package extends the definitions as follows: -```cds +```cds [FooBarBoo: FooBar] extend Foo with { boo : String; } ``` With that, queries on `Bar` and `Boo` would return different results: -```cql +```cds live [FooBarBoo] SELECT * from Bar //> { foo, car, boo } +``` +```cds live [FooBar] SELECT * from Boo //> { foo, car } ``` From d3dbc9bf369fb2283f47c34d997c70a5e5dd469c Mon Sep 17 00:00:00 2001 From: Christian Georgi Date: Mon, 10 Aug 2026 11:10:40 +0200 Subject: [PATCH 2/4] Worker isolation --- .vitepress/config.js | 10 +- .vitepress/lib/cds-playground/md-live-code.ts | 64 ++- .../components/cds-playground/LiveCode.vue | 22 +- .../components/cds-playground/cds-worker.js | 61 +++ .../components/cds-playground/runners.js | 35 ++ cds/cql.md | 4 +- package-lock.json | 470 +++++++++++++++++- package.json | 1 + patches/vite-plugin-cds+0.3.5.patch | 48 ++ 9 files changed, 695 insertions(+), 20 deletions(-) create mode 100644 .vitepress/theme/components/cds-playground/cds-worker.js create mode 100644 patches/vite-plugin-cds+0.3.5.patch diff --git a/.vitepress/config.js b/.vitepress/config.js index 298b5b49e..8b0bf0edd 100644 --- a/.vitepress/config.js +++ b/.vitepress/config.js @@ -83,7 +83,7 @@ const config = defineConfig({ head: [ ['meta', { name: 'theme-color', content: '#db8b0b' }], - ['meta', { 'http-equiv': 'Content-Security-Policy', content: "script-src 'self' https://www.capire-matomo.cloud.sap 'unsafe-inline' 'unsafe-eval'" }], + ['meta', { 'http-equiv': 'Content-Security-Policy', content: "script-src 'self' https://www.capire-matomo.cloud.sap 'unsafe-inline' 'unsafe-eval'; worker-src 'self' blob:" }], ['link', { rel: 'icon', href: base+'favicon.ico' }], ['link', { rel: 'shortcut icon', href: base+'favicon.ico' }], ['link', { rel: 'apple-touch-icon', sizes: '180x180', href: base+'logos/cap.png' }], @@ -98,6 +98,14 @@ const config = defineConfig({ build: { chunkSizeWarningLimit: 6000, // chunk for local search index dominates }, + // cds.worker.js is constructed with `type: 'module'`; match that at build time so its + // dynamic import('@sap/cds') is emitted as native ESM instead of an iife require() shim + worker: { + format: 'es', + // Vite doesn't reuse the main `plugins` array for worker bundles; without vite-plugin-cds's + // node()/cap() here, the worker build misses their Node built-in shims (e.g. lazify's module.require) + plugins: () => [...playground.plugins()], + }, css: { preprocessorOptions: { scss: { diff --git a/.vitepress/lib/cds-playground/md-live-code.ts b/.vitepress/lib/cds-playground/md-live-code.ts index 69c06fe31..7a94c5fc3 100644 --- a/.vitepress/lib/cds-playground/md-live-code.ts +++ b/.vitepress/lib/cds-playground/md-live-code.ts @@ -8,7 +8,7 @@ import { enabled } from '.' * ```cds live * select from Books { title } * ``` - * + * * ```js live * await INSERT.into('Books').entries( * { ID: 2, author_ID: 150, title: 'Eldorado' } @@ -20,11 +20,57 @@ import { enabled } from '.' * example: ```cds live as cql * - readonly: make the code block readonly * example: ```cds live readonly + * - [ModelName]: run query against a named model defined elsewhere on the page + * example: ```cds live [FooBar] + * + * Named model definitions (static, non-live): + * - ```cds [FooBar] — defines a named model; rendered as a plain code block + * - ```cds [FooBarBoo: FooBar] — extends FooBar; combined source is resolved at render time */ + +const MODEL_ARG_RE = /^\[.+\]$/ + +function buildModelMap(tokens: any[]): Record { + const raw: Record = {} + for (const token of tokens) { + if (token.type !== 'fence') continue + // Match the bracket first since its content may contain spaces (e.g. "[Foo: Bar]"), + // which would otherwise be broken apart by a naive split(' '). + const bracketMatch = token.info.match(/\[([^\]]+)\]/) + if (!bracketMatch) continue + const before = token.info.slice(0, bracketMatch.index).trim().split(/\s+/) + const [lang] = before + if (lang !== 'cds') continue + // Only pick up non-live model definition blocks + if (before.includes('live')) continue + const inner = bracketMatch[1] + const colonIdx = inner.indexOf(':') + const name = colonIdx === -1 ? inner.trim() : inner.slice(0, colonIdx).trim() + const base = colonIdx === -1 ? undefined : inner.slice(colonIdx + 1).trim() + raw[name] = { source: token.content.trim(), base } + } + const resolved: Record = {} + function resolve(name: string): string { + if (name in resolved) return resolved[name] + const def = raw[name] + if (!def) return '' + const baseSource = def.base ? resolve(def.base) : '' + return (resolved[name] = baseSource ? `${baseSource}\n${def.source}` : def.source) + } + Object.keys(raw).forEach(resolve) + return resolved +} + export function install(md: MarkdownRenderer) { if (!enabled) return const fence = md.renderer.rules.fence md.renderer.rules.fence = (tokens, idx, options, env: MarkdownEnv, ...args) => { + // Build the model map before any fence is rendered: VitePress's preWrapperPlugin + // strips "[...]" from token.info as a side effect of rendering (for code-group tab + // titles), so scanning tokens lazily would miss brackets on already-rendered fences. + if (!(env as any)._modelMap) { + (env as any)._modelMap = buildModelMap(tokens) + } const { info } = tokens[idx] const [language, live, ...rest] = info.split(' ') @@ -38,20 +84,30 @@ export function install(md: MarkdownRenderer) { const idx = rest.findIndex(k => k === key) return idx > -1 ? [key, rest.splice(idx+1, 1)[0]] : []; })) - const props = { + + const modelArg = rest.find((p: string) => MODEL_ARG_RE.test(p)) + const modelName = modelArg ? modelArg.slice(1, -1) : null + let modelSource = '' + if (modelName) { + modelSource = (env as any)._modelMap[modelName] ?? '' + } + + const props: Record = { language: opts.as ?? language, } + if (modelSource) props.modelSource = md.utils.escapeHtml(modelSource) + const flags = ['readonly'].filter(k => rest.includes(k)) const content = tokens[idx].content.trim() - return ` `${k}="${v}"`)} ${flags.join(' ')}>` + return ` `${k}="${v}"`).join(' ')} ${flags.join(' ')}>` } return fence!(tokens, idx, options, env, ...args) } } function insertScriptSetup(env: MarkdownEnv, imp: string) { - const sfcBlocks = env.sfcBlocks! + const sfcBlocks = env.sfcBlocks! if (!sfcBlocks.scriptSetup) { sfcBlocks.scriptSetup = { content: '', diff --git a/.vitepress/theme/components/cds-playground/LiveCode.vue b/.vitepress/theme/components/cds-playground/LiveCode.vue index 87935b2aa..a39cc0c77 100644 --- a/.vitepress/theme/components/cds-playground/LiveCode.vue +++ b/.vitepress/theme/components/cds-playground/LiveCode.vue @@ -74,7 +74,7 @@ import { computed, onMounted, ref, useId } from 'vue' import MonacoEditor from './MonacoEditor.vue' import { useData } from 'vitepress' import play from '/icons/play.svg?url&raw' -import { runners } from './runners' +import { runners, runWithModel } from './runners' import highlighter from './highlighter' import templates from 'virtual:templates' @@ -95,6 +95,10 @@ const props = defineProps({ type: String, default: 'js' }, + modelSource: { + type: String, + default: '' + }, onEvaluate: { type: Function } @@ -111,10 +115,15 @@ const evalStatus = ref(null) // the model the query runs against, shown on demand so it doesn't clutter the snippet const modelVisible = ref(false) -const modelTabs = computed(() => (templates.bookshop ?? []) - .filter(file => file.path.endsWith('.cds')) - .sort((f1, f2) => f1.path.localeCompare(f2.path)) - .map(file => ({ key: `${uid}-model-${file.path}`, kind: 'cds', name: file.path, value: file.content }))) +const modelTabs = computed(() => { + if (props.modelSource) { + return [{ key: `${uid}-model-custom`, kind: 'cds', name: 'model', value: props.modelSource }] + } + return (templates.bookshop ?? []) + .filter(file => file.path.endsWith('.cds')) + .sort((f1, f2) => f1.path.localeCompare(f2.path)) + .map(file => ({ key: `${uid}-model-${file.path}`, kind: 'cds', name: file.path, value: file.content })) +}) // eval tabs (if any) come first, model tabs are appended at the end const combinedTabs = computed(() => [ @@ -194,7 +203,8 @@ async function evaluate() { } queryResult.value = null try { - const exec = props.onEvaluate ?? runners[props.language] + const exec = props.onEvaluate + ?? (props.modelSource ? (q) => runWithModel(q, props.modelSource) : runners[props.language]) if (!exec) throw new Error(`No runner found for language: ${props.language}. Available runners: ${Object.keys(runners).join(', ')}`) const result = await exec(queryText.value) tabs.value = formatTabs(result).filter(({ value }) => value) diff --git a/.vitepress/theme/components/cds-playground/cds-worker.js b/.vitepress/theme/components/cds-playground/cds-worker.js new file mode 100644 index 000000000..17da16ee5 --- /dev/null +++ b/.vitepress/theme/components/cds-playground/cds-worker.js @@ -0,0 +1,61 @@ +function simpleSqlFormat(sql) { + return sql + .replace(/\b(select|from|where|group by|order by|having|limit|offset|join|left join|right join|inner join|outer join)\b/gi, "\n$1") + .replace(/\b(and|or)\b/gi, "\n $1") + .replace(/,\s*/g, ",\n ") + .replace(/\n{2,}/g, "\n") + .trim(); +} + +const sqlLog = []; + +function injectLogger(sqlite) { + const { prototype } = sqlite().constructor; + const { prepare: original } = prototype; + prototype.prepare = function prepare(sql) { + sqlLog.push(sql); + return original.call(this, sql); + } +} + +let cds; +let initialized = false; + +async function init(modelSource) { + cds = (await import('@sap/cds')).default; + const sqlite = (await import('better-sqlite3')).default; + + await sqlite.initialized; + injectLogger(sqlite); + + const csn = cds.compile({ 'model.cds': modelSource }); + cds.model = csn; + + cds.db = await cds.connect.to('db'); + + const csvs = {} + await cds.deploy(csn, null, csvs).to(cds.db); + initialized = true; +} + +self.onmessage = async ({ data: { type, id, payload } }) => { + try { + if (type === 'init') { + await init(payload.modelSource); + self.postMessage({ type: 'ready' }); + } else if (type === 'query') { + if (!initialized) throw new Error('Worker not initialized'); + sqlLog.length = 0; + const cqn = cds.ql(payload.query); + const result = await cds.db.run(cqn); + const formatted = sqlLog.map(simpleSqlFormat).join('\n\n-------\n'); + self.postMessage({ type: 'result', id, result: [ + { value: result, kind: 'json', name: 'Result' }, + { value: formatted, kind: 'sql', name: 'SQL' }, + { value: cqn, kind: 'json', name: 'CQN' }, + ]}); + } + } catch (err) { + self.postMessage({ type: 'error', id, error: err.message ?? String(err) }); + } +}; diff --git a/.vitepress/theme/components/cds-playground/runners.js b/.vitepress/theme/components/cds-playground/runners.js index 7b7683e7d..cc2939cea 100644 --- a/.vitepress/theme/components/cds-playground/runners.js +++ b/.vitepress/theme/components/cds-playground/runners.js @@ -91,9 +91,44 @@ async function cdsQL(query) { ]; } +// Worker pool: one worker per model source string, shared across all LiveCode instances +const workerPool = new Map(); + +function getOrCreateWorker(modelSource) { + if (workerPool.has(modelSource)) return workerPool.get(modelSource); + const worker = new Worker(new URL('./cds-worker.js', import.meta.url), { type: 'module' }); + const initPromise = new Promise((resolve, reject) => { + worker.addEventListener('message', function once(e) { + if (e.data.type !== 'ready' && e.data.type !== 'error') return; + worker.removeEventListener('message', once); + e.data.type === 'ready' ? resolve() : reject(new Error(e.data.error)); + }); + worker.postMessage({ type: 'init', payload: { modelSource } }); + }); + const entry = { worker, initPromise }; + workerPool.set(modelSource, entry); + return entry; +} + +async function runWithModel(query, modelSource) { + const { worker, initPromise } = getOrCreateWorker(modelSource); + await initPromise; + return new Promise((resolve, reject) => { + const id = crypto.randomUUID(); + function handler(e) { + if (e.data.id !== id) return; + worker.removeEventListener('message', handler); + e.data.type === 'error' ? reject(new Error(e.data.error)) : resolve(e.data.result); + } + worker.addEventListener('message', handler); + worker.postMessage({ type: 'query', id, payload: { query } }); + }); +} + export { evalJS, cdsQL, + runWithModel, } export const runners = { diff --git a/cds/cql.md b/cds/cql.md index 374566958..e590ceb54 100644 --- a/cds/cql.md +++ b/cds/cql.md @@ -190,7 +190,7 @@ SELECT from Books { * } excluding { author } The effect is about **late materialization** of signatures and staying open to late extensions. For example, assume the following definitions: -```cds [FooBar] +```cds [FooBar, data: {'data/Foo.csv': 'foo,bar,car\nFoo,Bar,Car'}] entity Foo { foo : String; bar : String; car : String; } entity Bar as select from Foo excluding { bar }; entity Boo as select from Foo { foo, car }; @@ -216,7 +216,7 @@ With that, queries on `Bar` and `Boo` would return different results: ```cds live [FooBarBoo] SELECT * from Bar //> { foo, car, boo } ``` -```cds live [FooBar] +```cds live [FooBarBoo] SELECT * from Boo //> { foo, car } ``` diff --git a/package-lock.json b/package-lock.json index ca24d7ec8..0232065f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "globals": "^17.4.0", "htmlparser2": "^12", "monaco-editor": "^0", + "patch-package": "^8.0.1", "sass": "^1.62.1", "vite-plugin-cds": "^0.3.1", "vitepress": "2.0.0-alpha.18" @@ -2128,6 +2129,13 @@ "vue": "^3.5.0" } }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -2202,6 +2210,22 @@ "dev": true, "license": "MIT" }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2281,6 +2305,19 @@ "node": "18 || 20 || >=22" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2291,6 +2328,25 @@ "node": ">= 0.8" } }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2333,6 +2389,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/character-entities": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", @@ -2382,6 +2455,42 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -2443,7 +2552,6 @@ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2500,6 +2608,24 @@ "license": "MIT", "peer": true }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3023,6 +3149,19 @@ "node": ">=16.0.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -3063,6 +3202,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -3136,6 +3285,21 @@ "node": ">= 0.8" } }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3240,6 +3404,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -3442,6 +3636,22 @@ "node": ">= 0.10" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3465,6 +3675,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -3472,13 +3692,32 @@ "dev": true, "license": "MIT" }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/json-buffer": { "version": "3.0.1", @@ -3496,6 +3735,26 @@ "license": "MIT", "peer": true }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -3504,6 +3763,29 @@ "license": "MIT", "peer": true }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3515,6 +3797,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4668,6 +4960,33 @@ ], "license": "MIT" }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -4711,6 +5030,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minisearch": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", @@ -4801,6 +5130,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ohash": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", @@ -4850,6 +5189,23 @@ "regex-recursion": "^6.0.2" } }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4913,6 +5269,36 @@ "node": ">= 0.8" } }, + "node_modules/patch-package": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", + "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^10.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.2.4", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -4937,7 +5323,6 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -5288,6 +5673,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -5301,7 +5704,6 @@ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -5315,7 +5717,6 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -5416,6 +5817,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5462,6 +5873,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tabbable": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", @@ -5486,6 +5910,29 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -5716,6 +6163,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5950,7 +6407,6 @@ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "isexe": "^2.0.0" }, diff --git a/package.json b/package.json index 4b53a2679..d6b1bea02 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "globals": "^17.4.0", "htmlparser2": "^12", "monaco-editor": "^0", + "patch-package": "^8.0.1", "sass": "^1.62.1", "vite-plugin-cds": "^0.3.1", "vitepress": "2.0.0-alpha.18" diff --git a/patches/vite-plugin-cds+0.3.5.patch b/patches/vite-plugin-cds+0.3.5.patch new file mode 100644 index 000000000..8dcc2e418 --- /dev/null +++ b/patches/vite-plugin-cds+0.3.5.patch @@ -0,0 +1,48 @@ +diff --git a/node_modules/vite-plugin-cds/node/vite.js b/node_modules/vite-plugin-cds/node/vite.js +index 1245549..a28263d 100644 +--- a/node_modules/vite-plugin-cds/node/vite.js ++++ b/node_modules/vite-plugin-cds/node/vite.js +@@ -113,7 +113,43 @@ export function nodeVite() { + if (/node_modules\/vite\/dist\/client\/env.mjs$/.test(id)) { + return `${windowBootstrap}\n${code}`; + } ++ // @sap/cds's lazify() calls the real Node `module.require`, which bundler-emulated ++ // `module` objects don't implement; route it through the polyfilled global `require` instead. ++ // Matched by content only (not `id`) since the same file can be bundled under multiple ++ // resolved ids (e.g. once for the client build, once for the worker sub-build) and the ++ // literal path segment isn't reliably present in every id variant. ++ if (code.includes('module.require(id)')) { ++ return code.replaceAll('module.require(id)', 'require(id)'); ++ } ++ // @sap/cds's Query.init() derives the CQN keyword ("SELECT", "INSERT", ...) from the ++ // query-builder class's own `.name` at module-init time (`kind = self.name`), then uses ++ // `q[kind] = x` to build the CQN object. Rolldown's isolated worker sub-build can lose ++ // these classes' `.name` (resolving to `""`), corrupting every query built there (e.g. ++ // dropping the `from` clause). Make `kind` an explicit parameter instead of relying on it. ++ if (code.includes('static init() {') && code.includes('kind = self.name')) { ++ return code.replace( ++ 'static init() {\n const self = this, kind = self.name', ++ 'static init(kind = this.name) {\n const self = this' ++ ); ++ } ++ const initCall = code.match(/module\.exports = (SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|UPSERT)\.init\(\)/); ++ if (initCall) { ++ return code.replace(initCall[0], `module.exports = ${initCall[1]}.init('${initCall[1]}')`); ++ } ++ return null; ++ }, ++ renderChunk(code) { ++ // Rolldown's CJS/ESM interop can synthesize its own lazy-require wrapper (a Proxy calling ++ // `.require()`) for @sap/cds's lazify() pattern, in addition to (or instead ++ // of) the literal source text handled above. Catch it here, post-bundling, by matching the ++ // generic `.require()` member-call shape, which real Node's `module.require` ++ // is the only realistic source of in this codebase. ++ const re = /\b([a-zA-Z_$][\w$]*)\.require\(([a-zA-Z_$][\w$]*)\)/g; ++ if (re.test(code)) { ++ return { code: code.replace(re, (m, obj, arg) => `require(${arg})`), map: null }; ++ } + return null; + }, + }; + } ++ From df3b980b22749ee517c4010426f8c1f1ddf906f6 Mon Sep 17 00:00:00 2001 From: Christian Georgi Date: Tue, 11 Aug 2026 13:30:26 +0200 Subject: [PATCH 3/4] Show sample data --- .vitepress/config.js | 2 +- .vitepress/lib/cds-playground/md-live-code.ts | 80 ++++++++++++++----- .../components/cds-playground/LiveCode.vue | 30 +++++-- .../components/cds-playground/cds-worker.js | 7 +- .../components/cds-playground/runners.js | 13 +-- cds/cql.md | 14 +++- 6 files changed, 109 insertions(+), 37 deletions(-) diff --git a/.vitepress/config.js b/.vitepress/config.js index 8b0bf0edd..168e1d124 100644 --- a/.vitepress/config.js +++ b/.vitepress/config.js @@ -98,7 +98,7 @@ const config = defineConfig({ build: { chunkSizeWarningLimit: 6000, // chunk for local search index dominates }, - // cds.worker.js is constructed with `type: 'module'`; match that at build time so its + // cds-worker.js is constructed with `type: 'module'`; match that at build time so its // dynamic import('@sap/cds') is emitted as native ESM instead of an iife require() shim worker: { format: 'es', diff --git a/.vitepress/lib/cds-playground/md-live-code.ts b/.vitepress/lib/cds-playground/md-live-code.ts index 7a94c5fc3..9725bf8f7 100644 --- a/.vitepress/lib/cds-playground/md-live-code.ts +++ b/.vitepress/lib/cds-playground/md-live-code.ts @@ -26,12 +26,54 @@ import { enabled } from '.' * Named model definitions (static, non-live): * - ```cds [FooBar] — defines a named model; rendered as a plain code block * - ```cds [FooBarBoo: FooBar] — extends FooBar; combined source is resolved at render time + * - ```cds [FooBar, data: FooData] — attaches a named CSV data set to the model + * + * Named CSV data sets (static, non-live): + * - ```csv [FooData: data/Foo.csv] — defines a named data set; rendered as a plain code block + * - ```csv hidden [FooData: data/Foo.csv] — same, but suppressed from output (not rendered) + * + * CSV and model blocks may appear anywhere on the page — they are collected in a full token pass + * before any fence is rendered, so forward references work. */ const MODEL_ARG_RE = /^\[.+\]$/ -function buildModelMap(tokens: any[]): Record { - const raw: Record = {} +interface ModelDef { source: string; csvs?: Record } + +function parseBracketKV(inner: string): { name: string; base?: string; data?: string } { + const commaIdx = inner.indexOf(',') + const namePart = commaIdx === -1 ? inner.trim() : inner.slice(0, commaIdx).trim() + const colonIdx = namePart.indexOf(':') + const name = colonIdx === -1 ? namePart : namePart.slice(0, colonIdx).trim() + const base = colonIdx === -1 ? undefined : namePart.slice(colonIdx + 1).trim() + let data: string | undefined + if (commaIdx !== -1) { + const dataMatch = inner.slice(commaIdx + 1).match(/\bdata\s*:\s*(\S+)/) + if (dataMatch) data = dataMatch[1] + } + return { name, base, data } +} + +function buildDataMap(tokens: any[]): Record> { + const result: Record> = {} + for (const token of tokens) { + if (token.type !== 'fence') continue + const bracketMatch = token.info.match(/\[([^\]]+)\]/) + if (!bracketMatch) continue + const [lang] = token.info.slice(0, bracketMatch.index).trim().split(/\s+/) + if (lang !== 'csv') continue + const inner = bracketMatch[1] + const colonIdx = inner.indexOf(':') + if (colonIdx === -1) continue + const name = inner.slice(0, colonIdx).trim() + const path = inner.slice(colonIdx + 1).trim() + result[name] = { [path]: token.content.trim() } + } + return result +} + +function buildModelMap(tokens: any[], dataMap: Record>): Record { + const raw: Record }> = {} for (const token of tokens) { if (token.type !== 'fence') continue // Match the bracket first since its content may contain spaces (e.g. "[Foo: Bar]"), @@ -43,19 +85,18 @@ function buildModelMap(tokens: any[]): Record { if (lang !== 'cds') continue // Only pick up non-live model definition blocks if (before.includes('live')) continue - const inner = bracketMatch[1] - const colonIdx = inner.indexOf(':') - const name = colonIdx === -1 ? inner.trim() : inner.slice(0, colonIdx).trim() - const base = colonIdx === -1 ? undefined : inner.slice(colonIdx + 1).trim() - raw[name] = { source: token.content.trim(), base } + const { name, base, data } = parseBracketKV(bracketMatch[1]) + raw[name] = { source: token.content.trim(), base, csvs: data ? dataMap[data] : undefined } } - const resolved: Record = {} - function resolve(name: string): string { + const resolved: Record = {} + function resolve(name: string): ModelDef { if (name in resolved) return resolved[name] const def = raw[name] - if (!def) return '' - const baseSource = def.base ? resolve(def.base) : '' - return (resolved[name] = baseSource ? `${baseSource}\n${def.source}` : def.source) + if (!def) return { source: '' } + const baseDef = def.base ? resolve(def.base) : null + const source = baseDef ? `${baseDef.source}\n${def.source}` : def.source + const csvs = def.csvs ?? baseDef?.csvs + return (resolved[name] = { source, csvs }) } Object.keys(raw).forEach(resolve) return resolved @@ -69,11 +110,16 @@ export function install(md: MarkdownRenderer) { // strips "[...]" from token.info as a side effect of rendering (for code-group tab // titles), so scanning tokens lazily would miss brackets on already-rendered fences. if (!(env as any)._modelMap) { - (env as any)._modelMap = buildModelMap(tokens) + const dataMap = buildDataMap(tokens) + ;(env as any)._modelMap = buildModelMap(tokens, dataMap) } const { info } = tokens[idx] const [language, live, ...rest] = info.split(' ') + + // Suppress named CSV data blocks marked hidden — content is captured in the pre-pass and shown as a model tab. + if (language === 'csv' && live === 'hidden' && /\[[^\]]+:[^\]]+\]/.test(info)) return '' + if (live === 'live') { const mdDir = dirname(env.realPath ?? env.path) const filePath = './' + relative(mdDir, join(__dirname, '../../theme/components/cds-playground/LiveCode.vue')) @@ -87,15 +133,13 @@ export function install(md: MarkdownRenderer) { const modelArg = rest.find((p: string) => MODEL_ARG_RE.test(p)) const modelName = modelArg ? modelArg.slice(1, -1) : null - let modelSource = '' - if (modelName) { - modelSource = (env as any)._modelMap[modelName] ?? '' - } + const modelDef: ModelDef | undefined = modelName ? (env as any)._modelMap[modelName] : undefined const props: Record = { language: opts.as ?? language, } - if (modelSource) props.modelSource = md.utils.escapeHtml(modelSource) + if (modelDef?.source) props.modelSource = md.utils.escapeHtml(modelDef.source) + if (modelDef?.csvs) props.modelData = md.utils.escapeHtml(JSON.stringify(modelDef.csvs)) const flags = ['readonly'].filter(k => rest.includes(k)) diff --git a/.vitepress/theme/components/cds-playground/LiveCode.vue b/.vitepress/theme/components/cds-playground/LiveCode.vue index a39cc0c77..479897be8 100644 --- a/.vitepress/theme/components/cds-playground/LiveCode.vue +++ b/.vitepress/theme/components/cds-playground/LiveCode.vue @@ -30,7 +30,7 @@