Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .changeset/runner-vite-alias-transitive-closure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
"@object-ui/runner": patch
---

Complete `packages/runner/vite.config.ts`'s workspace alias table to the full
transitive import closure, so `@object-ui/runner` boots and builds from the
monorepo sources without a prior `pnpm -w build` (objectui#3575).

The table aliased 7 `@object-ui/*` specifiers to `packages/*/src`, but those
`src` trees import 8 more workspace packages that were not aliased. Those fell
back to Node resolution and landed on `packages/<pkg>/dist`, which does not
exist in a fresh install-only checkout — so the "From Source" flow documented in
`content/docs/utilities/runner.mdx` (`pnpm install` then `pnpm dev`, no build
step) failed with "Failed to run dependency scan" and served HTTP 500 for every
module on the chain. `pnpm --filter @object-ui/runner build` failed the same way.

Newly aliased: `i18n`, `sdui-parser`, `react-runtime`, `fields`, `plugin-detail`
(first layer), `providers` and `permissions` (only reachable once the first layer
resolves to src), and `data-objectstack` (a type-only import that esbuild erases,
so the dependency scan never reported it).

This is user-visible in the published artifact, because the alias table is not
scoped by `command` and therefore applies to `vite build` as well. Bundling the
newly aliased packages from src stops the per-icon `lucide-react/dynamic.mjs`
chunks from being inlined, so the build now emits ~1.7k lazy icon micro-chunks
like `apps/console` does. `build.modulePreload` is disabled to match console, so
those chunks are not all preloaded on first paint: the measured initial eager
payload drops from 4231003 to 591795 bytes, while total `dist` size grows about
5.5% because the previously inlined icons are now separate files.
86 changes: 86 additions & 0 deletions packages/runner/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,99 @@ export default defineConfig({
"@app": path.resolve(__dirname, "./src/app-data"),

// ⚡️ DX: Map imports to source code for Hot Module Replacement
//
// This table IS the mechanism that lets runner boot straight from the
// monorepo sources with NO `pnpm -w build` first — exactly what
// `content/docs/utilities/runner.mdx` "From Source" documents
// (`pnpm install` → `pnpm dev`). Any `@object-ui/*` specifier NOT listed
// here falls back to Node resolution and lands on `packages/<pkg>/dist`,
// which does not exist in a fresh install-only checkout — Vite then
// reports "Failed to run dependency scan" and serves HTTP 500 for every
// module on that import chain (objectui#3575).
//
// ⚠️ INVARIANT — the table must be the *transitive* closure, not just
// runner's own direct imports: every `@object-ui/*` specifier imported
// anywhere under the `src` of a package that is itself aliased here must
// also be aliased. Adding only the directly-missing packages just moves
// the hole one layer down (that is how #3575 happened: `plugin-kanban`
// was aliased, but the `fields` / `plugin-detail` it imports were not).
//
// Re-derive the closure — run from the repo root and re-run until it
// prints nothing; every package you add brings its own src into the
// sweep, so this reaches a fixpoint by iteration:
//
// comm -13 \
// <(grep -oP '"\K@object-ui/[a-z0-9-]+(?=":)' packages/runner/vite.config.ts | sort -u) \
// <(grep -rhP "(?<![@\w])(?:from|import)\s*\(?\s*['\"]@object-ui/" \
// packages/runner/src \
// $(grep -oP 'packages/\K[a-z0-9-]+(?=/src")' packages/runner/vite.config.ts \
// | sort -u | sed 's|^|packages/|;s|$|/src|') 2>/dev/null \
// | grep -vP '^\s*(\*|//)' \
// | grep -oP "['\"]\K@object-ui/[a-z0-9-]+" | sort -u)
//
// Anything it prints is a package importable from the aliased sources but
// missing below — add it, then run again. Notes on the filters, each of
// which suppresses a real false positive in this repo:
// - the `(?<![@\w])` lookbehind skips CSS `@import`, so doc comments
// quoting `@import '@object-ui/app-shell/styles.css'` are not hits;
// a real CSS `@import` of a workspace package must be swept for
// separately (there are none today);
// - `grep -vP '^\s*(\*|//)'` drops JSDoc/line-comment examples such as
// `* () => import('@object-ui/plugin-grid')` in react/LazyPluginLoader
// and core/registry/Registry.ts, which are documentation, not edges;
// - `2>/dev/null` tolerates a table entry whose package dir no longer
// exists (`data-objectql` is such a leftover — objectui#3593).
// Type-only imports are invisible to Vite's esbuild dependency scan but
// still belong in the table — see `data-objectstack` below.
"@object-ui/components": path.resolve(__dirname, "../../packages/components/src"),
"@object-ui/react": path.resolve(__dirname, "../../packages/react/src"),
"@object-ui/core": path.resolve(__dirname, "../../packages/core/src"),
"@object-ui/types": path.resolve(__dirname, "../../packages/types/src"),
"@object-ui/data-objectql": path.resolve(__dirname, "../../packages/data-objectql/src"),
"@object-ui/plugin-kanban": path.resolve(__dirname, "../../packages/plugin-kanban/src"),
"@object-ui/plugin-charts": path.resolve(__dirname, "../../packages/plugin-charts/src"),

// Reached transitively through the packages above — see the closure note.
// `@object-ui/i18n` <- react/src/index.ts, components/src/lib/close-label.tsx
"@object-ui/i18n": path.resolve(__dirname, "../../packages/i18n/src"),
// `@object-ui/sdui-parser` <- components/src/renderers/layout/page.tsx
"@object-ui/sdui-parser": path.resolve(__dirname, "../../packages/sdui-parser/src"),
// `@object-ui/react-runtime` <- components/src/renderers/layout/react-page.tsx
"@object-ui/react-runtime": path.resolve(__dirname, "../../packages/react-runtime/src"),
// `@object-ui/fields` <- plugin-kanban/src/ObjectKanban.tsx
"@object-ui/fields": path.resolve(__dirname, "../../packages/fields/src"),
// `@object-ui/plugin-detail` <- plugin-kanban/src/ObjectKanban.tsx
"@object-ui/plugin-detail": path.resolve(__dirname, "../../packages/plugin-detail/src"),
// `@object-ui/providers` <- fields/src/widgets/{FileField,ImageField}.tsx (2nd layer)
"@object-ui/providers": path.resolve(__dirname, "../../packages/providers/src"),
// `@object-ui/permissions` <- plugin-detail/src/DetailView.tsx et al. (2nd layer)
"@object-ui/permissions": path.resolve(__dirname, "../../packages/permissions/src"),
// `@object-ui/data-objectstack` <- react/src/context/AppShellContext.tsx, `import type`
// only. esbuild erases it, so Vite's dependency scan never flags it — it is
// in the closure by the invariant above, and aliased so that the day the
// import turns into a value import the dev server does not break.
"@object-ui/data-objectstack": path.resolve(__dirname, "../../packages/data-objectstack/src"),
},
},
build: {
// The alias table above is NOT scoped by `command`, so it applies to
// `vite build` too. That is deliberate and matches apps/console: resolving
// the plugin packages to src keeps the ComponentRegistry singleton
// single-instanced, which importing their prebuilt dist/ bundles does not
// guarantee (duplicate registries surface as "Unknown component type").
//
// Measured consequence of completing the table (objectui#3575): once
// `@object-ui/fields` & co. are bundled from src, the per-icon chunks that
// `components/src/lib/lazy-icon.tsx` creates via `lucide-react/dynamic.mjs`
// stop being inlined — this build goes from 10 assets to 1776, ~1761 of
// them sub-2KB icon micro-chunks. That is exactly the shape apps/console
// has ("1700+ icon chunks", see its build config).
//
// Vite's default `modulePreload: true` then emits a preload link for every
// one of them: index.html measured 546 B -> 145 KB with 1765
// `rel="modulepreload"` links, so the browser eagerly fetches all the icon
// chunks on first paint and the lazy split becomes a pessimisation.
// apps/console disables it for this exact reason; runner needs the same.
modulePreload: false,
},
})
Loading