Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
b07f9b8
Init Vite playground with custom plugin for Inferno + experimental de…
aleksei-semikozov Mar 20, 2026
00b8bca
Fix Vite plugin: remove type-only class fields, add jQuery integratio…
aleksei-semikozov Mar 15, 2026
71691d8
Fix class field initialization order in Vite plugin
aleksei-semikozov Mar 15, 2026
0016a20
Update pnpm-lock.yaml with vite and babel plugin deps
aleksei-semikozov Mar 20, 2026
97bef08
Playground - use jQuery API for Scheduler
aleksei-semikozov Mar 20, 2026
e4c3905
Playground - add widget catalog with all 83 widgets and Playwright sm…
aleksei-semikozov Mar 20, 2026
a5c3cf6
Playground - add team grouping, search filter and recent history to s…
aleksei-semikozov Mar 20, 2026
599b781
Playground - serve official jQuery demos via Vite with HMR and demo d…
aleksei-semikozov Mar 20, 2026
29e83ab
Playground - Add official jQuery demo browser with pin, recents heat-…
aleksei-semikozov Mar 20, 2026
412d71f
Playground - add static build and Storybook PR preview deployment
aleksei-semikozov Mar 20, 2026
1bf2cab
Playground - Fix iframe absolute path and hide source links in static…
aleksei-semikozov Mar 21, 2026
79db0bb
Playground - fix image paths in static demo build (../../../../ -> ..…
aleksei-semikozov Mar 21, 2026
6cb7103
Playground - fix HTML image paths, skip missing scripts, add CardView
aleksei-semikozov Mar 21, 2026
321f962
Playground - add fake server for WebAPI and SignalR demos with banner
aleksei-semikozov Mar 21, 2026
04141d4
Playground - search demos by title in sidebar nav
aleksei-semikozov Mar 21, 2026
cd176de
Playground - fix fake server: return totalCount for remote ops, ISO d…
aleksei-semikozov Mar 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/pr-storybook-deploy-manual.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@ jobs:
run: |
pnpx nx build devextreme-react-storybook

- name: Build playground (static)
if: inputs.action == 'deploy'
run: |
cd packages/devextreme
pnpm run build:playground

- name: Copy playground into Storybook output
if: inputs.action == 'deploy'
run: |
cp -r packages/devextreme/dist/playground apps/react-storybook/storybook-static/playground

- name: Deploy/remove PR preview
uses: rossjrw/pr-preview-action@ffa7509e91a3ec8dfc2e5536c4d5c1acdf7a6de9 # v1.8.1
with:
Expand Down
255 changes: 255 additions & 0 deletions packages/devextreme/build/vite-plugin-demo-html.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
import fs from 'fs';
import path from 'path';
import type { PluginOption, ViteDevServer } from 'vite';

const demosRoot = path.resolve(__dirname, '../../../apps/demos/Demos');
const demosImagesRoot = path.resolve(__dirname, '../../../apps/demos/images');
const demosDataRoot = path.resolve(__dirname, '../../../apps/demos/data');
const demosSharedRoot = path.resolve(__dirname, '../../../apps/demos/shared');
const mustacheRoot = path.resolve(__dirname, '../../../apps/demos/node_modules/mustache');
const vectormapDataRoot = path.resolve(__dirname, '../artifacts/js/vectormap-data');
const menuMetaPath = path.resolve(__dirname, '../../../apps/demos/menuMeta.json');

type DemoEntry = { title: string; name: string; files: string[] };
type DemosMap = Record<string, DemoEntry[]>;

const DEMO_FILE_EXTENSIONS = ['.html', '.js', '.css', '.json'];

function getDemoFiles(jqueryDir: string): string[] {
if (!fs.existsSync(jqueryDir)) return [];
return fs.readdirSync(jqueryDir)
.filter((f) => DEMO_FILE_EXTENSIONS.includes(path.extname(f)))
.sort();
}

function buildDemosMap(): DemosMap {
const result: DemosMap = {};
const menuMeta: unknown[] = JSON.parse(fs.readFileSync(menuMetaPath, 'utf-8'));

function traverse(groups: unknown[]): void {
for (const group of groups as Array<{ Groups?: unknown[]; Demos?: Array<{ Title: string; Name: string; Widget?: string }> }>) {
if (group.Demos) {
for (const demo of group.Demos) {
if (!demo.Widget || !demo.Name) continue;
const jqueryDir = path.join(demosRoot, demo.Widget, demo.Name, 'jQuery');
if (!fs.existsSync(path.join(jqueryDir, 'index.html'))) continue;
if (!result[demo.Widget]) result[demo.Widget] = [];
result[demo.Widget].push({ title: demo.Title, name: demo.Name, files: getDemoFiles(jqueryDir) });
}
}
if (group.Groups) traverse(group.Groups);
}
}

traverse(menuMeta);
return result;
}

function transformDemoHtml(html: string): string {
const relativeScripts: string[] = [];

const scriptRe = /<script\s+src="([^"]+)"\s*><\/script>/gi;
let m: RegExpExecArray | null;
while ((m = scriptRe.exec(html)) !== null) {
if (!m[1].includes('node_modules')) {
relativeScripts.push(m[1]);
}
}

