From af9a3b190b6949381e6b58b10090d4a98eca44e5 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Sat, 18 Jul 2026 23:42:43 -0400 Subject: [PATCH] ADR-013 Phase 4: frontend staging machinery + relocate printedparts; fix router crash The staging step that makes lean per-site frontend builds possible, plus the first plugin relocated as the pilot. - scripts/stage-frontend.mjs: copies each chosen plugin's plugins//frontend/ into frontend/src/.plugins-staged// and codegens routes.gen.js. Plugin selection via SITE_PLUGINS (comma-separated); empty = all plugins that have a frontend/ (the full build). Wired as npm predev/prebuild; outputs gitignored. - Router imports routes.gen.js and merges staged routes with the in-tree ./routes/*.js glob - dual-location during the transition. - printedparts relocated: its 6 views (list/detail/form/kiosk + the settings and labels views from the shared dirs) moved into plugins/printedparts/frontend/ views/, core imports rewritten to the @/ alias; routes.js is the self-contained route module. Its old in-tree route file is removed. Also fixes a crash the previous commit (37c764b) shipped: slides.js exports only `toplevel` (its child routes live in core.js), so the router's flatMap(m => m.default) produced an undefined child and threw "Cannot read properties of undefined (reading 'path')" at load - the whole SPA went blank. Guarded with `m.default || []`. (The earlier "print pages are blank" reading was this crash, not page nature.) Verified live: /machines renders again; the relocated /printedparts list renders identically from the staged plugin frontend; SITE_PLUGINS=machines excludes printedparts from routes.gen. Build (via npm, runs stage) + vitest + naming green. --- frontend/.gitignore | 4 ++ frontend/package.json | 3 + frontend/src/router/index.js | 17 +++++- frontend/src/router/routes/printedparts.js | 61 ------------------- plugins/printedparts/frontend/routes.js | 58 ++++++++++++++++++ .../frontend/views}/PartsKiosk.vue | 6 +- .../frontend/views}/PrintedItemDetail.vue | 6 +- .../frontend/views}/PrintedItemForm.vue | 4 +- .../frontend/views}/PrintedItemsList.vue | 6 +- .../frontend/views}/PrintedPartsLabels.vue | 2 +- .../frontend/views}/PrintedPartsSettings.vue | 0 scripts/stage-frontend.mjs | 61 +++++++++++++++++++ 12 files changed, 153 insertions(+), 75 deletions(-) create mode 100644 frontend/.gitignore delete mode 100644 frontend/src/router/routes/printedparts.js create mode 100644 plugins/printedparts/frontend/routes.js rename {frontend/src/views/printedparts => plugins/printedparts/frontend/views}/PartsKiosk.vue (98%) rename {frontend/src/views/printedparts => plugins/printedparts/frontend/views}/PrintedItemDetail.vue (98%) rename {frontend/src/views/printedparts => plugins/printedparts/frontend/views}/PrintedItemForm.vue (98%) rename {frontend/src/views/printedparts => plugins/printedparts/frontend/views}/PrintedItemsList.vue (96%) rename {frontend/src/views/print => plugins/printedparts/frontend/views}/PrintedPartsLabels.vue (99%) rename {frontend/src/views/settings => plugins/printedparts/frontend/views}/PrintedPartsSettings.vue (100%) create mode 100644 scripts/stage-frontend.mjs diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..dc5f215 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,4 @@ + +# ADR-013 Phase 4 staged plugin frontends (generated by scripts/stage-frontend.mjs) +src/.plugins-staged/ +src/router/routes.gen.js diff --git a/frontend/package.json b/frontend/package.json index bad73bd..b6d303c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,10 @@ "private": true, "type": "module", "scripts": { + "stage": "node ../scripts/stage-frontend.mjs", + "predev": "npm run stage", "dev": "vite", + "prebuild": "npm run stage", "build": "vite build", "preview": "vite preview", "test": "vitest run", diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index ba99a3e..8bcb0a9 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -6,14 +6,27 @@ import { setupComplete, setupSkipped, isSetupLoaded, refreshSetupState } from '. import { loadEnabledPlugins, isPluginEnabled } from '../composables/enabledPlugins' import { useToast } from '../composables/toast' import { getFacilityName } from '../utils/siteSettings' +import { stagedChildren, stagedToplevel } from './routes.gen' // Auto-discover all route modules from routes/ directory. A module's default // export is AppLayout child routes; an optional `toplevel` export is full-screen // routes (kiosk, print pages) that live OUTSIDE AppLayout. Both let a plugin own // its routes, so pruning the plugin removes them with no core edit (ADR-013). const routeModules = import.meta.glob('./routes/*.js', { eager: true }) -const rawChildren = Object.values(routeModules).flatMap(m => m.default) -const pluginTopLevel = Object.values(routeModules).flatMap(m => m.toplevel || []) +// Staged plugin frontends (relocated to plugins//frontend/) are +// aggregated into routes.gen.js by scripts/stage-frontend.mjs. In-tree route +// files (./routes/*.js) cover plugins not yet relocated - dual-location during +// the Phase 4 transition. +const rawChildren = [ + // A route file may export only `toplevel` (no AppLayout children), so guard + // the default with an empty array. + ...Object.values(routeModules).flatMap(m => m.default || []), + ...stagedChildren, +] +const pluginTopLevel = [ + ...Object.values(routeModules).flatMap(m => m.toplevel || []), + ...stagedToplevel, +] // Gather the settings pages (spread across plugin route files) and nest them // under a single two-pane shell so the grouped rail stays put while the right diff --git a/frontend/src/router/routes/printedparts.js b/frontend/src/router/routes/printedparts.js deleted file mode 100644 index 91f6066..0000000 --- a/frontend/src/router/routes/printedparts.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Printedparts plugin routes. - * - * Auto-discovered by the router via import.meta.glob, so no registration - * edit is needed. Every route carries meta.plugin 'printedparts' so the ADR-009 - * guard redirects to the dashboard when the printedparts backend plugin is - * disabled. Form routes add requiresAuth so anonymous users cannot reach - * create or edit. - */ -export default [ - { - path: 'printedparts', - name: 'printedparts', - component: () => import('../../views/printedparts/PrintedItemsList.vue'), - meta: { requiresAuth: true, plugin: 'printedparts' } - }, - { - path: 'printedparts/new', - name: 'printedparts-new', - component: () => import('../../views/printedparts/PrintedItemForm.vue'), - meta: { requiresAuth: true, plugin: 'printedparts' } - }, - { - path: 'printedparts/:id', - name: 'printedparts-detail', - component: () => import('../../views/printedparts/PrintedItemDetail.vue'), - meta: { requiresAuth: true, plugin: 'printedparts' } - }, - { - path: 'printedparts/:id/edit', - name: 'printedparts-edit', - component: () => import('../../views/printedparts/PrintedItemForm.vue'), - meta: { requiresAuth: true, plugin: 'printedparts' } - }, - { - path: 'settings/printedparts', - name: 'settings-printedparts', - component: () => import('../../views/settings/PrintedPartsSettings.vue'), - meta: { requiresAuth: true, requiresAdmin: true, plugin: 'printedparts' } - } -] - -// Full-screen routes (outside AppLayout), collected by the router's `toplevel` -// pass so pruning printedparts removes them without a core edit (ADR-013). -export const toplevel = [ - { - // Touch kiosk for taking 3D-printed parts: scan bin, scan badge, keypad. - // Open on purpose - see the decision record in the printedparts proposal. - path: '/parts-kiosk', - name: 'parts-kiosk', - component: () => import('../../views/printedparts/PartsKiosk.vue'), - meta: { plugin: 'printedparts' } - }, - { - // Requires login: lists the whole catalog, printedparts.view-gated at the API. - path: '/print/printedparts-labels', - name: 'print-printedparts-labels', - component: () => import('../../views/print/PrintedPartsLabels.vue'), - meta: { requiresAuth: true, plugin: 'printedparts' } - } -] diff --git a/plugins/printedparts/frontend/routes.js b/plugins/printedparts/frontend/routes.js new file mode 100644 index 0000000..46c11b3 --- /dev/null +++ b/plugins/printedparts/frontend/routes.js @@ -0,0 +1,58 @@ +/** + * Printedparts plugin frontend routes (ADR-013 Phase 4: self-contained plugin + * frontend). Views live beside this file under views/; core imports use the + * @/ alias. The stage-frontend step copies this whole frontend/ dir into the + * Vite tree and aggregates these routes, so a per-site build that omits + * printedparts carries none of this code. + * + * `default` = AppLayout child routes; `toplevel` = full-screen routes. + */ +export default [ + { + path: 'printedparts', + name: 'printedparts', + component: () => import('./views/PrintedItemsList.vue'), + meta: { requiresAuth: true, plugin: 'printedparts' } + }, + { + path: 'printedparts/new', + name: 'printedparts-new', + component: () => import('./views/PrintedItemForm.vue'), + meta: { requiresAuth: true, plugin: 'printedparts' } + }, + { + path: 'printedparts/:id', + name: 'printedparts-detail', + component: () => import('./views/PrintedItemDetail.vue'), + meta: { requiresAuth: true, plugin: 'printedparts' } + }, + { + path: 'printedparts/:id/edit', + name: 'printedparts-edit', + component: () => import('./views/PrintedItemForm.vue'), + meta: { requiresAuth: true, plugin: 'printedparts' } + }, + { + path: 'settings/printedparts', + name: 'settings-printedparts', + component: () => import('./views/PrintedPartsSettings.vue'), + meta: { requiresAuth: true, requiresAdmin: true, plugin: 'printedparts' } + } +] + +export const toplevel = [ + { + // Touch kiosk for taking 3D-printed parts: scan bin, scan badge, keypad. + path: '/parts-kiosk', + name: 'parts-kiosk', + component: () => import('./views/PartsKiosk.vue'), + meta: { plugin: 'printedparts' } + }, + { + // Requires login: lists the whole catalog, printedparts.view-gated at the API. + path: '/print/printedparts-labels', + name: 'print-printedparts-labels', + component: () => import('./views/PrintedPartsLabels.vue'), + meta: { requiresAuth: true, plugin: 'printedparts' } + } +] diff --git a/frontend/src/views/printedparts/PartsKiosk.vue b/plugins/printedparts/frontend/views/PartsKiosk.vue similarity index 98% rename from frontend/src/views/printedparts/PartsKiosk.vue rename to plugins/printedparts/frontend/views/PartsKiosk.vue index 543d547..dcf49b3 100644 --- a/frontend/src/views/printedparts/PartsKiosk.vue +++ b/plugins/printedparts/frontend/views/PartsKiosk.vue @@ -91,9 +91,9 @@