Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ The `pnpm test` script intentionally runs `build` first so `tsnapi` snapshots co

Ahead-of-time build artifacts that live under `src/` - the shadow-root stylesheets in `packages/hub-ui/src/client/.generated/` and `packages/json-render-ui/src/.generated/` - are **generated, not committed** (`.generated` is gitignored). Each owning package builds its own with `pnpm run build:css`, and three things guarantee the file is on disk before anything imports it: the root `postinstall` runs `turbo run build:css`, the Turbo `typecheck` task depends on both `build:css` tasks, and each package's `build` script chains `build:css` first. A new generated-under-`src` artifact follows the same shape - its own build script, declared `outputs` in `turbo.json`, and a `typecheck` dependency - rather than being checked in, since a minified single-line blob conflicts on every concurrent edit.

**`starter/`** is the top-level, self-contained template for creating a new devframe integration (Vanilla TS, Vite client, playgrounds, tests). It uses real versions in its `package.json` (no catalogs, no `workspace:*`) so it's copy-paste ready for users. Pnpm links its `devframe`/`@devframes/*` dependencies to the local workspace copies during development. When `bumpp -r` bumps the repo versions, `bumpp.config.ts` runs `scripts/sync-starter-version.ts` to update the starter's dependencies to match.

`pnpm knip` finds unused files, dependencies, and exports across every workspace (config in `knip.jsonc`). It runs against source directly - no prior build needed. Most workspaces need no configuration; `knip.jsonc` only carries per-workspace overrides for cases knip's defaults can't infer on their own: a package's non-`index.ts` `exports` subpaths (knip's package.json→`dist`→`src` source mapping needs a workspace `tsconfig.json` `outDir`, which conflicts with this repo's cross-workspace `src/*.ts` imports, so multi-entry packages list their `exports`-mapped entry files explicitly instead - keep that list in sync with each `tsdown.config.ts`), config files knip's plugins don't discover in a nested location (a Next.js app rooted below the workspace root, `storybook-solidjs-vite` not matching the Storybook plugin trigger), and dependencies referenced dynamically outside its static import graph (icon collections consumed by UnoCSS at build time, plugin packages loaded via a runtime `import()` string). Prefer fixing the underlying gap or a scoped `ignoreDependencies`/`entry` override over a blanket `ignore`.

## Conventions
Expand Down
10 changes: 10 additions & 0 deletions bump.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from 'bumpp'
import { syncStarterVersion } from './scripts/sync-starter-version.ts'

export default defineConfig({
// `starter/` pins real `devframe`/`@devframes/*` versions (it's a
// copy-paste-ready template, not a workspace member consuming
// `catalog:`/`workspace:*`), so `bumpp -r` can't reach it on its own -
// sync it here, before the version-bump commit is made.
execute: operation => syncStarterVersion(operation.state.newVersion),
})
124 changes: 124 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ minimumReleaseAgeExclude:
- tsdown@0.22.14
- verkit@0.3.0
- structured-clone-es@2.0.1
- '@devframes/hub-ui@0.9.4'
- '@devframes/hub@0.9.4'
- '@devframes/json-render@0.9.4'
- '@devframes/vite@0.9.4'
shamefullyHoist: true
shellEmulator: true
strictPeerDependencies: false
Expand All @@ -28,6 +32,7 @@ packages:
- plugins/*/assets-pkg
- services/*
- examples/*
- starter
- storybook
- docs

Expand Down
36 changes: 36 additions & 0 deletions scripts/sync-starter-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'

const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const starterPkgPath = path.resolve(rootDir, 'starter/package.json')

/**
* Rewrites `starter/package.json`'s `devframe`/`@devframes/*` dependency
* ranges to `^<version>` — the starter is a self-contained, copy-paste-ready
* template that pins real versions rather than `catalog:`/`workspace:*`, so
* a repo-wide bump has to touch it explicitly. Called from `bump.config.ts`'s
* `execute` hook so `bumpp -r` keeps it in lockstep automatically.
*/
export async function syncStarterVersion(version: string): Promise<void> {
const raw = await readFile(starterPkgPath, 'utf-8')
const pkg = JSON.parse(raw)

for (const name of Object.keys(pkg.dependencies ?? {})) {
if (name === 'devframe' || name.startsWith('@devframes/'))
pkg.dependencies[name] = `^${version}`
}

await writeFile(starterPkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
}

// Allow standalone invocation: `tsx scripts/sync-starter-version.ts <version>`.
if (import.meta.url === `file://${process.argv[1]}`) {
const version = process.argv[2]
if (!version) {
console.error('Usage: tsx scripts/sync-starter-version.ts <version>')
process.exit(1)
}
await syncStarterVersion(version)
}
2 changes: 1 addition & 1 deletion scripts/verify-typecheck-coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '..')
/**
* Workspace globs, mirrored from `pnpm-workspace.yaml`'s `packages:` list.
*/
const WORKSPACE_PATTERNS = ['packages/*', 'plugins/*', 'examples/*', 'storybook', 'docs']
const WORKSPACE_PATTERNS = ['packages/*', 'plugins/*', 'examples/*', 'starter', 'storybook', 'docs']

