tldraw/apps/examples/vite.config.ts
alex 64e1fac897
[5/5] Move bemo from dotcom to examples (#4135)
Move our bemo playground from dotcom to the examples app. In preparation
for more multiplayer examples, I built our a little bit of chrome around
example room IDs: if you create an example with `multiplayer: true`, the
examples app will render a little room ID picker above your example. The
room IDs are scoped to each example, and each deploy of the examples
app. By default people on the same example will be in the same room, but
the default ID changes every hour.

As I was doing this, I noticed you could get an ugly situation where the
docs site was in dark mode, tldraw was in dark mode, but the little bit
of examples chrome was in light mode. To fix this I through together an
extremely rough dark mode for the examples which switches on whenever
the tldraw instance inside is in dark mode.

### Change type

- [x] `other`

---------

Co-authored-by: David Sheldrick <d.j.sheldrick@gmail.com>
2024-07-12 10:36:31 +00:00

182 lines
5.5 KiB
TypeScript

import react from '@vitejs/plugin-react-swc'
import path from 'path'
import { PluginOption, defineConfig } from 'vite'
const PR_NUMBER = process.env.VERCEL_GIT_PULL_REQUEST_ID
function getEnv() {
if (!process.env.VERCEL_ENV) {
return 'development'
}
if (PR_NUMBER !== undefined && PR_NUMBER !== '') {
return 'preview'
}
if (process.env.VERCEL_ENV === 'production') {
return 'production'
}
return 'canary'
}
const env = getEnv()
// eslint-disable-next-line no-console
console.log('build env:', env)
function urlOrLocalFallback(mode: string, url: string | undefined, localFallbackPort: number) {
if (url) {
return JSON.stringify(url)
}
if (mode === 'development') {
// in dev, vite lets us inline javascript expressions - so we return a template string that
// will be evaluated on the client
return '`http://${location.hostname}:' + localFallbackPort + '`'
} else {
// in production, we have to fall back to a hardcoded value
return JSON.stringify(`http://localhost:${localFallbackPort}`)
}
}
const TLDRAW_BEMO_URL_STRING =
env === 'production'
? '"https://demo.tldraw.xyz"'
: env === 'canary'
? '"https://canary-demo.tldraw.xyz"'
: PR_NUMBER
? `"https://pr-${PR_NUMBER}-demo.tldraw.xyz"`
: undefined
export default defineConfig(({ mode }) => ({
plugins: [react({ tsDecorators: true }), exampleReadmePlugin()],
root: path.join(__dirname, 'src'),
publicDir: path.join(__dirname, 'public'),
build: {
outDir: path.join(__dirname, 'dist'),
assetsInlineLimit: 0,
target: 'es2022',
},
esbuild: {
target: 'es2022',
},
server: {
port: 5420,
},
clearScreen: false,
optimizeDeps: {
exclude: ['@tldraw/assets'],
esbuildOptions: {
target: 'es2022',
},
},
define: {
'process.env.TLDRAW_ENV': JSON.stringify(process.env.VERCEL_ENV ?? 'development'),
'process.env.TLDRAW_DEPLOY_ID': JSON.stringify(
process.env.VERCEL_GIT_COMMIT_SHA ?? `local-${Date.now()}`
),
'process.env.TLDRAW_BEMO_URL': urlOrLocalFallback(mode, TLDRAW_BEMO_URL_STRING, 8989),
'process.env.TLDRAW_IMAGE_URL': urlOrLocalFallback(
mode,
env === 'development' ? undefined : 'https://images.tldraw.xyz',
8989
),
},
}))
function exampleReadmePlugin(): PluginOption {
return {
name: 'example-readme',
async transform(src, id) {
const match = id.match(/examples\/src\/examples\/(.*)\/README.md$/)
if (!match) return
const remark = (await import('remark')).remark
const remarkFrontmatter = (await import('remark-frontmatter')).default
const remarkHtml = (await import('remark-html')).default
const matter = (await import('vfile-matter')).matter
const file = await remark()
.use(remarkFrontmatter)
.use(remarkHtml)
.use(() => (_, file) => matter(file))
.process(src)
const frontmatter = parseFrontMatter(file.data.matter, id)
const separator = '\n<hr>\n'
const parts = String(file).split(separator)
const description = parts[0]
const details = parts.slice(1).join(separator)
const path = `/${match[1]}`
const codeUrl = `https://github.com/tldraw/tldraw/tree/main/apps/examples/src/examples${path}`
const result = [
`export const title = ${JSON.stringify(frontmatter.title)};`,
`export const priority = ${JSON.stringify(frontmatter.priority ?? '100000')};`,
`export const category = ${JSON.stringify(frontmatter.category)};`,
`export const hide = ${JSON.stringify(frontmatter.hide)};`,
`export const multiplayer = ${JSON.stringify(frontmatter.multiplayer)};`,
`export const description = ${JSON.stringify(description)};`,
`export const details = ${JSON.stringify(details)};`,
`export const codeUrl = ${JSON.stringify(codeUrl)};`,
`export const path = ${JSON.stringify(path)};`,
`export const componentFile = ${JSON.stringify(frontmatter.component)};`,
`import {lazy} from 'react';`,
`export const loadComponent = async () => {`,
` return (await import(${JSON.stringify(frontmatter.component)})).default;`,
`};`,
`export const keywords = ${JSON.stringify(frontmatter.keywords)};`,
]
return result.join('\n')
},
}
}
function parseFrontMatter(data: unknown, fileName: string) {
if (!data || typeof data !== 'object') {
throw new Error(`Frontmatter missing in ${fileName}`)
}
if (!('title' in data && typeof data.title === 'string')) {
throw new Error(`Frontmatter key 'title' must be string in ${fileName}`)
}
if (!('component' in data && typeof data.component === 'string')) {
throw new Error(`Frontmatter key 'component' must be string in ${fileName}`)
}
const priority = 'priority' in data ? data.priority : 999999
if (typeof priority !== 'number') {
throw new Error(`Frontmatter key 'priority' must be number in ${fileName}`)
}
const category = 'category' in data ? data.category : null
if (typeof category !== 'string') {
throw new Error(`Frontmatter key 'category' must be string in ${fileName}`)
}
const hide = 'hide' in data ? data.hide : false
if (hide !== false && hide !== true) {
throw new Error(`Frontmatter key 'hide' must be boolean in ${fileName}`)
}
const keywords = 'keywords' in data ? data.keywords : []
if (!Array.isArray(keywords)) {
throw new Error(`Frontmatter key 'keywords' must be array in ${fileName}`)
}
const multiplayer = 'multiplayer' in data ? data.multiplayer : false
if (typeof multiplayer !== 'boolean') {
throw new Error(`Frontmatter key 'multiplayer' must be boolean in ${fileName}`)
}
return {
title: data.title,
component: data.component,
priority,
category,
hide,
keywords,
multiplayer,
}
}