@biffo/cli 0.314.2 → 0.315.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_skeletons/plugin-template/.github/workflows/ci.yml +62 -2
- package/_skeletons/plugin-template/web/_gitignore +5 -0
- package/_skeletons/plugin-template/web/eslint.config.js +8 -0
- package/_skeletons/plugin-template/web/index.html +12 -0
- package/_skeletons/plugin-template/web/package.json +40 -0
- package/_skeletons/plugin-template/web/pnpm-lock.yaml +2756 -0
- package/_skeletons/plugin-template/web/src/App.test.tsx +17 -0
- package/_skeletons/plugin-template/web/src/App.tsx +23 -0
- package/_skeletons/plugin-template/web/src/base-path.test.ts +70 -0
- package/_skeletons/plugin-template/web/src/index.css +33 -0
- package/_skeletons/plugin-template/web/src/main.tsx +11 -0
- package/_skeletons/plugin-template/web/src/test-setup.ts +1 -0
- package/_skeletons/plugin-template/web/tsconfig.json +21 -0
- package/_skeletons/plugin-template/web/vite.config.ts +26 -0
- package/_skeletons/plugin-template/web-admin/src/base-path.test.ts +20 -8
- package/package.json +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import App from './App'
|
|
5
|
+
|
|
6
|
+
describe('App', () => {
|
|
7
|
+
it('renders text naming the plugin', () => {
|
|
8
|
+
render(<App />)
|
|
9
|
+
|
|
10
|
+
// A blank shell that merely mounts proves nothing — this asserts the
|
|
11
|
+
// actual plugin name is on the page. `biffo plugin create` rewrites
|
|
12
|
+
// `example-plugin` to the real slug everywhere (see
|
|
13
|
+
// ../.scaffold-tokens.json two directories up), including this literal,
|
|
14
|
+
// so the assertion stays true for whatever plugin gets scaffolded.
|
|
15
|
+
expect(screen.getByRole('heading', { name: 'example-plugin' })).toBeInTheDocument()
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Starter founder-facing surface (`user_frontend`, ADR-0021 §2). Served
|
|
3
|
+
* UNAUTHENTICATED by the shared plugin host at
|
|
4
|
+
* `/api/v1/plugins/example-plugin/ui/` — unlike web-admin's `App`, there is no
|
|
5
|
+
* session to read here: `required_group` on the manifest's `user_frontend`
|
|
6
|
+
* block does not gate this shell (see docs/guides/plugins.md), so this screen
|
|
7
|
+
* must not assume a signed-in caller.
|
|
8
|
+
*
|
|
9
|
+
* It exists to prove the `user_frontend` bundle actually builds and renders
|
|
10
|
+
* something that names the plugin — not to be a feature. Replace it with this
|
|
11
|
+
* plugin's own founder-facing UI.
|
|
12
|
+
*/
|
|
13
|
+
export default function App() {
|
|
14
|
+
return (
|
|
15
|
+
<main className="page">
|
|
16
|
+
<h1>example-plugin</h1>
|
|
17
|
+
<p className="muted">
|
|
18
|
+
This is the founder-facing starter screen for the example-plugin plugin. Replace it with
|
|
19
|
+
this plugin's own UI.
|
|
20
|
+
</p>
|
|
21
|
+
</main>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { describe, expect, it } from 'vitest'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The Vite `base` must name THIS plugin, and the built HTML's asset
|
|
8
|
+
* references must actually live under it.
|
|
9
|
+
*
|
|
10
|
+
* Ported from web-admin/src/base-path.test.ts, itself ported from
|
|
11
|
+
* idea-scout's guard (biffo-template#1492): a vite.config.ts pasted from a
|
|
12
|
+
* sibling and left pointing at the sibling's base — 503, blank page, and
|
|
13
|
+
* every other local gate (eslint, tsc, unit tests, `vite build` itself)
|
|
14
|
+
* passed, because `base` only affects the URLs inside the emitted HTML. This
|
|
15
|
+
* copy exists because `user_frontend` (`/ui/`) is a separate mount from
|
|
16
|
+
* `admin_ingress` (`/admin/`) with its own base string to get wrong.
|
|
17
|
+
*
|
|
18
|
+
* The expected plugin name comes from `biffo.plugin.json`'s own `name`
|
|
19
|
+
* field — an independent ground truth scaffolded alongside vite.config.ts —
|
|
20
|
+
* NOT from parsing it back out of the config under test. An earlier version
|
|
21
|
+
* of this file did the latter and was proven tautological
|
|
22
|
+
* (biffo-template#2024): it regex-extracted the "expected" name from the same
|
|
23
|
+
* base string it then checked, so a base that was internally self-consistent
|
|
24
|
+
* but named the WRONG plugin — exactly the idea-scout#1492
|
|
25
|
+
* paste-from-a-sibling shape — still passed every assertion. `biffo.plugin.json`'s
|
|
26
|
+
* `name` is set once, at scaffold time, by `biffo plugin create`'s token
|
|
27
|
+
* substitution — the same operation that rewrites vite.config.ts's own
|
|
28
|
+
* `example-plugin` token — so it cannot be derived from a wrong
|
|
29
|
+
* vite.config.ts and cannot be fooled by a bad paste from it.
|
|
30
|
+
*/
|
|
31
|
+
const ROOT = join(__dirname, '..')
|
|
32
|
+
|
|
33
|
+
function expectedPluginName(): string {
|
|
34
|
+
const manifest = JSON.parse(readFileSync(join(ROOT, '..', 'biffo.plugin.json'), 'utf8')) as {
|
|
35
|
+
name?: string
|
|
36
|
+
}
|
|
37
|
+
expect(manifest.name, 'biffo.plugin.json has no top-level `name`').toBeTruthy()
|
|
38
|
+
return manifest.name!
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('vite base path', () => {
|
|
42
|
+
const plugin = expectedPluginName()
|
|
43
|
+
const config = readFileSync(join(ROOT, 'vite.config.ts'), 'utf8')
|
|
44
|
+
|
|
45
|
+
it('is the full API Gateway path for THIS plugin', () => {
|
|
46
|
+
expect(config).toContain(`base: '/api/v1/plugins/${plugin}/ui/'`)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Deliberately no "the config mentions no other plugin" test — see
|
|
50
|
+
// web-admin/src/base-path.test.ts's own comment on why that shape is wrong.
|
|
51
|
+
|
|
52
|
+
it('the built index.html requests assets under that base', () => {
|
|
53
|
+
// Skipped when dist/ is absent (a source checkout, not a built one). CI
|
|
54
|
+
// runs `build` before `test` — but if it ever does not, this must not
|
|
55
|
+
// pass silently, so the skip is explicit and visible.
|
|
56
|
+
let html: string
|
|
57
|
+
try {
|
|
58
|
+
html = readFileSync(join(ROOT, 'dist', 'index.html'), 'utf8')
|
|
59
|
+
} catch {
|
|
60
|
+
console.warn('dist/index.html absent — build not run; base-path check skipped')
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
const srcs = [...html.matchAll(/(?:src|href)="([^"]+)"/g)].map((m) => m[1])
|
|
64
|
+
const assetRefs = srcs.filter((s) => s.includes('/assets/'))
|
|
65
|
+
expect(assetRefs.length, 'no asset references in the built HTML').toBeGreaterThan(0)
|
|
66
|
+
for (const ref of assetRefs) {
|
|
67
|
+
expect(ref.startsWith(`/api/v1/plugins/${plugin}/ui/`), `bad asset path: ${ref}`).toBe(true)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* Shares the design tokens the rest of the estate uses so a plugin's
|
|
2
|
+
founder-facing shell does not become its own brand blue (the drift
|
|
3
|
+
@biffo/design-tokens was created to end) — same reasoning as
|
|
4
|
+
web-admin/src/index.css. */
|
|
5
|
+
@import '@biffo/design-tokens/tokens.css';
|
|
6
|
+
|
|
7
|
+
* {
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
body {
|
|
12
|
+
margin: 0;
|
|
13
|
+
font-family: var(--font-sans, system-ui, sans-serif);
|
|
14
|
+
color: var(--text, #1b1c1f);
|
|
15
|
+
background: var(--bg, #f7f8fa);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.page {
|
|
19
|
+
max-width: 60rem;
|
|
20
|
+
margin: 0 auto;
|
|
21
|
+
padding: 2rem 1.5rem 4rem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
h1 {
|
|
25
|
+
font-size: 1.55rem;
|
|
26
|
+
margin: 0 0 0.35rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.muted {
|
|
30
|
+
color: var(--text-muted, #5b6070);
|
|
31
|
+
font-size: 0.9rem;
|
|
32
|
+
margin: 0 0 1.25rem;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"jsx": "react-jsx",
|
|
14
|
+
"strict": true,
|
|
15
|
+
"noUnusedLocals": true,
|
|
16
|
+
"noUnusedParameters": true,
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
|
+
"types": ["vitest/globals", "@testing-library/jest-dom"]
|
|
19
|
+
},
|
|
20
|
+
"include": ["src", "vite.config.ts"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
|
|
4
|
+
// Served by the shared plugin host at /api/v1/plugins/example-plugin/ui/*
|
|
5
|
+
// (docs/guides/plugins.md's "User-facing frontend" section, ADR-0021 §2) —
|
|
6
|
+
// UNAUTHENTICATED, unlike web-admin's sibling: a plain browser navigation can
|
|
7
|
+
// never attach a bearer token, so this shell is served with no group_gate at
|
|
8
|
+
// all (services/_plugin-host/src/plugin_host/mount.py). Every asset/link URL
|
|
9
|
+
// must carry the full prefix, INCLUDING THIS PLUGIN'S OWN NAME. `biffo plugin
|
|
10
|
+
// create` rewrites `example-plugin` to the real slug (see
|
|
11
|
+
// .scaffold-tokens.json); do not hand-edit this after scaffolding without
|
|
12
|
+
// updating BOTH this file and base-path.test.ts.
|
|
13
|
+
//
|
|
14
|
+
// Mirrors web-admin/vite.config.ts's own comment almost verbatim, because the
|
|
15
|
+
// trap it describes is identical here: idea-scout's copy of THAT file was
|
|
16
|
+
// pasted from ideation's and kept ideation's base, and no local gate caught
|
|
17
|
+
// it — lint, typecheck, unit tests and the production build all passed,
|
|
18
|
+
// because `base` only affects the URLs inside the emitted HTML. It was only
|
|
19
|
+
// visible by loading the page and reading the network log. Hence the full
|
|
20
|
+
// path, and hence base-path.test.ts asserting it stays correct.
|
|
21
|
+
export default defineConfig({
|
|
22
|
+
base: '/api/v1/plugins/example-plugin/ui/',
|
|
23
|
+
plugins: [react()],
|
|
24
|
+
build: { outDir: 'dist' },
|
|
25
|
+
test: { environment: 'jsdom', globals: true, setupFiles: ['./src/test-setup.ts'] },
|
|
26
|
+
})
|
|
@@ -16,21 +16,33 @@ import { describe, expect, it } from 'vitest'
|
|
|
16
16
|
* check, generalised so a plugin scaffolded from this skeleton inherits it
|
|
17
17
|
* automatically rather than every plugin author re-discovering the bug.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* The expected plugin name comes from `biffo.plugin.json`'s own `name`
|
|
20
|
+
* field — an independent ground truth scaffolded alongside vite.config.ts —
|
|
21
|
+
* NOT from parsing it back out of the config under test. An earlier version
|
|
22
|
+
* of this file did the latter and was proven tautological
|
|
23
|
+
* (biffo-template#2024): it regex-extracted the "expected" name from the same
|
|
24
|
+
* base string it then checked, so a base that was internally self-consistent
|
|
25
|
+
* but named the WRONG plugin — exactly the idea-scout#1492
|
|
26
|
+
* paste-from-a-sibling shape this file is named after — still passed every
|
|
27
|
+
* assertion. `biffo.plugin.json`'s `name` is set once, at scaffold time, by
|
|
28
|
+
* `biffo plugin create`'s token substitution — the same operation that
|
|
29
|
+
* rewrites vite.config.ts's own `example-plugin` token — so it cannot be
|
|
30
|
+
* derived from a wrong vite.config.ts and cannot be fooled by a bad paste
|
|
31
|
+
* from it.
|
|
22
32
|
*/
|
|
23
33
|
const ROOT = join(__dirname, '..')
|
|
24
34
|
|
|
25
|
-
function
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
function expectedPluginName(): string {
|
|
36
|
+
const manifest = JSON.parse(readFileSync(join(ROOT, '..', 'biffo.plugin.json'), 'utf8')) as {
|
|
37
|
+
name?: string
|
|
38
|
+
}
|
|
39
|
+
expect(manifest.name, 'biffo.plugin.json has no top-level `name`').toBeTruthy()
|
|
40
|
+
return manifest.name!
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
describe('vite base path', () => {
|
|
44
|
+
const plugin = expectedPluginName()
|
|
32
45
|
const config = readFileSync(join(ROOT, 'vite.config.ts'), 'utf8')
|
|
33
|
-
const plugin = pluginFromConfig(config)
|
|
34
46
|
|
|
35
47
|
it('is the full API Gateway path for THIS plugin', () => {
|
|
36
48
|
expect(config).toContain(`base: '/api/v1/plugins/${plugin}/admin/'`)
|