/**
* Packages with a `tsconfig.json` that intentionally don't have a
Expand Down
9 changes: 9 additions & 0 deletions starter/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.DS_Store
.turbo
*.log
coverage
dist
node_modules
playwright-report
test-results
temp
38 changes: 38 additions & 0 deletions starter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# devframe-starter

Self-contained starter for a single [devframe](https://github.com/devframes/devframe) integration: a vanilla-TS Vite client, a CLI dev/build/MCP shell, single + hub playgrounds, and unit + e2e tests. Copy this folder out of the monorepo as the seed for a new devframe - it pins real dependency versions (no `catalog:`/`workspace:*`) so it installs standalone.

## Run

```sh
pnpm install
pnpm run build # build the vanilla-TS SPA into dist/client
pnpm run dev # CLI dev server - http://localhost:7391/__devframe-starter/
pnpm run cli:build # static deploy in ./dist/static
pnpm run play:single # Vite playground: SPA at /, RPC bridge at /__devframe-starter/
pnpm run play:hub # Vite playground: mounted as a dock inside a devframes hub
pnpm run test # unit tests (vitest)
pnpm run test:e2e # e2e tests (playwright, against the single playground)
pnpm run lint
pnpm run typecheck
```

`pnpm run dev` and both playgrounds gate by default: opening the printed URL walks you through devframe's interactive OTP handshake (a 6-digit code) before the SPA can call RPC. That's intentional - see the `auth` comments in `src/devframe.ts` and `playground/*/vite.config.ts` before reaching for `auth: false`, which trusts every connection that can reach the port. For a one-off loopback-only session, pass `--no-auth` to the CLI instead (`pnpm run dev -- --no-auth`).

## File map

| Path | Purpose |
|------|---------|
| `src/devframe.ts` | The single `DevframeDefinition` every surface below consumes. |
| `src/rpc/` | RPC function definitions (`get-info` static, `list-items` query+snapshot) and their namespace declaration. |
| `src/client/` | The vanilla-TS SPA: `index.html`, `main.ts`, `app.ts`, `styles.css`. |
| `src/shared/base-path.ts` | The devframe's base path, shared between the node-side definition and browser-side client entries. |
| `bin.mjs` | `createCac(devframe).parse()` - exposes `dev`, `build`, `mcp`. |
| `playground/single/` | Vite dev-serves the SPA (with HMR) while `devframeViteBridge` answers RPC/discovery at the devframe's own base - see the comment in its `vite.config.ts` for why the two can't share one base. |
| `playground/hub/` | A minimal `@devframes/hub` host that mounts this devframe as an iframe dock (requires `pnpm run build` first). |
| `test/` | Unit tests - RPC functions over a real WebSocket, no browser. |
| `e2e/` | Playwright tests against the single playground, using the checked-in `e2e/fixtures/` directory as a fixed working directory. |

## Versioning

Dependencies here are real semver ranges, not the monorepo's pnpm catalog - so this folder is copy-paste ready outside the workspace. When developed in-repo, pnpm links `devframe`/`@devframes/*` to the local workspace packages automatically. See the root `AGENTS.md`'s `starter/` note for how a repo-wide `bumpp -r` release keeps these versions in sync.
14 changes: 14 additions & 0 deletions starter/bin.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env node
import process from 'node:process'
import { createCac } from 'devframe/adapters/cac'
import devframe from './src/devframe.ts'

async function main() {
const cli = createCac(devframe)
await cli.parse()
}

main().catch((error) => {
console.error(error)
process.exit(1)
})
33 changes: 33 additions & 0 deletions starter/e2e/app.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import process from 'node:process'
import { expect, test } from '@playwright/test'

// Exercises the single playground end to end: the vanilla-TS SPA connects
// over the real WebSocket RPC bridge and renders the `list-items` /
// `get-info` results for the fixed `./fixtures` directory (wired via
// `playwright.config.ts`'s `webServer.env.DEVFRAME_E2E_CWD`).
test('connects and renders the list of items', async ({ page }) => {
await page.goto('/')

const list = page.locator('ul')
await expect(list).toBeVisible()

const items = list.locator('li')
await expect(items).toHaveCount(2)
await expect(items.nth(0)).toContainText('dir1')
await expect(items.nth(0)).toContainText('dir')
await expect(items.nth(1)).toContainText('file1.txt')
await expect(items.nth(1)).toContainText('file')

// The "cwd" / "node" info line renders from the `get-info` RPC call.
await expect(page.locator('.meta code').first()).toContainText(process.version)
})

test('refresh re-fetches the list without a page reload', async ({ page }) => {
await page.goto('/')

const refreshButton = page.getByRole('button', { name: 'Refresh' })
await expect(refreshButton).toBeEnabled()
await refreshButton.click()

await expect(page.locator('ul li')).toHaveCount(2)
})
Empty file.
1 change: 1 addition & 0 deletions starter/e2e/fixtures/file1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello
19 changes: 19 additions & 0 deletions starter/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// @ts-check
import antfu from '@antfu/eslint-config'

export default antfu(
{
type: 'app',
pnpm: true,
},
{
// This starter is a self-contained, copy-paste-ready template — it
// intentionally pins real versions instead of pnpm catalog references
// (see the root AGENTS.md's "starter/" note), so the pnpm plugin's own
// catalog-enforcement rule doesn't apply to its `package.json`.
files: ['package.json'],
rules: {
'pnpm/json-enforce-catalog': 'off',
},
},
)
Loading
Loading