Files
Rotimi Best 367ddcddd7 feat(embeds): add embed.classroomio.com public widget host
Serve widgets from short public URLs via tenant-router with R2-backed
assets and runtime API_URL injection, so copy-paste snippets hide infra
paths. Update widget payloads, CSP, upload purge URLs, and dev-only API
inject; document format/stage workflow in AGENTS.md and fix ui-prefix-check
YAML syntax.
2026-06-12 02:06:37 +01:00

136 lines
4.4 KiB
TypeScript

import path from 'node:path';
import { fileURLToPath } from 'node:url';
import tailwindcss from '@tailwindcss/vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { defineConfig } from 'vite';
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';
const dirname = path.dirname(fileURLToPath(import.meta.url));
const devApiUrl = process.env.API_URL ?? 'http://localhost:3002';
function embedApiUrlInjectPlugin() {
const bootstrapFiles = [
path.resolve(dirname, 'src/widgets/course-widget/bootstrap.ts'),
path.resolve(dirname, 'src/widgets/question-type-picker/bootstrap.ts')
];
return {
name: 'embed-api-url-inject',
transform(code: string, id: string) {
if (!bootstrapFiles.includes(id) || !devApiUrl) {
return null;
}
const snippet = `window.CIO=window.CIO||{};window.CIO.apiBaseUrl=${JSON.stringify(devApiUrl.replace(/\/$/, ''))};`;
return { code: `${snippet}\n${code}`, map: null };
}
};
}
const embedEntries = {
'question-type-picker': path.resolve(dirname, 'src/widgets/question-type-picker/bootstrap.ts'),
'course-widget': path.resolve(dirname, 'src/widgets/course-widget/bootstrap.ts')
} as const;
const embedDevEntrypoints = {
'/question-type-picker': '/src/widgets/question-type-picker/bootstrap.ts',
'/course-widget': '/src/widgets/course-widget/bootstrap.ts',
'/embeds/question-type-picker/question-type-picker.js': '/src/widgets/question-type-picker/bootstrap.ts',
'/embeds/course-widget/course-widget.js': '/src/widgets/course-widget/bootstrap.ts',
'/embeds/course-widget/embed.html': '/src/widgets/course-widget/embed.html'
} as const;
function embedDevEntryPlugin() {
return {
name: 'embed-dev-entry-plugin',
configureServer(server: {
middlewares: { use: (handler: (req: { url?: string }, res: unknown, next: () => void) => void) => void };
}) {
server.middlewares.use((req, _res, next) => {
const requestUrl = req.url ?? '';
const [pathname, search = ''] = requestUrl.split('?');
const querySuffix = search ? `?${search}` : '';
if (pathname === '/course-widget') {
const params = new URLSearchParams(search);
const target = params.has('key')
? '/src/widgets/course-widget/embed.html'
: '/src/widgets/course-widget/bootstrap.ts';
req.url = `${target}${querySuffix}`;
next();
return;
}
if (pathname === '/question-type-picker') {
req.url = `/src/widgets/question-type-picker/bootstrap.ts${querySuffix}`;
next();
return;
}
const rewrittenPath = embedDevEntrypoints[pathname as keyof typeof embedDevEntrypoints];
if (rewrittenPath) {
req.url = `${rewrittenPath}${querySuffix}`;
}
next();
});
}
};
}
export default defineConfig(({ command, mode }) => {
const buildTarget = mode in embedEntries ? (mode as keyof typeof embedEntries) : null;
const buildEntry = buildTarget ? { [buildTarget]: embedEntries[buildTarget] } : embedEntries;
return {
root: dirname,
plugins: [
embedDevEntryPlugin(),
...(command === 'serve' ? [embedApiUrlInjectPlugin()] : []),
tailwindcss(),
svelte(),
cssInjectedByJsPlugin()
],
resolve: {
alias: {
'@cio/ui': path.resolve(dirname, 'node_modules/@cio/ui/src'),
'@cio/ui/*': path.resolve(dirname, 'node_modules/@cio/ui/src/*'),
$src: path.resolve(dirname, 'node_modules/@cio/ui/src')
},
conditions: ['svelte', 'browser', 'import']
},
optimizeDeps: {
include: ['svelte-tiptap'],
exclude: ['@tiptap/core', '@tiptap/pm']
},
build:
command === 'build'
? {
outDir: buildTarget ? `dist/${buildTarget}` : 'dist',
emptyOutDir: false,
cssCodeSplit: false,
lib: {
entry: buildEntry,
name: 'CIOEmbeds',
formats: ['es'],
fileName: (_format, entryName) => `${entryName}.js`
},
rollupOptions: {
output: {
inlineDynamicImports: false,
chunkFileNames: buildTarget ? `${buildTarget}-[name]-[hash].js` : '[name]-[hash].js'
}
}
}
: {},
server: {
port: 5180,
cors: true
},
preview: {
port: 4180,
cors: true
}
};
});