tldraw/apps/dotcom/scripts/build.ts
Mime Čuvalo cbf7c2c605
assets: preload fonts (#3927)
Looking at the waterfall of fonts/images/etc. we wanted the "Loading
assets..." bit to commence earlier so it's not fighting for bandwidth
with the icons loading all at the same time.

This writes to the index.html file to start preloading the fonts we
need.

### Change Type

<!--  Please select a 'Scope' label ️ -->

- [ ] `sdk` — Changes the tldraw SDK
- [x] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff

<!--  Please select a 'Type' label ️ -->

- [ ] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [x] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know


### Release Notes

- Perf: improve font loading timing on dotcom.

---------

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
2024-06-16 16:02:34 +00:00

139 lines
4.1 KiB
TypeScript

import glob from 'fast-glob'
import { mkdirSync, readFileSync, writeFileSync } from 'fs'
import { exec } from '../../../scripts/lib/exec'
import { Config } from './vercel-output-config'
import { config } from 'dotenv'
import json5 from 'json5'
import { nicelog } from '../../../scripts/lib/nicelog'
import { T } from '@tldraw/validate'
import { getMultiplayerServerURL } from '../vite.config'
const commonSecurityHeaders = {
'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload',
'X-Content-Type-Options': 'nosniff',
'Referrer-Policy': 'no-referrer-when-downgrade',
// 'Content-Security-Policy': `default-src 'unsafe-inline' data: blob: ws: *`,
}
// We load the list of routes that should be forwarded to our SPA's index.html here.
// It uses a jest snapshot file because deriving the set of routes from our
// react-router config works fine in our test environment, but is tricky to get running in this
// build script environment for various reasons (no global React, tsx being weird about decorators, etc).
function loadSpaRoutes() {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const routesJson = require('../src/__snapshots__/routes.test.tsx.snap')['the_routes 1']
const routes = T.arrayOf(
T.object({
reactRouterPattern: T.string,
vercelRouterPattern: T.string,
})
).validate(json5.parse(routesJson))
return routes.map((route) => ({
check: true,
src: route.vercelRouterPattern,
dest: '/index.html',
headers: commonSecurityHeaders,
}))
}
config({
path: './.env.local',
})
nicelog('The multiplayer server is', process.env.MULTIPLAYER_SERVER)
async function build() {
// make sure we have the latest routes
await exec('yarn', ['test', 'src/routes.test.tsx'])
const spaRoutes = loadSpaRoutes()
await exec('vite', ['build', '--emptyOutDir'])
await exec('yarn', ['run', '-T', 'sentry-cli', 'sourcemaps', 'inject', 'dist/assets'])
// Clear output static folder (in case we are running locally and have already built the app once before)
await exec('rm', ['-rf', '.vercel/output'])
mkdirSync('.vercel/output', { recursive: true })
await exec('cp', ['-r', 'dist', '.vercel/output/static'])
await exec('rm', ['-rf', ...glob.sync('.vercel/output/static/**/*.js.map')])
// Add fonts to preload into index.html
const assetsList = (await exec('ls', ['-1', 'dist/assets'])).split('\n').filter(Boolean)
const fontsToPreload = [
'Shantell_Sans-Tldrawish',
'IBMPlexSerif-Medium',
'IBMPlexSans-Medium',
'IBMPlexMono-Medium',
]
const indexHtml = await readFileSync('.vercel/output/static/index.html', 'utf8')
await writeFileSync(
'.vercel/output/static/index.html',
indexHtml.replace(
'<!-- $PRELOADED_FONTS -->',
fontsToPreload
.map(
(font) => `<link
rel="preload"
href="/assets/${assetsList.find((a) => a.startsWith(font))}"
as="font"
type="font/woff2"
/>`
)
.join('\n')
)
)
const multiplayerServerUrl = getMultiplayerServerURL() ?? 'http://localhost:8787'
writeFileSync(
'.vercel/output/config.json',
JSON.stringify(
{
version: 3,
routes: [
// rewrite api calls to the multiplayer server
{
src: '^/api(/(.*))?$',
dest: `${multiplayerServerUrl}$1`,
check: true,
},
// cache static assets immutably
{
src: '^/assets/(.*)$',
headers: {
'Cache-Control': 'public, max-age=31536000, immutable',
'X-Content-Type-Options': 'nosniff',
},
},
// server up index.html specifically because we want to include
// security headers. otherwise, it goes to the handle: 'miss'
// part below (and _not_ to the spaRoutes as maybe expected!)
{
check: true,
src: '/',
dest: '/index.html',
headers: commonSecurityHeaders,
},
// serve static files
{
handle: 'miss',
},
// finally handle SPA routing
...spaRoutes,
// react router will handle drawing the 404 page
{
check: true,
src: '.*',
dest: '/index.html',
status: 404,
headers: commonSecurityHeaders,
},
],
overrides: {},
} satisfies Config,
null,
2
)
)
}
build()