const loaderScript = `<script type="module">
import '/demo-init.ts';
const srcs = ${JSON.stringify(relativeScripts)};
for (const src of srcs) {
await new Promise((resolve) => {
const s = document.createElement('script');
s.src = src;
s.onload = resolve;
document.body.appendChild(s);
});
}
</script>`;

return html
.replace(/<script[^>]+node_modules[^>]*><\/script>/gi, '')
.replace(/<link[^>]+devextreme-dist[^>]*>/gi, '')
.replace(/<script\s+src="(?!http)[^"]*\.js"[^>]*><\/script>/gi, '')
.replace(/<\/body>/, `${loaderScript}\n</body>`)
.replace(/<head>/, `<head>\n <script type="module" src="/@vite/client"></script>`);
}

function transformDemoHtmlForBuild(html: string, existingFiles?: Set<string>): string {
const relativeScripts: string[] = [];

const scriptRe = /<script\s+src="([^"]+)"\s*><\/script>/gi;
let m: RegExpExecArray | null;
while ((m = scriptRe.exec(html)) !== null) {
const src = m[1];
if (src.includes('node_modules')) {
const vmMatch = src.match(/\/vectormap-data\/([^"]+)/);
if (vmMatch) {
relativeScripts.push(`../../../vectormap-data/${vmMatch[1]}`);
continue;
}
const mustacheMatch = src.match(/\/mustache\/(mustache(?:\.min)?\.js)/);
if (mustacheMatch) {
relativeScripts.push(`../../../mustache/${mustacheMatch[1]}`);
continue;
}
} else if (!existingFiles || existingFiles.has(path.basename(src))) {
relativeScripts.push(src);
}
}

const indexIdx = relativeScripts.indexOf('index.js');
if (indexIdx !== -1) {
relativeScripts.splice(indexIdx, 1);
relativeScripts.push('index.js');
}

const loaderScript = `<script type="module">
import '../../../demo-init.js';
const srcs = ${JSON.stringify(relativeScripts)};
for (const src of srcs) {
await new Promise((resolve) => {
const s = document.createElement('script');
s.src = src;
s.onload = resolve;
document.body.appendChild(s);
});
}
</script>`;

return html
.replace(/<script[^>]+node_modules[^>]*><\/script>/gi, '')
.replace(/<link[^>]+(devextreme-dist|node_modules)[^>]*>/gi, '')
.replace(/<script\s+src="(?!http)[^"]*\.js"[^>]*><\/script>/gi, '')
.replace(/<\/body>/, `${loaderScript}\n</body>`)
.replaceAll('../../../../', '../../../');
}

function serveFile(res: import('http').ServerResponse, filePath: string): boolean {
if (!fs.existsSync(filePath)) return false;
const ext = path.extname(filePath);
const contentTypes: Record<string, string> = {
'.html': 'text/html; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.webp': 'image/webp',
};
const content = fs.readFileSync(filePath);
res.setHeader('Content-Type', contentTypes[ext] ?? 'application/octet-stream');
res.end(ext === '.html' ? transformDemoHtml(content.toString('utf-8')) : content);
return true;
}

export default function demoHtmlPlugin(): PluginOption {
const demosMap = buildDemosMap();
const VIRTUAL_ID = 'virtual:demos-meta';
const RESOLVED_ID = `\0${VIRTUAL_ID}`;
let isBuild = false;

return {
name: 'devextreme-demo-html',

configResolved(config: { command: string }) {
isBuild = config.command === 'build';
},

resolveId(id: string) {
if (id === VIRTUAL_ID) return RESOLVED_ID;
return null;
},

load(id: string) {
if (id === RESOLVED_ID) {
const root = isBuild ? '' : demosRoot;
return `export default ${JSON.stringify({ demosRoot: root, demos: demosMap })}`;
}
return null;
},

writeBundle(options: { dir?: string }) {
const outDir = options.dir ?? 'dist';
const demosOut = path.join(outDir, 'demos');

if (fs.existsSync(demosImagesRoot)) {
fs.cpSync(demosImagesRoot, path.join(outDir, 'images'), { recursive: true });
}
if (fs.existsSync(demosDataRoot)) {
fs.cpSync(demosDataRoot, path.join(outDir, 'data'), { recursive: true });
}
if (fs.existsSync(demosSharedRoot)) {
fs.cpSync(demosSharedRoot, path.join(outDir, 'shared'), { recursive: true });
}
if (fs.existsSync(vectormapDataRoot)) {
fs.cpSync(vectormapDataRoot, path.join(outDir, 'vectormap-data'), { recursive: true });
}
if (fs.existsSync(mustacheRoot)) {
fs.mkdirSync(path.join(outDir, 'mustache'), { recursive: true });
const mustacheFile = path.join(mustacheRoot, 'mustache.min.js');
if (fs.existsSync(mustacheFile)) {
fs.copyFileSync(mustacheFile, path.join(outDir, 'mustache', 'mustache.min.js'));
}
}

for (const [widget, demos] of Object.entries(demosMap)) {
for (const { name } of demos) {
const jqueryDir = path.join(demosRoot, widget, name, 'jQuery');
if (!fs.existsSync(jqueryDir)) continue;

const demoOut = path.join(demosOut, widget, name);
fs.mkdirSync(demoOut, { recursive: true });

for (const file of fs.readdirSync(jqueryDir)) {
const src = path.join(jqueryDir, file);
const dest = path.join(demoOut, file);
const ext = path.extname(file);
if (ext === '.html') {
const existingFiles = new Set(fs.readdirSync(jqueryDir));
fs.writeFileSync(dest, transformDemoHtmlForBuild(fs.readFileSync(src, 'utf-8'), existingFiles));
} else if (ext === '.js' || ext === '.css') {
fs.writeFileSync(dest, fs.readFileSync(src, 'utf-8').replaceAll('../../../../', '../../../'));
} else {
fs.copyFileSync(src, dest);
}
}
}
}
},

configureServer(server: ViteDevServer) {
server.watcher.add(path.join(demosRoot, '**', 'jQuery', '*.{js,css,html}'));

server.middlewares.use('/images', (req, res, next) => {
const filePath = path.join(demosImagesRoot, decodeURIComponent(req.url ?? '/'));
if (serveFile(res, filePath)) return;
next();
});

server.middlewares.use('/demos', (req, res, next) => {
const urlPath = decodeURIComponent(req.url ?? '/');
const segments = urlPath.replace(/^\//, '').split('/').filter(Boolean);

if (segments.length < 2) { next(); return; }

const [widget, name, ...rest] = segments;
const jqueryDir = path.join(demosRoot, widget, name, 'jQuery');

if (rest.length === 0 || urlPath.endsWith('/')) {
if (serveFile(res, path.join(jqueryDir, 'index.html'))) return;
} else {
const file = rest.join('/');
if (serveFile(res, path.join(jqueryDir, file))) return;
}

next();
});
},
};
}
Loading
Loading