diff --git a/.agents/skills/implement-spf-use-case/references/workflow.md b/.agents/skills/implement-spf-use-case/references/workflow.md index 11c62bae90..a68b7864e5 100644 --- a/.agents/skills/implement-spf-use-case/references/workflow.md +++ b/.agents/skills/implement-spf-use-case/references/workflow.md @@ -374,7 +374,7 @@ After Step 1's report: - **HTML custom element** — `packages/html/src/media/-video/` (~5 LOC; wraps the media host in `CustomMediaElement` + `MediaAttachMixin`) + `packages/html/src/define/media/-video.ts` - + `packages/html/src/cdn/media/-video.ts` for the CDN entry. + + `packages/cdn/src/media/-video.ts` for the CDN entry. Worked example: `hls-video/media.ts` → `class HlsVideo extends MediaAttachMixin(CustomMediaElement('video', HlsVideoMedia)) {}`. - **React component** — `packages/react/src/media/-video/` @@ -444,7 +444,7 @@ typical for use-case implementations: - **HTML custom element + define entry + CDN entry** — `packages/html/src/media/-video/media.ts`, `packages/html/src/define/media/-video.ts`, - `packages/html/src/cdn/media/-video.ts`. Inline implementation; + `packages/cdn/src/media/-video.ts`. Inline implementation; ~5 LOC + boilerplate. - **React component** — `packages/react/src/media/-video/media.tsx` exposing the props surface; ~37 LOC. Inline implementation. diff --git a/.github/release-please/.release-please-manifest.json b/.github/release-please/.release-please-manifest.json index 2959da435b..76ef4d1c8e 100644 --- a/.github/release-please/.release-please-manifest.json +++ b/.github/release-please/.release-please-manifest.json @@ -1,4 +1,5 @@ { + "packages/cdn": "10.0.0-beta.32", "packages/cli": "10.0.0-beta.32", "packages/cloudflare-video": "10.0.0-beta.32", "packages/core": "10.0.0-beta.32", diff --git a/.github/release-please/release-please-config.json b/.github/release-please/release-please-config.json index f134c125ee..d270642ded 100644 --- a/.github/release-please/release-please-config.json +++ b/.github/release-please/release-please-config.json @@ -13,6 +13,7 @@ "type": "linked-versions", "groupName": "videojs", "components": [ + "@videojs/cdn", "@videojs/cli", "@videojs/cloudflare-video", "@videojs/core", @@ -43,6 +44,12 @@ } ], "packages": { + "packages/cdn": { + "component": "@videojs/cdn", + "prerelease": true, + "prerelease-type": "beta", + "versioning": "prerelease" + }, "packages/cli": { "component": "@videojs/cli", "prerelease": true, diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 6ea5fe2d14..95eedab2ab 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -24,22 +24,13 @@ jobs: id-token: write steps: - # release-please needs a token other than GITHUB_TOKEN so the release - # branch push, the release PR, and the published releases still trigger - # release-pr.yml, CI, and changelog-prose.yml - # (see https://github.com/peter-evans/create-pull-request/issues/48). - # A GitHub App installation token attributes the release PRs and tags to - # the app's bot user rather than a maintainer. - - uses: actions/create-github-app-token@v2 - id: app-token - with: - app-id: ${{ vars.RELEASE_APP_ID }} - private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - - uses: googleapis/release-please-action@v4 id: release with: - token: ${{ steps.app-token.outputs.token }} + # Use a fine grained token for release-please so this workflow + # can trigger other workflows like release-pr.yml + # see https://github.com/peter-evans/create-pull-request/issues/48 + token: ${{ secrets.V10_WORKFLOW_TRIGGER_WORKFLOW_GH_TOKEN }} config-file: .github/release-please/release-please-config.json manifest-file: .github/release-please/.release-please-manifest.json @@ -51,8 +42,8 @@ jobs: fetch-depth: 0 # Fetch all history for all tags and branches # release-please creates the tags while this job is running, so the # triggering SHA is still the commit before the release PR. Build the - # exact commit tagged for @videojs/html instead. - ref: ${{ steps.release.outputs['packages/html--sha'] }} + # exact commit tagged for @videojs/cdn instead. + ref: ${{ steps.release.outputs['packages/cdn--sha'] }} - name: Setup pnpm if: ${{ steps.release.outputs.releases_created == 'true' }} @@ -70,10 +61,9 @@ jobs: - name: Verify release checkout if: ${{ steps.release.outputs.releases_created == 'true' }} - run: | - node -e "process.exit(require('./packages/html/package.json').version === process.env.VERSION ? 0 : 1)" + run: node -e "process.exit(require('./packages/cdn/package.json').version === process.env.VERSION ? 0 : 1)" env: - VERSION: ${{ steps.release.outputs['packages/html--version'] }} + VERSION: ${{ steps.release.outputs['packages/cdn--version'] }} - name: Restore Vite Task cache if: ${{ steps.release.outputs.releases_created == 'true' }} @@ -100,11 +90,11 @@ jobs: # CDN at runtime, which would break self-hosted and archive installs. - name: Check CDN bundles are self-contained if: ${{ steps.release.outputs.releases_created == 'true' }} - run: pnpm -F @videojs/html check:cdn + run: pnpm -F @videojs/cdn check:cdn - name: Build distribution archive if: ${{ steps.release.outputs.releases_created == 'true' }} - run: pnpm -F @videojs/html build:archive + run: pnpm -F @videojs/cdn build:archive # The site build emits per-framework markdown to site/dist/docs/framework/{html,react}/. # The @videojs/html and @videojs/react packages' prepack scripts (fired by `pnpm publish` @@ -124,26 +114,26 @@ jobs: env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - # Attach the archive to the @videojs/html release so installers outside npm + # Attach the archive to the @videojs/cdn release so installers outside npm # (Composer, Drupal) have a stable, versioned download. Use the outputs from # the release that this run created, matching the SHA checked out above. - name: Upload distribution archive to the release if: ${{ steps.release.outputs.releases_created == 'true' }} run: | gh release upload "$RELEASE_TAG" \ - "packages/html/archive/videojs-html-${VERSION}.zip" \ - "packages/html/archive/videojs-html-${VERSION}.tar.gz" \ - packages/html/archive/SHA256SUMS \ + "packages/cdn/archive/videojs-cdn-${VERSION}.zip" \ + "packages/cdn/archive/videojs-cdn-${VERSION}.tar.gz" \ + packages/cdn/archive/SHA256SUMS \ --clobber ASSETS=$(gh release view "$RELEASE_TAG" --json assets --jq '.assets[].name') - grep -Fx "videojs-html-${VERSION}.zip" <<< "$ASSETS" - grep -Fx "videojs-html-${VERSION}.tar.gz" <<< "$ASSETS" + grep -Fx "videojs-cdn-${VERSION}.zip" <<< "$ASSETS" + grep -Fx "videojs-cdn-${VERSION}.tar.gz" <<< "$ASSETS" grep -Fx "SHA256SUMS" <<< "$ASSETS" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ steps.release.outputs['packages/html--tag_name'] }} - VERSION: ${{ steps.release.outputs['packages/html--version'] }} + RELEASE_TAG: ${{ steps.release.outputs['packages/cdn--tag_name'] }} + VERSION: ${{ steps.release.outputs['packages/cdn--version'] }} - name: Update site/v10 branch if: ${{ steps.release.outputs.releases_created == 'true' }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 054867e668..737c3012fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,6 +101,7 @@ jobs: matrix: package: - '@videojs/cli' + - '@videojs/cdn' - 'vjsc' - '@videojs/core' - '@videojs/icons' @@ -248,10 +249,10 @@ jobs: - name: Build CDN bundles run: pnpm build:cdn - # Mirroring cdn/ onto another origin is supported, so the output must not + # Mirroring @videojs/cdn onto another origin is supported, so the output must not # reference an external CDN or resolve outside the directory. - name: Check CDN bundles are self-contained - run: pnpm -F @videojs/html check:cdn + run: pnpm -F @videojs/cdn check:cdn - name: Save Vite Task cache if: success() diff --git a/.github/workflows/pkg-pr-new.yml b/.github/workflows/pkg-pr-new.yml index 5b377c0e58..eb5ab345f9 100644 --- a/.github/workflows/pkg-pr-new.yml +++ b/.github/workflows/pkg-pr-new.yml @@ -87,11 +87,9 @@ jobs: - name: Build packages run: pnpm build:packages - # `build:packages` runs the `build` task, which does not produce - # @videojs/html's `cdn/` tree; `build:cdn` is a separate task. `cdn` is in - # that package's `files`, and the sandbox resolves its CDN demos through - # the published export map, so skipping this leaves `./cdn/*` pointing at - # nothing in the preview tarball. + # `build:packages` intentionally excludes @videojs/cdn because its browser + # bundles have a separate task. The sandbox resolves its CDN demos through + # that package's published export map, so build it before packing previews. - name: Build CDN bundles run: pnpm build:cdn @@ -123,6 +121,7 @@ jobs: VIDEOJS_SKIP_PACKAGE_DOCS: '1' run: > pnpm exec pkg-pr-new publish --pnpm --previewVersion --peerDeps --packageManager=pnpm + './packages/cdn' './packages/core' './packages/element' './packages/html' diff --git a/apps/e2e/scripts/generate-pages.ts b/apps/e2e/scripts/generate-pages.ts index a487e0bad4..0aec180c76 100644 --- a/apps/e2e/scripts/generate-pages.ts +++ b/apps/e2e/scripts/generate-pages.ts @@ -138,8 +138,8 @@ const REACT_MEDIA: Record = { // CDN import paths (override standard imports) const CDN_IMPORTS: Record = { - video: ['@videojs/html/cdn/video'], - 'hlsjs-video': ['@videojs/html/cdn/video', '@videojs/html/cdn/media/hlsjs-video'], + video: ['@videojs/cdn/video'], + 'hlsjs-video': ['@videojs/cdn/video', '@videojs/cdn/media/hlsjs-video'], }; // --------------------------------------------------------------------------- diff --git a/apps/sandbox/README.md b/apps/sandbox/README.md index 9d9e7bc77e..e167e93200 100644 --- a/apps/sandbox/README.md +++ b/apps/sandbox/README.md @@ -24,7 +24,7 @@ Open the printed URL. The root route renders an interactive shell — a navbar w **Captions**, under **Playback** in the **Options** panel, adds one or two subtitle tracks to a video so the captions menu has something to show; the tracks are the page's, so no template spells them out. The source picker also carries a **Missing file** entry that opens the player's error dialog without waiting on a network. -**Language** is in the **Options** panel for every media (HTML, React, and CDN). **CDN** registers copy through `@videojs/html/cdn/i18n` (the same registry as the CDN player bundle), not source `@videojs/html/i18n`. After pulling template changes, restart `pnpm dev:sandbox` so `scripts/setup.ts` refreshes `src/` from `templates/`. +**Language** is in the **Options** panel for every media (HTML, React, and CDN). **CDN** registers copy through `@videojs/cdn/i18n` (the same registry as the CDN player bundle), not source `@videojs/html/i18n`. After pulling template changes, restart `pnpm dev:sandbox` so `scripts/setup.ts` refreshes `src/` from `templates/`. The shell covers the main combinatorial matrix. One-off templates not in that matrix (e.g. `firefox-mse-repro`, `spf-segment-loading`, `hls-video-html`) are reachable by navigating directly to `//`. See `apps/sandbox/templates/` for the full list. @@ -83,7 +83,7 @@ This previews every change first and prompts before doing anything. It overwrite Every pull request publishes this directory as a StackBlitz template through [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new), booting it against that commit's preview packages. That makes the sandbox the one app here that must also run as a standalone project, which constrains it in two ways: -- **Nothing may reference a path outside this directory.** `vite.config.ts` locates the prebuilt `@videojs/html` CDN bundle through Node resolution rather than `../../packages/html`, carries its own copies of the task helpers from `build/task.ts`, and `tsconfig.json` is self-contained instead of extending `../../tsconfig.base.json` — Vite fails to start if a config import or that `extends` cannot be resolved. The one project file that does extend the base, `tsconfig.shared.json`, sits beside it under a name the transformer never looks up, so the shared sources still compile against `tsconfig.json`. The one exception is `vite.workspace.config.ts`, which imports the skins preset from `packages/skins`; the tasks only name it when that directory exists, so StackBlitz never loads it. Tasks that build sibling packages, such as `@videojs/html#build:cdn`, are likewise only declared when `pnpm-workspace.yaml` exists two directories up. +- **Nothing may reference a path outside this directory.** `vite.config.ts` locates the prebuilt `@videojs/cdn` bundles through Node resolution rather than `../../packages/cdn`, carries its own copies of the task helpers from `build/task.ts`, and `tsconfig.json` is self-contained instead of extending `../../tsconfig.base.json` — Vite fails to start if a config import or that `extends` cannot be resolved. The one project file that does extend the base, `tsconfig.shared.json`, sits beside it under a name the transformer never looks up, so the shared sources still compile against `tsconfig.json`. The one exception is `vite.workspace.config.ts`, which imports the skins preset from `packages/skins`; the tasks only name it when that directory exists, so StackBlitz never loads it. Tasks that build sibling packages, such as `@videojs/cdn#build:cdn`, are likewise only declared when `pnpm-workspace.yaml` exists two directories up. - **Only published packages may be dependencies.** Package skins come from `@videojs/html` and `@videojs/react`. Setup uses the stock Shadcn CLI to install the ignored registry skins, three catalogs of eight, from the local built registry in the monorepo or `https://shadcn.videojs.org/r` elsewhere. The one private dependency, `@videojs/icons`, exists so authored skins dedupe onto a single copy inside the workspace; `scripts/prepare-template.ts`, which CI runs right before the upload, drops private workspace dependencies and inlines the `catalog:` versions from `pnpm-workspace.yaml`, since a lone `package.json` can resolve neither. When the hosted registry cannot be reached, setup carries on without the registry skins and the shell offers the package skins only. - **The package manager has to be declared here.** Only the repo root says pnpm, and the root is never uploaded, so StackBlitz would otherwise default to npm. The `stackblitz` field in `package.json` turns off its automatic install and boots with pnpm instead and runs the `dev` task through `vp run dev`; Vite+ owns the task names, so `package.json` declares no `dev` script. `--ignore-scripts` is there because pnpm refuses to silently skip dependency build scripts and fails the install if it has to; the sandbox needs none of them, esbuild's native binary included. `--config.blockExoticSubdeps=false` is there because the preview packages depend on each other through pkg.pr.new URLs, which pnpm 11 otherwise refuses to follow from inside a dependency. The task runs with `--config.verifyDepsBeforeRun=false` because pnpm would otherwise re-run the install with its default settings first, and that install fails on the very build scripts the first one skipped. - **Every cross-origin subresource has to be CORS-enabled.** StackBlitz previews are cross-origin isolated (`Cross-Origin-Embedder-Policy: require-corp`), so a no-CORS load from `stream.mux.com` or `image.mux.com` is blocked outright — neither host sends `Cross-Origin-Resource-Policy`. That is why every media element here carries a bare `crossorigin` (the CORS-settings attribute treats it as `anonymous`), which also puts the storyboard `` into CORS mode and, through it, the thumbnail sprites. The poster has to be the template's own image — an HTML skin renders one only where you slot it, and a React skin left to itself renders an `` no prop can reach — so the HTML templates slot `` and the React templates hand one to `renderPoster`. Both still pass the URL through the player and let the poster fill in the `src`; an image carrying its own would opt out of the blur-up load state. React has no bare-attribute form, so its templates write `crossOrigin=""`. One thing stays broken in a preview and cannot be fixed from here: a CSS `url()` can never be CORS-enabled, so the `placeholdersrc` blur-up does not render. diff --git a/apps/sandbox/app/shared/i18n/cdn-locale-loaders.generated.ts b/apps/sandbox/app/shared/i18n/cdn-locale-loaders.generated.ts index b9140abff2..888752bb9c 100644 --- a/apps/sandbox/app/shared/i18n/cdn-locale-loaders.generated.ts +++ b/apps/sandbox/app/shared/i18n/cdn-locale-loaders.generated.ts @@ -1,59 +1,59 @@ /** Generated by scripts/generate-cdn-locale-loaders.ts — do not edit. */ import type { SandboxLocalePack } from './locale-meta'; -/** CDN locale chunks register via `../i18n.dev.js` — same module graph as `@videojs/html/cdn/video`. */ +/** CDN locale chunks register via `../i18n.dev.js` — same module graph as `@videojs/cdn/video`. */ export const cdnLocaleLoaders = { - ar: () => import('@videojs/html/cdn/locales/ar'), - az: () => import('@videojs/html/cdn/locales/az'), - bg: () => import('@videojs/html/cdn/locales/bg'), - bn: () => import('@videojs/html/cdn/locales/bn'), - bs: () => import('@videojs/html/cdn/locales/bs'), - ca: () => import('@videojs/html/cdn/locales/ca'), - cs: () => import('@videojs/html/cdn/locales/cs'), - cy: () => import('@videojs/html/cdn/locales/cy'), - da: () => import('@videojs/html/cdn/locales/da'), - de: () => import('@videojs/html/cdn/locales/de'), - el: () => import('@videojs/html/cdn/locales/el'), - es: () => import('@videojs/html/cdn/locales/es'), - et: () => import('@videojs/html/cdn/locales/et'), - eu: () => import('@videojs/html/cdn/locales/eu'), - fa: () => import('@videojs/html/cdn/locales/fa'), - fi: () => import('@videojs/html/cdn/locales/fi'), - fr: () => import('@videojs/html/cdn/locales/fr'), - gd: () => import('@videojs/html/cdn/locales/gd'), - gl: () => import('@videojs/html/cdn/locales/gl'), - he: () => import('@videojs/html/cdn/locales/he'), - hi: () => import('@videojs/html/cdn/locales/hi'), - hr: () => import('@videojs/html/cdn/locales/hr'), - hu: () => import('@videojs/html/cdn/locales/hu'), - id: () => import('@videojs/html/cdn/locales/id'), - it: () => import('@videojs/html/cdn/locales/it'), - ja: () => import('@videojs/html/cdn/locales/ja'), - ko: () => import('@videojs/html/cdn/locales/ko'), - lt: () => import('@videojs/html/cdn/locales/lt'), - lv: () => import('@videojs/html/cdn/locales/lv'), - mr: () => import('@videojs/html/cdn/locales/mr'), - nb: () => import('@videojs/html/cdn/locales/nb'), - ne: () => import('@videojs/html/cdn/locales/ne'), - nl: () => import('@videojs/html/cdn/locales/nl'), - nn: () => import('@videojs/html/cdn/locales/nn'), - oc: () => import('@videojs/html/cdn/locales/oc'), - pl: () => import('@videojs/html/cdn/locales/pl'), - pt: () => import('@videojs/html/cdn/locales/pt'), - 'pt-BR': () => import('@videojs/html/cdn/locales/pt-BR'), - 'pt-PT': () => import('@videojs/html/cdn/locales/pt-PT'), - ro: () => import('@videojs/html/cdn/locales/ro'), - ru: () => import('@videojs/html/cdn/locales/ru'), - sk: () => import('@videojs/html/cdn/locales/sk'), - sl: () => import('@videojs/html/cdn/locales/sl'), - sr: () => import('@videojs/html/cdn/locales/sr'), - sv: () => import('@videojs/html/cdn/locales/sv'), - te: () => import('@videojs/html/cdn/locales/te'), - th: () => import('@videojs/html/cdn/locales/th'), - tr: () => import('@videojs/html/cdn/locales/tr'), - uk: () => import('@videojs/html/cdn/locales/uk'), - vi: () => import('@videojs/html/cdn/locales/vi'), - zh: () => import('@videojs/html/cdn/locales/zh'), - 'zh-CN': () => import('@videojs/html/cdn/locales/zh-CN'), - 'zh-TW': () => import('@videojs/html/cdn/locales/zh-TW'), + ar: () => import('@videojs/cdn/locales/ar'), + az: () => import('@videojs/cdn/locales/az'), + bg: () => import('@videojs/cdn/locales/bg'), + bn: () => import('@videojs/cdn/locales/bn'), + bs: () => import('@videojs/cdn/locales/bs'), + ca: () => import('@videojs/cdn/locales/ca'), + cs: () => import('@videojs/cdn/locales/cs'), + cy: () => import('@videojs/cdn/locales/cy'), + da: () => import('@videojs/cdn/locales/da'), + de: () => import('@videojs/cdn/locales/de'), + el: () => import('@videojs/cdn/locales/el'), + es: () => import('@videojs/cdn/locales/es'), + et: () => import('@videojs/cdn/locales/et'), + eu: () => import('@videojs/cdn/locales/eu'), + fa: () => import('@videojs/cdn/locales/fa'), + fi: () => import('@videojs/cdn/locales/fi'), + fr: () => import('@videojs/cdn/locales/fr'), + gd: () => import('@videojs/cdn/locales/gd'), + gl: () => import('@videojs/cdn/locales/gl'), + he: () => import('@videojs/cdn/locales/he'), + hi: () => import('@videojs/cdn/locales/hi'), + hr: () => import('@videojs/cdn/locales/hr'), + hu: () => import('@videojs/cdn/locales/hu'), + id: () => import('@videojs/cdn/locales/id'), + it: () => import('@videojs/cdn/locales/it'), + ja: () => import('@videojs/cdn/locales/ja'), + ko: () => import('@videojs/cdn/locales/ko'), + lt: () => import('@videojs/cdn/locales/lt'), + lv: () => import('@videojs/cdn/locales/lv'), + mr: () => import('@videojs/cdn/locales/mr'), + nb: () => import('@videojs/cdn/locales/nb'), + ne: () => import('@videojs/cdn/locales/ne'), + nl: () => import('@videojs/cdn/locales/nl'), + nn: () => import('@videojs/cdn/locales/nn'), + oc: () => import('@videojs/cdn/locales/oc'), + pl: () => import('@videojs/cdn/locales/pl'), + pt: () => import('@videojs/cdn/locales/pt'), + 'pt-BR': () => import('@videojs/cdn/locales/pt-BR'), + 'pt-PT': () => import('@videojs/cdn/locales/pt-PT'), + ro: () => import('@videojs/cdn/locales/ro'), + ru: () => import('@videojs/cdn/locales/ru'), + sk: () => import('@videojs/cdn/locales/sk'), + sl: () => import('@videojs/cdn/locales/sl'), + sr: () => import('@videojs/cdn/locales/sr'), + sv: () => import('@videojs/cdn/locales/sv'), + te: () => import('@videojs/cdn/locales/te'), + th: () => import('@videojs/cdn/locales/th'), + tr: () => import('@videojs/cdn/locales/tr'), + uk: () => import('@videojs/cdn/locales/uk'), + vi: () => import('@videojs/cdn/locales/vi'), + zh: () => import('@videojs/cdn/locales/zh'), + 'zh-CN': () => import('@videojs/cdn/locales/zh-CN'), + 'zh-TW': () => import('@videojs/cdn/locales/zh-TW'), } as const satisfies Record, () => Promise>; diff --git a/apps/sandbox/app/shared/i18n/cdn-sandbox-locales.ts b/apps/sandbox/app/shared/i18n/cdn-sandbox-locales.ts index a24115f3ca..8903910315 100644 --- a/apps/sandbox/app/shared/i18n/cdn-sandbox-locales.ts +++ b/apps/sandbox/app/shared/i18n/cdn-sandbox-locales.ts @@ -1,4 +1,4 @@ -import { getI18nTranslations, registerI18n } from '@videojs/html/cdn/i18n'; +import { getI18nTranslations, registerI18n } from '@videojs/cdn/i18n'; import { createBrowserSandboxPrefetch } from './browser-sandbox-prefetch'; import { cdnLocaleLoaders } from './cdn-locale-loaders.generated'; diff --git a/apps/sandbox/package.json b/apps/sandbox/package.json index 537694f63f..3cdb5e7194 100644 --- a/apps/sandbox/package.json +++ b/apps/sandbox/package.json @@ -10,6 +10,7 @@ "test": "vp test run" }, "dependencies": { + "@videojs/cdn": "workspace:*", "@videojs/cloudflare-video": "workspace:*", "@videojs/core": "workspace:*", "@videojs/dash-video": "workspace:*", diff --git a/apps/sandbox/scripts/generate-cdn-locale-loaders.ts b/apps/sandbox/scripts/generate-cdn-locale-loaders.ts index ad71d9b3c7..253d6c9d97 100644 --- a/apps/sandbox/scripts/generate-cdn-locale-loaders.ts +++ b/apps/sandbox/scripts/generate-cdn-locale-loaders.ts @@ -13,13 +13,13 @@ const localePacks = [...LOCALES, ...localeAliases(LOCALES)]; const entries = localePacks .sort((a, b) => a.localeCompare(b, 'en')) - .map((tag) => ` ${localeObjectKey(tag)}: () => import('@videojs/html/cdn/locales/${tag}'),`) + .map((tag) => ` ${localeObjectKey(tag)}: () => import('@videojs/cdn/locales/${tag}'),`) .join('\n'); const source = `/** Generated by scripts/generate-cdn-locale-loaders.ts — do not edit. */ import type { SandboxLocalePack } from './locale-meta'; -/** CDN locale chunks register via \`../i18n.dev.js\` — same module graph as \`@videojs/html/cdn/video\`. */ +/** CDN locale chunks register via \`../i18n.dev.js\` — same module graph as \`@videojs/cdn/video\`. */ export const cdnLocaleLoaders = { ${entries} } as const satisfies Record, () => Promise>; diff --git a/apps/sandbox/templates/cdn/main.ts b/apps/sandbox/templates/cdn/main.ts index 25d6adf3dc..ca03ada35b 100644 --- a/apps/sandbox/templates/cdn/main.ts +++ b/apps/sandbox/templates/cdn/main.ts @@ -29,7 +29,7 @@ import { withMuxMaxResolution, } from '@app/shared/sources'; import type { Skin } from '@app/types'; -import { getI18nTranslations } from '@videojs/html/cdn/i18n'; +import { getI18nTranslations } from '@videojs/cdn/i18n'; import { escapeHtml } from '@videojs/utils/string'; const html = String.raw; @@ -163,28 +163,28 @@ async function loadCdnPlayer(skin: Skin, live: boolean) { switch (descriptor.player) { case 'video': if (live) { - if (skin === 'minimal') await import('@videojs/html/cdn/live-video-minimal'); - else await import('@videojs/html/cdn/live-video'); + if (skin === 'minimal') await import('@videojs/cdn/live-video-minimal'); + else await import('@videojs/cdn/live-video'); } else { - if (skin === 'minimal') await import('@videojs/html/cdn/video-minimal'); - else await import('@videojs/html/cdn/video'); + if (skin === 'minimal') await import('@videojs/cdn/video-minimal'); + else await import('@videojs/cdn/video'); } break; case 'audio': if (live) { - if (skin === 'minimal') await import('@videojs/html/cdn/live-audio-minimal'); - else await import('@videojs/html/cdn/live-audio'); + if (skin === 'minimal') await import('@videojs/cdn/live-audio-minimal'); + else await import('@videojs/cdn/live-audio'); } else { - if (skin === 'minimal') await import('@videojs/html/cdn/audio-minimal'); - else await import('@videojs/html/cdn/audio'); + if (skin === 'minimal') await import('@videojs/cdn/audio-minimal'); + else await import('@videojs/cdn/audio'); } break; // Player and skin are shared; the element each one renders is what differs, // and that arrives from `loadCdnMedia`. case 'background': - await import('@videojs/html/cdn/background'); + await import('@videojs/cdn/background'); break; } } @@ -192,70 +192,70 @@ async function loadCdnPlayer(skin: Skin, live: boolean) { async function loadCdnMedia(media: MediaId) { switch (media) { case 'hlsjs-video': - await import('@videojs/html/cdn/media/hlsjs-video'); + await import('@videojs/cdn/media/hlsjs-video'); break; case 'mux-video': - await import('@videojs/html/cdn/media/mux-video'); + await import('@videojs/cdn/media/mux-video'); break; // One media loads one flavor, so the SPF-backed element is the only claimant // and registers as `` — the CDN page is where that rule is visible, // since a page picks bundles at runtime rather than by import path. case 'mux-video-spf': - await import('@videojs/html/cdn/media/mux-video/spf'); + await import('@videojs/cdn/media/mux-video/spf'); break; case 'mux-audio': - await import('@videojs/html/cdn/media/mux-audio'); + await import('@videojs/cdn/media/mux-audio'); break; case 'mux-audio-spf': - await import('@videojs/html/cdn/media/mux-audio/spf'); + await import('@videojs/cdn/media/mux-audio/spf'); break; // `` rides along inside the `background` bundle above; the // SPF-backed tags are their own bundles, so the page loads them the way it // loads every other media element. Both tags are one element, and a CDN page // loads bundles at runtime — so it loads only the one the media names. case 'hls-background-video': - await import('@videojs/html/cdn/media/hls-background-video'); + await import('@videojs/cdn/media/hls-background-video'); break; case 'mux-background-video': - await import('@videojs/html/cdn/media/mux-background-video'); + await import('@videojs/cdn/media/mux-background-video'); break; case 'native-hls-video': - await import('@videojs/html/cdn/media/native-hls-video'); + await import('@videojs/cdn/media/native-hls-video'); break; case 'hls-video': - await import('@videojs/html/cdn/media/hls-video'); + await import('@videojs/cdn/media/hls-video'); break; case 'hls-audio': - await import('@videojs/html/cdn/media/hls-audio'); + await import('@videojs/cdn/media/hls-audio'); break; case 'dash-video': - await import('@videojs/html/cdn/media/dash-video'); + await import('@videojs/cdn/media/dash-video'); break; case 'shaka-video': - await import('@videojs/html/cdn/media/shaka-video'); + await import('@videojs/cdn/media/shaka-video'); break; // Each embed is one bundle beside the rest, so a page reaches a third-party // player the same way it reaches an HLS one — no npm-only step. case 'vimeo-video': - await import('@videojs/html/cdn/media/vimeo-video'); + await import('@videojs/cdn/media/vimeo-video'); break; case 'youtube-video': - await import('@videojs/html/cdn/media/youtube-video'); + await import('@videojs/cdn/media/youtube-video'); break; case 'cloudflare-video': - await import('@videojs/html/cdn/media/cloudflare-video'); + await import('@videojs/cdn/media/cloudflare-video'); break; case 'spotify-audio': - await import('@videojs/html/cdn/media/spotify-audio'); + await import('@videojs/cdn/media/spotify-audio'); break; case 'tiktok-video': - await import('@videojs/html/cdn/media/tiktok-video'); + await import('@videojs/cdn/media/tiktok-video'); break; case 'twitch-video': - await import('@videojs/html/cdn/media/twitch-video'); + await import('@videojs/cdn/media/twitch-video'); break; case 'wistia-video': - await import('@videojs/html/cdn/media/wistia-video'); + await import('@videojs/cdn/media/wistia-video'); break; } } diff --git a/apps/sandbox/vite.config.ts b/apps/sandbox/vite.config.ts index c91cedd61d..d1a9dc3d84 100644 --- a/apps/sandbox/vite.config.ts +++ b/apps/sandbox/vite.config.ts @@ -9,13 +9,12 @@ import { defineConfig, normalizePath, type Plugin, type PluginOption } from 'vit import { mirrorTemplatesToSrc } from './scripts/shared'; -// Locate @videojs/html through Node resolution rather than a workspace-relative +// Locate @videojs/cdn through Node resolution rather than a workspace-relative // path, so the sandbox also works when the package is installed from a registry. -// The manifest is the anchor because cdn/ only exists after `pnpm build:cdn`. -const htmlPackageDir = normalizePath(dirname(createRequire(__filename).resolve('@videojs/html/package.json'))); -const htmlCdnDir = `${htmlPackageDir}/cdn`; -const htmlCdnI18nRegistry = `${htmlCdnDir}/i18n.dev.js`; -const htmlCdnSourceI18n = `${htmlPackageDir}/src/cdn/i18n.ts`; +// The manifest is the anchor because bundles only exist after `pnpm build:cdn`. +const cdnDir = normalizePath(dirname(createRequire(__filename).resolve('@videojs/cdn/package.json'))); +const cdnI18nRegistry = `${cdnDir}/i18n.dev.js`; +const cdnSourceI18n = `${cdnDir}/src/i18n.ts`; const cdnSandboxMainSrc = resolve(__dirname, 'src/cdn/main.ts'); const cdnSandboxMainTemplate = resolve(__dirname, 'templates/cdn/main.ts'); @@ -81,36 +80,32 @@ export interface SkinsSource { // dev and build commands read `vite.workspace.config.ts` instead, which adds the preset once its dependencies exist. const workspaceConfig = hasWorkspaceSkins ? ' --config vite.workspace.config.ts' : ''; -/** True when the importer is one of the prebuilt @videojs/html CDN chunks. */ -function isHtmlCdnChunk(importer?: string): boolean { - return importer !== undefined && normalizePath(importer).startsWith(`${htmlCdnDir}/`); +/** True when the importer is one of the prebuilt @videojs/cdn chunks. */ +function isCdnChunk(importer?: string): boolean { + return importer !== undefined && normalizePath(importer).startsWith(`${cdnDir}/`); } /** True when this import should share the single CDN i18n registry module instance. */ function resolvesToCdnI18nRegistry(source: string, importer?: string): boolean { const normalizedSource = normalizePath(source); - if ( - source === '@videojs/html/cdn/i18n' || - normalizedSource === htmlCdnI18nRegistry || - normalizedSource === htmlCdnSourceI18n - ) { + if (source === '@videojs/cdn/i18n' || normalizedSource === cdnI18nRegistry || normalizedSource === cdnSourceI18n) { return true; } const isRelativeI18nChunk = source === './i18n.dev.js' || source === '../i18n.dev.js' || source.endsWith('/i18n.dev.js'); - if (isRelativeI18nChunk && isHtmlCdnChunk(importer)) return true; + if (isRelativeI18nChunk && isCdnChunk(importer)) return true; - if (source === '@videojs/core/i18n' && isHtmlCdnChunk(importer)) { + if (source === '@videojs/core/i18n' && isCdnChunk(importer)) { return true; } return false; } -function resolveHtmlCdnDevEntry(subpath: string): string | null { - const devPath = resolve(htmlCdnDir, `${subpath}.dev.js`); +function resolveCdnDevEntry(subpath: string): string | null { + const devPath = resolve(cdnDir, `${subpath}.dev.js`); return existsSync(devPath) ? devPath : null; } @@ -122,7 +117,7 @@ function cdnSandboxI18nPlugin(): Plugin { enforce: 'pre', resolveId: { filter: { - id: /^@videojs\/(?:core\/i18n|html\/cdn(?:\/.*)?)$|(?:^|\/)i18n\.dev\.js$|\/src\/cdn\/(?:i18n\.ts|main\.ts)$/, + id: /^@videojs\/(?:core\/i18n|cdn(?:\/.*)?)$|(?:^|\/)i18n\.dev\.js$|\/cdn\/src\/i18n\.ts$|\/src\/cdn\/main\.ts$/, }, handler(source, importer) { if (source === cdnSandboxMainSrc && existsSync(cdnSandboxMainTemplate)) { @@ -130,13 +125,13 @@ function cdnSandboxI18nPlugin(): Plugin { } if (resolvesToCdnI18nRegistry(source, importer)) { - return htmlCdnI18nRegistry; + return cdnI18nRegistry; } - const cdnEntryMatch = source.match(/^@videojs\/html\/cdn\/(.+)$/); + const cdnEntryMatch = source.match(/^@videojs\/cdn\/(.+)$/); if (cdnEntryMatch && cdnEntryMatch[1] !== 'i18n') { - const devEntry = resolveHtmlCdnDevEntry(cdnEntryMatch[1]); + const devEntry = resolveCdnDevEntry(cdnEntryMatch[1]); if (devEntry) return devEntry; } @@ -223,7 +218,7 @@ export function createSandboxConfig(skinsSource?: SkinsSource) { dev: { command: `vp dev --host${workspaceConfig}`, cache: false, - dependsOn: ['setup', ...workspaceTaskDependencies(), ...(hasWorkspace ? ['@videojs/html#build:cdn'] : [])], + dependsOn: ['setup', ...workspaceTaskDependencies(), ...(hasWorkspace ? ['@videojs/cdn#build:cdn'] : [])], }, setup: { command: 'tsx scripts/setup.ts', @@ -251,7 +246,7 @@ export function createSandboxConfig(skinsSource?: SkinsSource) { }, build: { command: `vp build${workspaceConfig}`, - dependsOn: ['setup', ...workspaceTaskDependencies(), ...(hasWorkspace ? ['@videojs/html#build:cdn'] : [])], + dependsOn: ['setup', ...workspaceTaskDependencies(), ...(hasWorkspace ? ['@videojs/cdn#build:cdn'] : [])], // The app-shell plugin creates this file for the build and removes it // afterwards. Workspace dependencies are fingerprinted through the task // graph, not their mutable package-local node_modules links. @@ -295,7 +290,7 @@ export function createSandboxConfig(skinsSource?: SkinsSource) { // The registry's React CSS catalog, installed beside the Tailwind one under its own alias so the two never share files. '@css': resolve(__dirname, 'app/_generated/css'), '@app': resolve(__dirname, 'app'), - '@videojs/html/cdn/i18n': htmlCdnI18nRegistry, + '@videojs/cdn/i18n': cdnI18nRegistry, ...(existsSync(cdnSandboxMainTemplate) ? { [cdnSandboxMainSrc]: cdnSandboxMainTemplate } : {}), }, conditions: ['development', 'import', 'module', 'browser', 'default'], diff --git a/build/plugins/copy-css-plugin.ts b/build/plugins/copy-css-plugin.ts index bb4619a24d..f9a74584bf 100644 --- a/build/plugins/copy-css-plugin.ts +++ b/build/plugins/copy-css-plugin.ts @@ -8,6 +8,10 @@ import type { BuildPlugin } from './types.ts'; interface CopyCssPluginOptions { outDir: string; + /** Package root whose CSS files are copied. Defaults to the current working directory. */ + rootDir?: string; + /** Glob, relative to `rootDir`, selecting the CSS files to copy. Defaults to every `.css` file under `src`. */ + pattern?: string; inline?: boolean; rebuild?: boolean; /** @@ -25,12 +29,25 @@ function mirrorSrcTree(file: string): string { } export function copyCssPlugin(options: CopyCssPluginOptions): BuildPlugin { - const { outDir, inline = true, rebuild = true, rename = mirrorSrcTree, omitImport, minify = false } = options; + const { + outDir, + rootDir = process.cwd(), + pattern = 'src/**/*.css', + inline = true, + rebuild = true, + rename = mirrorSrcTree, + omitImport, + minify = false, + } = options; let poll: ReturnType | undefined; let state = new Map(); + function getSourceCssFiles() { + return globSync(pattern, { cwd: rootDir }); + } + function getCssFiles() { - return new Set(globSync('src/**/*.css')); + return new Set(getSourceCssFiles().map((file) => join(rootDir, file))); } function getState() { @@ -44,15 +61,16 @@ export function copyCssPlugin(options: CopyCssPluginOptions): BuildPlugin { } function writeCss() { - for (const file of globSync('src/**/*.css')) { + for (const file of getSourceCssFiles()) { const target = rename(file); if (target === null) continue; - const content = readFileSync(file, 'utf-8'); - let output = inline ? resolveImports(content, dirname(file), omitImport) : content; + const source = join(rootDir, file); + const content = readFileSync(source, 'utf-8'); + let output = inline ? resolveImports(content, dirname(source), omitImport) : content; if (minify) { - output = transform({ filename: file, code: Buffer.from(output), minify: true }).code.toString(); + output = transform({ filename: source, code: Buffer.from(output), minify: true }).code.toString(); } const outFile = join(outDir, target); diff --git a/commitlint.config.js b/commitlint.config.js index 34d542179f..9605a43c36 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -16,6 +16,7 @@ export default { [ 'build', 'cd', + 'cdn', 'ci', 'claude', 'cli', diff --git a/internal/design/spf/use-cases/audio-only-mode-override.md b/internal/design/spf/use-cases/audio-only-mode-override.md index 9f41b31921..a60eb3d67b 100644 --- a/internal/design/spf/use-cases/audio-only-mode-override.md +++ b/internal/design/spf/use-cases/audio-only-mode-override.md @@ -314,8 +314,8 @@ Public re-export: `@videojs/spf/hls-audio`. | `HlsAudio` | `media/hls-audio/media.ts` | Applies `MediaAttachMixin` + `CustomMediaElement('audio', HlsAudioMedia)` | | `HlsAudioElement` (tag `hls-audio`) | `media/hls-audio/element.ts` | Custom-element implementation; `define/media/hls-audio.ts` registers it via `safeDefine` | -CDN entry: `packages/html/src/cdn/media/hls-audio.ts` → -`@videojs/html/cdn/media/hls-audio`. +CDN entry: `packages/html/src/define/media/hls-audio.ts` → +`@videojs/cdn/media/hls-audio`. **React — component** (`packages/react/src/media/hls-audio/`): diff --git a/package.json b/package.json index 3bfd3b336b..ee444dce32 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,10 @@ "scripts": { "prepare": "vp config --no-agent", "postinstall": "node build/scripts/link-aliases.mjs", - "build:packages": "vp run --fail-if-no-match --filter='./packages/*' --filter='!@videojs/cli' build", + "build:packages": "vp run --fail-if-no-match --filter='./packages/*' --filter='!@videojs/cdn' --filter='!@videojs/cli' build", "build:cli": "vp run @videojs/cli#build", "build:sandbox": "vp run @videojs/sandbox#build", - "build:cdn": "vp run @videojs/html#build:cdn", + "build:cdn": "vp run @videojs/cdn#build:cdn", "build:shadcn": "vp run @videojs/skins#build:shadcn", "build:site": "vp run site#build", "build": "vp run -r build", diff --git a/packages/cdn/.gitignore b/packages/cdn/.gitignore new file mode 100644 index 0000000000..515784b8c3 --- /dev/null +++ b/packages/cdn/.gitignore @@ -0,0 +1,9 @@ +/*.css +/*.d.ts +/*.js +/*.js.map +/archive/ +/chunks/ +/locales/ +/media/ +/extensions/ diff --git a/packages/cdn/README.md b/packages/cdn/README.md new file mode 100644 index 0000000000..9347333957 --- /dev/null +++ b/packages/cdn/README.md @@ -0,0 +1,44 @@ +# @videojs/cdn + +Browser-ready Video.js bundles for script-tag and self-hosted installations. This package assembles the HTML player, +selected playback adapters, shared chunks, source maps, and standalone stylesheets in one build graph. + +## Build output + +Run `pnpm build:cdn` from the workspace root. The task writes publishable files directly to `packages/cdn/`, which is +the npm package root; it does not use a separate `dist` directory. Player entries and hashed shared chunks sit at the +top level, while locale and media entries sit under `locales` and `media`. + +`pnpm --filter @videojs/cdn run build:archive` writes the self-hosting zip, tarball, and checksums to +`packages/cdn/archive/` after the CDN build completes. + +## CDN installation + +Load a version-pinned player and one media implementation: + +```html + + +``` + +Every browser-ready media entry uses the same URL shape: `@videojs/cdn@10/media/.js`, and extensions use +`@videojs/cdn@10/extensions/.js`. Adapter and extension runtimes are already included in these +bundles, so do not add the npm packages to a script-tag installation. + +## Self-hosting + +Install the package when the browser-ready directory will be copied to your own origin: + +```bash +pnpm add @videojs/cdn +``` + +Copy the entire published package, including the top-level hashed chunks and the `locales` and `media` directories; +individual entry files can import shared chunks by relative URL. + +Application builds should install `@videojs/html` or `@videojs/react` instead. Neither package depends on +`@videojs/cdn`, so normal npm installs do not include these prebuilt bundles. + +## License + +[Apache-2.0](../../LICENSE) diff --git a/packages/cdn/package.json b/packages/cdn/package.json new file mode 100644 index 0000000000..cce5f36a3b --- /dev/null +++ b/packages/cdn/package.json @@ -0,0 +1,101 @@ +{ + "name": "@videojs/cdn", + "version": "10.0.0-beta.32", + "description": "Browser-ready Video.js distribution bundles", + "keywords": [ + "cdn", + "html", + "media", + "player", + "videojs" + ], + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/videojs/v10", + "directory": "packages/cdn" + }, + "files": [ + "*.css", + "*.d.ts", + "*.js", + "*.js.map", + "chunks", + "extensions", + "locales", + "media" + ], + "type": "module", + "sideEffects": [ + "./**/*.js" + ], + "exports": { + "./package.json": "./package.json", + "./extensions/*": { + "types": "./extensions/*.dev.d.ts", + "development": "./extensions/*.dev.js", + "default": "./extensions/*.js" + }, + "./media/*": { + "types": "./media/*.dev.d.ts", + "development": "./media/*.dev.js", + "default": "./media/*.js" + }, + "./i18n": { + "types": "./i18n.dev.d.ts", + "development": "./i18n.dev.js", + "default": "./i18n.js" + }, + "./i18n-registry": { + "types": "./i18n.dev.d.ts", + "development": "./i18n.dev.js", + "default": "./i18n.js" + }, + "./locales/*": { + "types": "./locales/*.dev.d.ts", + "development": "./locales/*.dev.js", + "default": "./locales/*.js" + }, + "./*.css": "./*.css", + "./*": { + "types": "./*.dev.d.ts", + "development": "./*.dev.js", + "default": "./*.js" + } + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "build:archive": "node --import tsx ./scripts/build-dist-archive.ts", + "check:cdn": "node --import tsx ./scripts/check-cdn-self-contained.ts && node --import tsx ./scripts/check-cdn-skins.ts", + "test": "vp test run", + "clean": "rimraf --glob '*.css' '*.d.ts' '*.js' '*.js.map' archive chunks extensions locales media 'src/locales/*.ts'" + }, + "devDependencies": { + "@videojs/cloudflare-video": "workspace:*", + "@videojs/core": "workspace:*", + "@videojs/dash-video": "workspace:*", + "@videojs/google-cast": "workspace:*", + "@videojs/hlsjs-video": "workspace:*", + "@videojs/html": "workspace:*", + "@videojs/mux-audio": "workspace:*", + "@videojs/mux-data": "workspace:*", + "@videojs/mux-video": "workspace:*", + "@videojs/native-hls-video": "workspace:*", + "@videojs/shaka-video": "workspace:*", + "@videojs/spf": "workspace:*", + "@videojs/spotify-audio": "workspace:*", + "@videojs/tiktok-video": "workspace:*", + "@videojs/twitch-video": "workspace:*", + "@videojs/vimeo-video": "workspace:*", + "@videojs/wistia-video": "workspace:*", + "@videojs/youtube-video": "workspace:*", + "happy-dom": "^20.11.1", + "rolldown": "~1.2.5", + "tsx": "^4.23.1", + "typescript": "^6.0.2", + "vite-plus": "catalog:", + "vitest": "catalog:" + } +} diff --git a/packages/html/scripts/build-cdn-locales.ts b/packages/cdn/scripts/build-cdn-locales.ts similarity index 89% rename from packages/html/scripts/build-cdn-locales.ts rename to packages/cdn/scripts/build-cdn-locales.ts index d9ff8ba397..1732e20561 100644 --- a/packages/html/scripts/build-cdn-locales.ts +++ b/packages/cdn/scripts/build-cdn-locales.ts @@ -4,8 +4,8 @@ import { fileURLToPath } from 'node:url'; import { LOCALES, localeAliases } from '../../core/src/core/i18n/locales.ts'; -const REGISTRY = '@videojs/html/cdn/i18n-registry'; -const outDir = resolve(dirname(fileURLToPath(import.meta.url)), '../src/cdn/locales'); +const REGISTRY = '@videojs/cdn/i18n-registry'; +const outDir = resolve(dirname(fileURLToPath(import.meta.url)), '../src/locales'); const tags = [...LOCALES, ...localeAliases(LOCALES)]; diff --git a/packages/html/scripts/build-dist-archive.ts b/packages/cdn/scripts/build-dist-archive.ts similarity index 95% rename from packages/html/scripts/build-dist-archive.ts rename to packages/cdn/scripts/build-dist-archive.ts index e04a7b4c02..d8a11beaa8 100644 --- a/packages/html/scripts/build-dist-archive.ts +++ b/packages/cdn/scripts/build-dist-archive.ts @@ -1,5 +1,5 @@ /** - * Build the distribution archives attached to each `@videojs/html` GitHub release. + * Build the distribution archives attached to each `@videojs/cdn` GitHub release. * * Package managers outside npm — Composer for Drupal, and anything vendoring a versioned tarball — need a downloadable, * browser-ready copy of the player. This assembles one from the production CDN bundles: entry points, the shared chunks @@ -9,7 +9,7 @@ * bundles the CDN serves. Sourcemaps are left out to keep the download small, and their now-dangling `sourceMappingURL` * comments are stripped so browsers do not request them. * - * Prerequisites: `@videojs/html`'s `build:cdn`. Requires `zip` and `tar` on PATH. + * Prerequisites: `@videojs/cdn`'s `build:cdn`. Requires `zip` and `tar` on PATH. */ import { spawnSync } from 'node:child_process'; @@ -21,7 +21,7 @@ import { findUnresolvableSpecifiers, resolveClosure } from './cdn-graph.ts'; const PACKAGE_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const REPO_ROOT = resolve(PACKAGE_DIR, '../..'); -const CDN_DIR = resolve(PACKAGE_DIR, 'cdn'); +const CDN_DIR = PACKAGE_DIR; const OUT_DIR = resolve(PACKAGE_DIR, 'archive'); const PREFIX = '\x1b[35m[dist-archive]\x1b[0m'; @@ -64,7 +64,7 @@ async function main() { import('../package.json', { with: { type: 'json' } }).then((module) => module.default), ]); - const name = `videojs-html-${pkg.version}`; + const name = `videojs-cdn-${pkg.version}`; const stageDir = resolve(OUT_DIR, name); rmSync(OUT_DIR, { recursive: true, force: true }); diff --git a/packages/html/scripts/cdn-graph.ts b/packages/cdn/scripts/cdn-graph.ts similarity index 100% rename from packages/html/scripts/cdn-graph.ts rename to packages/cdn/scripts/cdn-graph.ts diff --git a/build/plugins/cdn-i18n-external-plugin.ts b/packages/cdn/scripts/cdn-i18n-external-plugin.ts similarity index 83% rename from build/plugins/cdn-i18n-external-plugin.ts rename to packages/cdn/scripts/cdn-i18n-external-plugin.ts index 8230a54847..f3bec0077c 100644 --- a/build/plugins/cdn-i18n-external-plugin.ts +++ b/packages/cdn/scripts/cdn-i18n-external-plugin.ts @@ -1,7 +1,7 @@ -import type { BuildPlugin } from './types.ts'; +import type { BuildPlugin } from '../../../build/plugins/types.ts'; /** Rolldown external id for the shared CDN i18n registry module. */ -export const CDN_I18N_REGISTRY = '@videojs/html/cdn/i18n-registry'; +export const CDN_I18N_REGISTRY = '@videojs/cdn/i18n-registry'; export interface CdnI18nExternalPluginOptions { prod: boolean; @@ -10,8 +10,8 @@ export interface CdnI18nExternalPluginOptions { /** * Keeps the i18n registry out of every CDN bundle by pointing them at the shared `i18n` entry. * - * The rewritten specifier is always relative, so a mirrored copy of `cdn/` resolves entirely within its own origin and - * works offline. Sharing one registry instance no longer depends on every bundle naming the same absolute URL — + * The rewritten specifier is always relative, so a mirrored copy of the package resolves entirely within its own origin + * and works offline. Sharing one registry instance no longer depends on every bundle naming the same absolute URL — * `@videojs/core/i18n` keeps its state on `globalThis`, so duplicate instances converge on their own. */ export function cdnI18nExternalPlugin(options: CdnI18nExternalPluginOptions): BuildPlugin { @@ -20,7 +20,7 @@ export function cdnI18nExternalPlugin(options: CdnI18nExternalPluginOptions): Bu function isCdnI18nEntry(importer: string): boolean { const normalized = importer.replaceAll('\\', '/'); - return normalized.includes('/cdn/i18n.ts') || normalized.endsWith('/cdn/i18n.js'); + return normalized.includes('/cdn/src/i18n.ts') || normalized.endsWith('/cdn/i18n.js'); } return { diff --git a/packages/html/scripts/check-cdn-self-contained.ts b/packages/cdn/scripts/check-cdn-self-contained.ts similarity index 63% rename from packages/html/scripts/check-cdn-self-contained.ts rename to packages/cdn/scripts/check-cdn-self-contained.ts index 64f284794b..21bc2226b4 100644 --- a/packages/html/scripts/check-cdn-self-contained.ts +++ b/packages/cdn/scripts/check-cdn-self-contained.ts @@ -1,12 +1,12 @@ /** * Verify the built CDN output is self-contained. * - * Mirroring `cdn/` onto your own origin is a supported way to run the player (see the site's "Self-host the player" - * guide), and the release distribution archive is cut from this output. That only holds while every module specifier is - * relative and resolves inside the directory — a single absolute URL silently reintroduces a runtime dependency on a - * public CDN, which fails closed in offline, air-gapped, and strict-CSP deployments. + * Mirroring `@videojs/cdn` onto your own origin is a supported way to run the player (see the site's "Self-host the + * player" guide), and the release distribution archive is cut from this output. That only holds while every module + * specifier is relative and resolves inside the directory — a single absolute URL silently reintroduces a runtime + * dependency on a public CDN, which fails closed in offline, air-gapped, and strict-CSP deployments. * - * Prerequisites: `@videojs/html`'s `build:cdn`. + * Prerequisites: `@videojs/cdn`'s `build:cdn`. */ import { existsSync, globSync } from 'node:fs'; @@ -15,7 +15,7 @@ import { fileURLToPath } from 'node:url'; import { findUnresolvableSpecifiers } from './cdn-graph.ts'; -const CDN_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '../cdn'); +const CDN_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const PREFIX = '\x1b[35m[check-cdn]\x1b[0m'; const log = { @@ -29,7 +29,9 @@ function main() { process.exit(1); } - const files = globSync('**/*.js', { cwd: CDN_DIR }).sort(); + const files = globSync(['*.js', 'chunks/**/*.js', 'extensions/**/*.js', 'locales/**/*.js', 'media/**/*.js'], { + cwd: CDN_DIR, + }).sort(); if (files.length === 0) { log.error(`No JavaScript bundles found in ${CDN_DIR}.`); @@ -46,7 +48,7 @@ function main() { process.exit(1); } - log.info(`✅ ${files.length} bundles — every specifier resolves inside cdn/`); + log.info(`✅ ${files.length} bundles — every specifier resolves inside @videojs/cdn`); } main(); diff --git a/packages/html/scripts/check-cdn-skins.ts b/packages/cdn/scripts/check-cdn-skins.ts similarity index 88% rename from packages/html/scripts/check-cdn-skins.ts rename to packages/cdn/scripts/check-cdn-skins.ts index 0dc18505ba..8f78cf88bb 100644 --- a/packages/html/scripts/check-cdn-skins.ts +++ b/packages/cdn/scripts/check-cdn-skins.ts @@ -1,13 +1,18 @@ /** Verify that every generated skin survives the public CDN build as a browser-ready entry. */ import { existsSync, readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; import { dirname, relative, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { resolveClosure } from './cdn-graph.ts'; -const PACKAGE_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..'); -const CDN_DIR = resolve(PACKAGE_DIR, 'cdn'); +const CDN_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +// The bundles are built from `@videojs/html`'s published output, so that is where the generated skins are read from. +const HTML_DIST_DIR = resolve( + dirname(createRequire(import.meta.url).resolve('@videojs/html/package.json')), + 'dist/default' +); const PREFIX = '\x1b[35m[check-cdn-skins]\x1b[0m'; const forbiddenRuntime = /(?:virtual:vjsc|vjsc\/components|vjsc\/target|@videojs\/core\/vjsc)/; const skins = [ @@ -78,7 +83,7 @@ function main(): void { assert(stylesheet.length > 10_000, `${skin.entry}.css is unexpectedly incomplete`); assert(stylesheet.includes(skin.scope), `${skin.entry}.css is missing its skin scope`); - const registration = readSource(`src/internal/skins/${skin.generated}/register.ts`); + const registration = readSource(`internal/skins/${skin.generated}/register.js`); const expectedSources = registrationImports(registration); for (const mode of ['dev', 'prod'] as const) { @@ -94,7 +99,7 @@ function main(): void { assert(readCdn(entry).includes(`sourceMappingURL=${entry}.map`), `${entry} does not reference its source map`); const sources = mappedSources(closure); - const generatedRegister = `src/internal/skins/${skin.generated}/register.ts`; + const generatedRegister = `internal/skins/${skin.generated}/register.js`; assert( sources.some((value) => value.endsWith(generatedRegister)), @@ -122,7 +127,7 @@ function readCdn(file: string): string { } function readSource(file: string): string { - const path = resolve(PACKAGE_DIR, file); + const path = resolve(HTML_DIST_DIR, file); if (!existsSync(path)) fail(`Expected generated skin source is missing: ${file}`); @@ -155,13 +160,9 @@ function mappedSources(files: Iterable): string[] { function registrationImports(registration: string): string[] { return [...registration.matchAll(/^import ['"]([^'"]+)['"];$/gm)].map((match) => { - const source = resolve(PACKAGE_DIR, 'src/internal/skins/default-video', match[1]!); + const source = resolve(HTML_DIST_DIR, 'internal/skins/default-video', match[1]!); - return ( - relative(PACKAGE_DIR, source) - .replaceAll('\\', '/') - .replace(/\.[cm]?[jt]s$/, '') + '.ts' - ); + return relative(HTML_DIST_DIR, source).replaceAll('\\', '/'); }); } diff --git a/packages/html/scripts/tests/cdn-graph.test.ts b/packages/cdn/scripts/tests/cdn-graph.test.ts similarity index 100% rename from packages/html/scripts/tests/cdn-graph.test.ts rename to packages/cdn/scripts/tests/cdn-graph.test.ts diff --git a/packages/html/scripts/tests/native-magic-string.test.ts b/packages/cdn/scripts/tests/native-magic-string.test.ts similarity index 97% rename from packages/html/scripts/tests/native-magic-string.test.ts rename to packages/cdn/scripts/tests/native-magic-string.test.ts index 34982eee03..429d8db160 100644 --- a/packages/html/scripts/tests/native-magic-string.test.ts +++ b/packages/cdn/scripts/tests/native-magic-string.test.ts @@ -1,8 +1,8 @@ import { type OutputAsset, type OutputChunk, type Plugin, RolldownMagicString, rolldown } from 'rolldown'; import { describe, expect, it } from 'vite-plus/test'; -import { CDN_I18N_REGISTRY, cdnI18nExternalPlugin } from '../../../../build/plugins/cdn-i18n-external-plugin'; import { inlineTemplatePlugin } from '../../../../build/plugins/inline-template-plugin'; +import { CDN_I18N_REGISTRY, cdnI18nExternalPlugin } from '../cdn-i18n-external-plugin'; const ENTRY_ID = 'native-magic-string-fixture.js'; diff --git a/packages/cdn/src/audio-minimal.ts b/packages/cdn/src/audio-minimal.ts new file mode 100644 index 0000000000..2f0bbd008b --- /dev/null +++ b/packages/cdn/src/audio-minimal.ts @@ -0,0 +1,2 @@ +import '@videojs/html/audio/player'; +import '@videojs/html/audio/minimal-skin'; diff --git a/packages/cdn/src/audio-player.ts b/packages/cdn/src/audio-player.ts new file mode 100644 index 0000000000..389881e3eb --- /dev/null +++ b/packages/cdn/src/audio-player.ts @@ -0,0 +1 @@ +import '@videojs/html/audio/player'; diff --git a/packages/cdn/src/audio.ts b/packages/cdn/src/audio.ts new file mode 100644 index 0000000000..c551a9afd1 --- /dev/null +++ b/packages/cdn/src/audio.ts @@ -0,0 +1,2 @@ +import '@videojs/html/audio/player'; +import '@videojs/html/audio/skin'; diff --git a/packages/cdn/src/background.ts b/packages/cdn/src/background.ts new file mode 100644 index 0000000000..28ca90778c --- /dev/null +++ b/packages/cdn/src/background.ts @@ -0,0 +1,3 @@ +import '@videojs/html/background/player'; +import '@videojs/html/background/skin'; +import '@videojs/html/background/video'; diff --git a/packages/cdn/src/extensions/google-cast.ts b/packages/cdn/src/extensions/google-cast.ts new file mode 100644 index 0000000000..a278b55534 --- /dev/null +++ b/packages/cdn/src/extensions/google-cast.ts @@ -0,0 +1 @@ +import '@videojs/html/extensions/google-cast'; diff --git a/packages/cdn/src/extensions/mux-data.ts b/packages/cdn/src/extensions/mux-data.ts new file mode 100644 index 0000000000..aaaf669459 --- /dev/null +++ b/packages/cdn/src/extensions/mux-data.ts @@ -0,0 +1 @@ +import '@videojs/html/extensions/mux-data'; diff --git a/packages/html/src/cdn/i18n-registry.ts b/packages/cdn/src/i18n-registry.ts similarity index 100% rename from packages/html/src/cdn/i18n-registry.ts rename to packages/cdn/src/i18n-registry.ts diff --git a/packages/html/src/cdn/i18n.ts b/packages/cdn/src/i18n.ts similarity index 100% rename from packages/html/src/cdn/i18n.ts rename to packages/cdn/src/i18n.ts diff --git a/packages/cdn/src/live-audio-minimal.ts b/packages/cdn/src/live-audio-minimal.ts new file mode 100644 index 0000000000..401cf9910a --- /dev/null +++ b/packages/cdn/src/live-audio-minimal.ts @@ -0,0 +1,2 @@ +import '@videojs/html/live-audio/player'; +import '@videojs/html/live-audio/minimal-skin'; diff --git a/packages/cdn/src/live-audio-player.ts b/packages/cdn/src/live-audio-player.ts new file mode 100644 index 0000000000..ee03e4dfe0 --- /dev/null +++ b/packages/cdn/src/live-audio-player.ts @@ -0,0 +1 @@ +import '@videojs/html/live-audio/player'; diff --git a/packages/cdn/src/live-audio.ts b/packages/cdn/src/live-audio.ts new file mode 100644 index 0000000000..e198b870b8 --- /dev/null +++ b/packages/cdn/src/live-audio.ts @@ -0,0 +1,2 @@ +import '@videojs/html/live-audio/player'; +import '@videojs/html/live-audio/skin'; diff --git a/packages/cdn/src/live-video-minimal.ts b/packages/cdn/src/live-video-minimal.ts new file mode 100644 index 0000000000..08f2c59974 --- /dev/null +++ b/packages/cdn/src/live-video-minimal.ts @@ -0,0 +1,2 @@ +import '@videojs/html/live-video/player'; +import '@videojs/html/live-video/minimal-skin'; diff --git a/packages/cdn/src/live-video-player.ts b/packages/cdn/src/live-video-player.ts new file mode 100644 index 0000000000..ff0e5c8ebf --- /dev/null +++ b/packages/cdn/src/live-video-player.ts @@ -0,0 +1 @@ +import '@videojs/html/live-video/player'; diff --git a/packages/cdn/src/live-video.ts b/packages/cdn/src/live-video.ts new file mode 100644 index 0000000000..f3f0a3fe10 --- /dev/null +++ b/packages/cdn/src/live-video.ts @@ -0,0 +1,2 @@ +import '@videojs/html/live-video/player'; +import '@videojs/html/live-video/skin'; diff --git a/packages/html/src/cdn/locales/.gitignore b/packages/cdn/src/locales/.gitignore similarity index 100% rename from packages/html/src/cdn/locales/.gitignore rename to packages/cdn/src/locales/.gitignore diff --git a/packages/cdn/src/media/background-video.ts b/packages/cdn/src/media/background-video.ts new file mode 100644 index 0000000000..a4c61a6aeb --- /dev/null +++ b/packages/cdn/src/media/background-video.ts @@ -0,0 +1 @@ +import '@videojs/html/media/background-video'; diff --git a/packages/cdn/src/media/cloudflare-video.ts b/packages/cdn/src/media/cloudflare-video.ts new file mode 100644 index 0000000000..bbe2061746 --- /dev/null +++ b/packages/cdn/src/media/cloudflare-video.ts @@ -0,0 +1 @@ +import '@videojs/html/media/cloudflare-video'; diff --git a/packages/cdn/src/media/dash-video.ts b/packages/cdn/src/media/dash-video.ts new file mode 100644 index 0000000000..c09aa106fd --- /dev/null +++ b/packages/cdn/src/media/dash-video.ts @@ -0,0 +1 @@ +import '@videojs/html/media/dash-video'; diff --git a/packages/cdn/src/media/hls-audio.ts b/packages/cdn/src/media/hls-audio.ts new file mode 100644 index 0000000000..43d432c192 --- /dev/null +++ b/packages/cdn/src/media/hls-audio.ts @@ -0,0 +1 @@ +import '@videojs/html/media/hls-audio'; diff --git a/packages/cdn/src/media/hls-background-video.ts b/packages/cdn/src/media/hls-background-video.ts new file mode 100644 index 0000000000..ed22ab1127 --- /dev/null +++ b/packages/cdn/src/media/hls-background-video.ts @@ -0,0 +1 @@ +import '@videojs/html/media/hls-background-video'; diff --git a/packages/cdn/src/media/hls-video.ts b/packages/cdn/src/media/hls-video.ts new file mode 100644 index 0000000000..498e833c30 --- /dev/null +++ b/packages/cdn/src/media/hls-video.ts @@ -0,0 +1 @@ +import '@videojs/html/media/hls-video'; diff --git a/packages/cdn/src/media/hlsjs-video.ts b/packages/cdn/src/media/hlsjs-video.ts new file mode 100644 index 0000000000..e5544bb36e --- /dev/null +++ b/packages/cdn/src/media/hlsjs-video.ts @@ -0,0 +1 @@ +import '@videojs/html/media/hlsjs-video'; diff --git a/packages/cdn/src/media/mux-audio/hls-js.ts b/packages/cdn/src/media/mux-audio/hls-js.ts new file mode 100644 index 0000000000..76b4a72eb0 --- /dev/null +++ b/packages/cdn/src/media/mux-audio/hls-js.ts @@ -0,0 +1 @@ +import '@videojs/html/media/mux-audio/hls-js'; diff --git a/packages/cdn/src/media/mux-audio/index.ts b/packages/cdn/src/media/mux-audio/index.ts new file mode 100644 index 0000000000..c68a1ac511 --- /dev/null +++ b/packages/cdn/src/media/mux-audio/index.ts @@ -0,0 +1 @@ +import '@videojs/html/media/mux-audio'; diff --git a/packages/cdn/src/media/mux-audio/spf.ts b/packages/cdn/src/media/mux-audio/spf.ts new file mode 100644 index 0000000000..c689774d01 --- /dev/null +++ b/packages/cdn/src/media/mux-audio/spf.ts @@ -0,0 +1 @@ +import '@videojs/html/media/mux-audio/spf'; diff --git a/packages/cdn/src/media/mux-background-video.ts b/packages/cdn/src/media/mux-background-video.ts new file mode 100644 index 0000000000..ffa0aeadc1 --- /dev/null +++ b/packages/cdn/src/media/mux-background-video.ts @@ -0,0 +1 @@ +import '@videojs/html/media/mux-background-video'; diff --git a/packages/cdn/src/media/mux-video/hls-js.ts b/packages/cdn/src/media/mux-video/hls-js.ts new file mode 100644 index 0000000000..eef3e04e4a --- /dev/null +++ b/packages/cdn/src/media/mux-video/hls-js.ts @@ -0,0 +1 @@ +import '@videojs/html/media/mux-video/hls-js'; diff --git a/packages/cdn/src/media/mux-video/index.ts b/packages/cdn/src/media/mux-video/index.ts new file mode 100644 index 0000000000..1d43a094f7 --- /dev/null +++ b/packages/cdn/src/media/mux-video/index.ts @@ -0,0 +1 @@ +import '@videojs/html/media/mux-video'; diff --git a/packages/cdn/src/media/mux-video/spf.ts b/packages/cdn/src/media/mux-video/spf.ts new file mode 100644 index 0000000000..a8e0f2b953 --- /dev/null +++ b/packages/cdn/src/media/mux-video/spf.ts @@ -0,0 +1 @@ +import '@videojs/html/media/mux-video/spf'; diff --git a/packages/cdn/src/media/native-hls-video.ts b/packages/cdn/src/media/native-hls-video.ts new file mode 100644 index 0000000000..3b5b4659cc --- /dev/null +++ b/packages/cdn/src/media/native-hls-video.ts @@ -0,0 +1 @@ +import '@videojs/html/media/native-hls-video'; diff --git a/packages/cdn/src/media/shaka-video.ts b/packages/cdn/src/media/shaka-video.ts new file mode 100644 index 0000000000..ab2f6ae7a8 --- /dev/null +++ b/packages/cdn/src/media/shaka-video.ts @@ -0,0 +1 @@ +import '@videojs/html/media/shaka-video'; diff --git a/packages/cdn/src/media/spotify-audio.ts b/packages/cdn/src/media/spotify-audio.ts new file mode 100644 index 0000000000..a5dc23cd57 --- /dev/null +++ b/packages/cdn/src/media/spotify-audio.ts @@ -0,0 +1 @@ +import '@videojs/html/media/spotify-audio'; diff --git a/packages/cdn/src/media/tiktok-video.ts b/packages/cdn/src/media/tiktok-video.ts new file mode 100644 index 0000000000..9fa29df819 --- /dev/null +++ b/packages/cdn/src/media/tiktok-video.ts @@ -0,0 +1 @@ +import '@videojs/html/media/tiktok-video'; diff --git a/packages/cdn/src/media/twitch-video.ts b/packages/cdn/src/media/twitch-video.ts new file mode 100644 index 0000000000..3171acb726 --- /dev/null +++ b/packages/cdn/src/media/twitch-video.ts @@ -0,0 +1 @@ +import '@videojs/html/media/twitch-video'; diff --git a/packages/cdn/src/media/vimeo-video.ts b/packages/cdn/src/media/vimeo-video.ts new file mode 100644 index 0000000000..995759843f --- /dev/null +++ b/packages/cdn/src/media/vimeo-video.ts @@ -0,0 +1 @@ +import '@videojs/html/media/vimeo-video'; diff --git a/packages/cdn/src/media/wistia-video.ts b/packages/cdn/src/media/wistia-video.ts new file mode 100644 index 0000000000..08db285ab8 --- /dev/null +++ b/packages/cdn/src/media/wistia-video.ts @@ -0,0 +1 @@ +import '@videojs/html/media/wistia-video'; diff --git a/packages/cdn/src/media/youtube-video.ts b/packages/cdn/src/media/youtube-video.ts new file mode 100644 index 0000000000..cd540a362c --- /dev/null +++ b/packages/cdn/src/media/youtube-video.ts @@ -0,0 +1 @@ +import '@videojs/html/media/youtube-video'; diff --git a/packages/cdn/src/tests/entries.test.ts b/packages/cdn/src/tests/entries.test.ts new file mode 100644 index 0000000000..2e28862b47 --- /dev/null +++ b/packages/cdn/src/tests/entries.test.ts @@ -0,0 +1,36 @@ +// @vitest-environment node +import { globSync, readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, it } from 'vite-plus/test'; + +const cdnSrc = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const htmlDefine = resolve(cdnSrc, '../../html/src/define'); + +/** Entry names under a directory: `` for a file, `` and `/` for a directory of flavors. */ +function entryNames(dir: string): string[] { + return globSync('**/*.ts', { cwd: dir }) + .filter((file) => !file.startsWith('tests/')) + .map((file) => file.replace(/\/index\.ts$/, '').replace(/\.ts$/, '')) + .sort(); +} + +describe('cdn entries', () => { + for (const subpath of ['media', 'extensions'] as const) { + it(`ships one ${subpath} bundle per @videojs/html definition`, () => { + expect(entryNames(resolve(cdnSrc, subpath))).toEqual(entryNames(resolve(htmlDefine, subpath))); + }); + + it(`imports each ${subpath} entry from the @videojs/html package`, () => { + for (const file of globSync(`${subpath}/**/*.ts`, { cwd: cdnSrc })) { + const name = file + .replace(/^[a-z]+\//, '') + .replace(/\/index\.ts$/, '') + .replace(/\.ts$/, ''); + + expect(readFileSync(resolve(cdnSrc, file), 'utf8').trim()).toBe(`import '@videojs/html/${subpath}/${name}';`); + } + }); + } +}); diff --git a/packages/html/src/define/tests/video-ejected.test.ts b/packages/cdn/src/tests/video-ejected.test.ts similarity index 94% rename from packages/html/src/define/tests/video-ejected.test.ts rename to packages/cdn/src/tests/video-ejected.test.ts index eac9a663c2..4c15dc803d 100644 --- a/packages/html/src/define/tests/video-ejected.test.ts +++ b/packages/cdn/src/tests/video-ejected.test.ts @@ -1,3 +1,5 @@ +// @vitest-environment happy-dom + import { describe, expect, it } from 'vite-plus/test'; async function waitForUpdates(elements: Element[]): Promise { @@ -8,7 +10,7 @@ async function waitForUpdates(elements: Element[]): Promise { } } -describe('video CDN registration with an ejected layout', () => { +describe('@videojs/cdn/video with an ejected layout', () => { it('updates volume slider child parts when light DOM exists before registration', async () => { document.body.innerHTML = /*html*/ ` @@ -40,7 +42,7 @@ describe('video CDN registration with an ejected layout', () => { value: () => Promise.resolve(), }); - await import('../../cdn/video'); + await import('../video'); const volumeSlider = document.querySelector('media-volume-slider')! as HTMLElement & { orientation: string }; const volumeTrack = volumeSlider.querySelector('media-slider-track')!; diff --git a/packages/cdn/src/video-minimal.ts b/packages/cdn/src/video-minimal.ts new file mode 100644 index 0000000000..fd768b17b6 --- /dev/null +++ b/packages/cdn/src/video-minimal.ts @@ -0,0 +1,2 @@ +import '@videojs/html/video/player'; +import '@videojs/html/video/minimal-skin'; diff --git a/packages/cdn/src/video-player.ts b/packages/cdn/src/video-player.ts new file mode 100644 index 0000000000..0fa31fcbfa --- /dev/null +++ b/packages/cdn/src/video-player.ts @@ -0,0 +1 @@ +import '@videojs/html/video/player'; diff --git a/packages/cdn/src/video.ts b/packages/cdn/src/video.ts new file mode 100644 index 0000000000..e539ba272f --- /dev/null +++ b/packages/cdn/src/video.ts @@ -0,0 +1,3 @@ +import '@videojs/html/i18n'; +import '@videojs/html/video/player'; +import '@videojs/html/video/skin'; diff --git a/packages/cdn/tsconfig.dts.json b/packages/cdn/tsconfig.dts.json new file mode 100644 index 0000000000..c95a8f1c2c --- /dev/null +++ b/packages/cdn/tsconfig.dts.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": false, + "incremental": false, + "declarationMap": false, + "lib": ["ES2022", "DOM", "DOM.Iterable"] + }, + "include": ["src/i18n.ts"] +} diff --git a/packages/cdn/vite.config.ts b/packages/cdn/vite.config.ts new file mode 100644 index 0000000000..61e5f7159d --- /dev/null +++ b/packages/cdn/vite.config.ts @@ -0,0 +1,253 @@ +import { globSync, rmSync, writeFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { defineConfig } from 'vite-plus'; +import type { UserConfig as PackUserConfig } from 'vite-plus/pack'; + +import { baseConfig } from '../../build/pack.ts'; +import { copyCssPlugin } from '../../build/plugins/copy-css-plugin.ts'; +import type { BuildPlugin } from '../../build/plugins/types.ts'; +import { cachedTaskInputs, workspaceTaskDependencies } from '../../build/task.ts'; +import { LOCALES, localeAliases } from '../core/src/core/i18n/locales.ts'; +import { cdnI18nExternalPlugin } from './scripts/cdn-i18n-external-plugin.ts'; + +type CdnBuildMode = 'dev' | 'prod'; + +const packageDir = dirname(fileURLToPath(import.meta.url)); +// The bundles are built from `@videojs/html`'s published output, resolved the same way a consumer would, so the CDN +// never reaches into another package's sources. Its stylesheets are copied from that output too. +const htmlDistDir = resolve( + dirname(createRequire(import.meta.url).resolve('@videojs/html/package.json')), + 'dist/default' +); +// Generated skin registrations register elements as a side effect and export nothing. +const generatedSkinRegistration = /\/internal\/skins\/.+\/register\.[cm]?[jt]s$/; +const localeTags = [...LOCALES, ...localeAliases(LOCALES)]; + +const cdnPresets = [ + 'video', + 'video-player', + 'video-minimal', + 'live-video', + 'live-video-player', + 'live-video-minimal', + 'audio', + 'audio-player', + 'audio-minimal', + 'live-audio', + 'live-audio-player', + 'live-audio-minimal', + 'background', +]; + +/** + * One CDN bundle per media or extension entry under `src/`, or per flavor for an entry that is a directory. Each file + * is a one-line import of the matching `@videojs/html` module; `src/tests/entries.test.ts` keeps them in step with the + * definitions html ships so the npm and CDN delivery surfaces cannot drift. + */ +function getCdnEntries(subpath: 'media' | 'extensions') { + return globSync(`src/${subpath}/**/*.ts`, { cwd: packageDir }) + .map((src) => ({ + src, + name: src + .replace(/^src\//, '') + .replace(/\/index\.ts$/, '') + .replace(/\.ts$/, ''), + })) + .sort((a, b) => a.name.localeCompare(b.name)); +} + +const cdnMediaEntries = getCdnEntries('media'); +const cdnExtensionEntries = getCdnEntries('extensions'); + +const cdnLocaleEntries = localeTags.map((tag) => ({ + src: `src/locales/${tag}.ts`, + name: `locales/${tag}`, +})); + +export const entries = [ + { src: 'src/i18n.ts', name: 'i18n' }, + ...cdnLocaleEntries, + ...cdnPresets.map((name) => ({ src: `src/${name}.ts`, name })), + ...cdnMediaEntries, + ...cdnExtensionEntries, +]; + +function cdnStylesheetName(file: string): string | null { + const match = /^define\/(?:([\w-]+)\/)?([\w-]+)\.css$/.exec(file); + if (!match) return null; + + const [, preset, name] = match; + if (!preset) return `${name}.css`; + + if (preset === 'background') return name === 'skin' ? 'background.css' : null; + + const suffix = name === 'skin' ? '' : name === 'minimal-skin' ? '-minimal' : null; + if (suffix === null) return null; + + return `${preset}${suffix}.css`; +} + +function cleanCdnOutputPlugin(): BuildPlugin { + return { + name: 'clean-cdn-output', + buildStart() { + for (const dir of ['chunks', 'extensions', 'locales', 'media']) { + rmSync(resolve(packageDir, dir), { recursive: true, force: true }); + } + + for (const file of globSync('*.{css,js,js.map}', { cwd: packageDir })) { + rmSync(resolve(packageDir, file), { force: true }); + } + + for (const file of globSync('*.d.ts', { cwd: packageDir })) { + rmSync(resolve(packageDir, file), { force: true }); + } + }, + }; +} + +/** + * Every bundle except `i18n` registers elements as a side effect and exports nothing, so an empty module is its true + * type. `i18n` is the one entry consumers import by name; its declarations are generated by the `cdn-i18n-types` pack. + */ +function dtsStubsPlugin(outDir: string): BuildPlugin { + return { + name: 'cdn-dts-stubs', + writeBundle(_options, bundle) { + for (const file of Object.keys(bundle)) { + if (!file.endsWith('.dev.js') || file === 'i18n.dev.js') continue; + + writeFileSync(resolve(outDir, file.replace('.dev.js', '.dev.d.ts')), 'export {};\n'); + } + }, + }; +} + +const cdnPackConfigs: PackUserConfig[] = []; + +for (const mode of ['dev', 'prod'] satisfies CdnBuildMode[]) { + const isProd = mode === 'prod'; + const entry = Object.fromEntries(entries.map(({ src, name }) => [isProd ? name : `${name}.dev`, src])); + + cdnPackConfigs.push({ + ...baseConfig, + name: 'cdn', + entry, + platform: 'browser', + format: 'es', + target: 'es2022', + sourcemap: true, + clean: false, + dts: false, + minify: isProd, + deps: { + alwaysBundle: [/.*/], + onlyBundle: false, + }, + treeshake: { + moduleSideEffects: [ + { test: generatedSkinRegistration, sideEffects: true }, + { test: /\/define\//, sideEffects: true }, + { test: /\/icons\/(?:dist\/)?element\//, sideEffects: true }, + ], + }, + outDir: packageDir, + define: { __DEV__: isProd ? 'false' : 'true' }, + plugins: [ + ...(!isProd ? [cleanCdnOutputPlugin()] : []), + cdnI18nExternalPlugin({ prod: isProd }), + ...(isProd + ? [ + // html's build already resolved every `@import`, so the sheets are copied as-is and only minified. + copyCssPlugin({ + rootDir: htmlDistDir, + pattern: 'define/**/*.css', + outDir: packageDir, + rename: cdnStylesheetName, + inline: false, + minify: true, + }), + ] + : [dtsStubsPlugin(packageDir)]), + ], + inputOptions: { + ...baseConfig.inputOptions, + onwarn(warning, defaultHandler) { + if (warning.code === 'COMMONJS_VARIABLE_IN_ESM') return; + + defaultHandler(warning); + }, + ...(!isProd && { + resolve: { conditionNames: ['development', 'import', 'browser', 'default'] }, + }), + }, + }); +} + +// Declarations for the `i18n` entry, bundled from `@videojs/core/i18n` the same way its JavaScript is, so +// `@videojs/cdn/i18n` types resolve without installing any other package. +cdnPackConfigs.push({ + ...baseConfig, + name: 'cdn', + entry: { 'i18n.dev': 'src/i18n.ts' }, + platform: 'browser', + format: 'es', + clean: false, + outDir: packageDir, + dts: { emitDtsOnly: true, tsgo: true, tsconfig: 'tsconfig.dts.json' }, + deps: { dts: { alwaysBundle: [/^@videojs\//] } }, +}); + +export default defineConfig({ + define: { + __DEV__: 'true', + }, + run: { + tasks: { + 'build:cdn': { + command: 'node --import tsx ./scripts/build-cdn-locales.ts && vp pack --filter cdn', + dependsOn: workspaceTaskDependencies(), + input: [ + ...cachedTaskInputs, + '!src/locales/', + '!src/locales/**', + '!*.css', + '!*.d.ts', + '!*.js', + '!*.js.map', + '!chunks/', + '!chunks/**', + '!extensions/', + '!extensions/**', + '!locales/', + '!locales/**', + '!media/', + '!media/**', + ], + output: [ + 'src/locales/**', + '*.css', + '*.d.ts', + '*.js', + '*.js.map', + 'chunks/**', + 'extensions/**', + 'locales/**', + 'media/**', + ], + }, + 'test:ci': { + command: 'pnpm test', + cache: false, + dependsOn: workspaceTaskDependencies(), + }, + }, + }, + test: { + environment: 'node', + }, + pack: cdnPackConfigs, +}); diff --git a/packages/cli/src/commands/tests/docs.test.ts b/packages/cli/src/commands/tests/docs.test.ts index 44de9752df..ae462124d4 100644 --- a/packages/cli/src/commands/tests/docs.test.ts +++ b/packages/cli/src/commands/tests/docs.test.ts @@ -51,6 +51,7 @@ vi.mock('@clack/prompts', () => ({ import * as p from '@clack/prompts'; +import cdnPackage from '../../../../cdn/package.json' with { type: 'json' }; import { getConfigValue } from '../../utils/config.js'; import { docExistsInAnyFramework, readBundledDoc, readLlmsTxt } from '../../utils/docs.js'; import { handleDocs } from '../docs.js'; @@ -364,7 +365,7 @@ describe('handleDocs', () => { ]); const out = output(); - expect(out).toContain('cdn/live-video.js'); + expect(out).toContain(`@videojs/cdn@${cdnPackage.version}/live-video.js`); expect(out).toContain('media/hlsjs-video.js'); }); }); diff --git a/packages/core/vite.config.ts b/packages/core/vite.config.ts index 3717cecae1..15e26932ba 100644 --- a/packages/core/vite.config.ts +++ b/packages/core/vite.config.ts @@ -60,8 +60,16 @@ export default defineConfig({ ...cachedTaskInputs, { pattern: '!packages/cli/docs', base: 'workspace' }, { pattern: '!packages/cli/docs/**', base: 'workspace' }, - { pattern: '!packages/html/cdn', base: 'workspace' }, - { pattern: '!packages/html/cdn/**', base: 'workspace' }, + { pattern: '!packages/cdn/*.css', base: 'workspace' }, + { pattern: '!packages/cdn/*.d.ts', base: 'workspace' }, + { pattern: '!packages/cdn/*.js', base: 'workspace' }, + { pattern: '!packages/cdn/*.js.map', base: 'workspace' }, + { pattern: '!packages/cdn/archive/**', base: 'workspace' }, + { pattern: '!packages/cdn/chunks/**', base: 'workspace' }, + { pattern: '!packages/cdn/extensions/**', base: 'workspace' }, + { pattern: '!packages/cdn/locales/**', base: 'workspace' }, + { pattern: '!packages/cdn/media/**', base: 'workspace' }, + { pattern: '!packages/cdn/src/locales/**', base: 'workspace' }, ], output: [ 'dist/**', diff --git a/packages/html/package.json b/packages/html/package.json index e32d50fca0..3cb538d1a2 100644 --- a/packages/html/package.json +++ b/packages/html/package.json @@ -18,7 +18,6 @@ }, "files": [ "dist", - "cdn", "docs" ], "type": "module", @@ -28,9 +27,7 @@ "./dist/*/i18n/index.js", "./dist/*/i18n/locales/*/register.js", "./dist/*/icons/element/**/*.js", - "./dist/*/icons/dist/element/**/*.js", - "./cdn/**/*.js", - "./cdn/*.css" + "./dist/*/icons/dist/element/**/*.js" ], "main": "dist/default/index.js", "module": "dist/default/index.js", @@ -117,11 +114,6 @@ "development": "./dist/dev/define/feature/*.js", "default": "./dist/default/define/feature/*.js" }, - "./extensions/*": { - "types": "./dist/dev/define/extensions/*.d.ts", - "development": "./dist/dev/define/extensions/*.js", - "default": "./dist/default/define/extensions/*.js" - }, "./media/mux-video": { "types": "./dist/dev/define/media/mux-video/index.d.ts", "development": "./dist/dev/define/media/mux-video/index.js", @@ -137,37 +129,6 @@ "development": "./dist/dev/define/media/*.js", "default": "./dist/default/define/media/*.js" }, - "./cdn/media/*": { - "types": "./cdn/media/*.dev.d.ts", - "development": "./cdn/media/*.dev.js", - "default": "./cdn/media/*.js" - }, - "./cdn/extensions/*": { - "types": "./cdn/extensions/*.dev.d.ts", - "development": "./cdn/extensions/*.dev.js", - "default": "./cdn/extensions/*.js" - }, - "./cdn/i18n": { - "types": "./dist/dev/i18n/index.d.ts", - "development": "./cdn/i18n.dev.js", - "default": "./cdn/i18n.js" - }, - "./cdn/i18n-registry": { - "types": "./dist/dev/i18n/index.d.ts", - "development": "./cdn/i18n.dev.js", - "default": "./cdn/i18n.js" - }, - "./cdn/locales/*": { - "types": "./cdn/locales/*.dev.d.ts", - "development": "./cdn/locales/*.dev.js", - "default": "./cdn/locales/*.js" - }, - "./cdn/*.css": "./cdn/*.css", - "./cdn/*": { - "types": "./cdn/*.dev.d.ts", - "development": "./cdn/*.dev.js", - "default": "./cdn/*.js" - }, "./i18n": { "types": "./dist/dev/i18n/index.d.ts", "development": "./dist/dev/i18n/index.js", @@ -189,12 +150,10 @@ "access": "public" }, "scripts": { - "check:cdn": "node --import tsx ./scripts/check-cdn-self-contained.ts && node --import tsx ./scripts/check-cdn-skins.ts", - "build:archive": "node --import tsx ./scripts/build-dist-archive.ts", - "build:watch": "vp pack --filter package --watch ./src --no-clean", + "build:watch": "vp pack --watch ./src --no-clean", "dev": "pnpm run build:watch", "test": "vp test run", - "clean": "rimraf --glob dist cdn archive docs types '*.tsbuildinfo'", + "clean": "rimraf --glob dist docs types '*.tsbuildinfo'", "prepack": "node --import tsx ../../site/scripts/copy-package-docs.ts html" }, "dependencies": { @@ -217,6 +176,7 @@ "@videojs/mux-data": "workspace:*", "@videojs/mux-video": "workspace:*", "@videojs/shaka-video": "workspace:*", + "@videojs/skins": "workspace:*", "@videojs/spotify-audio": "workspace:*", "@videojs/tiktok-video": "workspace:*", "@videojs/twitch-video": "workspace:*", @@ -224,8 +184,6 @@ "@videojs/wistia-video": "workspace:*", "@videojs/youtube-video": "workspace:*", "happy-dom": "^20.11.1", - "rolldown": "~1.2.5", - "tsx": "^4.23.1", "typescript": "^6.0.2", "vite-plus": "catalog:", "vitest": "catalog:" diff --git a/packages/html/src/cdn/audio-minimal.ts b/packages/html/src/cdn/audio-minimal.ts deleted file mode 100644 index 5fece6d69f..0000000000 --- a/packages/html/src/cdn/audio-minimal.ts +++ /dev/null @@ -1,2 +0,0 @@ -import '../define/audio/player'; -import '../define/audio/minimal-skin'; diff --git a/packages/html/src/cdn/audio-player.ts b/packages/html/src/cdn/audio-player.ts deleted file mode 100644 index 2f2fd6b425..0000000000 --- a/packages/html/src/cdn/audio-player.ts +++ /dev/null @@ -1 +0,0 @@ -import '../define/audio/player'; diff --git a/packages/html/src/cdn/audio.ts b/packages/html/src/cdn/audio.ts deleted file mode 100644 index 278494305e..0000000000 --- a/packages/html/src/cdn/audio.ts +++ /dev/null @@ -1,2 +0,0 @@ -import '../define/audio/player'; -import '../define/audio/skin'; diff --git a/packages/html/src/cdn/background.ts b/packages/html/src/cdn/background.ts deleted file mode 100644 index a590e19735..0000000000 --- a/packages/html/src/cdn/background.ts +++ /dev/null @@ -1,3 +0,0 @@ -import '../define/background/player'; -import '../define/background/skin'; -import '../define/background/video'; diff --git a/packages/html/src/cdn/live-audio-minimal.ts b/packages/html/src/cdn/live-audio-minimal.ts deleted file mode 100644 index fc4262c4c3..0000000000 --- a/packages/html/src/cdn/live-audio-minimal.ts +++ /dev/null @@ -1,2 +0,0 @@ -import '../define/live-audio/player'; -import '../define/live-audio/minimal-skin'; diff --git a/packages/html/src/cdn/live-audio-player.ts b/packages/html/src/cdn/live-audio-player.ts deleted file mode 100644 index c072e1199e..0000000000 --- a/packages/html/src/cdn/live-audio-player.ts +++ /dev/null @@ -1 +0,0 @@ -import '../define/live-audio/player'; diff --git a/packages/html/src/cdn/live-audio.ts b/packages/html/src/cdn/live-audio.ts deleted file mode 100644 index 3aa332bd94..0000000000 --- a/packages/html/src/cdn/live-audio.ts +++ /dev/null @@ -1,2 +0,0 @@ -import '../define/live-audio/player'; -import '../define/live-audio/skin'; diff --git a/packages/html/src/cdn/live-video-minimal.ts b/packages/html/src/cdn/live-video-minimal.ts deleted file mode 100644 index d79e03e75c..0000000000 --- a/packages/html/src/cdn/live-video-minimal.ts +++ /dev/null @@ -1,2 +0,0 @@ -import '../define/live-video/player'; -import '../define/live-video/minimal-skin'; diff --git a/packages/html/src/cdn/live-video-player.ts b/packages/html/src/cdn/live-video-player.ts deleted file mode 100644 index 5179684f33..0000000000 --- a/packages/html/src/cdn/live-video-player.ts +++ /dev/null @@ -1 +0,0 @@ -import '../define/live-video/player'; diff --git a/packages/html/src/cdn/live-video.ts b/packages/html/src/cdn/live-video.ts deleted file mode 100644 index b62745b423..0000000000 --- a/packages/html/src/cdn/live-video.ts +++ /dev/null @@ -1,2 +0,0 @@ -import '../define/live-video/player'; -import '../define/live-video/skin'; diff --git a/packages/html/src/cdn/video-minimal.ts b/packages/html/src/cdn/video-minimal.ts deleted file mode 100644 index 917f2732f2..0000000000 --- a/packages/html/src/cdn/video-minimal.ts +++ /dev/null @@ -1,2 +0,0 @@ -import '../define/video/player'; -import '../define/video/minimal-skin'; diff --git a/packages/html/src/cdn/video-player.ts b/packages/html/src/cdn/video-player.ts deleted file mode 100644 index 7d4267452b..0000000000 --- a/packages/html/src/cdn/video-player.ts +++ /dev/null @@ -1 +0,0 @@ -import '../define/video/player'; diff --git a/packages/html/src/cdn/video.ts b/packages/html/src/cdn/video.ts deleted file mode 100644 index d163167b4d..0000000000 --- a/packages/html/src/cdn/video.ts +++ /dev/null @@ -1,3 +0,0 @@ -import '../define/i18n'; -import '../define/video/player'; -import '../define/video/skin'; diff --git a/packages/html/tsconfig.json b/packages/html/tsconfig.json index 1fd70fd756..18de02111d 100644 --- a/packages/html/tsconfig.json +++ b/packages/html/tsconfig.json @@ -9,6 +9,5 @@ "useDefineForClassFields": false }, "include": ["src"], - "exclude": ["src/cdn/locales"], "references": [{ "path": "../core" }, { "path": "../icons" }] } diff --git a/packages/html/vite.config.ts b/packages/html/vite.config.ts index c3ae03d3e2..a977ace262 100644 --- a/packages/html/vite.config.ts +++ b/packages/html/vite.config.ts @@ -1,32 +1,20 @@ -import { globSync, readdirSync, writeFileSync } from 'node:fs'; -import { basename, dirname, resolve } from 'node:path'; +import { globSync } from 'node:fs'; +import { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { defineConfig } from 'vite-plus'; import type { UserConfig as PackUserConfig } from 'vite-plus/pack'; -import { - baseConfig, - isDevBuildMode, - type PackageBuildMode, - packageBuildConfig, - packageBuildModes, -} from '../../build/pack.ts'; -import { cdnI18nExternalPlugin } from '../../build/plugins/cdn-i18n-external-plugin.ts'; +import { isDevBuildMode, type PackageBuildMode, packageBuildConfig, packageBuildModes } from '../../build/pack.ts'; 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'; import { cachedTaskInputs, packageTestTask, workspaceTaskDependencies } from '../../build/task.ts'; import { LOCALES, localeAliases } from '../core/src/core/i18n/locales.ts'; -type CdnBuildMode = 'dev' | 'prod'; - const packageDir = dirname(fileURLToPath(import.meta.url)); const srcDir = new URL('./src', import.meta.url).pathname; -/** The light-DOM sheet every non-background skin pulls in. Kept out of the shadow sheets. */ -const globalCss = resolve(packageDir, 'src/define/global.css'); - const srcAlias = { '@': srcDir }; const generatedSkinRegistration = /\/internal\/skins\/.+\/register\.[cm]?[jt]s$/; const localeTags = [...LOCALES, ...localeAliases(LOCALES)]; @@ -99,226 +87,15 @@ const createPackConfig = (mode: PackageBuildMode): PackUserConfig => ({ ], }); -const cdnBuildModes: CdnBuildMode[] = ['dev', 'prod']; -const cdnPresets = [ - 'video', - 'video-player', - 'video-minimal', - 'live-video', - 'live-video-player', - 'live-video-minimal', - 'audio', - 'audio-player', - 'audio-minimal', - 'live-audio', - 'live-audio-player', - 'live-audio-minimal', - 'background', -]; -const mediaDir = 'src/define/media'; -const mediaDirPath = resolve(packageDir, mediaDir); -const extensionsDir = 'src/define/extensions'; -const extensionsDirPath = resolve(packageDir, extensionsDir); - -/** - * One CDN bundle per definition module — or per flavor, for a module that is a directory. - * - * Discovered from the definitions so the npm and CDN delivery surfaces cannot drift. A directory ships its index as the - * flavor-neutral bundle and each flavor beside it, so `media/mux-video/spf` reads the same as the npm subpath it - * mirrors. - * - * A CDN page picks bundles at runtime rather than by import path, so this is where two flavors of one element can end - * up in a single realm — see the tag-collision note in `define/media/mux-video/spf`. - */ -function getCdnDefinitionEntries(dir: string, dirPath: string, subpath: string) { - return readdirSync(dirPath, { withFileTypes: true }) - .flatMap((entry) => { - if (entry.isDirectory()) { - return globSync(`${dir}/${entry.name}/*.ts`, { cwd: packageDir }).map((src) => { - const flavor = basename(src, '.ts'); - - return { src, name: flavor === 'index' ? `${subpath}/${entry.name}` : `${subpath}/${entry.name}/${flavor}` }; - }); - } - - return entry.name.endsWith('.ts') - ? [{ src: `${dir}/${entry.name}`, name: `${subpath}/${basename(entry.name, '.ts')}` }] - : []; - }) - .sort((a, b) => a.name.localeCompare(b.name)); -} - -const cdnMediaEntries = getCdnDefinitionEntries(mediaDir, mediaDirPath, 'media'); -const cdnExtensionEntries = getCdnDefinitionEntries(extensionsDir, extensionsDirPath, 'extensions'); - -const cdnLocaleEntries = localeTags.map((tag) => ({ - src: `src/cdn/locales/${tag}.ts`, - name: `locales/${tag}`, -})); - -/** - * Every CDN bundle the build emits, as `{ src, name }` where `name` is the output path without its extension. Exported - * so the distribution archive can take its entry points from the build definition rather than guessing which built - * files are entries and which are shared chunks. - * - * The `src` paths are relative to this package, so importers must run from the package root. - */ -export const entries = [ - { src: 'src/cdn/i18n.ts', name: 'i18n' }, - ...cdnLocaleEntries, - ...cdnPresets.map((name) => ({ src: `src/cdn/${name}.ts`, name })), - ...cdnMediaEntries, - ...cdnExtensionEntries, -]; - -/** - * Flat CDN filename for a stylesheet, or null for a source file that is not published. - * - * A bundle inlines its own CSS and applies it on upgrade, so these files are not needed to run the player. They exist - * for the two things a running element cannot do for a page: - * - * - **Light DOM, `` in ``.** `global.css` and `background.css` are what the elements inject into - * `document.head` themselves. Linking them instead applies the host and media box rules before the custom element - * upgrades, which is what stops the pre-upgrade layout shift. - * - **Shadow DOM, `` inside a `