@biffo/cli 0.314.3 → 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.
@@ -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&apos;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,11 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+
4
+ import App from './App'
5
+ import './index.css'
6
+
7
+ createRoot(document.getElementById('root')!).render(
8
+ <StrictMode>
9
+ <App />
10
+ </StrictMode>,
11
+ )
@@ -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
- * Reads the plugin name OUT of vite.config.ts itself rather than hardcoding
20
- * it, so this file needs no token substitution at scaffold time and stays
21
- * correct whatever `biffo plugin create` rewrites `base` to.
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 pluginFromConfig(config: string): string {
26
- const match = config.match(/base:\s*'\/api\/v1\/plugins\/([^/]+)\/admin\/'/)
27
- expect(match, "no `base: '/api/v1/plugins/<name>/admin/'` found in vite.config.ts").not.toBeNull()
28
- return match![1]
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/'`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.314.3",
3
+ "version": "0.315.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",