diff --git a/alias.ts b/alias.ts index 6cefefa01..3e8986693 100644 --- a/alias.ts +++ b/alias.ts @@ -77,7 +77,7 @@ export const alias = { '@devframes/json-render-ui/hub': r('json-render-ui/src/hub.ts'), '@devframes/json-render-ui/spa': r('json-render-ui/src/spa.ts'), '@devframes/json-render-ui': r('json-render-ui/src/index.ts'), - 'json-render/dashboard': fileURLToPath(new URL('./examples/json-render/src/dashboard.ts', import.meta.url)), + 'json-render/dashboard': fileURLToPath(new URL('./examples/json-render/src/node/dashboard.ts', import.meta.url)), '@devframes/plugin-code-server/node': p('code-server/src/node/setup.ts'), '@devframes/plugin-code-server/constants': p('code-server/src/node/constants.ts'), '@devframes/plugin-code-server/types': p('code-server/src/node/types.ts'), diff --git a/examples/files-inspector/src/client/app.tsx b/examples/files-inspector/app/app.tsx similarity index 100% rename from examples/files-inspector/src/client/app.tsx rename to examples/files-inspector/app/app.tsx diff --git a/examples/streaming-chat/src/client/design.ts b/examples/files-inspector/app/design.ts similarity index 76% rename from examples/streaming-chat/src/client/design.ts rename to examples/files-inspector/app/design.ts index d8e49c73a..0bf9debcd 100644 --- a/examples/streaming-chat/src/client/design.ts +++ b/examples/files-inspector/app/design.ts @@ -1,3 +1,3 @@ // Re-exports the shared devframe class-helper builders (see // `design/design.ts`) so this surface stays in lockstep with every other. -export * from '../../../../design/design' +export * from '../../../design/design' diff --git a/examples/files-inspector/src/client/index.html b/examples/files-inspector/app/index.html similarity index 100% rename from examples/files-inspector/src/client/index.html rename to examples/files-inspector/app/index.html diff --git a/examples/files-inspector/src/client/main.tsx b/examples/files-inspector/app/main.tsx similarity index 100% rename from examples/files-inspector/src/client/main.tsx rename to examples/files-inspector/app/main.tsx diff --git a/examples/files-inspector/src/client/routes/about.tsx b/examples/files-inspector/app/routes/about.tsx similarity index 100% rename from examples/files-inspector/src/client/routes/about.tsx rename to examples/files-inspector/app/routes/about.tsx diff --git a/examples/files-inspector/src/client/routes/home.tsx b/examples/files-inspector/app/routes/home.tsx similarity index 100% rename from examples/files-inspector/src/client/routes/home.tsx rename to examples/files-inspector/app/routes/home.tsx diff --git a/examples/files-inspector/src/client/shims.d.ts b/examples/files-inspector/app/shims.d.ts similarity index 100% rename from examples/files-inspector/src/client/shims.d.ts rename to examples/files-inspector/app/shims.d.ts diff --git a/examples/files-inspector/app/vite.config.ts b/examples/files-inspector/app/vite.config.ts new file mode 100644 index 000000000..2312cec06 --- /dev/null +++ b/examples/files-inspector/app/vite.config.ts @@ -0,0 +1,50 @@ +import type { Server } from 'node:http' +import type { Plugin } from 'vite' +import { fileURLToPath } from 'node:url' +import preact from '@preact/preset-vite' +import { initDevframe } from 'devframe/initiate' +import { resolveBasePath } from 'devframe/internal' +import UnoCSS from 'unocss/vite' +import { defineConfig } from 'vite' +import { alias } from '../../../alias' +import devframe from '../src/node/index.ts' + +/** + * Serve-only plugin that bridges the node side (RPC + WebSocket + + * `__connection.json`) onto Vite's dev server under the devframe base path, + * mounted as a post middleware so Vite serves the HMR SPA first and the + * devframe host only answers the routes it owns. Inert during `vite build`. + */ +function filesInspectorDevBridge(): Plugin { + return { + name: 'files-inspector-dev-bridge', + apply: 'serve', + configureServer(server) { + const instance = initDevframe(devframe, { + base: resolveBasePath(devframe, 'hosted'), + distDir: false, + server: server.httpServer as Server, + auth: false, + }) + return () => server.middlewares.use(instance.nodeMiddleware) + }, + } +} + +/** + * `base: './'` for the build keeps the mount path portable: the same output + * works whether devframe serves it at `/` (standalone) or under a base path + * (mounted in a hub). In `vite dev` the panel is served under the devframe + * base path so its `document.baseURI` matches production, and the dev bridge + * bridges the node side there with HMR. + */ +export default defineConfig(({ command }) => ({ + base: command === 'serve' ? resolveBasePath(devframe, 'hosted') : './', + root: fileURLToPath(new URL('.', import.meta.url)), + resolve: { alias }, + plugins: [UnoCSS(), preact(), filesInspectorDevBridge()], + build: { + outDir: fileURLToPath(new URL('../dist/client', import.meta.url)), + emptyOutDir: true, + }, +})) diff --git a/examples/files-inspector/bin.mjs b/examples/files-inspector/bin.mjs index c1ed8f007..5e7c4709e 100755 --- a/examples/files-inspector/bin.mjs +++ b/examples/files-inspector/bin.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import process from 'node:process' import { createCac } from 'devframe/adapters/cac' -import devframe from './src/devframe.ts' +import devframe from './src/node/index.ts' async function main() { // Serve the agent surface at `/__mcp` and register for `devframe connect` diff --git a/examples/files-inspector/package.json b/examples/files-inspector/package.json index ade078f14..3c99b98c1 100644 --- a/examples/files-inspector/package.json +++ b/examples/files-inspector/package.json @@ -5,13 +5,15 @@ "private": true, "description": "Devframe demo that lists files in the working directory over RPC across the dev and build surfaces.", "homepage": "https://github.com/devframes/devframe/tree/main/examples/files-inspector", - "main": "src/devframe.ts", + "main": "src/node/index.ts", "bin": { "files-inspector": "./bin.mjs" }, "scripts": { - "build": "vite build --config src/client/vite.config.ts", - "dev": "node bin.mjs", + "build": "vite build --config app/vite.config.ts", + "build:app": "vite build --config app/vite.config.ts", + "dev": "vite --config app/vite.config.ts --host", + "play": "pnpm run build && node playgrounds/server.mjs", "cli:build": "node bin.mjs build --out-dir dist/static", "test": "vitest run", "typecheck": "tsc --noEmit" diff --git a/examples/files-inspector/playgrounds/server.mjs b/examples/files-inspector/playgrounds/server.mjs new file mode 100644 index 000000000..e6ac50eea --- /dev/null +++ b/examples/files-inspector/playgrounds/server.mjs @@ -0,0 +1,65 @@ +#!/usr/bin/env node +/** + * Playground host for the Files Inspector: boots the built tool and serves its + * SPA panel + live WebSocket RPC off one origin, mirroring how a hub would + * mount it under the devframe base path. + * + * node playgrounds/server.mjs → serves `dist/client` with live RPC + */ +import { existsSync } from 'node:fs' +import { createServer } from 'node:http' +import process from 'node:process' +import { fileURLToPath } from 'node:url' +import { initDevframe } from 'devframe/initiate' +import { getPort } from 'devframe/utils/get-port' +import { H3, toNodeHandler } from 'h3' +import { resolve } from 'pathe' +import devframe from '../src/node/index.ts' + +const HERE = fileURLToPath(new URL('.', import.meta.url)) +const ROOT = resolve(HERE, '..') + +const basePath = devframe.basePath +const panelDir = resolve(ROOT, 'dist/client') + +function requireBuilt(file, hint) { + if (!existsSync(file)) { + console.error(`\n[files-inspector playground] missing ${file}\n → run \`${hint}\` first.\n`) + process.exit(1) + } +} + +function banner(origin) { + process.stdout.write( + `\n Files Inspector demo: dev (live WebSocket RPC)\n` + + ` ▸ panel: ${origin}${basePath}\n\n`, + ) +} + +async function main() { + requireBuilt(resolve(panelDir, 'index.html'), 'pnpm -C examples/files-inspector build') + + const bindHost = '0.0.0.0' + const port = await getPort({ host: bindHost, port: 9876 }) + const origin = `http://localhost:${port}` + + const app = new H3() + const server = createServer(toNodeHandler(app)) + const instance = initDevframe(devframe, { + base: basePath, + distDir: panelDir, + app, + server, + host: bindHost, + origin, + auth: false, + }) + await new Promise(r => server.listen(port, bindHost, r)) + await instance.ready + banner(origin) +} + +main().catch((error) => { + console.error(error) + process.exit(1) +}) diff --git a/examples/files-inspector/src/client/vite.config.ts b/examples/files-inspector/src/client/vite.config.ts deleted file mode 100644 index d542a1bab..000000000 --- a/examples/files-inspector/src/client/vite.config.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { fileURLToPath } from 'node:url' -import preact from '@preact/preset-vite' -import UnoCSS from 'unocss/vite' -import { defineConfig } from 'vite' -import { alias } from '../../../../alias' - -export default defineConfig({ - base: './', - root: fileURLToPath(new URL('.', import.meta.url)), - resolve: { alias }, - plugins: [UnoCSS(), preact()], - build: { - outDir: fileURLToPath(new URL('../../dist/client', import.meta.url)), - emptyOutDir: true, - }, -}) diff --git a/examples/files-inspector/src/devframe.ts b/examples/files-inspector/src/node/index.ts similarity index 87% rename from examples/files-inspector/src/devframe.ts rename to examples/files-inspector/src/node/index.ts index 49be230de..674eb082c 100644 --- a/examples/files-inspector/src/devframe.ts +++ b/examples/files-inspector/src/node/index.ts @@ -1,10 +1,10 @@ import { fileURLToPath } from 'node:url' import { defineDevframe } from 'devframe' -import pkg from '../package.json' with { type: 'json' } +import pkg from '../../package.json' with { type: 'json' } import { NAMESPACE, serverFunctions } from './rpc/index.ts' const BASE_PATH = '/__devframe-files-inspector/' -const distDir = fileURLToPath(new URL('../dist/client', import.meta.url)) +const distDir = fileURLToPath(new URL('../../dist/client', import.meta.url)) export default defineDevframe({ id: 'example:files-inspector', @@ -39,7 +39,7 @@ export default defineDevframe({ description: 'Locate the Files Inspector\'s documentation on disk. Call before answering questions about how this tool works, then read the returned files directly.', safety: 'read', handler: () => ({ - readmePath: fileURLToPath(new URL('../README.md', import.meta.url)), + readmePath: fileURLToPath(new URL('../../README.md', import.meta.url)), hint: 'Read the file at readmePath with your own file tools; do not rely on training-data knowledge of this example.', }), }) diff --git a/examples/files-inspector/src/rpc/functions/get-cwd.ts b/examples/files-inspector/src/node/rpc/functions/get-cwd.ts similarity index 100% rename from examples/files-inspector/src/rpc/functions/get-cwd.ts rename to examples/files-inspector/src/node/rpc/functions/get-cwd.ts diff --git a/examples/files-inspector/src/rpc/functions/list-files.ts b/examples/files-inspector/src/node/rpc/functions/list-files.ts similarity index 100% rename from examples/files-inspector/src/rpc/functions/list-files.ts rename to examples/files-inspector/src/node/rpc/functions/list-files.ts diff --git a/examples/files-inspector/src/rpc/index.ts b/examples/files-inspector/src/node/rpc/index.ts similarity index 100% rename from examples/files-inspector/src/rpc/index.ts rename to examples/files-inspector/src/node/rpc/index.ts diff --git a/examples/files-inspector/tests/_utils.ts b/examples/files-inspector/tests/_utils.ts index ae40de659..e03d8b748 100644 --- a/examples/files-inspector/tests/_utils.ts +++ b/examples/files-inspector/tests/_utils.ts @@ -14,7 +14,7 @@ import { getPort } from 'get-port-please' import { H3 } from 'h3' import { resolve } from 'pathe' import { serveTestContext } from '../../../tests/helpers/serve-test-context' -import devframe from '../src/devframe' +import devframe from '../src/node/index' const HERE = fileURLToPath(new URL('.', import.meta.url)) const CLIENT_DIST = resolve(HERE, '../dist/client') diff --git a/examples/files-inspector/tests/static-build.test.ts b/examples/files-inspector/tests/static-build.test.ts index 5ccb21318..b05a4b397 100644 --- a/examples/files-inspector/tests/static-build.test.ts +++ b/examples/files-inspector/tests/static-build.test.ts @@ -8,7 +8,7 @@ import { DEVFRAME_RPC_DUMP_MANIFEST_FILENAME, } from 'devframe/constants' import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import devframe from '../src/devframe' +import devframe from '../src/node/index' import { assertClientBuilt, makeFixtureCwd } from './_utils' interface DumpManifest { diff --git a/examples/files-inspector/tests/static-serve.test.ts b/examples/files-inspector/tests/static-serve.test.ts index d7bca9598..8f79a2a9b 100644 --- a/examples/files-inspector/tests/static-serve.test.ts +++ b/examples/files-inspector/tests/static-serve.test.ts @@ -7,7 +7,7 @@ import { mountStaticHandler } from 'devframe/utils/serve-static' import { getPort } from 'get-port-please' import { H3, toNodeHandler } from 'h3' import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import devframe from '../src/devframe' +import devframe from '../src/node/index' import { assertClientBuilt, makeFixtureCwd } from './_utils' interface StaticServer { diff --git a/examples/files-inspector/tsconfig.json b/examples/files-inspector/tsconfig.json index d4bdee9d3..a1fa38032 100644 --- a/examples/files-inspector/tsconfig.json +++ b/examples/files-inspector/tsconfig.json @@ -10,5 +10,5 @@ "esModuleInterop": true, "isolatedDeclarations": false }, - "include": ["src", "tests", "bin.mjs"] + "include": ["src", "app", "tests", "playgrounds", "bin.mjs"] } diff --git a/examples/json-render/bin.mjs b/examples/json-render/bin.mjs index d35b5779b..eae5b5012 100755 --- a/examples/json-render/bin.mjs +++ b/examples/json-render/bin.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import process from 'node:process' import { createCac } from 'devframe/adapters/cac' -import devframe from './src/devframe.ts' +import devframe from './src/node/index.ts' async function main() { const cli = createCac(devframe) diff --git a/examples/json-render/package.json b/examples/json-render/package.json index 1aa9b8366..88b170ea8 100644 --- a/examples/json-render/package.json +++ b/examples/json-render/package.json @@ -6,10 +6,10 @@ "description": "Standalone devframe that serves a JSON view spec with live state and an action bridge.", "homepage": "https://github.com/devframes/devframe/tree/main/examples/json-render", "exports": { - ".": "./src/devframe.ts", - "./dashboard": "./src/dashboard.ts" + ".": "./src/node/index.ts", + "./dashboard": "./src/node/dashboard.ts" }, - "main": "src/devframe.ts", + "main": "src/node/index.ts", "bin": { "json-render": "./bin.mjs" }, diff --git a/examples/json-render/src/dashboard.ts b/examples/json-render/src/node/dashboard.ts similarity index 100% rename from examples/json-render/src/dashboard.ts rename to examples/json-render/src/node/dashboard.ts diff --git a/examples/json-render/src/devframe.ts b/examples/json-render/src/node/index.ts similarity index 93% rename from examples/json-render/src/devframe.ts rename to examples/json-render/src/node/index.ts index 6813b554c..89395e505 100644 --- a/examples/json-render/src/devframe.ts +++ b/examples/json-render/src/node/index.ts @@ -1,5 +1,5 @@ import { createJsonRenderDevframe } from '@devframes/json-render-ui/spa' -import pkg from '../package.json' with { type: 'json' } +import pkg from '../../package.json' with { type: 'json' } import { createDashboardView } from './dashboard.ts' /** diff --git a/examples/json-render/src/shared.ts b/examples/json-render/src/node/shared.ts similarity index 100% rename from examples/json-render/src/shared.ts rename to examples/json-render/src/node/shared.ts diff --git a/examples/next-runtime-snapshot/src/client/app/components/connect.tsx b/examples/next-runtime-snapshot/app/app/components/connect.tsx similarity index 100% rename from examples/next-runtime-snapshot/src/client/app/components/connect.tsx rename to examples/next-runtime-snapshot/app/app/components/connect.tsx diff --git a/examples/next-runtime-snapshot/src/client/app/components/snapshot-env.tsx b/examples/next-runtime-snapshot/app/app/components/snapshot-env.tsx similarity index 97% rename from examples/next-runtime-snapshot/src/client/app/components/snapshot-env.tsx rename to examples/next-runtime-snapshot/app/app/components/snapshot-env.tsx index d0e5ddeb7..8d8f09393 100644 --- a/examples/next-runtime-snapshot/src/client/app/components/snapshot-env.tsx +++ b/examples/next-runtime-snapshot/app/app/components/snapshot-env.tsx @@ -1,6 +1,6 @@ 'use client' -import type { EnvSnapshot } from '../../../devframe' +import type { EnvSnapshot } from '../../../src/node/index' import { useCallback, useEffect, useState } from 'react' import { card, input as inputClass } from '../design' import { useRpc } from './connect' diff --git a/examples/next-runtime-snapshot/src/client/app/components/snapshot-memory.tsx b/examples/next-runtime-snapshot/app/app/components/snapshot-memory.tsx similarity index 97% rename from examples/next-runtime-snapshot/src/client/app/components/snapshot-memory.tsx rename to examples/next-runtime-snapshot/app/app/components/snapshot-memory.tsx index 6b7ab9efc..635c80d34 100644 --- a/examples/next-runtime-snapshot/src/client/app/components/snapshot-memory.tsx +++ b/examples/next-runtime-snapshot/app/app/components/snapshot-memory.tsx @@ -1,6 +1,6 @@ 'use client' -import type { MemorySnapshot } from '../../../devframe' +import type { MemorySnapshot } from '../../../src/node/index' import { useCallback, useEffect, useState } from 'react' import { button, card } from '../design' import { useRpc } from './connect' diff --git a/examples/next-runtime-snapshot/src/client/app/components/snapshot-system.tsx b/examples/next-runtime-snapshot/app/app/components/snapshot-system.tsx similarity index 96% rename from examples/next-runtime-snapshot/src/client/app/components/snapshot-system.tsx rename to examples/next-runtime-snapshot/app/app/components/snapshot-system.tsx index 26d3a347f..8ebb0b8cb 100644 --- a/examples/next-runtime-snapshot/src/client/app/components/snapshot-system.tsx +++ b/examples/next-runtime-snapshot/app/app/components/snapshot-system.tsx @@ -1,6 +1,6 @@ 'use client' -import type { SystemInfo } from '../../../devframe' +import type { SystemInfo } from '../../../src/node/index' import { useEffect, useState } from 'react' import { card } from '../design' import { useRpc } from './connect' diff --git a/examples/files-inspector/src/client/design.ts b/examples/next-runtime-snapshot/app/app/design.ts similarity index 100% rename from examples/files-inspector/src/client/design.ts rename to examples/next-runtime-snapshot/app/app/design.ts diff --git a/examples/next-runtime-snapshot/src/client/app/globals.css b/examples/next-runtime-snapshot/app/app/globals.css similarity index 100% rename from examples/next-runtime-snapshot/src/client/app/globals.css rename to examples/next-runtime-snapshot/app/app/globals.css diff --git a/examples/next-runtime-snapshot/src/client/app/layout.tsx b/examples/next-runtime-snapshot/app/app/layout.tsx similarity index 100% rename from examples/next-runtime-snapshot/src/client/app/layout.tsx rename to examples/next-runtime-snapshot/app/app/layout.tsx diff --git a/examples/next-runtime-snapshot/src/client/app/page.tsx b/examples/next-runtime-snapshot/app/app/page.tsx similarity index 100% rename from examples/next-runtime-snapshot/src/client/app/page.tsx rename to examples/next-runtime-snapshot/app/app/page.tsx diff --git a/examples/next-runtime-snapshot/app/next.config.mjs b/examples/next-runtime-snapshot/app/next.config.mjs new file mode 100644 index 000000000..b2305bb15 --- /dev/null +++ b/examples/next-runtime-snapshot/app/next.config.mjs @@ -0,0 +1,27 @@ +import { PHASE_DEVELOPMENT_SERVER } from 'next/constants.js' + +/** + * The static-export options (`output: 'export'`, a relative `assetPrefix`, and + * `trailingSlash`) exist so `next build` emits a self-contained SPA that mounts + * at any base. They are wrong for `next dev`: a relative `assetPrefix` breaks + * the dev client runtime's chunk base, so the page never hydrates (no + * interactivity, and the RPC client never connects). Apply them only for the + * production build so `pnpm dev:client` hydrates normally. + * + * @type {(phase: string) => import('next').NextConfig} + */ +export default (phase) => { + const isDev = phase === PHASE_DEVELOPMENT_SERVER + return { + ...(isDev ? {} : { output: 'export', assetPrefix: '.', trailingSlash: true }), + images: { unoptimized: true }, + /** + * The workspace tsconfig uses path aliases that point at devframe's + * source so source-level edits HMR cleanly. Next.js's incremental TS + * check can't follow workspace project references through those aliases + * and ends up type-checking unrelated source. Defer typechecking to the + * workspace's own `tsc -b` (`pnpm typecheck`), which honors references. + */ + typescript: { ignoreBuildErrors: true }, + } +} diff --git a/examples/next-runtime-snapshot/src/client/postcss.config.mjs b/examples/next-runtime-snapshot/app/postcss.config.mjs similarity index 100% rename from examples/next-runtime-snapshot/src/client/postcss.config.mjs rename to examples/next-runtime-snapshot/app/postcss.config.mjs diff --git a/examples/next-runtime-snapshot/app/tsconfig.json b/examples/next-runtime-snapshot/app/tsconfig.json new file mode 100644 index 000000000..df7f7d07c --- /dev/null +++ b/examples/next-runtime-snapshot/app/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "incremental": true, + "jsx": "preserve", + "lib": ["ESNext", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", + "allowJs": true, + "noEmit": true, + "esModuleInterop": true, + "isolatedDeclarations": false, + "plugins": [{ "name": "next" }] + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules", + ".next", + "out" + ] +} diff --git a/examples/next-runtime-snapshot/bin.mjs b/examples/next-runtime-snapshot/bin.mjs index d35b5779b..eae5b5012 100755 --- a/examples/next-runtime-snapshot/bin.mjs +++ b/examples/next-runtime-snapshot/bin.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import process from 'node:process' import { createCac } from 'devframe/adapters/cac' -import devframe from './src/devframe.ts' +import devframe from './src/node/index.ts' async function main() { const cli = createCac(devframe) diff --git a/examples/next-runtime-snapshot/package.json b/examples/next-runtime-snapshot/package.json index eebbf8e4d..a02a05baf 100644 --- a/examples/next-runtime-snapshot/package.json +++ b/examples/next-runtime-snapshot/package.json @@ -5,15 +5,16 @@ "private": true, "description": "Devframe demo exposing the host Node runtime snapshot through a Next.js App Router SPA.", "homepage": "https://github.com/devframes/devframe/tree/main/examples/next-runtime-snapshot", - "main": "src/devframe.ts", + "main": "src/node/index.ts", "bin": { "next-runtime-snapshot": "./bin.mjs" }, "scripts": { - "build": "next build src/client && node scripts/build-spa.mjs", + "build": "next build app && node scripts/build-spa.mjs", "cli:build": "node bin.mjs build --out-dir dist/static", "dev": "node bin.mjs", - "next:dev": "next dev src/client", + "dev:client": "next dev app", + "play": "pnpm run build && node playgrounds/server.mjs", "test": "vitest run", "typecheck": "tsc --noEmit" }, diff --git a/examples/next-runtime-snapshot/playgrounds/server.mjs b/examples/next-runtime-snapshot/playgrounds/server.mjs new file mode 100644 index 000000000..b5d79b780 --- /dev/null +++ b/examples/next-runtime-snapshot/playgrounds/server.mjs @@ -0,0 +1,57 @@ +#!/usr/bin/env node +/** + * Standalone demo host for the Next Runtime Snapshot panel. + * + * `initDevframe` runs setup, serves the built Next.js panel SPA + + * `__connection.json` from `dist/client` under the devframe base path, and + * binds the WebSocket RPC upgrade onto our http server. + * + * node playgrounds/server.mjs → live WebSocket RPC (`dist/client`) + */ +import { existsSync } from 'node:fs' +import { createServer } from 'node:http' +import { dirname, resolve } from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' +import { initDevframe } from 'devframe/initiate' +import { getPort } from 'get-port-please' +import { H3, toNodeHandler } from 'h3' +import devframe from '../src/node/index.ts' + +const HERE = dirname(fileURLToPath(import.meta.url)) +const ROOT = resolve(HERE, '..') +const panelDir = resolve(ROOT, 'dist/client') + +const basePath = devframe.basePath + +async function main() { + if (!existsSync(resolve(panelDir, 'index.html'))) { + console.error('\n[next-runtime-snapshot playground] missing dist/client/index.html\n → run `pnpm -C examples/next-runtime-snapshot build` first.\n') + process.exit(1) + } + + const bindHost = '0.0.0.0' + const port = await getPort({ host: bindHost, port: 9899 }) + const origin = `http://localhost:${port}` + + const app = new H3() + const server = createServer(toNodeHandler(app)) + const instance = initDevframe(devframe, { + base: basePath, + distDir: panelDir, + app, + server, + host: bindHost, + origin, + auth: false, + }) + await new Promise(r => server.listen(port, bindHost, r)) + await instance.ready + + process.stdout.write(`\n Next Runtime Snapshot: dev (live WebSocket RPC)\n ▸ panel: ${origin}${basePath}\n\n`) +} + +main().catch((error) => { + console.error(error) + process.exit(1) +}) diff --git a/examples/next-runtime-snapshot/scripts/build-spa.mjs b/examples/next-runtime-snapshot/scripts/build-spa.mjs index 4b3d477e1..90d29fd38 100644 --- a/examples/next-runtime-snapshot/scripts/build-spa.mjs +++ b/examples/next-runtime-snapshot/scripts/build-spa.mjs @@ -1,25 +1,23 @@ -import { chmodSync, cpSync, mkdirSync, readdirSync, rmSync } from 'node:fs' +import { copyFileSync, mkdirSync, readdirSync, rmSync } from 'node:fs' import { join } from 'node:path' -rmSync('dist/client', { recursive: true, force: true }) -mkdirSync('dist', { recursive: true }) -cpSync('src/client/out', 'dist/client', { recursive: true }) +// The Next.js static export is this example's iframe SPA; it is served from +// `dist/client` by the standalone CLI, the playground host, and the tests. -// A built SPA must be readable to be served. Some filesystems (e.g. Docker -// Desktop's shared mounts) drop the read bits when copying, which yields -// unservable assets and trips downstream `createBuild` copies. Normalize the -// tree so directories are traversable and files are world-readable. -function normalizePermissions(dir) { - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const path = join(dir, entry.name) - if (entry.isDirectory()) { - chmodSync(path, 0o755) - normalizePermissions(path) - } - else if (entry.isFile()) { - chmodSync(path, 0o644) - } +// A manual walk with explicit modes: Next's export emits entries with +// restrictive modes that node's `cpSync` inherits and then trips over, so +// recreate the tree with normal directory/file permissions instead. +function copyTree(src, dest) { + mkdirSync(dest, { recursive: true, mode: 0o755 }) + for (const entry of readdirSync(src, { withFileTypes: true })) { + const from = join(src, entry.name) + const to = join(dest, entry.name) + if (entry.isDirectory()) + copyTree(from, to) + else if (entry.isFile()) + copyFileSync(from, to) } } -normalizePermissions('dist/client') +rmSync('dist/client', { recursive: true, force: true }) +copyTree('app/out', 'dist/client') diff --git a/examples/next-runtime-snapshot/src/client/next.config.mjs b/examples/next-runtime-snapshot/src/client/next.config.mjs deleted file mode 100644 index 75d1fe71b..000000000 --- a/examples/next-runtime-snapshot/src/client/next.config.mjs +++ /dev/null @@ -1,17 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - output: 'export', - assetPrefix: '.', - trailingSlash: true, - images: { unoptimized: true }, - /** - * The workspace tsconfig uses path aliases that point at devframe's - * source so source-level edits HMR cleanly. Next.js's incremental TS - * check can't follow workspace project references through those aliases - * and ends up type-checking unrelated source. Defer typechecking to the - * workspace's own `tsc -b` (`pnpm typecheck`), which honors references. - */ - typescript: { ignoreBuildErrors: true }, -} - -export default nextConfig diff --git a/examples/next-runtime-snapshot/src/client/tsconfig.json b/examples/next-runtime-snapshot/src/client/tsconfig.json deleted file mode 100644 index 62aa7e681..000000000 --- a/examples/next-runtime-snapshot/src/client/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx" - ], - "exclude": [ - "node_modules", - ".next", - "out" - ] -} diff --git a/examples/next-runtime-snapshot/src/devframe.ts b/examples/next-runtime-snapshot/src/node/index.ts similarity index 87% rename from examples/next-runtime-snapshot/src/devframe.ts rename to examples/next-runtime-snapshot/src/node/index.ts index e9160a93a..62bc486f1 100644 --- a/examples/next-runtime-snapshot/src/devframe.ts +++ b/examples/next-runtime-snapshot/src/node/index.ts @@ -1,6 +1,6 @@ import { fileURLToPath } from 'node:url' import { defineDevframe } from 'devframe' -import pkg from '../package.json' with { type: 'json' } +import pkg from '../../package.json' with { type: 'json' } import { NAMESPACE, serverFunctions } from './rpc/index.ts' export type { EnvEntry, EnvSnapshot } from './rpc/functions/env.ts' @@ -8,7 +8,7 @@ export type { MemorySnapshot } from './rpc/functions/memory.ts' export type { SystemInfo } from './rpc/functions/system.ts' const BASE_PATH = '/__next-runtime-snapshot/' -const distDir = fileURLToPath(new URL('../dist/client', import.meta.url)) +const distDir = fileURLToPath(new URL('../../dist/client', import.meta.url)) export default defineDevframe({ id: 'example:next-runtime-snapshot', diff --git a/examples/next-runtime-snapshot/src/rpc/functions/env.ts b/examples/next-runtime-snapshot/src/node/rpc/functions/env.ts similarity index 100% rename from examples/next-runtime-snapshot/src/rpc/functions/env.ts rename to examples/next-runtime-snapshot/src/node/rpc/functions/env.ts diff --git a/examples/next-runtime-snapshot/src/rpc/functions/memory.ts b/examples/next-runtime-snapshot/src/node/rpc/functions/memory.ts similarity index 100% rename from examples/next-runtime-snapshot/src/rpc/functions/memory.ts rename to examples/next-runtime-snapshot/src/node/rpc/functions/memory.ts diff --git a/examples/next-runtime-snapshot/src/rpc/functions/system.ts b/examples/next-runtime-snapshot/src/node/rpc/functions/system.ts similarity index 100% rename from examples/next-runtime-snapshot/src/rpc/functions/system.ts rename to examples/next-runtime-snapshot/src/node/rpc/functions/system.ts diff --git a/examples/next-runtime-snapshot/src/rpc/index.ts b/examples/next-runtime-snapshot/src/node/rpc/index.ts similarity index 100% rename from examples/next-runtime-snapshot/src/rpc/index.ts rename to examples/next-runtime-snapshot/src/node/rpc/index.ts diff --git a/examples/next-runtime-snapshot/tests/_utils.ts b/examples/next-runtime-snapshot/tests/_utils.ts index 83ecb24f2..b80d0c7db 100644 --- a/examples/next-runtime-snapshot/tests/_utils.ts +++ b/examples/next-runtime-snapshot/tests/_utils.ts @@ -8,7 +8,7 @@ import { getPort } from 'get-port-please' import { H3 } from 'h3' import { resolve } from 'pathe' import { serveTestContext } from '../../../tests/helpers/serve-test-context' -import devframe from '../src/devframe' +import devframe from '../src/node/index' export interface SnapshotServer extends StartedServer { basePath: string diff --git a/examples/next-runtime-snapshot/tests/next-runtime-snapshot.test.ts b/examples/next-runtime-snapshot/tests/next-runtime-snapshot.test.ts index fa1d60be9..e2ff3019a 100644 --- a/examples/next-runtime-snapshot/tests/next-runtime-snapshot.test.ts +++ b/examples/next-runtime-snapshot/tests/next-runtime-snapshot.test.ts @@ -1,4 +1,4 @@ -import type { EnvSnapshot, MemorySnapshot, SystemInfo } from '../src/devframe' +import type { EnvSnapshot, MemorySnapshot, SystemInfo } from '../src/node/index' import process from 'node:process' import { createRpcClient } from 'devframe/rpc/client' import { createWsRpcChannel } from 'devframe/rpc/transports/ws-client' diff --git a/examples/next-runtime-snapshot/tsconfig.json b/examples/next-runtime-snapshot/tsconfig.json index 91baff874..96b30a86a 100644 --- a/examples/next-runtime-snapshot/tsconfig.json +++ b/examples/next-runtime-snapshot/tsconfig.json @@ -1,24 +1,23 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": true, - "jsx": "preserve", - "lib": ["ESNext", "DOM"], - "module": "ESNext", - "moduleResolution": "Bundler", + "lib": ["esnext", "dom"], "allowJs": true, "noEmit": true, - "esModuleInterop": true, - "isolatedDeclarations": false, - "plugins": [{ "name": "next" }] + "esModuleInterop": true }, "include": [ "src", - "bin.mjs" + "tests", + "playgrounds", + "bin.mjs", + "uno.config.ts" ], "exclude": [ "dist", + "app", ".next", - "out" + "out", + "node_modules" ] } diff --git a/examples/next-runtime-snapshot/uno.config.ts b/examples/next-runtime-snapshot/uno.config.ts index 4f6e6bf8f..ad8efc708 100644 --- a/examples/next-runtime-snapshot/uno.config.ts +++ b/examples/next-runtime-snapshot/uno.config.ts @@ -3,17 +3,17 @@ import { mergeConfigs } from 'unocss' import { designConfig } from '../../design/uno.config' // This example composes the shared devframe base (see `design/uno.config.ts`). -// `@unocss/postcss` (see src/client/postcss.config.mjs) loads this config; the +// `@unocss/postcss` (see app/postcss.config.mjs) loads this config; the // absolute glob keeps class extraction working regardless of the directory Next // builds from. The co-located `app/design.ts` (carrying `@unocss-include`) is // covered by the same glob. -const client = fileURLToPath(new URL('./src/client', import.meta.url)) +const app = fileURLToPath(new URL('./app', import.meta.url)) export default mergeConfigs([ designConfig, { content: { - filesystem: [`${client}/app/**/*.{ts,tsx}`], + filesystem: [`${app}/app/**/*.{ts,tsx}`], }, }, ]) diff --git a/examples/sse-basic/README.md b/examples/sse-basic/README.md index e97951ccd..81ea44ada 100644 --- a/examples/sse-basic/README.md +++ b/examples/sse-basic/README.md @@ -3,7 +3,8 @@ The smallest SSE-only devframe: `ws: false` binds no WebSocket, so every RPC frame - calls, shared state, the auth handshake - rides plain HTTP at `/__sse-basic/__sse`. This is the transport for host frameworks and proxies where the WebSocket upgrade isn't available. ```sh -pnpm --filter sse-basic dev +pnpm --filter sse-basic dev # Vite app + the SSE node bridge, with HMR +pnpm --filter sse-basic play # build the SPA, then serve it over SSE from a standalone host ``` Open the printed URL. The page shows: @@ -14,12 +15,8 @@ Open the printed URL. The page shows: ## How it works -`vite.config.ts` holds the whole thing: a `defineDevframe` definition (two RPC functions and a shared-state clock) served by `initDevframe(def, { base: '/__sse-basic/', ws: false, auth: false })`, mounted with one line - +`src/node/index.ts` is the whole tool: a `defineDevframe` definition with two RPC functions and a shared-state clock. `app/` is the vanilla SPA (`app/main.ts` calls `connectDevframe({ baseURL: '/__sse-basic/' })`), and `app/vite.config.ts` bridges the node side onto the dev server with `initDevframe(def, { base: '/__sse-basic/', ws: false, auth: false })` for HMR. `playgrounds/server.mjs` boots the built SPA over the same SSE host. -```ts -server.middlewares.use(instance.nodeMiddleware) -``` - -There is no upgrade wiring anywhere; ordinary request middleware serves discovery (`__connection.json`), the SSE stream (`GET __sse`), and RPC frames (`POST __sse`). `src/main.ts` is the vanilla client: `connectDevframe({ baseURL: '/__sse-basic/' })` and everything else works exactly as it would over a WebSocket. +There is no upgrade wiring anywhere; ordinary request middleware serves discovery (`__connection.json`), the SSE stream (`GET __sse`), and RPC frames (`POST __sse`). Everything works exactly as it would over a WebSocket. See the [Transports guide](https://devfra.me/guide/transports) for the full transport model, including running both transports side by side and pinning one from the client. diff --git a/examples/sse-basic/index.html b/examples/sse-basic/app/index.html similarity index 95% rename from examples/sse-basic/index.html rename to examples/sse-basic/app/index.html index af0b64148..7e236a109 100644 --- a/examples/sse-basic/index.html +++ b/examples/sse-basic/app/index.html @@ -3,6 +3,7 @@
+