From c28fdadc2393c5cdbafe7135c1a533d7532ea77c Mon Sep 17 00:00:00 2001 From: Alexander Rybakov Date: Fri, 12 Jun 2026 13:49:53 +0200 Subject: [PATCH 1/5] peed up dev startup by pre-bundling external deps in optimizeDeps --- apps/aurora/news/+vite-optimize-deps.internal | 1 + apps/aurora/vite.config.ts | 35 ++++++ docs/development/index.md | 1 + docs/development/vite-optimize-deps.md | 115 ++++++++++++++++++ .../cmsui/news/+vite-optimize-deps.internal | 1 + packages/cmsui/vite.extend.js | 13 ++ .../layout/news/+vite-optimize-deps.internal | 1 + packages/layout/vite.extend.js | 14 +++ .../plate/news/+vite-optimize-deps.internal | 1 + packages/plate/vite.extend.js | 48 ++++++++ .../news/+vite-extend-support.internal | 1 + packages/registry/vite-plugin.js | 9 +- 12 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 apps/aurora/news/+vite-optimize-deps.internal create mode 100644 docs/development/vite-optimize-deps.md create mode 100644 packages/cmsui/news/+vite-optimize-deps.internal create mode 100644 packages/cmsui/vite.extend.js create mode 100644 packages/layout/news/+vite-optimize-deps.internal create mode 100644 packages/layout/vite.extend.js create mode 100644 packages/plate/news/+vite-optimize-deps.internal create mode 100644 packages/plate/vite.extend.js create mode 100644 packages/registry/news/+vite-extend-support.internal diff --git a/apps/aurora/news/+vite-optimize-deps.internal b/apps/aurora/news/+vite-optimize-deps.internal new file mode 100644 index 000000000..a9ab4d7d1 --- /dev/null +++ b/apps/aurora/news/+vite-optimize-deps.internal @@ -0,0 +1 @@ +Added `optimizeDeps.include` entries for non-addon workspace packages and app-level deps, eliminating lazy dependency discovery reloads on dev server startup. @arybakov05 diff --git a/apps/aurora/vite.config.ts b/apps/aurora/vite.config.ts index c7e9478f6..14a6ee0a8 100644 --- a/apps/aurora/vite.config.ts +++ b/apps/aurora/vite.config.ts @@ -46,6 +46,41 @@ export default defineConfig(({ command, mode, isSsrBuild }) => { ] : []), ] as PluginOption[], + optimizeDeps: { + include: [ + // App-level deps (in apps/aurora/package.json) + 'i18next', + 'i18next-browser-languagedetector', + 'i18next-fs-backend/cjs', + 'i18next-http-backend', + 'react-i18next', + // Injected by babel-plugin-react-compiler, not in any package.json + 'react/compiler-runtime', + 'remix-i18next/client', + 'remix-i18next/react', + 'remix-i18next/server', + // @plone/components and @plone/helpers are not registered add-ons, so + // their deps can't be declared in vite.extend.js — list them here + '@plone/components > @internationalized/date', + '@plone/components > @react-aria/utils', + '@plone/components > @react-spectrum/utils', + '@plone/components > clsx', + '@plone/components > react-aria', + '@plone/components > react-aria-components', + '@plone/components > react-aria-components/DropZone', + '@plone/components > react-aria-components/Group', + '@plone/components > react-aria-components/Modal', + '@plone/components > react-aria-components/Table', + '@plone/components > react-aria-components/Tooltip', + '@plone/components > react-aria-components/composeRenderProps', + '@plone/components > react-stately', + '@plone/components > tailwind-merge', + '@plone/components > tailwind-variants', + '@plone/helpers > jotai', + '@plone/helpers > jotai/utils', + '@plone/helpers > jotai-optics', + ], + }, resolve: { tsconfigPaths: true, }, diff --git a/docs/development/index.md b/docs/development/index.md index b04cd7580..e3bf6635a 100644 --- a/docs/development/index.md +++ b/docs/development/index.md @@ -19,4 +19,5 @@ images i18n editor-slash-menu configure-editor-block-widths +vite-optimize-deps ``` diff --git a/docs/development/vite-optimize-deps.md b/docs/development/vite-optimize-deps.md new file mode 100644 index 000000000..6b90fbd3e --- /dev/null +++ b/docs/development/vite-optimize-deps.md @@ -0,0 +1,115 @@ +--- +myst: + html_meta: + "description": "How to declare Vite optimizeDeps for add-ons in Aurora" + "property=og:description": "How to declare Vite optimizeDeps for add-ons in Aurora" + "property=og:title": "Vite dependency pre-bundling" + "keywords": "Plone Aurora, Vite, optimizeDeps, pnpm, add-ons" +--- + +# Vite dependency pre-bundling + +This guide shows you how to declare third-party dependencies for pre-bundling +so Vite resolves them at startup rather than lazily during dev. + +Aurora runs in a pnpm monorepo. +Vite treats workspace packages as source files and does not scan them upfront +for their third-party dependencies. +Instead, it discovers them the first time a page imports them, triggering a +re-bundle and a browser reload. +In a large project, this causes several reload cycles and noticeably slows +down the first page load in dev mode. + +The fix is to declare those third-party dependencies in `optimizeDeps.include`. +Aurora uses two places for these declarations depending on whether the package +is a registered add-on. + +```{important} +Only declare a package as a registered add-on if it configures the +application — registering slots, routes, or components into the Aurora +framework. +Pure libraries such as component kits and utility packages must not be +add-ons. +Registering them as add-ons causes the addon system to generate unnecessary +loaders and blurs the boundary between framework participants and libraries. +If your package is a library, add its entries to `vite.config.ts` in the app +that uses it. +``` + +## Registered add-ons: `vite.extend.js` + +Any package that is a registered Aurora add-on can ship a `vite.extend.js` +file in its root. +`PloneRegistryVitePlugin` picks these up automatically and merges them into +the Vite config at startup — no changes to the app are needed. + +The file must export a default function that receives the current config and +returns a modified copy: + +```js +// packages/my-addon/vite.extend.js +export default function (config) { + return { + ...config, + optimizeDeps: { + ...config.optimizeDeps, + include: [ + ...(config.optimizeDeps?.include ?? []), + // Use "pkg > dep" syntax for pnpm — resolves the dep through the + // workspace package that owns it + '@plone/my-addon > some-library', + '@plone/my-addon > some-library/subpath', + ], + }, + }; +} +``` + +### When to add an entry + +Add a `"pkg > dep"` entry for every direct dependency of your add-on that is +not itself a workspace package. +You do not need entries for: + +- Other Aurora add-ons or workspace packages — Vite excludes them from + optimization automatically because it treats them as source files. +- Dev dependencies. +- Peer dependencies that the host app provides. + +If a dependency exports subpaths you use (for example `some-lib/react` or +`some-lib/client`), add a separate entry for each subpath. +Vite does not discover subpath exports automatically from the main entry. + +### Existing core add-on files + +| Add-on | File | +|--------|------| +| `@plone/plate` | `packages/plate/vite.extend.js` | +| `@plone/layout` | `packages/layout/vite.extend.js` | +| `@plone/cmsui` | `packages/cmsui/vite.extend.js` | + +## Non-add-on workspace packages and app deps: `vite.config.ts` + +Packages that are not registered add-ons — currently `@plone/components` and +`@plone/helpers` — cannot use `vite.extend.js` because `PloneRegistryVitePlugin` +never loads their files. +List their dependencies directly in `apps/aurora/vite.config.ts` under +`optimizeDeps.include`. + +The same rule applies to packages that are direct dependencies of the app +itself (listed in `apps/aurora/package.json`). +Those use plain names without the `>` syntax because Vite resolves them +directly from the app root. + +```ts +// apps/aurora/vite.config.ts +optimizeDeps: { + include: [ + // App-level dep — plain name works + 'some-app-dep', + // Non-add-on workspace package dep — requires "pkg > dep" syntax + '@plone/components > react-aria-components', + '@plone/helpers > jotai', + ], +}, +``` diff --git a/packages/cmsui/news/+vite-optimize-deps.internal b/packages/cmsui/news/+vite-optimize-deps.internal new file mode 100644 index 000000000..10750a5ab --- /dev/null +++ b/packages/cmsui/news/+vite-optimize-deps.internal @@ -0,0 +1 @@ +Added `vite.extend.js` to pre-bundle CMS UI dependencies, reducing dev server startup reloads. @arybakov05 \ No newline at end of file diff --git a/packages/cmsui/vite.extend.js b/packages/cmsui/vite.extend.js new file mode 100644 index 000000000..17024e6b5 --- /dev/null +++ b/packages/cmsui/vite.extend.js @@ -0,0 +1,13 @@ +export default function (config) { + return { + ...config, + optimizeDeps: { + ...config.optimizeDeps, + include: [ + ...(config.optimizeDeps?.include ?? []), + '@plone/cmsui > jwt-decode', + '@plone/cmsui > usehooks-ts', + ], + }, + }; +} diff --git a/packages/layout/news/+vite-optimize-deps.internal b/packages/layout/news/+vite-optimize-deps.internal new file mode 100644 index 000000000..e0077f0cc --- /dev/null +++ b/packages/layout/news/+vite-optimize-deps.internal @@ -0,0 +1 @@ +Added `vite.extend.js` to pre-bundle layout dependencies, reducing dev server startup reloads. @arybakov05 \ No newline at end of file diff --git a/packages/layout/vite.extend.js b/packages/layout/vite.extend.js new file mode 100644 index 000000000..467c1313c --- /dev/null +++ b/packages/layout/vite.extend.js @@ -0,0 +1,14 @@ +export default function (config) { + return { + ...config, + optimizeDeps: { + ...config.optimizeDeps, + include: [ + ...(config.optimizeDeps?.include ?? []), + '@plone/layout > lodash.sortby', + '@plone/layout > pretty-bytes', + '@plone/layout > rrule', + ], + }, + }; +} diff --git a/packages/plate/news/+vite-optimize-deps.internal b/packages/plate/news/+vite-optimize-deps.internal new file mode 100644 index 000000000..35c6c680d --- /dev/null +++ b/packages/plate/news/+vite-optimize-deps.internal @@ -0,0 +1 @@ +Added `vite.extend.js` to pre-bundle platejs and Radix UI dependencies, reducing dev server startup reloads. @arybakov05 \ No newline at end of file diff --git a/packages/plate/vite.extend.js b/packages/plate/vite.extend.js new file mode 100644 index 000000000..6e5e7dfe5 --- /dev/null +++ b/packages/plate/vite.extend.js @@ -0,0 +1,48 @@ +export default function (config) { + return { + ...config, + optimizeDeps: { + ...config.optimizeDeps, + include: [ + ...(config.optimizeDeps?.include ?? []), + '@plone/plate > @platejs/ai/react', + '@plone/plate > @platejs/basic-nodes', + '@plone/plate > @platejs/basic-nodes/react', + '@plone/plate > @platejs/basic-styles', + '@plone/plate > @platejs/callout', + '@plone/plate > @platejs/caption', + '@plone/plate > @platejs/code-block', + '@plone/plate > @platejs/comment', + '@plone/plate > @platejs/comment/react', + '@plone/plate > @platejs/floating', + '@plone/plate > @platejs/indent', + '@plone/plate > @platejs/layout', + '@plone/plate > @platejs/link', + '@plone/plate > @platejs/link/react', + '@plone/plate > @platejs/list', + '@plone/plate > @platejs/list/react', + '@plone/plate > @platejs/media', + '@plone/plate > @platejs/mention', + '@plone/plate > @platejs/selection/react', + '@plone/plate > @platejs/suggestion', + '@plone/plate > @platejs/table', + '@plone/plate > @platejs/table/react', + '@plone/plate > @platejs/toc', + '@plone/plate > @platejs/toggle', + '@plone/plate > @platejs/toggle/react', + '@plone/plate > @radix-ui/react-avatar', + '@plone/plate > @radix-ui/react-dropdown-menu', + '@plone/plate > @radix-ui/react-separator', + '@plone/plate > @radix-ui/react-slot', + '@plone/plate > @radix-ui/react-toolbar', + '@plone/plate > @radix-ui/react-tooltip', + '@plone/plate > @udecode/cn', + '@plone/plate > class-variance-authority', + '@plone/plate > lowlight', + '@plone/plate > lucide-react', + '@plone/plate > platejs', + '@plone/plate > platejs/react', + ], + }, + }; +} diff --git a/packages/registry/news/+vite-extend-support.internal b/packages/registry/news/+vite-extend-support.internal new file mode 100644 index 000000000..039f17f01 --- /dev/null +++ b/packages/registry/news/+vite-extend-support.internal @@ -0,0 +1 @@ +Check whether `.plone/vite.loader.js` content has changed before writing it in `PloneRegistryVitePlugin`. @arybakov05 \ No newline at end of file diff --git a/packages/registry/vite-plugin.js b/packages/registry/vite-plugin.js index c8c8dcd8f..1bbf0d7d2 100644 --- a/packages/registry/vite-plugin.js +++ b/packages/registry/vite-plugin.js @@ -79,7 +79,14 @@ const load = (config, context = {}) => { export default load; `; - fs.writeFileSync(viteLoaderPath, code); + // Only write if content changed — vite.loader.js is imported by vite.config.ts + // so any write triggers a Vite server restart, causing an infinite loop + const existing = fs.existsSync(viteLoaderPath) + ? fs.readFileSync(viteLoaderPath, 'utf-8') + : null; + if (existing !== code) { + fs.writeFileSync(viteLoaderPath, code); + } return viteLoaderPath; } From b5f5c7256d98c62d841f808d90ea2992a0398264 Mon Sep 17 00:00:00 2001 From: Alexander Rybakov Date: Fri, 12 Jun 2026 13:49:53 +0200 Subject: [PATCH 2/5] speed up dev startup by pre-bundling external deps in optimizeDeps --- apps/aurora/news/+vite-optimize-deps.internal | 1 + apps/aurora/vite.config.ts | 35 ++++++ docs/development/index.md | 1 + docs/development/vite-optimize-deps.md | 115 ++++++++++++++++++ .../cmsui/news/+vite-optimize-deps.internal | 1 + packages/cmsui/vite.extend.js | 13 ++ .../layout/news/+vite-optimize-deps.internal | 1 + packages/layout/vite.extend.js | 14 +++ .../plate/news/+vite-optimize-deps.internal | 1 + packages/plate/vite.extend.js | 48 ++++++++ .../news/+vite-extend-support.internal | 1 + packages/registry/vite-plugin.js | 9 +- 12 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 apps/aurora/news/+vite-optimize-deps.internal create mode 100644 docs/development/vite-optimize-deps.md create mode 100644 packages/cmsui/news/+vite-optimize-deps.internal create mode 100644 packages/cmsui/vite.extend.js create mode 100644 packages/layout/news/+vite-optimize-deps.internal create mode 100644 packages/layout/vite.extend.js create mode 100644 packages/plate/news/+vite-optimize-deps.internal create mode 100644 packages/plate/vite.extend.js create mode 100644 packages/registry/news/+vite-extend-support.internal diff --git a/apps/aurora/news/+vite-optimize-deps.internal b/apps/aurora/news/+vite-optimize-deps.internal new file mode 100644 index 000000000..a9ab4d7d1 --- /dev/null +++ b/apps/aurora/news/+vite-optimize-deps.internal @@ -0,0 +1 @@ +Added `optimizeDeps.include` entries for non-addon workspace packages and app-level deps, eliminating lazy dependency discovery reloads on dev server startup. @arybakov05 diff --git a/apps/aurora/vite.config.ts b/apps/aurora/vite.config.ts index c7e9478f6..14a6ee0a8 100644 --- a/apps/aurora/vite.config.ts +++ b/apps/aurora/vite.config.ts @@ -46,6 +46,41 @@ export default defineConfig(({ command, mode, isSsrBuild }) => { ] : []), ] as PluginOption[], + optimizeDeps: { + include: [ + // App-level deps (in apps/aurora/package.json) + 'i18next', + 'i18next-browser-languagedetector', + 'i18next-fs-backend/cjs', + 'i18next-http-backend', + 'react-i18next', + // Injected by babel-plugin-react-compiler, not in any package.json + 'react/compiler-runtime', + 'remix-i18next/client', + 'remix-i18next/react', + 'remix-i18next/server', + // @plone/components and @plone/helpers are not registered add-ons, so + // their deps can't be declared in vite.extend.js — list them here + '@plone/components > @internationalized/date', + '@plone/components > @react-aria/utils', + '@plone/components > @react-spectrum/utils', + '@plone/components > clsx', + '@plone/components > react-aria', + '@plone/components > react-aria-components', + '@plone/components > react-aria-components/DropZone', + '@plone/components > react-aria-components/Group', + '@plone/components > react-aria-components/Modal', + '@plone/components > react-aria-components/Table', + '@plone/components > react-aria-components/Tooltip', + '@plone/components > react-aria-components/composeRenderProps', + '@plone/components > react-stately', + '@plone/components > tailwind-merge', + '@plone/components > tailwind-variants', + '@plone/helpers > jotai', + '@plone/helpers > jotai/utils', + '@plone/helpers > jotai-optics', + ], + }, resolve: { tsconfigPaths: true, }, diff --git a/docs/development/index.md b/docs/development/index.md index b04cd7580..e3bf6635a 100644 --- a/docs/development/index.md +++ b/docs/development/index.md @@ -19,4 +19,5 @@ images i18n editor-slash-menu configure-editor-block-widths +vite-optimize-deps ``` diff --git a/docs/development/vite-optimize-deps.md b/docs/development/vite-optimize-deps.md new file mode 100644 index 000000000..6b90fbd3e --- /dev/null +++ b/docs/development/vite-optimize-deps.md @@ -0,0 +1,115 @@ +--- +myst: + html_meta: + "description": "How to declare Vite optimizeDeps for add-ons in Aurora" + "property=og:description": "How to declare Vite optimizeDeps for add-ons in Aurora" + "property=og:title": "Vite dependency pre-bundling" + "keywords": "Plone Aurora, Vite, optimizeDeps, pnpm, add-ons" +--- + +# Vite dependency pre-bundling + +This guide shows you how to declare third-party dependencies for pre-bundling +so Vite resolves them at startup rather than lazily during dev. + +Aurora runs in a pnpm monorepo. +Vite treats workspace packages as source files and does not scan them upfront +for their third-party dependencies. +Instead, it discovers them the first time a page imports them, triggering a +re-bundle and a browser reload. +In a large project, this causes several reload cycles and noticeably slows +down the first page load in dev mode. + +The fix is to declare those third-party dependencies in `optimizeDeps.include`. +Aurora uses two places for these declarations depending on whether the package +is a registered add-on. + +```{important} +Only declare a package as a registered add-on if it configures the +application — registering slots, routes, or components into the Aurora +framework. +Pure libraries such as component kits and utility packages must not be +add-ons. +Registering them as add-ons causes the addon system to generate unnecessary +loaders and blurs the boundary between framework participants and libraries. +If your package is a library, add its entries to `vite.config.ts` in the app +that uses it. +``` + +## Registered add-ons: `vite.extend.js` + +Any package that is a registered Aurora add-on can ship a `vite.extend.js` +file in its root. +`PloneRegistryVitePlugin` picks these up automatically and merges them into +the Vite config at startup — no changes to the app are needed. + +The file must export a default function that receives the current config and +returns a modified copy: + +```js +// packages/my-addon/vite.extend.js +export default function (config) { + return { + ...config, + optimizeDeps: { + ...config.optimizeDeps, + include: [ + ...(config.optimizeDeps?.include ?? []), + // Use "pkg > dep" syntax for pnpm — resolves the dep through the + // workspace package that owns it + '@plone/my-addon > some-library', + '@plone/my-addon > some-library/subpath', + ], + }, + }; +} +``` + +### When to add an entry + +Add a `"pkg > dep"` entry for every direct dependency of your add-on that is +not itself a workspace package. +You do not need entries for: + +- Other Aurora add-ons or workspace packages — Vite excludes them from + optimization automatically because it treats them as source files. +- Dev dependencies. +- Peer dependencies that the host app provides. + +If a dependency exports subpaths you use (for example `some-lib/react` or +`some-lib/client`), add a separate entry for each subpath. +Vite does not discover subpath exports automatically from the main entry. + +### Existing core add-on files + +| Add-on | File | +|--------|------| +| `@plone/plate` | `packages/plate/vite.extend.js` | +| `@plone/layout` | `packages/layout/vite.extend.js` | +| `@plone/cmsui` | `packages/cmsui/vite.extend.js` | + +## Non-add-on workspace packages and app deps: `vite.config.ts` + +Packages that are not registered add-ons — currently `@plone/components` and +`@plone/helpers` — cannot use `vite.extend.js` because `PloneRegistryVitePlugin` +never loads their files. +List their dependencies directly in `apps/aurora/vite.config.ts` under +`optimizeDeps.include`. + +The same rule applies to packages that are direct dependencies of the app +itself (listed in `apps/aurora/package.json`). +Those use plain names without the `>` syntax because Vite resolves them +directly from the app root. + +```ts +// apps/aurora/vite.config.ts +optimizeDeps: { + include: [ + // App-level dep — plain name works + 'some-app-dep', + // Non-add-on workspace package dep — requires "pkg > dep" syntax + '@plone/components > react-aria-components', + '@plone/helpers > jotai', + ], +}, +``` diff --git a/packages/cmsui/news/+vite-optimize-deps.internal b/packages/cmsui/news/+vite-optimize-deps.internal new file mode 100644 index 000000000..10750a5ab --- /dev/null +++ b/packages/cmsui/news/+vite-optimize-deps.internal @@ -0,0 +1 @@ +Added `vite.extend.js` to pre-bundle CMS UI dependencies, reducing dev server startup reloads. @arybakov05 \ No newline at end of file diff --git a/packages/cmsui/vite.extend.js b/packages/cmsui/vite.extend.js new file mode 100644 index 000000000..17024e6b5 --- /dev/null +++ b/packages/cmsui/vite.extend.js @@ -0,0 +1,13 @@ +export default function (config) { + return { + ...config, + optimizeDeps: { + ...config.optimizeDeps, + include: [ + ...(config.optimizeDeps?.include ?? []), + '@plone/cmsui > jwt-decode', + '@plone/cmsui > usehooks-ts', + ], + }, + }; +} diff --git a/packages/layout/news/+vite-optimize-deps.internal b/packages/layout/news/+vite-optimize-deps.internal new file mode 100644 index 000000000..e0077f0cc --- /dev/null +++ b/packages/layout/news/+vite-optimize-deps.internal @@ -0,0 +1 @@ +Added `vite.extend.js` to pre-bundle layout dependencies, reducing dev server startup reloads. @arybakov05 \ No newline at end of file diff --git a/packages/layout/vite.extend.js b/packages/layout/vite.extend.js new file mode 100644 index 000000000..467c1313c --- /dev/null +++ b/packages/layout/vite.extend.js @@ -0,0 +1,14 @@ +export default function (config) { + return { + ...config, + optimizeDeps: { + ...config.optimizeDeps, + include: [ + ...(config.optimizeDeps?.include ?? []), + '@plone/layout > lodash.sortby', + '@plone/layout > pretty-bytes', + '@plone/layout > rrule', + ], + }, + }; +} diff --git a/packages/plate/news/+vite-optimize-deps.internal b/packages/plate/news/+vite-optimize-deps.internal new file mode 100644 index 000000000..35c6c680d --- /dev/null +++ b/packages/plate/news/+vite-optimize-deps.internal @@ -0,0 +1 @@ +Added `vite.extend.js` to pre-bundle platejs and Radix UI dependencies, reducing dev server startup reloads. @arybakov05 \ No newline at end of file diff --git a/packages/plate/vite.extend.js b/packages/plate/vite.extend.js new file mode 100644 index 000000000..6e5e7dfe5 --- /dev/null +++ b/packages/plate/vite.extend.js @@ -0,0 +1,48 @@ +export default function (config) { + return { + ...config, + optimizeDeps: { + ...config.optimizeDeps, + include: [ + ...(config.optimizeDeps?.include ?? []), + '@plone/plate > @platejs/ai/react', + '@plone/plate > @platejs/basic-nodes', + '@plone/plate > @platejs/basic-nodes/react', + '@plone/plate > @platejs/basic-styles', + '@plone/plate > @platejs/callout', + '@plone/plate > @platejs/caption', + '@plone/plate > @platejs/code-block', + '@plone/plate > @platejs/comment', + '@plone/plate > @platejs/comment/react', + '@plone/plate > @platejs/floating', + '@plone/plate > @platejs/indent', + '@plone/plate > @platejs/layout', + '@plone/plate > @platejs/link', + '@plone/plate > @platejs/link/react', + '@plone/plate > @platejs/list', + '@plone/plate > @platejs/list/react', + '@plone/plate > @platejs/media', + '@plone/plate > @platejs/mention', + '@plone/plate > @platejs/selection/react', + '@plone/plate > @platejs/suggestion', + '@plone/plate > @platejs/table', + '@plone/plate > @platejs/table/react', + '@plone/plate > @platejs/toc', + '@plone/plate > @platejs/toggle', + '@plone/plate > @platejs/toggle/react', + '@plone/plate > @radix-ui/react-avatar', + '@plone/plate > @radix-ui/react-dropdown-menu', + '@plone/plate > @radix-ui/react-separator', + '@plone/plate > @radix-ui/react-slot', + '@plone/plate > @radix-ui/react-toolbar', + '@plone/plate > @radix-ui/react-tooltip', + '@plone/plate > @udecode/cn', + '@plone/plate > class-variance-authority', + '@plone/plate > lowlight', + '@plone/plate > lucide-react', + '@plone/plate > platejs', + '@plone/plate > platejs/react', + ], + }, + }; +} diff --git a/packages/registry/news/+vite-extend-support.internal b/packages/registry/news/+vite-extend-support.internal new file mode 100644 index 000000000..039f17f01 --- /dev/null +++ b/packages/registry/news/+vite-extend-support.internal @@ -0,0 +1 @@ +Check whether `.plone/vite.loader.js` content has changed before writing it in `PloneRegistryVitePlugin`. @arybakov05 \ No newline at end of file diff --git a/packages/registry/vite-plugin.js b/packages/registry/vite-plugin.js index c8c8dcd8f..1bbf0d7d2 100644 --- a/packages/registry/vite-plugin.js +++ b/packages/registry/vite-plugin.js @@ -79,7 +79,14 @@ const load = (config, context = {}) => { export default load; `; - fs.writeFileSync(viteLoaderPath, code); + // Only write if content changed — vite.loader.js is imported by vite.config.ts + // so any write triggers a Vite server restart, causing an infinite loop + const existing = fs.existsSync(viteLoaderPath) + ? fs.readFileSync(viteLoaderPath, 'utf-8') + : null; + if (existing !== code) { + fs.writeFileSync(viteLoaderPath, code); + } return viteLoaderPath; } From 961d5f5f505eb6a220a23e8a0395bf69dc9a6c79 Mon Sep 17 00:00:00 2001 From: Alexander Rybakov Date: Fri, 12 Jun 2026 13:56:20 +0200 Subject: [PATCH 3/5] remove documentation line breaks --- docs/development/vite-optimize-deps.md | 60 +++++++++----------------- 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/docs/development/vite-optimize-deps.md b/docs/development/vite-optimize-deps.md index 6b90fbd3e..c7ed78f1f 100644 --- a/docs/development/vite-optimize-deps.md +++ b/docs/development/vite-optimize-deps.md @@ -9,42 +9,29 @@ myst: # Vite dependency pre-bundling -This guide shows you how to declare third-party dependencies for pre-bundling -so Vite resolves them at startup rather than lazily during dev. +This guide shows you how to declare third-party dependencies for pre-bundling so Vite resolves them at startup rather than lazily during dev. Aurora runs in a pnpm monorepo. -Vite treats workspace packages as source files and does not scan them upfront -for their third-party dependencies. -Instead, it discovers them the first time a page imports them, triggering a -re-bundle and a browser reload. -In a large project, this causes several reload cycles and noticeably slows -down the first page load in dev mode. +Vite treats workspace packages as source files and does not scan them upfront for their third-party dependencies. +Instead, it discovers them the first time a page imports them, triggering a re-bundle and a browser reload. +In a large project, this causes several reload cycles and noticeably slows down the first page load in dev mode. The fix is to declare those third-party dependencies in `optimizeDeps.include`. -Aurora uses two places for these declarations depending on whether the package -is a registered add-on. +Aurora uses two places for these declarations depending on whether the package is a registered add-on. ```{important} -Only declare a package as a registered add-on if it configures the -application — registering slots, routes, or components into the Aurora -framework. -Pure libraries such as component kits and utility packages must not be -add-ons. -Registering them as add-ons causes the addon system to generate unnecessary -loaders and blurs the boundary between framework participants and libraries. -If your package is a library, add its entries to `vite.config.ts` in the app -that uses it. +Only declare a package as a registered add-on if it configures the application — registering slots, routes, or components into the Aurora framework. +Pure libraries such as component kits and utility packages must not be add-ons. +Registering them as add-ons causes the addon system to generate unnecessary loaders and blurs the boundary between framework participants and libraries. +If your package is a library, add its entries to `vite.config.ts` in the app that uses it. ``` ## Registered add-ons: `vite.extend.js` -Any package that is a registered Aurora add-on can ship a `vite.extend.js` -file in its root. -`PloneRegistryVitePlugin` picks these up automatically and merges them into -the Vite config at startup — no changes to the app are needed. +Any package that is a registered Aurora add-on can ship a `vite.extend.js` file in its root. +`PloneRegistryVitePlugin` picks these up automatically and merges them into the Vite config at startup — no changes to the app are needed. -The file must export a default function that receives the current config and -returns a modified copy: +The file must export a default function that receives the current config and returns a modified copy: ```js // packages/my-addon/vite.extend.js @@ -67,8 +54,7 @@ export default function (config) { ### When to add an entry -Add a `"pkg > dep"` entry for every direct dependency of your add-on that is -not itself a workspace package. +Add a `"pkg > dep"` entry for every direct dependency of your add-on that is not itself a workspace package. You do not need entries for: - Other Aurora add-ons or workspace packages — Vite excludes them from @@ -76,8 +62,7 @@ You do not need entries for: - Dev dependencies. - Peer dependencies that the host app provides. -If a dependency exports subpaths you use (for example `some-lib/react` or -`some-lib/client`), add a separate entry for each subpath. +If a dependency exports subpaths you use (for example `some-lib/react` or `some-lib/client`), add a separate entry for each subpath. Vite does not discover subpath exports automatically from the main entry. ### Existing core add-on files @@ -90,16 +75,11 @@ Vite does not discover subpath exports automatically from the main entry. ## Non-add-on workspace packages and app deps: `vite.config.ts` -Packages that are not registered add-ons — currently `@plone/components` and -`@plone/helpers` — cannot use `vite.extend.js` because `PloneRegistryVitePlugin` -never loads their files. -List their dependencies directly in `apps/aurora/vite.config.ts` under -`optimizeDeps.include`. +Packages that are not registered add-ons — currently `@plone/components` and `@plone/helpers` — cannot use `vite.extend.js` because `PloneRegistryVitePlugin` never loads their files. +List their dependencies directly in `apps/aurora/vite.config.ts` under `optimizeDeps.include`. -The same rule applies to packages that are direct dependencies of the app -itself (listed in `apps/aurora/package.json`). -Those use plain names without the `>` syntax because Vite resolves them -directly from the app root. +The same rule applies to packages that are direct dependencies of the app itself (listed in `apps/aurora/package.json`). +Those use plain names without the `>` syntax because Vite resolves them directly from the app root. ```ts // apps/aurora/vite.config.ts @@ -110,6 +90,6 @@ optimizeDeps: { // Non-add-on workspace package dep — requires "pkg > dep" syntax '@plone/components > react-aria-components', '@plone/helpers > jotai', - ], -}, + ] +} ``` From dda0487c576b4fe062d76eb9d65687b2ed4a0984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20de=20Alba?= Date: Sun, 14 Jun 2026 12:13:41 +0200 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- apps/aurora/vite.config.ts | 7 +++++-- docs/development/vite-optimize-deps.md | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/aurora/vite.config.ts b/apps/aurora/vite.config.ts index 14a6ee0a8..3919fed8c 100644 --- a/apps/aurora/vite.config.ts +++ b/apps/aurora/vite.config.ts @@ -51,14 +51,12 @@ export default defineConfig(({ command, mode, isSsrBuild }) => { // App-level deps (in apps/aurora/package.json) 'i18next', 'i18next-browser-languagedetector', - 'i18next-fs-backend/cjs', 'i18next-http-backend', 'react-i18next', // Injected by babel-plugin-react-compiler, not in any package.json 'react/compiler-runtime', 'remix-i18next/client', 'remix-i18next/react', - 'remix-i18next/server', // @plone/components and @plone/helpers are not registered add-ons, so // their deps can't be declared in vite.extend.js — list them here '@plone/components > @internationalized/date', @@ -81,6 +79,11 @@ export default defineConfig(({ command, mode, isSsrBuild }) => { '@plone/helpers > jotai-optics', ], }, + ssr: { + optimizeDeps: { + include: ['i18next-fs-backend/cjs', 'remix-i18next/server'], + }, + }, resolve: { tsconfigPaths: true, }, diff --git a/docs/development/vite-optimize-deps.md b/docs/development/vite-optimize-deps.md index c7ed78f1f..5942c7cd7 100644 --- a/docs/development/vite-optimize-deps.md +++ b/docs/development/vite-optimize-deps.md @@ -65,6 +65,8 @@ You do not need entries for: If a dependency exports subpaths you use (for example `some-lib/react` or `some-lib/client`), add a separate entry for each subpath. Vite does not discover subpath exports automatically from the main entry. +If a dependency is only used in server-side code (for example `*.server.*` files), add it to `ssr.optimizeDeps.include` instead of `optimizeDeps.include` so it isn't pre-bundled for the browser. + ### Existing core add-on files | Add-on | File | From 023157ee9f20b77b41734d8c8dc063cb3944d41b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20de=20Alba?= Date: Wed, 29 Jul 2026 18:31:56 +0200 Subject: [PATCH 5/5] Audit and document Vite optimizeDeps declarations (#140) * Audit and document Vite optimizeDeps declarations * Address review feedback on optimizeDeps audit script - Declare typescript as an explicit devDependency of @plone/scripts instead of relying on it being hoisted from the repo root - Fix incorrect @openai attribution in the audit script news fragment - Dedupe the "OK" output line when multiple targets share a config file - Wire pnpm check:vite-optimize-deps into the Code Analysis CI workflow so drift between runtime imports and optimizeDeps declarations is caught automatically * Update pnpm-lock.yaml for typescript devDependency in @plone/scripts * Address arybakov05 review feedback on PR #140 - Exclude i18next-fs-backend and remix-i18next/server from the client optimizeDeps bundle: Vite's client-side scanner was still picking them up even though they were only declared under ssr.optimizeDeps.include - Scope check-vite-optimize-deps.js's declared-entries scan to actual `include` arrays instead of any string literal in the config file, so entries listed under `optimizeDeps.exclude` are no longer mistaken for satisfied declarations - Check named re-exports per-element for `isTypeOnly` instead of only at the export-statement level, so `export { type Foo } from 'pkg'` is no longer treated as a runtime dependency - Split expected specifiers into client vs. server-only (based on `*.server.*` filenames) and diff them against optimizeDeps.include and ssr.optimizeDeps.include separately, so drift in the SSR-only list is now actually caught - Document the client-exclude gotcha in vite-optimize-deps.md --- .github/workflows/code-analysis.yml | 14 + .../+vite-optimize-deps-ssr-exclude.internal | 1 + apps/aurora/news/+vite-optimize-deps.internal | 2 +- apps/aurora/vite.config.ts | 10 +- docs/development/vite-optimize-deps.md | 33 +- package.json | 1 + .../cmsui/news/+vite-optimize-deps.internal | 2 +- packages/cmsui/vite.extend.js | 18 + .../layout/news/+vite-optimize-deps.internal | 2 +- packages/layout/vite.extend.js | 5 + .../plate/news/+vite-optimize-deps.internal | 2 +- packages/plate/vite.extend.js | 43 ++ packages/scripts/README.md | 28 + packages/scripts/check-vite-optimize-deps.js | 504 ++++++++++++++++++ .../+vite-optimize-deps-audit-fixes.internal | 1 + .../news/+vite-optimize-deps-audit.internal | 1 + packages/scripts/package.json | 3 +- pnpm-lock.yaml | 5 +- 18 files changed, 667 insertions(+), 8 deletions(-) create mode 100644 apps/aurora/news/+vite-optimize-deps-ssr-exclude.internal create mode 100644 packages/scripts/check-vite-optimize-deps.js create mode 100644 packages/scripts/news/+vite-optimize-deps-audit-fixes.internal create mode 100644 packages/scripts/news/+vite-optimize-deps-audit.internal diff --git a/.github/workflows/code-analysis.yml b/.github/workflows/code-analysis.yml index 4359ca87c..45f681b44 100644 --- a/.github/workflows/code-analysis.yml +++ b/.github/workflows/code-analysis.yml @@ -61,6 +61,20 @@ jobs: - name: Main ESlint check run: pnpm lint + vite-optimize-deps: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + name: Vite optimizeDeps audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Set up Node.js environment + uses: ./.github/actions/node_env_setup + with: + node-version: ${{ env.node-version }} + + - name: Check optimizeDeps declarations are up to date + run: pnpm check:vite-optimize-deps + stylelint: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name name: Stylelint ${{ matrix.name }} diff --git a/apps/aurora/news/+vite-optimize-deps-ssr-exclude.internal b/apps/aurora/news/+vite-optimize-deps-ssr-exclude.internal new file mode 100644 index 000000000..fadf55777 --- /dev/null +++ b/apps/aurora/news/+vite-optimize-deps-ssr-exclude.internal @@ -0,0 +1 @@ +Excluded `i18next-fs-backend` and `remix-i18next/server` from the client `optimizeDeps` bundle — they were still being picked up by Vite's client-side dependency scanner despite only being declared in `ssr.optimizeDeps.include`. @sneridagh diff --git a/apps/aurora/news/+vite-optimize-deps.internal b/apps/aurora/news/+vite-optimize-deps.internal index a9ab4d7d1..ae4a04fa5 100644 --- a/apps/aurora/news/+vite-optimize-deps.internal +++ b/apps/aurora/news/+vite-optimize-deps.internal @@ -1 +1 @@ -Added `optimizeDeps.include` entries for non-addon workspace packages and app-level deps, eliminating lazy dependency discovery reloads on dev server startup. @arybakov05 +Added `optimizeDeps.include` entries for non-addon workspace packages and app-level deps to reduce lazy dependency discovery reloads on dev server startup. @arybakov05 diff --git a/apps/aurora/vite.config.ts b/apps/aurora/vite.config.ts index 3919fed8c..0f602f4d3 100644 --- a/apps/aurora/vite.config.ts +++ b/apps/aurora/vite.config.ts @@ -47,6 +47,13 @@ export default defineConfig(({ command, mode, isSsrBuild }) => { : []), ] as PluginOption[], optimizeDeps: { + // Server-only deps that Vite would otherwise still pick up for the + // client bundle — keep in sync with ssr.optimizeDeps.include below + exclude: [ + 'i18next-fs-backend', + 'i18next-fs-backend/cjs', + 'remix-i18next/server', + ], include: [ // App-level deps (in apps/aurora/package.json) 'i18next', @@ -66,6 +73,7 @@ export default defineConfig(({ command, mode, isSsrBuild }) => { '@plone/components > react-aria', '@plone/components > react-aria-components', '@plone/components > react-aria-components/DropZone', + '@plone/components > react-aria-components/Form', '@plone/components > react-aria-components/Group', '@plone/components > react-aria-components/Modal', '@plone/components > react-aria-components/Table', @@ -81,7 +89,7 @@ export default defineConfig(({ command, mode, isSsrBuild }) => { }, ssr: { optimizeDeps: { - include: ['i18next-fs-backend/cjs', 'remix-i18next/server'], + include: ['i18next-fs-backend/cjs', 'isbot', 'remix-i18next/server'], }, }, resolve: { diff --git a/docs/development/vite-optimize-deps.md b/docs/development/vite-optimize-deps.md index 5942c7cd7..e529675ea 100644 --- a/docs/development/vite-optimize-deps.md +++ b/docs/development/vite-optimize-deps.md @@ -9,7 +9,7 @@ myst: # Vite dependency pre-bundling -This guide shows you how to declare third-party dependencies for pre-bundling so Vite resolves them at startup rather than lazily during dev. +This guide shows you how to declare third-party dependencies for pre-bundling so Vite resolves more of them at startup instead of discovering them lazily during dev. Aurora runs in a pnpm monorepo. Vite treats workspace packages as source files and does not scan them upfront for their third-party dependencies. @@ -66,6 +66,37 @@ If a dependency exports subpaths you use (for example `some-lib/react` or `some- Vite does not discover subpath exports automatically from the main entry. If a dependency is only used in server-side code (for example `*.server.*` files), add it to `ssr.optimizeDeps.include` instead of `optimizeDeps.include` so it isn't pre-bundled for the browser. +Declaring it in `ssr.optimizeDeps.include` alone is not always enough — Vite's client-side dependency scanner can still pick it up through re-exports. +If that happens, also add it to the top-level `optimizeDeps.exclude` in `apps/aurora/vite.config.ts` so it's explicitly kept out of the client bundle. + +To keep the lists from drifting, run: + +```sh +pnpm check:vite-optimize-deps +``` + +The script inspects runtime imports in the workspace packages that require explicit declarations and reports missing or stale `pkg > dep` entries. + +Run it when: + +- you add or remove a runtime import in `@plone/components`, `@plone/helpers`, + `@plone/cmsui`, `@plone/layout`, or `@plone/plate` +- you switch an import to a different package subpath such as + `some-lib/react` +- you move an import between browser-only and server-only code and may need to + switch between `optimizeDeps.include` and `ssr.optimizeDeps.include` +- you touch one of the existing optimize-deps config files and want to verify + the list before opening a PR + +Recommended workflow: + +1. Make the code change that introduces or removes runtime imports. +2. Run `pnpm check:vite-optimize-deps`. +3. Copy the reported missing entries into the relevant `vite.extend.js` file or + `apps/aurora/vite.config.ts`. +4. Remove reported stale entries unless you have a concrete reason to keep + them. +5. Re-run the check until it passes. ### Existing core add-on files diff --git a/package.json b/package.json index 52e9ed0cb..c3f1f2426 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "test": "pnpm --filter @plone/aurora run test", "test:all": "pnpm test:ci", "test:ci": "pnpm --filter @plone/* --filter \"!@plone/scripts\" --filter \"!@plone/theming\" run test --run", + "check:vite-optimize-deps": "node packages/scripts/check-vite-optimize-deps.js", "prettier": "prettier --check '{apps,packages}/**/*.{js,jsx,ts,tsx}'", "prettier:fix": "prettier --write '{apps,packages}/**/*.{js,jsx,ts,tsx}'", "stylelint": "stylelint '{apps,packages}/**/*.{css,scss,less}'", diff --git a/packages/cmsui/news/+vite-optimize-deps.internal b/packages/cmsui/news/+vite-optimize-deps.internal index 10750a5ab..25a7501c4 100644 --- a/packages/cmsui/news/+vite-optimize-deps.internal +++ b/packages/cmsui/news/+vite-optimize-deps.internal @@ -1 +1 @@ -Added `vite.extend.js` to pre-bundle CMS UI dependencies, reducing dev server startup reloads. @arybakov05 \ No newline at end of file +Added `vite.extend.js` and a dependency audit path to pre-bundle CMS UI runtime dependencies, reducing dev server startup reloads. @arybakov05 diff --git a/packages/cmsui/vite.extend.js b/packages/cmsui/vite.extend.js index 17024e6b5..e7f9fb629 100644 --- a/packages/cmsui/vite.extend.js +++ b/packages/cmsui/vite.extend.js @@ -5,7 +5,25 @@ export default function (config) { ...config.optimizeDeps, include: [ ...(config.optimizeDeps?.include ?? []), + '@plone/cmsui > @platejs/floating', + '@plone/cmsui > @platejs/link/react', + '@plone/cmsui > @tanstack/react-form', + '@plone/cmsui > class-variance-authority', + '@plone/cmsui > clsx', + '@plone/cmsui > jotai', + '@plone/cmsui > jotai-optics', + '@plone/cmsui > jotai/utils', '@plone/cmsui > jwt-decode', + '@plone/cmsui > lucide-react', + '@plone/cmsui > platejs', + '@plone/cmsui > platejs/react', + '@plone/cmsui > react-aria', + '@plone/cmsui > react-aria-components', + '@plone/cmsui > react-i18next', + '@plone/cmsui > react-router', + '@plone/cmsui > rrule', + '@plone/cmsui > tailwind-merge', + '@plone/cmsui > tailwind-variants', '@plone/cmsui > usehooks-ts', ], }, diff --git a/packages/layout/news/+vite-optimize-deps.internal b/packages/layout/news/+vite-optimize-deps.internal index e0077f0cc..3a5112b9e 100644 --- a/packages/layout/news/+vite-optimize-deps.internal +++ b/packages/layout/news/+vite-optimize-deps.internal @@ -1 +1 @@ -Added `vite.extend.js` to pre-bundle layout dependencies, reducing dev server startup reloads. @arybakov05 \ No newline at end of file +Added `vite.extend.js` and a dependency audit path to pre-bundle layout runtime dependencies, reducing dev server startup reloads. @arybakov05 diff --git a/packages/layout/vite.extend.js b/packages/layout/vite.extend.js index 467c1313c..57d59c575 100644 --- a/packages/layout/vite.extend.js +++ b/packages/layout/vite.extend.js @@ -5,8 +5,13 @@ export default function (config) { ...config.optimizeDeps, include: [ ...(config.optimizeDeps?.include ?? []), + '@plone/layout > clsx', '@plone/layout > lodash.sortby', '@plone/layout > pretty-bytes', + '@plone/layout > react-aria', + '@plone/layout > react-aria-components', + '@plone/layout > react-i18next', + '@plone/layout > react-router', '@plone/layout > rrule', ], }, diff --git a/packages/plate/news/+vite-optimize-deps.internal b/packages/plate/news/+vite-optimize-deps.internal index 35c6c680d..590e9793c 100644 --- a/packages/plate/news/+vite-optimize-deps.internal +++ b/packages/plate/news/+vite-optimize-deps.internal @@ -1 +1 @@ -Added `vite.extend.js` to pre-bundle platejs and Radix UI dependencies, reducing dev server startup reloads. @arybakov05 \ No newline at end of file +Added `vite.extend.js` and a dependency audit path to pre-bundle Plate runtime dependencies, reducing dev server startup reloads. @arybakov05 diff --git a/packages/plate/vite.extend.js b/packages/plate/vite.extend.js index 6e5e7dfe5..0ea56d5a6 100644 --- a/packages/plate/vite.extend.js +++ b/packages/plate/vite.extend.js @@ -5,43 +5,86 @@ export default function (config) { ...config.optimizeDeps, include: [ ...(config.optimizeDeps?.include ?? []), + '@plone/plate > @ai-sdk/react', + '@plone/plate > @ariakit/react', + '@plone/plate > @platejs/ai', '@plone/plate > @platejs/ai/react', + '@plone/plate > @platejs/autoformat', '@plone/plate > @platejs/basic-nodes', '@plone/plate > @platejs/basic-nodes/react', '@plone/plate > @platejs/basic-styles', + '@plone/plate > @platejs/basic-styles/react', '@plone/plate > @platejs/callout', + '@plone/plate > @platejs/callout/react', '@plone/plate > @platejs/caption', + '@plone/plate > @platejs/caption/react', '@plone/plate > @platejs/code-block', + '@plone/plate > @platejs/code-block/react', + '@plone/plate > @platejs/combobox', + '@plone/plate > @platejs/combobox/react', '@plone/plate > @platejs/comment', '@plone/plate > @platejs/comment/react', + '@plone/plate > @platejs/dnd', + '@plone/plate > @platejs/docx', + '@plone/plate > @platejs/emoji/react', '@plone/plate > @platejs/floating', '@plone/plate > @platejs/indent', + '@plone/plate > @platejs/indent/react', + '@plone/plate > @platejs/juice', '@plone/plate > @platejs/layout', + '@plone/plate > @platejs/layout/react', '@plone/plate > @platejs/link', '@plone/plate > @platejs/link/react', '@plone/plate > @platejs/list', '@plone/plate > @platejs/list/react', + '@plone/plate > @platejs/markdown', '@plone/plate > @platejs/media', + '@plone/plate > @platejs/media/react', '@plone/plate > @platejs/mention', + '@plone/plate > @platejs/mention/react', + '@plone/plate > @platejs/playwright', + '@plone/plate > @platejs/resizable', '@plone/plate > @platejs/selection/react', + '@plone/plate > @platejs/slash-command/react', '@plone/plate > @platejs/suggestion', '@plone/plate > @platejs/table', '@plone/plate > @platejs/table/react', '@plone/plate > @platejs/toc', + '@plone/plate > @platejs/toc/react', '@plone/plate > @platejs/toggle', '@plone/plate > @platejs/toggle/react', + '@plone/plate > @radix-ui/react-alert-dialog', '@plone/plate > @radix-ui/react-avatar', + '@plone/plate > @radix-ui/react-checkbox', + '@plone/plate > @radix-ui/react-context-menu', + '@plone/plate > @radix-ui/react-dialog', '@plone/plate > @radix-ui/react-dropdown-menu', + '@plone/plate > @radix-ui/react-popover', '@plone/plate > @radix-ui/react-separator', '@plone/plate > @radix-ui/react-slot', '@plone/plate > @radix-ui/react-toolbar', '@plone/plate > @radix-ui/react-tooltip', '@plone/plate > @udecode/cn', + '@plone/plate > ai', '@plone/plate > class-variance-authority', + '@plone/plate > clsx', + '@plone/plate > cmdk', + '@plone/plate > html2canvas-pro', + '@plone/plate > jotai', + '@plone/plate > lodash.debounce', '@plone/plate > lowlight', '@plone/plate > lucide-react', + '@plone/plate > pdf-lib', '@plone/plate > platejs', '@plone/plate > platejs/react', + '@plone/plate > react-dnd', + '@plone/plate > react-dnd-html5-backend', + '@plone/plate > react-lite-youtube-embed', + '@plone/plate > remark-gfm', + '@plone/plate > sonner', + '@plone/plate > tailwind-merge', + '@plone/plate > use-file-picker', + '@plone/plate > zod', ], }, }; diff --git a/packages/scripts/README.md b/packages/scripts/README.md index 554ea77f6..b9744ee0e 100644 --- a/packages/scripts/README.md +++ b/packages/scripts/README.md @@ -9,3 +9,31 @@ It scans and detects i18n messages from the code and adds them to the i18n machi See https://6.docs.plone.org/volto/development/i18n.html for more information. This script is installed in the `node_modules/.bin` directory and can be called via `yarn i18n` or directly in the `scripts` `package.json` part. + +## Aurora optimize-deps audit + +Aurora includes a repository-level audit script at +`packages/scripts/check-vite-optimize-deps.js`. + +Run it from the repo root with: + +```sh +pnpm check:vite-optimize-deps +``` + +Use it when you change runtime imports in Aurora workspace packages that feed +the app through Vite: + +- `@plone/components` +- `@plone/helpers` +- `@plone/cmsui` +- `@plone/layout` +- `@plone/plate` + +The script scans those packages for runtime imports of direct third-party +dependencies and compares them with the explicit entries declared in +`apps/aurora/vite.config.ts` and `packages/*/vite.extend.js`. + +If it reports missing entries, add them to the relevant optimize-deps config. +If it reports extra entries, remove them unless they are intentionally kept for +some unresolved import path. diff --git a/packages/scripts/check-vite-optimize-deps.js b/packages/scripts/check-vite-optimize-deps.js new file mode 100644 index 000000000..05c7ab5a1 --- /dev/null +++ b/packages/scripts/check-vite-optimize-deps.js @@ -0,0 +1,504 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import ts from 'typescript'; + +const repoRoot = process.cwd(); + +const TARGETS = [ + { + id: 'components', + owner: '@plone/components', + packageDir: 'packages/components', + configFile: 'apps/aurora/vite.config.ts', + sourceRoots: ['src'], + }, + { + id: 'helpers', + owner: '@plone/helpers', + packageDir: 'packages/helpers', + configFile: 'apps/aurora/vite.config.ts', + sourceRoots: ['src'], + }, + { + id: 'cmsui', + owner: '@plone/cmsui', + packageDir: 'packages/cmsui', + configFile: 'packages/cmsui/vite.extend.js', + }, + { + id: 'layout', + owner: '@plone/layout', + packageDir: 'packages/layout', + configFile: 'packages/layout/vite.extend.js', + }, + { + id: 'plate', + owner: '@plone/plate', + packageDir: 'packages/plate', + configFile: 'packages/plate/vite.extend.js', + }, +]; + +const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx']); + +function toPosix(filePath) { + return filePath.split(path.sep).join('/'); +} + +function shouldIgnoreFile(relativePath) { + const normalized = toPosix(relativePath); + + return ( + normalized.includes('/dist/') || + normalized.includes('/.storybook/') || + normalized.includes('/coverage/') || + normalized.includes('/news/') || + normalized.includes('/node_modules/') || + normalized.includes('/acceptance/') || + normalized.includes('/__tests__/') || + normalized.endsWith('.test.js') || + normalized.endsWith('.test.jsx') || + normalized.endsWith('.test.ts') || + normalized.endsWith('.test.tsx') || + normalized.endsWith('.spec.js') || + normalized.endsWith('.spec.jsx') || + normalized.endsWith('.spec.ts') || + normalized.endsWith('.spec.tsx') || + normalized.endsWith('.stories.js') || + normalized.endsWith('.stories.jsx') || + normalized.endsWith('.stories.ts') || + normalized.endsWith('.stories.tsx') || + normalized.endsWith('.story.js') || + normalized.endsWith('.story.jsx') || + normalized.endsWith('.story.ts') || + normalized.endsWith('.story.tsx') || + normalized.endsWith('/vite.config.js') || + normalized.endsWith('/vite.config.ts') || + normalized.endsWith('/vitest.config.js') || + normalized.endsWith('/vitest.config.ts') || + normalized.endsWith('/tsup.config.js') || + normalized.endsWith('/tsup.config.ts') || + normalized.endsWith('/vite.extend.js') || + normalized.endsWith('/vite.extend.ts') || + normalized.endsWith('/setupTesting.js') || + normalized.endsWith('/setupTesting.ts') + ); +} + +function listFiles(rootDir, sourceRoots = ['']) { + const results = []; + const stack = sourceRoots.length > 0 ? [...sourceRoots] : ['']; + + while (stack.length) { + const current = stack.pop(); + const absoluteCurrent = path.join(rootDir, current); + const entries = fs.readdirSync(absoluteCurrent, { withFileTypes: true }); + + for (const entry of entries) { + const relativePath = current + ? path.join(current, entry.name) + : entry.name; + + if (entry.isDirectory()) { + if (shouldIgnoreFile(relativePath)) { + continue; + } + stack.push(relativePath); + continue; + } + + if (!SOURCE_EXTENSIONS.has(path.extname(entry.name))) { + continue; + } + + if (shouldIgnoreFile(relativePath)) { + continue; + } + + results.push(relativePath); + } + } + + return results; +} + +function loadJson(relativePath) { + return JSON.parse(fs.readFileSync(path.join(repoRoot, relativePath), 'utf8')); +} + +function getWorkspacePackages() { + const packageFiles = []; + for (const rootName of ['apps', 'packages']) { + const rootDir = path.join(repoRoot, rootName); + for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) { + if (!entry.isDirectory()) { + continue; + } + const packageFile = path.join(rootName, entry.name, 'package.json'); + if (fs.existsSync(path.join(repoRoot, packageFile))) { + packageFiles.push(packageFile); + } + } + } + + return new Set( + packageFiles.map((file) => loadJson(file).name).filter(Boolean), + ); +} + +function getPackageBase(specifier) { + if (specifier.startsWith('@')) { + const [scope, name] = specifier.split('/'); + return name ? `${scope}/${name}` : specifier; + } + const [name] = specifier.split('/'); + return name; +} + +function isRelative(specifier) { + return specifier.startsWith('.') || specifier.startsWith('/'); +} + +function isRuntimeImport(node) { + const clause = node.importClause; + if (!clause) { + return true; + } + if (clause.isTypeOnly) { + return false; + } + if (clause.name) { + return true; + } + if (!clause.namedBindings) { + return false; + } + if (ts.isNamespaceImport(clause.namedBindings)) { + return true; + } + return clause.namedBindings.elements.some((element) => !element.isTypeOnly); +} + +function isRuntimeExport(node) { + if (node.isTypeOnly) { + return false; + } + const clause = node.exportClause; + if (!clause) { + // `export * from 'pkg'` + return true; + } + if (ts.isNamespaceExport(clause)) { + // `export * as ns from 'pkg'` + return true; + } + // `export { a, type b } from 'pkg'` — only a type-only re-export list + // (every element marked `type`) should be treated as a non-runtime export. + return clause.elements.some((element) => !element.isTypeOnly); +} + +function isServerOnlyFile(relativeFile) { + return /\.server\.(js|jsx|ts|tsx)$/.test(toPosix(relativeFile)); +} + +function normalizeSpecifier(specifier) { + return specifier.replace(/\?.*$/, ''); +} + +function shouldIncludeSpecifier( + specifier, + directDependencies, + workspacePackages, +) { + if (!specifier || isRelative(specifier) || specifier.startsWith('node:')) { + return false; + } + + const base = getPackageBase(specifier); + if (workspacePackages.has(base)) { + return false; + } + + return directDependencies.has(base); +} + +function collectSpecifiers(packageDir, directDependencies, workspacePackages) { + const target = TARGETS.find((item) => item.packageDir === packageDir); + const files = listFiles( + path.join(repoRoot, packageDir), + target?.sourceRoots ?? [''], + ); + + // Track, per specifier, whether it was seen from a client-reachable file + // and/or from a `*.server.*` file — only specifiers seen exclusively from + // `*.server.*` files belong in ssr.optimizeDeps.include; anything else + // (client-only, or used from both) belongs in optimizeDeps.include. + const seenFrom = new Map(); + + const record = (specifier, isServerFile) => { + const entry = seenFrom.get(specifier) ?? { client: false, server: false }; + if (isServerFile) { + entry.server = true; + } else { + entry.client = true; + } + seenFrom.set(specifier, entry); + }; + + for (const relativeFile of files) { + const absoluteFile = path.join(repoRoot, packageDir, relativeFile); + if (!SOURCE_EXTENSIONS.has(path.extname(absoluteFile))) { + continue; + } + + const isServerFile = isServerOnlyFile(relativeFile); + const sourceText = fs.readFileSync(absoluteFile, 'utf8'); + const sourceFile = ts.createSourceFile( + absoluteFile, + sourceText, + ts.ScriptTarget.Latest, + true, + absoluteFile.endsWith('.tsx') || absoluteFile.endsWith('.jsx') + ? ts.ScriptKind.TSX + : ts.ScriptKind.TS, + ); + + const visit = (node) => { + if ( + ts.isImportDeclaration(node) && + ts.isStringLiteral(node.moduleSpecifier) && + isRuntimeImport(node) + ) { + const specifier = normalizeSpecifier(node.moduleSpecifier.text); + if ( + shouldIncludeSpecifier( + specifier, + directDependencies, + workspacePackages, + ) + ) { + record(specifier, isServerFile); + } + } + + if ( + ts.isExportDeclaration(node) && + node.moduleSpecifier && + ts.isStringLiteral(node.moduleSpecifier) && + isRuntimeExport(node) + ) { + const specifier = normalizeSpecifier(node.moduleSpecifier.text); + if ( + shouldIncludeSpecifier( + specifier, + directDependencies, + workspacePackages, + ) + ) { + record(specifier, isServerFile); + } + } + + if ( + ts.isCallExpression(node) && + node.expression.kind === ts.SyntaxKind.ImportKeyword && + node.arguments.length === 1 && + ts.isStringLiteral(node.arguments[0]) + ) { + const specifier = normalizeSpecifier(node.arguments[0].text); + if ( + shouldIncludeSpecifier( + specifier, + directDependencies, + workspacePackages, + ) + ) { + record(specifier, isServerFile); + } + } + + ts.forEachChild(node, visit); + }; + + visit(sourceFile); + } + + const client = []; + const serverOnly = []; + for (const [specifier, seen] of seenFrom) { + (seen.client ? client : serverOnly).push(specifier); + } + + return { + client: client.sort((a, b) => a.localeCompare(b)), + serverOnly: serverOnly.sort((a, b) => a.localeCompare(b)), + }; +} + +function isPropertyNamed(node, name) { + return ( + (ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node)) && + ts.isIdentifier(node.name) && + node.name.text === name + ); +} + +// Only entries actually declared in an `include` array count as declared — +// entries in a sibling `exclude` array must not be treated as covered. +function collectDeclaredEntries(configFile, owner) { + const contents = fs.readFileSync(path.join(repoRoot, configFile), 'utf8'); + const sourceFile = ts.createSourceFile( + configFile, + contents, + ts.ScriptTarget.Latest, + true, + configFile.endsWith('.ts') || configFile.endsWith('.tsx') + ? ts.ScriptKind.TS + : ts.ScriptKind.JS, + ); + + const prefix = `${owner} > `; + const client = new Set(); + const ssr = new Set(); + + const visit = (node, ancestorNames) => { + if ( + isPropertyNamed(node, 'include') && + node.initializer && + ts.isArrayLiteralExpression(node.initializer) + ) { + const target = ancestorNames.includes('ssr') ? ssr : client; + for (const element of node.initializer.elements) { + if ( + ts.isStringLiteralLike(element) && + element.text.startsWith(prefix) + ) { + target.add(element.text.slice(prefix.length)); + } + } + } + + const nextAncestors = + (ts.isPropertyAssignment(node) || + ts.isShorthandPropertyAssignment(node)) && + ts.isIdentifier(node.name) + ? [...ancestorNames, node.name.text] + : ancestorNames; + + ts.forEachChild(node, (child) => visit(child, nextAncestors)); + }; + + visit(sourceFile, []); + + return { + client: [...client].sort((a, b) => a.localeCompare(b)), + ssr: [...ssr].sort((a, b) => a.localeCompare(b)), + }; +} + +function diffEntries(expected, declared) { + const expectedSet = new Set(expected); + const declaredSet = new Set(declared); + + return { + missing: expected.filter((entry) => !declaredSet.has(entry)), + extra: declared.filter((entry) => !expectedSet.has(entry)), + }; +} + +function formatEntries(owner, entries) { + return entries.map((entry) => ` '${owner} > ${entry}',`).join('\n'); +} + +const workspacePackages = getWorkspacePackages(); + +const results = TARGETS.map((target) => { + const packageJson = loadJson(path.join(target.packageDir, 'package.json')); + const directDependencies = new Set( + Object.keys(packageJson.dependencies ?? {}), + ); + + const expected = collectSpecifiers( + target.packageDir, + directDependencies, + workspacePackages, + ); + const declared = collectDeclaredEntries(target.configFile, target.owner); + const clientDiff = diffEntries(expected.client, declared.client); + const ssrDiff = diffEntries(expected.serverOnly, declared.ssr); + + return { + ...target, + expected, + declared, + clientDiff, + ssrDiff, + }; +}); + +let hasDiff = false; +const reportedOk = new Set(); + +function reportSection(label, owner, diff, suggested) { + let printed = false; + + if (diff.missing.length) { + console.log(` Missing ${label} entries:`); + for (const entry of diff.missing) { + console.log(` ${owner} > ${entry}`); + } + printed = true; + } + + if (diff.extra.length) { + console.log(` Extra ${label} entries:`); + for (const entry of diff.extra) { + console.log(` ${owner} > ${entry}`); + } + printed = true; + } + + if (printed && suggested.length) { + console.log(` Suggested ${label} block:`); + console.log(formatEntries(owner, suggested)); + } +} + +for (const result of results) { + const clientOk = + !result.clientDiff.missing.length && !result.clientDiff.extra.length; + const ssrOk = !result.ssrDiff.missing.length && !result.ssrDiff.extra.length; + + if (clientOk && ssrOk) { + if (!reportedOk.has(result.configFile)) { + console.log(`OK ${result.configFile}`); + reportedOk.add(result.configFile); + } + continue; + } + + hasDiff = true; + console.log(`CHECK ${result.configFile}`); + + if (!clientOk) { + reportSection( + 'optimizeDeps.include', + result.owner, + result.clientDiff, + result.expected.client, + ); + } + + if (!ssrOk) { + reportSection( + 'ssr.optimizeDeps.include', + result.owner, + result.ssrDiff, + result.expected.serverOnly, + ); + } +} + +if (hasDiff) { + process.exitCode = 1; +} diff --git a/packages/scripts/news/+vite-optimize-deps-audit-fixes.internal b/packages/scripts/news/+vite-optimize-deps-audit-fixes.internal new file mode 100644 index 000000000..4e59a7366 --- /dev/null +++ b/packages/scripts/news/+vite-optimize-deps-audit-fixes.internal @@ -0,0 +1 @@ +Fixed the `check-vite-optimize-deps` audit script to ignore entries declared only in `optimizeDeps.exclude`, to correctly skip type-only named re-exports, and to check `ssr.optimizeDeps.include` separately for dependencies used only in `*.server.*` files. @sneridagh diff --git a/packages/scripts/news/+vite-optimize-deps-audit.internal b/packages/scripts/news/+vite-optimize-deps-audit.internal new file mode 100644 index 000000000..b5513c5f8 --- /dev/null +++ b/packages/scripts/news/+vite-optimize-deps-audit.internal @@ -0,0 +1 @@ +Added a workspace dependency audit script for Aurora Vite `optimizeDeps` declarations. @sneridagh diff --git a/packages/scripts/package.json b/packages/scripts/package.json index eed12d7e3..b589d912f 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -50,6 +50,7 @@ "wait-on": "^8.0.0" }, "devDependencies": { - "release-it": "catalog:" + "release-it": "catalog:", + "typescript": "catalog:" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8db8d53e..b4de4d677 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1559,6 +1559,9 @@ importers: release-it: specifier: 'catalog:' version: 19.0.5(@types/node@24.10.1)(magicast@0.3.5) + typescript: + specifier: 'catalog:' + version: 5.9.2 packages/theming: dependencies: @@ -6295,6 +6298,7 @@ packages: html2canvas-pro@1.5.12: resolution: {integrity: sha512-egtJIe6YXMKSLX/ls400OJD6tzEVtATJOE++mnXmxMWyqcu9HDXDoLiWeXnGv45QW2ZaIiDlXw46Gxqrqw6SEw==} engines: {node: '>=16.0.0'} + deprecated: All 1.5.x series are deprecated. Known rendering bugs exist and no further maintenance. Please upgrade to the latest 2.x release. htmlparser2@5.0.1: resolution: {integrity: sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==} @@ -6805,7 +6809,6 @@ packages: resolution: {integrity: sha512-Quz3MvAwHxVYNXsOByL7xI5EB2WYOeFswqaHIA3qOK3isRWTxiplBEocmmru6XmxDB2L7jDNYtYA4FyimoAFEw==} engines: {node: '>=8.17.0'} hasBin: true - bundledDependencies: [] jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}