From 5cb93c17010dbb5eabb3fff71250bea13b211043 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Fri, 24 Jul 2026 16:36:08 -0700 Subject: [PATCH] fix(sandbox): prevent css causing full app refresh (#1869) --- build/plugins/copy-css-plugin.ts | 65 +++++++++++++++++++++++++------- build/plugins/types.ts | 1 + package.json | 2 +- packages/react/tsdown.config.ts | 2 +- packages/skins/tsdown.config.ts | 23 +++-------- 5 files changed, 60 insertions(+), 33 deletions(-) diff --git a/build/plugins/copy-css-plugin.ts b/build/plugins/copy-css-plugin.ts index cf048de5..321aec9a 100644 --- a/build/plugins/copy-css-plugin.ts +++ b/build/plugins/copy-css-plugin.ts @@ -1,4 +1,4 @@ -import { globSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { existsSync, globSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { resolveImports } from './resolve-css-imports.ts'; import type { BuildPlugin } from './types.ts'; @@ -6,29 +6,68 @@ import type { BuildPlugin } from './types.ts'; interface CopyCssPluginOptions { skinsDir: string; outDir: string; + inline?: boolean; + rebuild?: boolean; } export function copyCssPlugin(options: CopyCssPluginOptions): BuildPlugin { - const { skinsDir, outDir } = options; + const { skinsDir, outDir, inline = true, rebuild = true } = options; + let poll: ReturnType | undefined; + let state = new Map(); + + function getCssFiles() { + return new Set([...globSync('src/**/*.css'), ...globSync(join(skinsDir, '**/*.css'))]); + } + + function getState() { + return new Map( + [...getCssFiles()].map((file) => { + const { mtimeMs, size } = statSync(file); + return [file, `${mtimeMs}:${size}`]; + }) + ); + } + + function writeCss() { + for (const file of globSync('src/**/*.css')) { + const content = readFileSync(file, 'utf-8'); + const output = inline ? resolveImports(content, dirname(file), skinsDir) : content; + const outFile = join(outDir, file.replace(/^src\//, '')); + if (existsSync(outFile) && readFileSync(outFile, 'utf-8') === output) continue; + mkdirSync(dirname(outFile), { recursive: true }); + writeFileSync(outFile, output); + } + } + + function checkCss() { + try { + const next = getState(); + if (next.size === state.size && [...next].every(([file, value]) => state.get(file) === value)) return; + writeCss(); + state = next; + } catch (error) { + console.error(error); + } + } return { name: 'copy-css', buildStart() { - for (const file of globSync('src/**/*.css')) { - this.addWatchFile(file); + if (!rebuild && process.argv.includes('--watch')) { + if (!poll) { + state = getState(); + poll = setInterval(checkCss, 100); + } + return; } - for (const file of globSync(join(skinsDir, '**/*.css'))) { + + for (const file of getCssFiles()) { this.addWatchFile(file); } }, - writeBundle() { - for (const file of globSync('src/**/*.css')) { - const content = readFileSync(file, 'utf-8'); - const resolved = resolveImports(content, dirname(file), skinsDir); - const outFile = join(outDir, file.replace(/^src\//, '')); - mkdirSync(dirname(outFile), { recursive: true }); - writeFileSync(outFile, resolved); - } + writeBundle: writeCss, + closeWatcher() { + clearInterval(poll); }, }; } diff --git a/build/plugins/types.ts b/build/plugins/types.ts index 9d284175..65f5bb82 100644 --- a/build/plugins/types.ts +++ b/build/plugins/types.ts @@ -14,4 +14,5 @@ export interface BuildPlugin { load?: (this: void, id: string) => { code: string; moduleSideEffects: boolean } | null; buildStart?: (this: { addWatchFile: (file: string) => void }) => void; writeBundle?: (this: void) => void; + closeWatcher?: (this: void) => void; } diff --git a/package.json b/package.json index f49850ea..bad24498 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "dev:site": "turbo run dev --filter=site...", "dev": "turbo run dev", "dev:packages": "turbo run dev --filter='./packages/*'", - "dev:sandbox": "turbo run dev --filter=@videojs/sandbox...", + "dev:sandbox": "turbo run build --filter=@videojs/sandbox^... && turbo run dev --filter=@videojs/sandbox... --only", "lint": "biome check .", "lint:fix": "biome check . --write", "lint:fix:file": "biome check --write", diff --git a/packages/react/tsdown.config.ts b/packages/react/tsdown.config.ts index bf1a71b3..ed061316 100644 --- a/packages/react/tsdown.config.ts +++ b/packages/react/tsdown.config.ts @@ -26,7 +26,7 @@ const createConfig = (mode: PackageBuildMode): UserConfig => ({ alias: { '@': new URL('./src', import.meta.url).pathname, }, - plugins: [copyCssPlugin({ skinsDir, outDir: `dist/${mode}` })], + plugins: [copyCssPlugin({ skinsDir, outDir: `dist/${mode}`, rebuild: false })], }); export default defineConfig(packageBuildModes.map((mode) => createConfig(mode))); diff --git a/packages/skins/tsdown.config.ts b/packages/skins/tsdown.config.ts index 284e32e3..4f954c2f 100644 --- a/packages/skins/tsdown.config.ts +++ b/packages/skins/tsdown.config.ts @@ -1,8 +1,12 @@ import { globSync } from 'node:fs'; +import { resolve } from 'node:path'; import type { UserConfig } from 'tsdown'; import { defineConfig } from 'tsdown'; +import { copyCssPlugin } from '../../build/plugins/copy-css-plugin.ts'; import { type PackageBuildMode, packageBuildConfig, packageBuildModes } from '../../build/tsdown.ts'; +const skinsDir = resolve('src'); + const entries = Object.fromEntries( globSync('src/**/*.tailwind.ts').map((file) => { const key = file.replace('src/', '').replace('.ts', ''); @@ -13,24 +17,7 @@ const entries = Object.fromEntries( const createConfig = (mode: PackageBuildMode): UserConfig => ({ ...packageBuildConfig(mode, 'browser'), entry: entries, - copy: [ - { - from: 'src/**/*.css', - to: `dist/${mode}`, - flatten: false, - }, - ], - plugins: [ - { - name: 'watch-css', - buildStart() { - const cssFiles = globSync('src/**/*.css'); - for (const file of cssFiles) { - this.addWatchFile(file); - } - }, - }, - ], + plugins: [copyCssPlugin({ skinsDir, outDir: `dist/${mode}`, inline: false, rebuild: false })], }); export default defineConfig(packageBuildModes.map((mode) => createConfig(mode)));