Repository navigation
Conversation
…N-4270) Adds frontend/src/v1/ as an isolated tree for the v1 frontend (P0-T01): - Folder taxonomy from the spec, a public entry (src/v1/index.ts, imported by v0 as `@v1`) and a cloud overlay placeholder. - src/v1/tsconfig.json: strict, no JS, unused locals/params are errors, `@v1` alias only. v0's tsconfig/jsconfig exclude src/v1. - Biome overrides for the two-way import boundary and v1 bans (no `../`, `@/`, prop-types, antd, component CSS; noExplicitAny). - scripts/check-v1.mjs for what Biome cannot express: TypeScript-only files, the folder taxonomy and import contract, PropTypes, `ant-`. - typecheck:v1, lint:v1 and test:v1 scripts; v0 scripts are unchanged. - test-fixtures/v1-boundary/ proves each rule fails and passes as specified, run with node --test so the v0 vitest suite never sees it. - .gitignore re-includes src/v1/lib/ (the Python `lib/` rule matched it). v0 build output, test counts, typecheck and Biome results are unchanged.
…270) check-v1.mjs: - Scan comments with a string/template-aware tokenizer; a `/*` or `//` inside a string or line comment no longer hides the imports after it. - Read template-literal specifiers, import.meta.glob and `import(..., opts)`. - Flag specifiers that leave src/v1 (`/`, `..`, `@/`, `src/`) itself, not only via Biome, which skips template literals. - Check `@import`/`url()` and `ant-` in styles/ CSS; report symlinks; catch built `ant-` names; treat .test/.spec .mts/.cts as test files; ignore .DS_Store; drop the unused --root option. biome.json: also ban `..`, `./../**`, `/**`, `antd/**`, `prop-types/**`. tsconfig: skipLibCheck off so v1's own .d.ts files are checked. .gitignore: re-include lib/ at any depth under src/v1. Fixtures: a generated matrix over every folder-contract pair, every import form, every outside spelling, a symlink case, and cases for each new rule; tsc cases assert error codes; output matching excludes the command text and spawn errors fail loudly. Comment and README corrections.
… (UN-4270) - ci-frontend-lint: the typecheck job also runs `typecheck:v1` and `lint:v1`, so the strict v1 tsconfig and the scripts/check-v1.mjs checks (folder contract, TypeScript-only files, PropTypes, `ant-`) gate PRs now rather than from P0-T10. - Pages may no longer import app/, test/ or the v1 entry, only the shared folders (components, shell, services, stores, hooks, lib, styles, plugins). Test files in a page may still import test/. Contract table, README and the fixture matrix updated together.
…(UN-4270) check-v1.mjs: split the comment scanner into small helpers (cognitive complexity 40 -> under 15), drop the nested ternary and nested template literal, use String.raw for the import regexes, keep CLI flags in a Set. Fixture runner: - Cases and the expected folder contract are parsed text tables instead of repeated object literals (the 10% duplication). - Sorts use an explicit localeCompare comparator. - The contract matrix is built by a helper. - Spawns /bin/sh by absolute path and no longer needs `git init` (Biome only needs the ignore file). The git check-ignore call keeps a PATH lookup, with the reason and a NOSONAR marker. Fixture cases: re-exports rewritten as small functions and read-only props, without changing what each case exercises.
…nnot see (UN-4270) Addresses Greptile review on #2380: - import.meta.glob([...]): every pattern in the array is checked, not only the first, so an allowed first pattern no longer hides a forbidden one. - check-v1 --code also scans v0 (src/ outside v1, including the cloud src/plugins overlay) for template-literal imports, globs, require(), @/v1, src/v1, relative paths and CSS @import that reach into v1 other than through `@v1`. Biome still covers plain imports. Fixtures: Greptile's example as a case, a glob array with a forbidden later pattern, and a generated test of every v0 -> v1 form (denied and allowed). The v0 scan reports nothing on the current tree.
v1 finds plugins at build time from src/v1/plugins/<id>/manifest.ts, which the cloud overlay copies in. An absent plugin has no manifest, so its slots show their fallbacks with no error; any other load or render error is reported, and each slot has its own error boundary. - lib/plugins: the typed slot contract, buildRegistry (distribution filter plus conflict checks for every distribution), <Slot> and pluginRoutes(). VITE_DISTRIBUTION picks oss, cloud or onprem. - scripts/v1-plugins.mjs runs in every Vite build: a slot filled twice, a route defined twice or a plugin with no distribution fails the build, and the distribution matrix is published as plugin-matrix.json. - v0's optional-plugin stub no longer applies under src/v1, where it turned a missing module into an empty object instead of an error. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The cloud workflows set it to cloud or onprem; the default, oss, keeps the public image unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The cloud overlay copies plugins into frontend/src/v1/plugins/; ignore them as frontend/src/plugins/* already is, keeping only the .gitkeep. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… lib through context The scaffold allows lib/ to import only lib/, and only plugins/ may see the overlaid manifests. plugins/index.ts now builds the registry and exports PluginSlot and pluginRoutes(); lib/plugins keeps the contract and a Slot that reads PluginSlot from PluginProvider, which app/ will mount. With no provider every slot shows its fallback. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…hai clash TEMPORARY. With skipLibCheck off, @types/chai 5.2.3 and vitest 3.2.6 both declare Chai containSubset, so typecheck:v1 fails once any v1 test imports vitest. Drop this commit when #2380 settles it (pin @types/chai or turn the option on). Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ts with esbuild The build check now transpiles the registry and each manifest with esbuild, so it runs on the Node versions the frontend supports. It also fails on a folder with no manifest.ts, resolves argv[1] before the main-guard compare, and has CLI tests. The README notes the distribution matrix. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… the check before installing dependencies Co-Authored-By: Claude Opus 5.5 <[email protected]>
[Medium impact] The reviewed changes appear safe to merge within the documented scope.
|
…ins in the ignore check (UN-4270) Reported by Athul while building #2394/#2395 on this branch: - src/v1/tsconfig.json: skipLibCheck back on, as in tsconfig.json. Off, tsc also checked third-party typings, and @types/chai 5.2.3 and vitest 3.2.6 both declare `containSubset`, so any v1 test importing vitest failed typecheck:v1. A fixture test now imports vitest to keep this covered. - The "no v1 file is git-ignored" check skips src/v1/plugins/<id>/: cloud plugins copied there are git-ignored on purpose (#2394).
…and test slot recovery in place
- v1-plugins.mjs: with no --dir, `flag + 1` was 0, so the first argument
(the output file) was skipped and the matrix went to stdout. A fixture
now covers the file argument with and without --dir.
- Slot.test.tsx: the "recovers in place" test wrapped the slot in a <div>,
which remounted it, so the boundary's resetKey reset never ran. It now
re-renders the same slot; with resetKey={id} the test fails.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From Ali's, Athul's and Vishnu's reviews on #2380, each reproduced first: - tsconfig/jsconfig map `@v1/*` too: once v0 imports the entry, tsc follows it into src/v1, where cross-folder `@v1/...` imports must resolve (would break `bun run typecheck` at UN-4271). Lint keeps v0 on `@v1`. - check-v1 scanner tracks regex literals, so a `/*` inside one no longer hides the rest of the file; CSS blanks only block comments (`//` is URL). - Imports are read by argument: literals, templates by their fixed prefix, every glob pattern, and new URL(..., import.meta.url). In v1 an import the guard cannot read fails closed; a computed v1 path is allowed only in folders that may import everything (the lazy route registry in app/). - v0 specifiers are normalized before the check (/src/v1, @/./v1, ...). - v1 styles may not @import antd packages. - Biome's v0 rule matches only alias spellings of src/v1, so `uuid/v1` or `@/api/v1` no longer fail; relative spellings are check-v1's (in CI). - CI runs `test:v1` (fixtures) on Node 22; setup-biome pinned to a SHA (SonarCloud S7637). Fixtures assert the exact message and count per form, cover each fix, and check that comments, JSX and look-alike class names stay quiet. Against the previous guard, 9 of the new tests fail.
Static files (icons, logos, images) get one home, src/v1/assets/, imported as @v1/assets/... (e.g. logo.svg?react). v1 cannot reach v0's src/assets/. Raised by Ali on #2380: Lane I's P1-T22 needs SVG icons and provider logos, and a .svg outside the taxonomy failed --files. components/ui, components/patterns, shell, pages and styles may import assets/; assets/ imports nothing but itself (room for a generated icon map). README, contract table and the fixture matrix updated together; v1-allowed-imports imports an SVG as a React component under typecheck:v1.
…s (UN-4270) Greptile on #2380: `export const example = "import(name)";` failed lint:v1, because the call matcher also ran over string contents. Static import text in a string ('import x from "@/helpers/x"') and in templates or regexes was read as an import too. The comment scanner now also returns a code-only view (string, template and regex bodies blanked, offsets kept). Statements and calls are located there; the specifier is read from the real text at the same offset. The import-form test gains a quiet case with import text in a string, a template and a regex; it fails against the previous guard.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The v1 vitest config never loads optionalPluginImports, so the old missing-import test passed with either guard removed. The fixture calls both resolvers: a v1 importer is never stubbed, a v0 one still is. Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The v1 error boundary clears a failure when the pathname changes, as v0's backstop does with resetKeys; it stays keyed by page. - Rename VITE_UNSTRACT_DISTRIBUTION to VITE_DISTRIBUTION, the name P0-T07's plugin manifest (UN-4286, #2394) uses. README and Dockerfile say the cloud and on-prem builds do not set it yet. - Tests: resolveUrl (choice, last user, declared-not-live, undeclared), the boundary's stale-chunk reload and its ten-second window, the boundary reset, load()'s OSS fallback and DEV throw, a numeric and an empty user id. - README: registering before P0-T06 ships an unguarded page; the same-page overlap rule allows identical routes. - Docstrings and comments: setOldVersion may last for the session only; resolveUrl reads the store when called; v1 styles are in the global stylesheet (cssCodeSplit: false); Router says "live", names P0-T09. - Remove unused selectOldVersion; drop redundant vi.restoreAllMocks (the config sets restoreMocks); toHaveLength for the storage check (S5906).
* feat(frontend-v1): version switch and route registry (UN-4271) Typed route registry for v1 pages (declare in PAGES, go live with one line in REGISTERED_PAGES), validated by a unit test that fails CI on overlapping patterns, unmapped v0 routes, missing components or a toggle outside Human Review (D14). VersionSwitch wraps v0's routes in Router.jsx: unregistered URLs render v0 unchanged, registered pages render v1 as their own lazily loaded tree, and their v0 URLs redirect to the mapped v1 route. Human Review's old-version preference is per user, per browser (localStorage); setOldVersion() moves the user to the mapped screen without a reload. VITE_V1_DEV_ROUTES (set only for the E2E image in ci-test.yaml) registers demo pages that prove the switch and honours a preview override for declared-but-unregistered pages. resolveUrl() resolves a page's v1 route to the URL the user gets, for global search (UN-4278). * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix(frontend-v1): address self-review of the version switch (UN-4271) - Validation rejects a mapping whose URL lands on a route of the other version (an endless redirect), malformed page keys, and any v1 route without a toV0 mapping, so resolveUrl never returns null for a declared page's route. - Redirects and resolveUrl encode param values, so %2F, %3F and %23 stay inside their segment instead of becoming separators. - resolveUrl decides liveness by page key, not object identity. - An unknown VITE_UNSTRACT_DISTRIBUTION fails `vite build`; at runtime it logs and falls back to oss instead of blanking the app. - Registry validation runs only in development (CI covers release). - The v1 error boundary reloads once on a stale chunk (as v0 does) and offers a Reload button. - Dockerfile build args move below the install layer so CI and release share the cached install; VITE_UNSTRACT_DISTRIBUTION is settable. - Public helpers move to app/api.ts so v1 code can import them without the entry. - Tests now catch the guards mutation testing showed were unprotected (stale-user bounce, history replace, preview gating, distribution, error boundary, encoding); comments corrected. * fix(frontend-v1): reject same-page route overlaps, encode demo links (UN-4271) - Validation rejects a v1 and a v0 route of the same page that match the same URL (identical patterns excepted). This covers the redirect loop the placeholder-URL check caught and the case it missed: a param equal to a static segment (/old/settings -> /new/settings -> /settings). - The demo review page encodes orgName in its links, so an encoded org name stays one segment. - Use a Set for the param lookup (Sonar S7776). * feat(v1): mount the Toaster in the v1 root The feedback components from main say to mount the Toaster once inside the v1 root; V1App is that root. It sits inside ThemeProvider (toasts portal into the themed #v1-portal) and outside Suspense/Routes, so it stays mounted while a page loads and across v1 route changes. The VersionSwitch test checks notify() shows a toast on a v1 page and falls back to the console once the user moves to a v0 screen. * fix(frontend-v1): address review of the version switch (UN-4271) - resolvePageUrl/resolveUrl return null when a :param of the pattern is missing or empty, instead of generatePath throwing or dropping the segment. A final splat may still be omitted. - PREVIEW_STORAGE_KEY moves to registry/types.ts (re-exported from live.ts) so the E2E harness can import it without Vite; the types.ts header now lists lib/oldVersion.ts as harness-importable too. - checkEntry splits into small rule functions (SonarCloud S3776: cognitive complexity 21 > 15); the two mapping-coverage loops share one helper. Problems and their order are unchanged. - README and a test record that a load starting on a v1 URL has no userId until a v0 page loads (the session loads in v0's PersistentLogin), so the old-version toggle relies on unstract.v1.lastUser until P0-T06. * fix(frontend-v1): address the version switch review (UN-4271) - The v1 error boundary clears a failure when the pathname changes, as v0's backstop does with resetKeys; it stays keyed by page. - Rename VITE_UNSTRACT_DISTRIBUTION to VITE_DISTRIBUTION, the name P0-T07's plugin manifest (UN-4286, #2394) uses. README and Dockerfile say the cloud and on-prem builds do not set it yet. - Tests: resolveUrl (choice, last user, declared-not-live, undeclared), the boundary's stale-chunk reload and its ten-second window, the boundary reset, load()'s OSS fallback and DEV throw, a numeric and an empty user id. - README: registering before P0-T06 ships an unguarded page; the same-page overlap rule allows identical routes. - Docstrings and comments: setOldVersion may last for the session only; resolveUrl reads the store when called; v1 styles are in the global stylesheet (cssCodeSplit: false); Router says "live", names P0-T09. - Remove unused selectOldVersion; drop redundant vi.restoreAllMocks (the config sets restoreMocks); toHaveLength for the storage check (S5906). --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Resolves frontend/package.json (keep main's seam, separation, bundle and unit-gate scripts plus this branch's plugin fixtures) and the frontend Dockerfile (main's VITE_V1_DEV_ROUTES/VITE_DISTRIBUTION build args). Main now passes VITE_DISTRIBUTION="" when unset, so the plugin registry treats an empty value as oss instead of throwing. Co-Authored-By: Claude Opus 5.5 <[email protected]>
ritwik-g
left a comment
There was a problem hiding this comment.
Thanks, the contract is clean and the cloud overlay (unstract-cloud#1896) fits it: check(dir), the matrix fields, the distribution names and the folder = id rule all match. I also merged this branch into current main locally: the only conflict is the test:v1 line in frontend/package.json (keep both unit-gate.fixtures.mjs and scripts/v1-plugins.fixtures.mjs), and main's new v1 checks (lint with warnings as errors, typecheck, separation, bundle, seam) and all 177 node fixtures pass on the merged tree.
Requesting changes for two spec rules from P0-T07 that are not met yet (OSS route clashes, reporting through the notification contract), a retry loop on failed chunk loads, the dev image never getting VITE_DISTRIBUTION, and untested distribution logic. Details inline.
Not tied to a line
- Stale references to the removed
plugins/manifest.ts: the coverage comment infrontend/vitest.v1.config.mts:42-44still calls it "OSS's placeholder plugins/manifest.ts" (it is now the loaderindex.ts), and main'sfrontend/test-fixtures/v1-boundary/git-case.sh:20still writes the old!frontend/src/v1/plugins/manifest.tsignore rule. Worth updating both when resolving the conflict with main. (Fixtures still pass.) - Acceptance "An OSS build renders an OSS page with empty slots; a cloud build fills them" can't be shown yet since no v1 page or provider exists. Could the Jira ticket say which task carries it?
…2, return focus, handle a removed organisation - Restore the v1 README's Plugins section, lost in the #2394 merges. - A 412 with a code goes to /error?code=&domain= as v0 does; an empty body still goes to /landing. - Closing the confirm dialog puts focus back on the organisation pill. - A 403 on switch says access was removed, keeps Switch disabled and reloads the list; other failures are logged. - Loading and load errors use Skeleton and InlineAlert; the dialog error uses InlineAlert. - Switcher panel shadow-md (dropdown), confirm dialog shadow-xl (dialog). - Log when sign-out falls back to the GET logout. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…report through notify, back off failed chunks - The build check and the runtime registry reject a plugin route on a page the v1 route registry declares (demo pages included), naming OSS and the plugin. - A plugin failure is logged and raises one error notification per slot. - A slot whose chunk failed waits 30 s before fetching it again and reports once until a load succeeds, so re-renders no longer refetch and re-report. - The development image stage takes VITE_DISTRIBUTION, so a dev-live overlay runs as its distribution. - plugin-matrix.json names the build's distribution; the Vite plugin is exported and tested (conflict fails buildStart, matrix emitted, plugin wired, unknown distribution fails the config). - plugins/index.ts is in core coverage; README and headers describe the wiring P2-T01 still has to do; stale manifest.ts references updated. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
Review body: b3a667a updates the coverage comment in |
ritwik-g
left a comment
There was a problem hiding this comment.
Re-reviewed b3a667a: 7 of the 9 threads are resolved, and the new guards are covered by tests. Two are partly done (route matching should use patternsOverlap, and README.md:188 still contradicts the matrix sentence), and there is one new issue inline on Slot.tsx:119.
… error The plugin route check took raw strings, so /:org/v1-demo, a case variant or /:orgName/* passed against the OSS page /:orgName/v1-demo. buildRegistry now takes an overlap rule; the app and the build check pass the route registry's patternsOverlap, and the build check bundles entries.ts and patterns.ts with esbuild under Node instead of transpiling them alone. A render error after a successful load left the slot marked reported for the life of the tab, so later errors there were silent. Only failed loads are deduplicated now. The README matrix bullet is rewritten to match. Co-Authored-By: Claude Opus 5.5 <[email protected]>
CodeQL flagged the helper's single-occurrence replace as incomplete sanitization. A fixed /billing rule says the same thing. Co-Authored-By: Claude Opus 5.5 <[email protected]>
vishnuszipstack
left a comment
There was a problem hiding this comment.
Review of PR 2394 — UN-4286 Add the v1 plugin manifest, slots and build-time plugin check
Mode: INITIAL, at the PR's current head. Reviewed head 5cc28d787200f53196f14c238ef894310dbffb39, merge base ed416ff66d99d5bda904d1036a37a2482657361d, base freshened (base_fetch: ok), 23 files, 1460 lines (matches GitHub). Rubric: unstract plugin v2.1.80.
How this was done: a full fan-out (code reviewer, silent-failure hunter, type design, comments, lifecycle and a whole-diff cold read) ran at an earlier head c015c2537. Before posting, every finding was re-checked against this head. Findings already fixed, already raised by another reviewer, or wrong on a second read were dropped (17 dropped: 10 already raised by ritwik-g and fixed in b3a667a or 5cc28d7, 2 more fixed, 5 withdrawn). The author's own commits since c015c2537 were reviewed for new defects, and the tests were re-run here.
Security deep-dive: degraded — the vendored security prompt ran once at this head, its filter step inline instead of as independent passes; no findings at confidence >= 8.
Verdict — APPROVE WITH NITS
Summary — Critical: 0 · High: 0 · Medium: 1 · Low: 8 · Lenses run: 19/19 (2 not covered, 4 N/A; see checklist).
The review rounds' fixes hold, including the overlap matching and per-error reporting in 5cc28d7. One Medium is left: nothing checks the shape of a cloud manifest. The root tsconfig.json excludes src/v1, the build check strips types, and assertDeclared looks only at ids, distributions and slot keys. So a cloud manifest with an undefined slot, a non-function load or a route without path passes both repos and fails only at render or routing time. One Low is new in 5cc28d7 (plugin route paths skip patternProblem). The rest are comment accuracy, an undeclared esbuild, a hand-copied distribution list, and fixture hygiene. Tests at this head, from frontend/: the changed vitest files pass (4 files, 31/31), the node fixtures pass (183/183), and so does the Bun branch of the build check (21/21). typecheck:v1, lint:v1 and biome ci on the 17 changed files all exit 0; biome's 4 warnings and 3 infos are in vite.config.js, and only one of those lines is new here. The full test:v1, vite build and real-browser chunk retries were not run.
Unanchored findings
- PR description is out of date (Low). The description says the scaffold's dynamic-import rule "is met by
missing-import.test.ts", but 365c924 replaced that file with the resolver fixture inboundary.fixtures.mjs. It also still describes an "earlier temporary commit" forskipLibCheck, and gives "20 vitest tests and 50 node tests" from before the review rounds. It says the fixture tests cover a "duplicate route", but the plugin-vs-plugin route case is tested only inregistry.test.ts; the fixtures cover plugin-vs-OSS routes. Please refresh the Testing notes against the current head.
Findings
Posted inline on the diff; index:
-
- [Medium] [Lens 7] — Cloud manifests are never type-checked, and the registry does not check their shape —
frontend/src/v1/lib/plugins/registry.ts:32
- [Medium] [Lens 7] — Cloud manifests are never type-checked, and the registry does not check their shape —
-
- [Low] [Lens 3] — Plugin route paths skip
patternProblem, so the overlap check can miss clashes —frontend/src/v1/lib/plugins/registry.ts:87
- [Low] [Lens 3] — Plugin route paths skip
-
- [Low] [Lens 10] — Manifest load errors lose their cause and always blame
import type—frontend/scripts/v1-plugins.mjs:65
- [Low] [Lens 10] — Manifest load errors lose their cause and always blame
-
- [Low] [Lens 16] — The contract comments say pages never import a plugin; the boundary check allows it —
frontend/src/v1/lib/plugins/index.tsx:2
- [Low] [Lens 16] — The contract comments say pages never import a plugin; the boundary check allows it —
-
- [Low] [Lens 16] —
plugins/index.tsheader still says every import error is reported —frontend/src/v1/plugins/index.ts:2
- [Low] [Lens 16] —
-
- [Low] [Lens 16] —
jest-dom.d.tsnames v0's setup file —frontend/src/v1/test/jest-dom.d.ts:1
- [Low] [Lens 16] —
-
- [Low] [Lens 2] — The distribution list is copied by hand, though the route registry already exports it —
frontend/src/v1/plugins/index.ts:22
- [Low] [Lens 2] — The distribution list is copied by hand, though the route registry already exports it —
-
- [Low] [Lens 14] —
esbuildis used at build time but not declared —frontend/scripts/v1-plugins.mjs:35
- [Low] [Lens 14] —
-
- [Low] [Lens 13] — Fixture uses a fixed shared tmp path and the bare CLI run reads the real plugins folder —
frontend/scripts/v1-plugins.fixtures.mjs:62
- [Low] [Lens 13] — Fixture uses a fixed shared tmp path and the bare CLI run reads the real plugins folder —
Lens checklist
- Spec & intent — Clean
- Architectural fit & precedent — See findings #7
- Correctness & edge cases — See findings #2
- Security — Not covered — the security deep-dive ran degraded (single pass, filters inline) and found nothing at confidence >= 8, which is not a clean bill
- Data integrity & migrations — N/A — no persisted data or schema
- Concurrency — Clean
- API & contract compatibility — See findings #1
- Reliability & resilience — Clean
- Performance & cost — Clean
- Observability — See findings #3
- Operational safety — Clean
- LLM/agent-specific — N/A — no model, prompt or agent path
- Testing — See findings #9
- Dependencies & build — See findings #8
- Code quality — Clean
- Doc & comment accuracy — See findings #4, #5, #6
- Cross-document rule consistency — Not covered — merge-result sweep unavailable: git 2.34 here, the script needs 2.38
- On-prem deployment experience — N/A — no chart, values or on-prem build path touched
- Scaling & workload lifecycle — N/A — no worker or scaling config
| function assertDeclared(manifests: readonly PluginManifest[]): void { | ||
| const seen = new Set<string>(); | ||
| for (const m of manifests) { | ||
| if (seen.has(m.id)) { | ||
| throw new PluginConflictError(`plugin id ${m.id} is used twice`); | ||
| } | ||
| seen.add(m.id); | ||
| if (m.distributions.length === 0) { | ||
| throw new PluginConflictError(`${m.id} declares no distributions`); | ||
| } | ||
| for (const d of m.distributions as readonly string[]) { | ||
| if (!(PLUGIN_DISTRIBUTIONS as readonly string[]).includes(d)) { | ||
| throw new PluginConflictError( | ||
| `${m.id} declares unknown distribution ${d}`, | ||
| ); | ||
| } | ||
| } | ||
| for (const id of Object.keys(m.slots ?? {})) { | ||
| if (!(SLOT_IDS as readonly string[]).includes(id)) { | ||
| throw new PluginConflictError(`${m.id} fills unknown slot ${id}`); | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
1. [Medium] [Lens 7] — Cloud manifests are never type-checked, and the registry does not check their shape
- Location:
frontend/src/v1/lib/plugins/registry.ts:L32-L55 - Only
satisfies PluginManifestunder tsc enforces the shape of a manifest: each slot value and routeloadis a loader function, and each route has a stringpath. The build check strips types (esbuildtransformunder Node, a direct import under Bun). The rootfrontend/tsconfig.jsonexcludessrc/v1, so cloud CI'sbun run typechecknever sees the overlaid manifests. In OSS,typecheck:v1coverssrc/v1/plugins, but that folder is empty here.assertDeclaredchecks ids, distributions and slot keys only. A cloud manifest withslots: { "shell.product-switcher": undefined }, a non-functionload, or a route withoutpaththerefore passes both repos. It fails only at render or routing time. An explicitundefinedslot also claims the slot incollect, so a real filler for it fails as a conflict. Either have the overlay runtsc -p src/v1/tsconfig.jsonafter copying and say so in the README, or addtypeof load === "function"andtypeof path === "string"checks toassertDeclared. - Verification: VERIFIED — read at this head:
frontend/tsconfig.jsonexcludessrc/v1, andassertDeclaredthrows only on ids, distributions and slot keys
There was a problem hiding this comment.
Fixed in 736790646. The registry now rejects a slot value that is not a function (an explicit undefined included), a route load that is not a function and a route without a string path, naming the plugin and the slot or route; tests cover each case in registry.test.ts and in the build-check fixtures.
| for (const route of m.routes ?? []) { | ||
| const taken = owned.find(([path]) => overlaps(path, route.path)); | ||
| if (taken) { | ||
| const [path, owner] = taken; | ||
| const what = | ||
| path === route.path ? route.path : `${route.path} (overlaps ${path})`; | ||
| throw new PluginConflictError( | ||
| `route ${what} is defined by both ${owner} and ${m.id}`, | ||
| ); | ||
| } | ||
| owned.push([route.path, m.id]); | ||
| routes.push({ ...route, owner: m.id }); |
There was a problem hiding this comment.
2. [Low] [Lens 3] — Plugin route paths skip patternProblem, so the overlap check can miss clashes
- Location:
frontend/src/v1/lib/plugins/registry.ts:L87-L98 patternsOverlapis exact only for patterns that passpatternProblem: absolute paths, static segments,:paramsand a final*.validate.tsruns that check on every page route. Plugin routes go straight intooverlaps(path, route.path)with no check. A plugin route/:orgName/v1-demo/:id?is compared segment by segment. Its:id?meets the end of/:orgName/v1-demo,patternsOverlapreturns false, and the build passes. React Router still matches/acme/v1-demofor both routes, so the plugin silently shadows the OSS page or is shadowed by it. A path without a leading/or with a mid-path*also gets in. The README now promises the check catches "two routes the router would match for the same URL". Reject a plugin route whose path fails the registry's pattern rule. Pass the validator in next tooverlaps(lib/ cannot import app/), and add a fixture for:id?.- Verification: VERIFIED — read at this head
There was a problem hiding this comment.
Fixed in 736790646. buildRegistry now takes { ossRoutes, overlaps, patternProblem }, both callers pass the route registry's patternProblem, and a plugin route it refuses fails the build; fixtures cover /:orgName/v1-demo/:id? and a path without a leading /.
| try { | ||
| mod = await importManifest(file); | ||
| } catch (err) { | ||
| throw new Error( | ||
| `${rel} could not be loaded (${err.message}). A manifest may only ` + | ||
| '`import type`; load components with `() => import("./X")`.', | ||
| ); | ||
| } |
There was a problem hiding this comment.
3. [Low] [Lens 10] — Manifest load errors lose their cause and always blame import type
- Location:
frontend/scripts/v1-plugins.mjs:L65-L72 - The catch in
load()throws a newErrorthat keeps onlyerr.message. The stack, esbuild's location frames and thecauseare dropped, and the CLI handler (L148-L151) prints onlyerr.messageagain. Every failure also gets the hint "A manifest may onlyimport type": a syntax error, a TypeError thrown while the manifest evaluates, oresbuildfailing to import under Node. A developer with a typo on line 40 is steered toward import rules. Pass{ cause: err }, and add the hint only for module-resolution failures (ERR_MODULE_NOT_FOUND, "Cannot find module", esbuild's "Could not resolve"). In the CLI catch, printerr.stackorerr.causeas well. - Verification: VERIFIED — read at this head
There was a problem hiding this comment.
Fixed in 736790646. The wrapped error keeps the original as cause, the import type hint appears only for module-resolution failures, and the CLI prints the cause with its stack; a syntax-error fixture checks that it gets no hint.
| * v1's extension-point contract. Shell and pages render `<Slot>` from here; | ||
| * they never import a plugin. The registry itself is built in | ||
| * `@v1/plugins` (the only folder that may see plugin manifests). The v1 shell |
There was a problem hiding this comment.
4. [Low] [Lens 16] — The contract comments say pages never import a plugin; the boundary check allows it
- Location:
frontend/src/v1/lib/plugins/index.tsx:L2-L4 - This header and the README Plugins paragraph (
README.md:L181) say pages "never import a plugin" and call@v1/plugins"the only folder that may see plugin manifests".CONTRACT.pagesinscripts/check-v1.mjslists"plugins", and the README import table letspages/importplugins/.app,entryandtestmay import everything. So neither sentence is enforced. A page that reaches intoplugins/<id>/passeslint:v1, and a reader who trusts the comment assumes it would be caught. The README folder table (README.md:176, outside this diff) also still saysplugins/holds "only a placeholder", though it now ships the loaderindex.ts. Either droppluginsfromCONTRACT.pagesand the table so the prose becomes true, or remove the two claims and update the table row. - Verification: VERIFIED — read at this head
There was a problem hiding this comment.
Fixed in 736790646. Both claims are removed (the lib/plugins/index.tsx header and the README Plugins paragraph), and the README row for plugins/ now says it holds the loader index.ts.
| * Builds the plugin registry from the manifests the cloud overlay copies in | ||
| * as `plugins/<id>/manifest.ts`. An absent plugin has no manifest, so its | ||
| * slots show their fallbacks; every import error left is a real failure and | ||
| * is reported. Nothing renders `PluginSlot` or `pluginRoutes()` yet: the v1 |
There was a problem hiding this comment.
5. [Low] [Lens 16] — plugins/index.ts header still says every import error is reported
- Location:
frontend/src/v1/plugins/index.ts:L2-L5 - The header says "every import error left is a real failure and is reported". Manifests load through an eager
import.meta.glob, so a broken manifest import fails the build or module evaluation and never reachesreportPluginError. Only filler load and render errors caught bySlotBoundaryare reported. Narrow it to "a filler that fails to load or render is reported", or drop the clause. ThemakeSlotdocstring already describes this correctly. - Verification: VERIFIED — read at this head
There was a problem hiding this comment.
Fixed in 736790646. The header now says a filler that fails to load or render is reported.
| @@ -0,0 +1,2 @@ | |||
| // jest-dom matchers for v1 tests; src/setupTests.js registers them at run time. | |||
There was a problem hiding this comment.
6. [Low] [Lens 16] — jest-dom.d.ts names v0's setup file
- Location:
frontend/src/v1/test/jest-dom.d.ts:L1 - The comment says
src/setupTests.jsregisters the matchers at run time. That is v0's setup file (vitest.config.mjs). v1 tests runsrc/v1/test/setup.ts(thesetupFilesinvitest.v1.config.mts), which imports@testing-library/jest-dom/vitest. Point the comment atsrc/v1/test/setup.ts. - Verification: VERIFIED — read at this head
There was a problem hiding this comment.
Fixed in 736790646. The comment now points at src/v1/test/setup.ts.
| type RouteEntry, | ||
| } from "@v1/lib/plugins"; | ||
|
|
||
| const DISTRIBUTIONS: readonly Distribution[] = ["oss", "cloud", "onprem"]; |
There was a problem hiding this comment.
7. [Low] [Lens 2] — The distribution list is copied by hand, though the route registry already exports it
- Location:
frontend/src/v1/plugins/index.ts:L22 DISTRIBUTIONShere is typedreadonly Distribution[], so it still type-checks if a distribution is added toPLUGIN_DISTRIBUTIONSand missed here. The same list is written out again invite.config.js(with a "mirrors" comment) and in the matrix keys inscripts/v1-plugins.mjs:L112-L116. If a new distribution is missed in one copy,VITE_DISTRIBUTION=<new>builds but then throws at module load, or the matrix leaves out a key the cloud overlay indexes (matrix.distributions[distribution]).app/registry/types.tsalready exportsDISTRIBUTIONSas a const tuple. Import it here, and build the matrix keys from["oss", ...PLUGIN_DISTRIBUTIONS].- Verification: VERIFIED — read at this head
There was a problem hiding this comment.
Fixed in 736790646. plugins/index.ts and the matrix keys in scripts/v1-plugins.mjs now come from DISTRIBUTIONS in app/registry/types.ts. vite.config.js keeps its own list because the config loads synchronously, and a new fixture checks that it accepts every distribution the matrix lists.
| return import("esbuild") | ||
| .then(({ build }) => | ||
| build({ | ||
| entryPoints: [file], | ||
| bundle: true, | ||
| write: false, | ||
| format: "esm", | ||
| platform: "node", | ||
| }), | ||
| ) | ||
| .then((r) => import(dataUrl(r.outputFiles[0].text))); |
There was a problem hiding this comment.
8. [Low] [Lens 14] — esbuild is used at build time but not declared
- Location:
frontend/scripts/v1-plugins.mjs:L35-L45 - Under Node,
vite buildruns this check, which importsesbuild. Since 5cc28d7 the check also uses esbuild'sbuild(bundling) API, not justtransform.esbuildis not infrontend/package.json. It resolves only because it is hoisted as Vite's dependency. An isolated or non-hoisted install would fail every build inbuildStart. Addesbuildto devDependencies, pinned to Vite's range. - Verification: VERIFIED — read at this head
There was a problem hiding this comment.
Fixed in 736790646. esbuild is a devDependency at Vite's range (^0.27.0), with a one-line bun.lock change.
| }); | ||
|
|
||
| test("no plugins directory gives an empty matrix", async () => { | ||
| assert.deepEqual(await check(path.join(os.tmpdir(), "v1-plugins-none")), { |
There was a problem hiding this comment.
9. [Low] [Lens 13] — Fixture uses a fixed shared tmp path and the bare CLI run reads the real plugins folder
- Location:
frontend/scripts/v1-plugins.fixtures.mjs:L62 - The "no plugins directory" case passes
os.tmpdir()/v1-plugins-none. That path stays absent only by convention: if a leftover or a concurrent run creates it, the test fails. None of themkdtempdirectories the other cases create is removed. The bare CLI case (L243-L248) runs without--dir, so it checks the repo's ownsrc/v1/plugins. In a cloud-overlaid checkout, any broken overlaid manifest fails this OSS fixture. Use a freshmkdtemppath, removed afterwards, for the "none" case, and clean up the temp dirs. Point the bare run at a fixture through an env override orcwd. - Verification: VERIFIED — read at this head
There was a problem hiding this comment.
Fixed in 736790646. Every fixture directory is a fresh mkdtemp removed after the run, including the "no plugins directory" case. The bare CLI run reads a fixture through a new V1_PLUGINS_DIR override instead of the repository's src/v1/plugins.
…, keep load-error causes - registry: reject a slot value or route load that is not a function and a route without a string path; buildRegistry takes an options object (ossRoutes, overlaps, patternProblem) and rejects plugin routes the pattern rule refuses (default: must start with /) - plugins/index.ts and scripts/v1-plugins.mjs pass the route registry's patternProblem; both take DISTRIBUTIONS from app/registry/types.ts - v1-plugins.mjs keeps the load error as cause, adds the import-type hint only for module-resolution failures, and the CLI prints the cause - declare esbuild as a devDependency (Vite's range, ^0.27.0) - fixtures: temp dirs removed afterwards, fresh path for the "none" case, bare CLI run reads V1_PLUGINS_DIR; new cases for shape, :id?, relative paths, syntax errors and Vite accepting every distribution - comments: drop unenforced "pages never import a plugin" claims, update the plugins/ README row, narrow the loader header, fix the jest-dom note Co-Authored-By: Claude Opus 5.5 <[email protected]>
Frontend Lint Report (Biome)✅ All checks passed! No linting or formatting issues found. |
|
Unstract test resultsPer-group results
Critical paths
|
ritwik-g
left a comment
There was a problem hiding this comment.
All threads resolved, and each fix is covered by a test. Approving. Thanks, Athul.



What
plugins/<id>/manifest.ts; shell and pages render<Slot id="…">from@v1/lib/plugins.plugins/index.tsdiscovers manifests withimport.meta.glob, keeps those for the currentVITE_DISTRIBUTION, and builds the registry.PluginProviderhands the slot renderer tolib/through context, solib/keeps importing onlylib/.scripts/v1-plugins.mjs) fails the build when two plugins fill a single-filler slot or claim the same route..gitignorekeeps overlaid cloud plugins out of OSS commits; the frontend Docker build passesVITE_DISTRIBUTIONthrough.Why
How
fallback, so OSS shows nothing where a cloud plugin would sit. A filler that throws is caught per slot and reported, and the rest of the page renders.skipLibCheckinsrc/v1/tsconfig.jsonnow comes from the scaffold (UN-4270 [FEAT] v1 scaffold, strict TypeScript and import boundary #2380, 664d1cf), which settled the@types/chai/ vitestcontainSubsetclash. This branch merged the scaffold and takes its tsconfig, so the earlier temporary commit no longer changes anything. The scaffold's new rule against dynamic imports it cannot analyse is met bymissing-import.test.ts(a template whose fixed prefix names the folder).Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)
frontend/src/v1/, which no v0 route renders. Outside it: a.gitignorerule forfrontend/src/v1/plugins/*, aVITE_DISTRIBUTIONbuild arg (defaults to OSS behaviour when unset) and a Vite plugin that only inspectssrc/v1/plugins.Database Migrations
Env Config
VITE_DISTRIBUTION(build time, optional). Unset means OSS.Relevant Docs
frontend/src/v1/README.md, section "Plugins".Related Issues or PRs
Dependencies Versions
Notes on Testing
bun run typecheck:v1,lint:v1,test:v1: clean; 20 vitest tests and 50 node tests (the scaffold's fixtures plus this PR's build-check and CLI tests).main, i.e. after UN-4270 [FEAT] v1 scaffold, strict TypeScript and import boundary #2380 merges. The commands above were run locally.Screenshots
None: no UI.
Checklist
I have read and understood the Contribution Guidelines.
🤖 Generated with Claude Code