ADR-013 Phase 4: relocate 4 self-contained plugin frontends

Relocate applications, geenforce, knowledgebase, and machines - each owns only
its own views dir, so a clean move to plugins/<name>/frontend/ (views/ +
routes.js, core imports rewritten to @/). geenforce's entryForm.js helper + its
vitest spec move with it (ManifestEditor imports it as a sibling).

Machinery fixes this batch surfaced:
- routes.gen.js codegen uses namespace imports (import * as p_x). A route file
  without a `toplevel` export is undefined on the namespace instead of a strict-
  ESM missing-binding build error.
- vitest gains a `pretest` stage so plugin-frontend specs (now under
  plugins/<name>/frontend/) run from their staged copy in src/.plugins-staged/.

Verified live: GE-Enforce (the most complex, uses the entryForm sibling helper)
renders fully from its staged frontend. Build + 58 vitest + naming green.
This commit is contained in:
cproudlock
2026-07-18 23:51:23 -04:00
parent af9a3b190b
commit 23dc9fa379
21 changed files with 52 additions and 49 deletions

View File

@@ -10,6 +10,7 @@
"prebuild": "npm run stage", "prebuild": "npm run stage",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"pretest": "npm run stage",
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest" "test:watch": "vitest"
}, },

View File

@@ -14,6 +14,8 @@ export default defineConfig({
test: { test: {
environment: 'jsdom', environment: 'jsdom',
globals: true, globals: true,
// Core specs plus plugin-frontend specs, which the pretest stage copies
// into src/.plugins-staged/ (under root, so this glob finds them).
include: ['src/**/*.spec.js'], include: ['src/**/*.spec.js'],
}, },
}) })

View File

@@ -5,23 +5,23 @@ export default [
{ {
path: 'applications', path: 'applications',
name: 'applications', name: 'applications',
component: () => import('../../views/applications/ApplicationsList.vue') component: () => import('./views/ApplicationsList.vue')
}, },
{ {
path: 'applications/new', path: 'applications/new',
name: 'application-new', name: 'application-new',
component: () => import('../../views/applications/ApplicationForm.vue'), component: () => import('./views/ApplicationForm.vue'),
meta: { requiresAuth: true } meta: { requiresAuth: true }
}, },
{ {
path: 'applications/:id', path: 'applications/:id',
name: 'application-detail', name: 'application-detail',
component: () => import('../../views/applications/ApplicationDetail.vue') component: () => import('./views/ApplicationDetail.vue')
}, },
{ {
path: 'applications/:id/edit', path: 'applications/:id/edit',
name: 'application-edit', name: 'application-edit',
component: () => import('../../views/applications/ApplicationForm.vue'), component: () => import('./views/ApplicationForm.vue'),
meta: { requiresAuth: true } meta: { requiresAuth: true }
} }
] ]

View File

@@ -156,8 +156,8 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { applicationsApi } from '../../api' import { applicationsApi } from '@/api'
import { getContactEmailDomain } from '../../utils/siteSettings' import { getContactEmailDomain } from '@/utils/siteSettings'
const route = useRoute() const route = useRoute()

View File

@@ -154,8 +154,8 @@
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { applicationsApi, supportteamsApi } from '../../api' import { applicationsApi, supportteamsApi } from '@/api'
import { apiError } from '../../utils/apiError' import { apiError } from '@/utils/apiError'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()

View File

@@ -99,8 +99,8 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { applicationsApi } from '../../api' import { applicationsApi } from '@/api'
import PaginationBar from '../../components/PaginationBar.vue' import PaginationBar from '@/components/PaginationBar.vue'
import { useListQuery } from '@/composables/listQuery' import { useListQuery } from '@/composables/listQuery'
const applications = ref([]) const applications = ref([])

View File

