From 1c49bedbee56b119496af492ce39c0b61f4de931 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Wed, 31 Dec 2025 02:17:06 +0800 Subject: [PATCH] Refactor website CDN url replacements (#18553) --- website/scripts/collect-dependencies.mjs | 106 +++++++++++++++++ website/vite.config.mjs | 141 ++++++++++------------- 2 files changed, 166 insertions(+), 81 deletions(-) create mode 100644 website/scripts/collect-dependencies.mjs diff --git a/website/scripts/collect-dependencies.mjs b/website/scripts/collect-dependencies.mjs new file mode 100644 index 000000000..cca915c9b --- /dev/null +++ b/website/scripts/collect-dependencies.mjs @@ -0,0 +1,106 @@ +import fs from "node:fs/promises"; +import { findPackageJSON } from "node:module"; + +async function _getPackageJson(packageJsonUrl) { + const packageJson = JSON.parse(await fs.readFile(packageJsonUrl, "utf8")); + const dependencies = new Set(); + + for (const { dependencyType, optional } of [ + { dependencyType: "dependencies", optional: false }, + { dependencyType: "peerDependencies", optional: true }, + ]) { + if (!packageJson[dependencyType]) { + continue; + } + + for (const name of Object.keys(packageJson[dependencyType])) { + let dependencyPackageJsonUrl; + + try { + dependencyPackageJsonUrl = findPackageJSON(name, packageJsonUrl); + } catch (error) { + if (optional && error.code === "ERR_MODULE_NOT_FOUND") { + continue; + } + throw error; + } + + dependencies.add({ name, packageJsonUrl: dependencyPackageJsonUrl }); + } + } + + return { + name: packageJson.name, + version: packageJson.version, + packageJsonUrl, + dependencies: [...dependencies], + }; +} + +const packageJsonCache = new Map(); +function getPackageJson(packageJsonUrl) { + if (!packageJsonCache.has(packageJsonUrl)) { + packageJsonCache.set(packageJsonUrl, _getPackageJson(packageJsonUrl)); + } + + return packageJsonCache.get(packageJsonUrl); +} + +/** +@param {{ + name: string, + base: string | URL, + overrideDependencies?: Map, + ignoreDependencies?: (name: string) => boolean, +}} param0 +@returns +*/ +async function getPackageDependencies({ + name, + base, + overrideDependencies = new Map(), + ignoreDependencies = () => false, +}) { + const packageJsonUrl = findPackageJSON(name, base); + const packageJsonUrls = new Set(); + + const packages = [{ name, packageJsonUrl }]; + while (packages.length > 0) { + const { name, packageJsonUrl } = packages.pop(); + if (ignoreDependencies(name) || packageJsonUrls.has(packageJsonUrl)) { + continue; + } + + packageJsonUrls.add(packageJsonUrl); + + let dependencies; + + if (overrideDependencies.has(name)) { + dependencies = overrideDependencies + .get(name) + .map((name) => ({ name, packageJsonUrl: findPackageJSON(name, base) })); + } else { + ({ dependencies } = await getPackageJson(packageJsonUrl)); + } + + packages.push(...dependencies); + } + + // Delete self + packageJsonUrls.delete(packageJsonUrl); + + const [packageJsonData, ...dependencies] = await Promise.all( + [packageJsonUrl, ...packageJsonUrls].map((packageJsonUrl) => + getPackageJson(packageJsonUrl), + ), + ); + + return { + ...packageJsonData, + dependencies: dependencies.toSorted(({ name: nameA }, { name: nameB }) => + nameA.localeCompare(nameB), + ), + }; +} + +export { getPackageDependencies }; diff --git a/website/vite.config.mjs b/website/vite.config.mjs index b0210d73c..48b3c1812 100644 --- a/website/vite.config.mjs +++ b/website/vite.config.mjs @@ -1,119 +1,98 @@ -import fs from "node:fs/promises"; -import module from "node:module"; +import assert from "node:assert/strict"; import vitePluginVue from "@vitejs/plugin-vue"; import vitePluginVueJsx from "@vitejs/plugin-vue-jsx"; import { visualizer as rollupPluginVisualizer } from "rollup-plugin-visualizer"; import { defineConfig } from "vite"; import packageJson from "./package.json" with { type: "json" }; +import { getPackageDependencies } from "./scripts/collect-dependencies.mjs"; const IS_PRODUCTION = process.env.NODE_ENV === "production"; const IS_CI = Boolean(process.env.CI); const OUT_DIRECTORY = "./static/playground/"; -const DEPENDENCIES_EXCLUDE_FROM_CDN = new Set(["@docusaurus/preset-classic"]); +const cdnDependencies = { + exclude: new Set(["@docusaurus/preset-classic"]), + overrideDependencies: new Map([ + // We don't need deps from `vue` + ["vue", []], + ]), + ignoreDependencies: (name) => name.startsWith("@types/"), +}; // Local develop requires `vue` for HMR if (!IS_PRODUCTION) { - DEPENDENCIES_EXCLUDE_FROM_CDN.add("vue"); + cdnDependencies.exclude.add("vue"); } -export default defineConfig(async () => ({ - base: IS_PRODUCTION ? "/playground/" : undefined, - publicDir: IS_PRODUCTION ? undefined : "./static/", - plugins: [ - vitePluginVue(), - vitePluginVueJsx(), - IS_CI || !IS_PRODUCTION - ? undefined - : rollupPluginVisualizer({ - filename: `${OUT_DIRECTORY}bundle-report.html`, - }), - ].filter(Boolean), - resolve: { - alias: await buildCdnAlias(), - }, - build: { - outDir: OUT_DIRECTORY, - minify: IS_CI, - }, -})); +export default defineConfig(async () => { + const alias = await buildCdnAlias(); + + return { + base: IS_PRODUCTION ? "/playground/" : undefined, + publicDir: IS_PRODUCTION ? undefined : "./static/", + plugins: [ + vitePluginVue(), + vitePluginVueJsx(), + IS_CI || !IS_PRODUCTION + ? undefined + : rollupPluginVisualizer({ + filename: `${OUT_DIRECTORY}bundle-report.html`, + }), + ].filter(Boolean), + resolve: { + alias, + }, + build: { + outDir: OUT_DIRECTORY, + minify: IS_CI, + }, + }; +}); async function buildCdnAlias() { - // if (!IS_PRODUCTION) { - // return; - // } + if (!IS_PRODUCTION) { + return; + } return Object.fromEntries( await Promise.all( Object.keys(packageJson.dependencies) - .filter((name) => !DEPENDENCIES_EXCLUDE_FROM_CDN.has(name)) + .filter((name) => !cdnDependencies.exclude.has(name)) .map(async (name) => [name, await getPackageCdnUrl(name)]), ), ); } -async function getPackageCdnUrl(name) { - const version = packageJson.dependencies[name]; +async function getPackageCdnUrl(dependencyName) { + const { name, version, dependencies } = await getPackageDependencies({ + name: dependencyName, + base: import.meta.url, + overrideDependencies: cdnDependencies.overrideDependencies, + ignoreDependencies: cdnDependencies.ignoreDependencies, + }); + + assertDependenciesUnique(name, dependencies); const url = new URL(`https://esm.sh/${name}@${version}`); - - let dependencies = await getPackageDependencies(name, import.meta.url); - - dependencies = dependencies - // Exclude self - .filter((dependency) => dependency.name !== name) - // Sort alphabetically - .toSorted(({ name: nameA }, { name: nameB }) => nameA.localeCompare(nameB)) - // Dedupe - .filter( - ({ name, version }, index, dependencies) => - dependencies.findIndex( - (searching) => - searching.name === name && searching.version === version, - ) === index, - ); + const { searchParams } = url; + // searchParams.set("conditions", ["browser"]); if (dependencies.length > 0) { - url.searchParams.set( + searchParams.set( "deps", dependencies.map(({ name, version }) => `${name}@${version}`).join(","), ); } - return url; + return url.href; } -async function getPackageJson(name, base) { - const packageJsonUrl = module.findPackageJSON(name, base); +function assertDependenciesUnique(name, dependencies) { + const versions = new Map(); - return { - url: packageJsonUrl, - packageJson: JSON.parse(await fs.readFile(packageJsonUrl, "utf8")), - }; -} - -async function getPackageDependenciesWithoutCache(name, base) { - const { url, packageJson } = await getPackageJson(name, base); - const dependencies = [{ name, version: packageJson.version }]; - for (const name of Object.keys({ - ...packageJson.dependencies, - ...packageJson.peerDependencies, - })) { - dependencies.push(...(await getPackageDependencies(name, url))); + for (const dependency of dependencies) { + assert.ok( + !versions.has(dependency.name), + `Multiple version of '${dependency.name}' in dependency tree of '${name}', '${versions.get(dependency.name)?.version}' and '${dependency.version}'.`, + ); + versions.set(dependency.name, dependency); } - return dependencies; -} - -const cache = new Map(); -function getPackageDependencies(name, base) { - // We don't need deps from `vue` - if (name === "vue") { - return []; - } - - const cacheKey = JSON.stringify({ name, base }); - - if (!cache.has(cacheKey)) { - cache.set(cacheKey, getPackageDependenciesWithoutCache(name, base)); - } - - return cache.get(cacheKey); }