fix(packages): add server-only bundles (#1349)

This commit is contained in:
rahim
2026-04-16 00:59:14 -07:00
committed by GitHub
parent 4ecc870685
commit 3331fdaf25
60 changed files with 654 additions and 215 deletions
+55 -22
View File
@@ -23,56 +23,86 @@
"exports": {
".": {
"types": "./dist/dev/index.d.ts",
"development": "./dist/dev/index.js",
"default": "./dist/default/index.js"
"browser": {
"development": "./dist/dev/index.js",
"default": "./dist/default/index.js"
},
"default": "./dist/server/index.js"
},
"./video": {
"types": "./dist/dev/presets/video.d.ts",
"development": "./dist/dev/presets/video.js",
"default": "./dist/default/presets/video.js"
"browser": {
"development": "./dist/dev/presets/video.js",
"default": "./dist/default/presets/video.js"
},
"default": "./dist/server/presets/video.js"
},
"./video/*.css": "./dist/default/define/video/*.css",
"./video/*": {
"types": "./dist/dev/define/video/*.d.ts",
"development": "./dist/dev/define/video/*.js",
"default": "./dist/default/define/video/*.js"
"browser": {
"development": "./dist/dev/define/video/*.js",
"default": "./dist/default/define/video/*.js"
},
"default": "./dist/server/define/video/*.js"
},
"./audio": {
"types": "./dist/dev/presets/audio.d.ts",
"development": "./dist/dev/presets/audio.js",
"default": "./dist/default/presets/audio.js"
"browser": {
"development": "./dist/dev/presets/audio.js",
"default": "./dist/default/presets/audio.js"
},
"default": "./dist/server/presets/audio.js"
},
"./audio/*.css": "./dist/default/define/audio/*.css",
"./audio/*": {
"types": "./dist/dev/define/audio/*.d.ts",
"development": "./dist/dev/define/audio/*.js",
"default": "./dist/default/define/audio/*.js"
"browser": {
"development": "./dist/dev/define/audio/*.js",
"default": "./dist/default/define/audio/*.js"
},
"default": "./dist/server/define/audio/*.js"
},
"./background": {
"types": "./dist/dev/presets/background.d.ts",
"development": "./dist/dev/presets/background.js",
"default": "./dist/default/presets/background.js"
"browser": {
"development": "./dist/dev/presets/background.js",
"default": "./dist/default/presets/background.js"
},
"default": "./dist/server/presets/background.js"
},
"./background/*.css": "./dist/default/define/background/*.css",
"./background/*": {
"types": "./dist/dev/define/background/*.d.ts",
"development": "./dist/dev/define/background/*.js",
"default": "./dist/default/define/background/*.js"
"browser": {
"development": "./dist/dev/define/background/*.js",
"default": "./dist/default/define/background/*.js"
},
"default": "./dist/server/define/background/*.js"
},
"./ui/*": {
"types": "./dist/dev/define/ui/*.d.ts",
"development": "./dist/dev/define/ui/*.js",
"default": "./dist/default/define/ui/*.js"
"browser": {
"development": "./dist/dev/define/ui/*.js",
"default": "./dist/default/define/ui/*.js"
},
"default": "./dist/server/define/ui/*.js"
},
"./feature/*": {
"types": "./dist/dev/define/feature/*.d.ts",
"development": "./dist/dev/define/feature/*.js",
"default": "./dist/default/define/feature/*.js"
"browser": {
"development": "./dist/dev/define/feature/*.js",
"default": "./dist/default/define/feature/*.js"
},
"default": "./dist/server/define/feature/*.js"
},
"./media/*": {
"types": "./dist/dev/define/media/*.d.ts",
"development": "./dist/dev/define/media/*.js",
"default": "./dist/default/define/media/*.js"
"browser": {
"development": "./dist/dev/define/media/*.js",
"default": "./dist/default/define/media/*.js"
},
"default": "./dist/server/define/media/*.js"
},
"./cdn/media/*": {
"types": "./cdn/media/*.dev.d.ts",
@@ -87,8 +117,11 @@
"./*.css": "./dist/default/define/*.css",
"./*": {
"types": "./dist/dev/define/*.d.ts",
"development": "./dist/dev/define/*.js",
"default": "./dist/default/define/*.js"
"browser": {
"development": "./dist/dev/define/*.js",
"default": "./dist/default/define/*.js"
},
"default": "./dist/server/define/*.js"
}
},
"scripts": {
+3 -3
View File
@@ -2,8 +2,8 @@ type DefinableElement = CustomElementConstructor & { tagName: string };
/** Define a custom element only if not already registered. */
export function safeDefine(element: DefinableElement): void {
const registry = globalThis.customElements;
if (!registry || registry.get(element.tagName)) return;
if (!__BROWSER__) return;
if (customElements.get(element.tagName)) return;
registry.define(element.tagName, element);
customElements.define(element.tagName, element);
}
@@ -1,11 +1,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { describe, expect, it } from 'vitest';
import { safeDefine } from '../safe-define';
describe('safeDefine', () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it('registers a custom element', () => {
class TestElement extends HTMLElement {
static tagName = 'test-sd-register';
@@ -37,14 +33,4 @@ describe('safeDefine', () => {
safeDefine(Replacement);
expect(customElements.get('test-sd-no-replace')).toBe(Original);
});
it('does nothing when customElements is unavailable', () => {
vi.stubGlobal('customElements', undefined);
class TestElement extends HTMLElement {
static tagName = 'test-sd-ssr';
}
expect(() => safeDefine(TestElement)).not.toThrow();
});
});
@@ -1,21 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
describe('SSR-safe define imports', () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.resetModules();
});
it('imports video skin without browser-only globals', async () => {
vi.stubGlobal('customElements', undefined);
vi.stubGlobal('CSSStyleSheet', undefined);
await expect(import('../video/skin')).resolves.toBeDefined();
});
it('imports simple-hls-video without customElements', async () => {
vi.stubGlobal('customElements', undefined);
await expect(import('../media/simple-hls-video')).resolves.toBeDefined();
});
});
+1
View File
@@ -1,4 +1,5 @@
declare const __DEV__: boolean;
declare const __BROWSER__: boolean;
declare module '*.css' {
const content: string;
+1
View File
@@ -89,6 +89,7 @@ for (const mode of buildModes) {
},
define: {
__DEV__: isProd ? 'false' : 'true',
__BROWSER__: 'true',
},
plugins: [
inlineCssPlugin({ skinsDir, minify: isProd }),
+54 -11
View File
@@ -1,4 +1,4 @@
import { globSync } from 'node:fs';
import { globSync, readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { UserConfig } from 'tsdown';
@@ -7,12 +7,51 @@ import { copyCssPlugin } from '../../build/plugins/copy-css-plugin.ts';
import { inlineCssPlugin } from '../../build/plugins/inline-css-plugin.ts';
import { inlineTemplatePlugin } from '../../build/plugins/inline-template-plugin.ts';
type BuildMode = 'dev' | 'default';
type BuildMode = 'dev' | 'default' | 'server';
const buildModes: BuildMode[] = ['dev', 'default'];
const buildModes: BuildMode[] = ['dev', 'default', 'server'];
const isServer = (mode: BuildMode) => mode === 'server';
const skinsDir = resolve(dirname(fileURLToPath(import.meta.url)), '../skins/src');
/** Stub `*.css?inline` imports with empty strings for the server build. */
function stubCssInlinePlugin() {
return {
name: 'stub-css-inline',
resolveId(source: string) {
if (source.endsWith('.css?inline')) return { id: `\0stub-css:${source}`, external: false };
},
load(id: string) {
if (id.startsWith('\0stub-css:')) return 'export default ""';
},
};
}
/**
* Stub all `define/*` modules for the server build.
*
* Reads each source file, extracts `export class` names, and emits stub
* classes so that preset modules can still re-export them by name.
* Everything else (custom element registration, templates, styles) is dropped.
*/
function stubDefinePlugin() {
const defineDir = resolve(dirname(fileURLToPath(import.meta.url)), 'src/define');
return {
name: 'stub-define',
load(id: string) {
if (!id.startsWith(defineDir) || id === defineDir) return;
const source = readFileSync(id, 'utf8');
const classNames = [...source.matchAll(/export class (\w+)/g)].map((m) => m[1]);
if (classNames.length === 0) return 'export {}';
return classNames.map((name) => `export class ${name} {}`).join('\n');
},
};
}
const defineEntries = Object.fromEntries(
globSync('src/define/**/*.ts')
.filter((file) => !file.includes('.test.'))
@@ -35,32 +74,36 @@ const createConfig = (mode: BuildMode): UserConfig => ({
...defineEntries,
...presetEntries,
},
platform: 'browser',
platform: isServer(mode) ? 'node' : 'browser',
format: 'es',
sourcemap: true,
clean: true,
hash: false,
unbundle: true,
outExtensions: isServer(mode) ? () => ({ js: '.js', dts: '.d.ts' }) : undefined,
treeshake: {
// The sideEffects field in package.json uses dist paths, but the build
// runs against source. Ensure define/* modules (which register custom
// elements as a side effect) are never tree-shaken from skin bundles.
moduleSideEffects: [{ test: /\/define\//, sideEffects: true }],
},
noExternal: [/^@videojs\/icons/, /^@videojs\/skins/],
noExternal: isServer(mode) ? [] : [/^@videojs\/icons/, /^@videojs\/skins/],
alias: {
'@': new URL('./src', import.meta.url).pathname,
},
outDir: `dist/${mode}`,
define: {
__DEV__: mode === 'dev' ? 'true' : 'false',
__DEV__: mode === 'dev' || isServer(mode) ? 'true' : 'false',
__BROWSER__: isServer(mode) ? 'false' : 'true',
},
dts: mode === 'dev',
plugins: [
copyCssPlugin({ skinsDir, outDir: `dist/${mode}` }),
inlineCssPlugin({ skinsDir, minify: mode !== 'dev' }),
inlineTemplatePlugin({ minify: mode !== 'dev' }),
],
plugins: isServer(mode)
? [stubCssInlinePlugin(), stubDefinePlugin()]
: [
copyCssPlugin({ skinsDir, outDir: `dist/${mode}` }),
inlineCssPlugin({ skinsDir, minify: mode !== 'dev' }),
inlineTemplatePlugin({ minify: mode !== 'dev' }),
],
});
export default defineConfig(buildModes.map((mode) => createConfig(mode)));
+3 -1
View File
@@ -3,10 +3,12 @@ import { defineConfig } from 'vitest/config';
export default defineConfig({
define: {
__DEV__: 'true',
__BROWSER__: 'true',
},
test: {
include: ['src/**/*.test.ts'],
passWithNoTests: true,
onConsoleLog: (log) => !log.includes('Lit is in dev mode'),
onConsoleLog: (log: string) => !log.includes('Lit is in dev mode'),
environment: 'happy-dom',
},
});