@@ -9,20 +9,20 @@
export default [ export default [
{ {
path: 'geenforce', path: 'geenforce',
component: () => import('../../views/geenforce/GeEnforceLayout.vue'), component: () => import('./views/GeEnforceLayout.vue'),
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'geenforce' }, meta: { requiresAuth: true, requiresAdmin: true, plugin: 'geenforce' },
children: [ children: [
{ path: '', redirect: '/geenforce/manifests' }, { path: '', redirect: '/geenforce/manifests' },
{ {
path: 'manifests', path: 'manifests',
name: 'geenforce-manifests', name: 'geenforce-manifests',
component: () => import('../../views/geenforce/ManifestEditor.vue'), component: () => import('./views/ManifestEditor.vue'),
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'geenforce' } meta: { requiresAuth: true, requiresAdmin: true, plugin: 'geenforce' }
}, },
{ {
path: 'reports', path: 'reports',
name: 'geenforce-reports', name: 'geenforce-reports',
component: () => import('../../views/geenforce/EnforcementReports.vue'), component: () => import('./views/EnforcementReports.vue'),
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'geenforce' } meta: { requiresAuth: true, requiresAdmin: true, plugin: 'geenforce' }
} }
] ]

View File

@@ -104,7 +104,7 @@
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import api from '../../api' import api from '@/api'
const reports = ref([]) const reports = ref([])
const detail = ref(null) const detail = ref(null)

View File

@@ -570,7 +570,7 @@
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import api from '../../api' import api from '@/api'
// Pure entry-form logic lives in entryForm.js (unit-tested by entryForm.spec.js); // Pure entry-form logic lives in entryForm.js (unit-tested by entryForm.spec.js);
// this component wires it to reactive state so the tests cover the shipped code. // this component wires it to reactive state so the tests cover the shipped code.
import { import {

View File

@@ -5,25 +5,25 @@ export default [
{ {
path: 'knowledgebase', path: 'knowledgebase',
name: 'knowledgebase', name: 'knowledgebase',
component: () => import('../../views/knowledgebase/KnowledgeBaseList.vue'), component: () => import('./views/KnowledgeBaseList.vue'),
meta: { plugin: 'knowledgebase' } meta: { plugin: 'knowledgebase' }
}, },
{ {
path: 'knowledgebase/new', path: 'knowledgebase/new',
name: 'knowledgebase-new', name: 'knowledgebase-new',
component: () => import('../../views/knowledgebase/KnowledgeBaseForm.vue'), component: () => import('./views/KnowledgeBaseForm.vue'),
meta: { requiresAuth: true, plugin: 'knowledgebase' } meta: { requiresAuth: true, plugin: 'knowledgebase' }
}, },
{ {
path: 'knowledgebase/:id', path: 'knowledgebase/:id',
name: 'knowledgebase-detail', name: 'knowledgebase-detail',
component: () => import('../../views/knowledgebase/KnowledgeBaseDetail.vue'), component: () => import('./views/KnowledgeBaseDetail.vue'),
meta: { plugin: 'knowledgebase' } meta: { plugin: 'knowledgebase' }
}, },
{ {
path: 'knowledgebase/:id/edit', path: 'knowledgebase/:id/edit',
name: 'knowledgebase-edit', name: 'knowledgebase-edit',
component: () => import('../../views/knowledgebase/KnowledgeBaseForm.vue'), component: () => import('./views/KnowledgeBaseForm.vue'),
meta: { requiresAuth: true, plugin: 'knowledgebase' } meta: { requiresAuth: true, plugin: 'knowledgebase' }
} }
] ]

View File

@@ -79,7 +79,7 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { knowledgebaseApi } from '../../api' import { knowledgebaseApi } from '@/api'
const route = useRoute() const route = useRoute()

View File

@@ -82,8 +82,8 @@
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { knowledgebaseApi, applicationsApi } from '../../api' import { knowledgebaseApi, applicationsApi } from '@/api'
import { apiError } from '../../utils/apiError' import { apiError } from '@/utils/apiError'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()

View File

@@ -109,8 +109,8 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { knowledgebaseApi, applicationsApi } from '../../api' import { knowledgebaseApi, applicationsApi } from '@/api'
import PaginationBar from '../../components/PaginationBar.vue' import PaginationBar from '@/components/PaginationBar.vue'
import { useListQuery } from '@/composables/listQuery' import { useListQuery } from '@/composables/listQuery'
const loading = ref(true) const loading = ref(true)

View File

@@ -5,25 +5,25 @@ export default [
{ {
path: 'machines', path: 'machines',
name: 'machines', name: 'machines',
component: () => import('../../views/machines/MachinesList.vue'), component: () => import('./views/MachinesList.vue'),
meta: { plugin: 'machines' } meta: { plugin: 'machines' }
}, },
{ {
path: 'machines/new', path: 'machines/new',
name: 'machine-new', name: 'machine-new',
component: () => import('../../views/machines/MachineForm.vue'), component: () => import('./views/MachineForm.vue'),
meta: { requiresAuth: true, plugin: 'machines' } meta: { requiresAuth: true, plugin: 'machines' }
}, },
{ {
path: 'machines/:id', path: 'machines/:id',
name: 'machine-detail', name: 'machine-detail',
component: () => import('../../views/machines/MachineDetail.vue'), component: () => import('./views/MachineDetail.vue'),
meta: { plugin: 'machines' } meta: { plugin: 'machines' }
}, },
{ {
path: 'machines/:id/edit', path: 'machines/:id/edit',
name: 'machine-edit', name: 'machine-edit',
component: () => import('../../views/machines/MachineForm.vue'), component: () => import('./views/MachineForm.vue'),
meta: { requiresAuth: true, plugin: 'machines' } meta: { requiresAuth: true, plugin: 'machines' }
} }
] ]

View File

@@ -234,13 +234,13 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { machinesApi } from '../../api' import { machinesApi } from '@/api'
import LocationMapTooltip from '../../components/LocationMapTooltip.vue' import LocationMapTooltip from '@/components/LocationMapTooltip.vue'
import CustomFieldsSection from '../../components/CustomFieldsSection.vue' import CustomFieldsSection from '@/components/CustomFieldsSection.vue'
import PluginAssetPanels from '../../components/PluginAssetPanels.vue' import PluginAssetPanels from '@/components/PluginAssetPanels.vue'
import AssetRelationships from '../../components/AssetRelationships.vue' import AssetRelationships from '@/components/AssetRelationships.vue'
import { useWarrantyBadge } from '../../composables/warrantyBadge' import { useWarrantyBadge } from '@/composables/warrantyBadge'
import { useIdentifierFlags } from '../../composables/identifierSettings' import { useIdentifierFlags } from '@/composables/identifierSettings'
const route = useRoute() const route = useRoute()
const { isEnabled } = useIdentifierFlags() const { isEnabled } = useIdentifierFlags()

View File

@@ -347,13 +347,13 @@
<script setup> <script setup>
import { ref, onMounted, computed, watch } from 'vue' import { ref, onMounted, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { machinesApi, vendorsApi, locationsApi, modelsApi, businessunitsApi, computersApi, assetsApi, relationshipTypesApi } from '../../api' import { machinesApi, vendorsApi, locationsApi, modelsApi, businessunitsApi, computersApi, assetsApi, relationshipTypesApi } from '@/api'
import ShopFloorMap from '../../components/ShopFloorMap.vue' import ShopFloorMap from '@/components/ShopFloorMap.vue'
import Modal from '../../components/Modal.vue' import Modal from '@/components/Modal.vue'
import CustomFieldsInputs from '../../components/CustomFieldsInputs.vue' import CustomFieldsInputs from '@/components/CustomFieldsInputs.vue'
import { currentTheme } from '../../stores/theme' import { currentTheme } from '@/stores/theme'
import { useIdentifierFlags } from '../../composables/identifierSettings' import { useIdentifierFlags } from '@/composables/identifierSettings'
import { apiError } from '../../utils/apiError' import { apiError } from '@/utils/apiError'
const { isEnabled } = useIdentifierFlags() const { isEnabled } = useIdentifierFlags()

View File

@@ -85,8 +85,8 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { colorStyle } from "@/utils/colorStyle" import { colorStyle } from "@/utils/colorStyle"
import { machinesApi } from '../../api' import { machinesApi } from '@/api'
import PaginationBar from '../../components/PaginationBar.vue' import PaginationBar from '@/components/PaginationBar.vue'
import { useListQuery } from '@/composables/listQuery' import { useListQuery } from '@/composables/listQuery'
const machines = ref([]) const machines = ref([])

View File

@@ -45,11 +45,11 @@ const childParts = []
const topParts = [] const topParts = []
for (const name of chosen) { for (const name of chosen) {
const id = 'p_' + name.replace(/[^a-zA-Z0-9]/g, '_') const id = 'p_' + name.replace(/[^a-zA-Z0-9]/g, '_')
lines.push( // Namespace import so a route file without a `toplevel` export is not a
`import ${id}Default, { toplevel as ${id}Top } from ` // (strict ESM) build error - the binding is just undefined on the namespace.
+ `'../.plugins-staged/${name}/routes.js'`) lines.push(`import * as ${id} from '../.plugins-staged/${name}/routes.js'`)
childParts.push(`...(${id}Default || [])`) childParts.push(`...(${id}.default || [])`)
topParts.push(`...(${id}Top || [])`) topParts.push(`...(${id}.toplevel || [])`)
} }
lines.push('') lines.push('')
lines.push(`export const stagedChildren = [${childParts.join(', ')}]`) lines.push(`export const stagedChildren = [${childParts.join(', ')}]`)