Skip to content
Merged
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
1 change: 1 addition & 0 deletions alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const alias = {
'devframe/utils/launch-editor': r('devframe/src/utils/launch-editor.ts'),
'devframe/utils/nanoid': r('devframe/src/utils/nanoid.ts'),
'devframe/utils/open': r('devframe/src/utils/open.ts'),
'devframe/utils/remote-assets': r('devframe/src/utils/remote-assets.ts'),
'devframe/utils/simple-schema': r('devframe/src/utils/simple-schema.ts'),
'devframe/utils/serve-static': r('devframe/src/utils/serve-static.ts'),
'devframe/utils/shared-state': r('devframe/src/utils/shared-state.ts'),
Expand Down
39 changes: 39 additions & 0 deletions docs/errors/DF0059.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
outline: deep
---

# DF0059: Remote Assets File Listing Failed

## Message

> Failed to fetch the file listing for "`{package}`@`{version}`" from `{provider}`: `{reason}`

## Cause

A remote-assets source (`{ package, version }` passed where a static mount accepts a dist directory) resolves request paths against the CDN provider's file-listing API — `data.jsdelivr.com` for jsDelivr, `?meta` for unpkg, or a custom provider's `listFiles`. That listing request failed, typically because the provider is unreachable (offline machine, blocked domain) or returned an error status.

## Example

```ts
defineDevframe({
cli: {
distDir: {
package: '@devframes/plugin-git-client',
version: '1.2.3',
},
},
})
```

Starting this devframe without network access to `data.jsdelivr.com` reports `DF0059` on the first request.

## Fix

Requests keep working in a degraded probe mode (each candidate path is tried against the provider directly). To resolve it:

- Check network access to the configured provider, or switch providers (`provider: 'unpkg'` or a custom mirror).
- Install the assets package locally (`npm install <package>`) — a locally installed copy is served with zero network and needs no listing.

## Source

- [`packages/devframe/src/utils/remote-assets.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/utils/remote-assets.ts) — `createRemoteAssetsStore()` reports this (once per store) when the provider's file listing cannot be fetched or parsed.
37 changes: 37 additions & 0 deletions docs/errors/DF0060.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
outline: deep
---

# DF0060: Remote Asset Fetch Failed

## Message

> Failed to fetch a remote asset of "`{package}`" (`{url}`): `{reason}`

## Cause

A file of a remote-assets source was requested that is neither in the locally installed assets package nor in the on-disk cache, and streaming it through the CDN provider failed — the network request errored, the provider returned a non-OK status, or the source is `offline: true` while the file is missing from the cache.

## Example

```ts
defineDevframe({
cli: {
distDir: {
package: '@devframes/plugin-git-client',
version: '1.2.3',
},
},
})
```

Opening the tool's UI with `cdn.jsdelivr.net` unreachable throws `DF0060` for each uncached file; HTML navigations respond with a styled error page carrying this code.

## Fix

- Install the assets package locally (`npm install <package>`) to serve it with zero network — the recommended path for offline and air-gapped machines.
- Otherwise check network access to the configured provider, or point `provider` at a reachable mirror.

## Source

- [`packages/devframe/src/utils/remote-assets.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/utils/remote-assets.ts) — `createRemoteAssetsStore()`'s `serve()` throws this when a provider fetch fails, returns a non-OK status, or an `offline` store misses its cache.
43 changes: 43 additions & 0 deletions docs/errors/DF0061.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
outline: deep
---

# DF0061: Installed Assets Package Major Version Mismatch

## Message

> The locally installed "`{package}`@`{installed}`" is a different major version than the required "`{required}`".

## Cause

A remote-assets source found a locally installed copy of its assets package (resolved from the declaration's `resolveFrom` module), but the installed version differs from the declared one by a **major** version. Assets and node code are published in lockstep; across a major boundary the served UI can be incompatible with its node backend, so devframe refuses to serve it.

## Example

```ts
defineDevframe({
cli: {
distDir: {
package: '@devframes/plugin-git-client',
version: '2.0.0',
resolveFrom: import.meta.url,
},
},
})
```

With `@devframes/plugin-git-client@1.9.0` installed locally, mounting this devframe throws `DF0061`.

## Fix

Install the assets package at the version its node package declares (they are published in lockstep):

```sh
npm install @devframes/plugin-git-client@2.0.0
```

Or uninstall the stale local copy so the assets stream from the CDN back-proxy at the exact declared version.

## Source

- [`packages/devframe/src/utils/remote-assets.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/utils/remote-assets.ts) — `resolveInstalledRemoteAssets()` throws this when the installed package's major version differs from the declared one.
31 changes: 31 additions & 0 deletions docs/errors/DF0062.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
outline: deep
---

# DF0062: Installed Assets Package Version Skew

## Message

> The locally installed "`{package}`@`{installed}`" differs from the required "`{required}`" — serving the installed one.

## Cause

A remote-assets source found a locally installed copy of its assets package whose version differs from the declared one within the same major version. The local install wins — it keeps offline and air-gapped setups working — but the served assets are not byte-identical to the declared release, so the skew is surfaced.

## Example

With the node package declaring `version: '1.2.3'` and `@devframes/plugin-git-client@1.2.4` installed locally, the installed `1.2.4` assets are served and `DF0062` is reported.

## Fix

Install the exact declared version to serve byte-identical assets:

```sh
npm install @devframes/plugin-git-client@1.2.3
```

A major-version mismatch is rejected instead — see [DF0061](./DF0061.md).

## Source

- [`packages/devframe/src/utils/remote-assets.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/utils/remote-assets.ts) — `resolveInstalledRemoteAssets()` reports this when the installed version differs from the declared one within the same major.
23 changes: 23 additions & 0 deletions docs/errors/DF0063.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
outline: deep
---

# DF0063: Remote Asset Cache Write Failed

## Message

> Failed to persist a remote asset into the cache at "`{filepath}`": `{reason}`

## Cause

A remote asset streamed through the CDN back-proxy to the browser, but writing the teed copy into the local cache directory (`<project storage>/.remote-assets/<package>@<version>/…`) failed — usually a permissions problem, a full disk, or a removed `node_modules`.

The response itself was served; only caching failed, so the same file will stream through the provider again on the next request.

## Fix

Check that the project storage directory (conventionally `node_modules/.<app>/devframe/`) is writable and has free space.

## Source

- [`packages/devframe/src/utils/remote-assets.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/utils/remote-assets.ts) — `createRemoteAssetsStore()`'s background cache write reports this when persisting a fetched file fails.
30 changes: 30 additions & 0 deletions docs/errors/DF0064.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
outline: deep
---

# DF0064: Remote Assets Materialization Failed

## Message

> Failed to materialize the remote assets of "`{package}`@`{version}`": `{reason}`

## Cause

A static build (`createBuild`) with a remote-assets `distDir` needs every asset file up front — the output must be self-contained. Materialization walks the provider's file listing and downloads each file, and one of those steps failed: the provider has no `listFiles` (custom providers may omit it), the listing request failed, or an individual file download errored.

## Example

```sh
my-tool build
```

Running a static build on a machine without network access to the CDN provider — and without the assets package installed locally — throws `DF0064`.

## Fix

- Install the assets package locally (`npm install <package>@<version>`) — builds copy from the local install and touch no network.
- Otherwise ensure the provider and its file-listing API are reachable during the build, or configure a custom provider that implements `listFiles`.

## Source

- [`packages/devframe/src/utils/remote-assets.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/utils/remote-assets.ts) — `createRemoteAssetsStore()`'s `materialize()` throws this when the file listing is unavailable or a download fails.
45 changes: 45 additions & 0 deletions docs/errors/DF0065.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
outline: deep
---

# DF0065: Invalid Remote Assets Package Or Version

## Message

> Invalid remote-assets `{field}` "`{value}`".

## Cause

A remote-assets source's `package` and `version` are interpolated into CDN URLs (`https://cdn.jsdelivr.net/npm/<package>@<version>/…`) and into the on-disk cache path (`.remote-assets/<package>@<version>/`). To keep those safe and well-formed, the `package` must be a valid npm package name and the `version` an exact semver version — a value carrying path separators, `@`, whitespace, or traversal segments (`..`) is rejected.

## Example

```ts
defineDevframe({
cli: {
distDir: {
package: '@devframes/plugin-git-client',
version: '../etc', // ✗ not a semver version
},
},
})
```

## Fix

Use a valid npm package name and an exact version:

```ts
defineDevframe({
cli: {
distDir: {
package: '@devframes/plugin-git-client',
version: '1.2.3',
},
},
})
```

## Source

- [`packages/devframe/src/utils/remote-assets.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/utils/remote-assets.ts) — `resolveStaticAssetsSource()` validates a remote source before resolving it.
2 changes: 2 additions & 0 deletions examples/files-inspector/tests/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export async function startInspectorServer(
{ cwd }: { cwd: string },
): Promise<InspectorServer> {
const distDir = devframe.cli!.distDir!
if (typeof distDir !== 'string')
throw new TypeError('these tests serve the local dist directory — build the SPA first')
const basePath = devframe.basePath!
const host = '127.0.0.1'
const port = await getPort({ host, random: true })
Expand Down
2 changes: 2 additions & 0 deletions examples/next-runtime-snapshot/tests/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export interface SnapshotServer extends StartedServer {
*/
export async function startSnapshotServer(): Promise<SnapshotServer> {
const distDir = devframe.cli!.distDir!
if (typeof distDir !== 'string')
throw new TypeError('these tests serve the local dist directory — build the SPA first')
const basePath = devframe.basePath!
const host = '127.0.0.1'
const port = await getPort({ host, random: true })
Expand Down
2 changes: 2 additions & 0 deletions examples/streaming-chat/tests/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export async function startStreamingChatServer(): Promise<StartedServer & {
// tests don't need the dist (we don't call assertClientBuilt unless the
// test fetches index.html).
const distDir = devframe.cli!.distDir!
if (typeof distDir !== 'string')
throw new TypeError('these tests serve the local dist directory — build the SPA first')
const basePath = devframe.basePath!
const host = '127.0.0.1'
const port = await getPort({ host, random: true })
Expand Down
1 change: 1 addition & 0 deletions packages/devframe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"./utils/launch-editor": "./dist/utils/launch-editor.mjs",
"./utils/nanoid": "./dist/utils/nanoid.mjs",
"./utils/open": "./dist/utils/open.mjs",
"./utils/remote-assets": "./dist/utils/remote-assets.mjs",
"./utils/simple-schema": "./dist/utils/simple-schema.mjs",
"./utils/serve-static": "./dist/utils/serve-static.mjs",
"./utils/shared-state": "./dist/utils/shared-state.mjs",
Expand Down
34 changes: 24 additions & 10 deletions packages/devframe/src/adapters/build.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
/* eslint-disable no-console */
import type { DevframeDefinition } from '../types/devframe'
import type { StaticAssetsSource } from '../types/remote-assets'
import { existsSync } from 'node:fs'
import fs from 'node:fs/promises'
import process from 'node:process'
import { colors as c } from 'devframe/utils/colors'
import { resolveStaticAssetsSource } from 'devframe/utils/remote-assets'
import { structuredCloneStringify } from 'devframe/utils/structured-clone'
import { dirname, resolve } from 'pathe'
import {
Expand All @@ -21,11 +23,12 @@ export interface CreateBuildOptions {
/** Output directory. Defaults to `dist-static`. */
outDir?: string
/**
* Override the SPA dist directory to copy into `outDir`. When omitted
* the adapter reads `devframe.cli?.distDir` — authors typically set this
* once on the definition itself.
* Override the SPA dist to copy into `outDir` — a local directory or a
* remote-assets declaration (materialized in full at build time). When
* omitted the adapter reads `devframe.cli?.distDir` — authors typically
* set this once on the definition itself.
*/
distDir?: string
distDir?: StaticAssetsSource
/**
* Pretty-print RPC dump JSON files. Defaults to `false` so payload
* shards (which can be multiple MB for graph-heavy tools) ship
Expand Down Expand Up @@ -57,22 +60,33 @@ export async function createBuild(d: DevframeDefinition, options: CreateBuildOpt
throw diagnostics.DF0042({ id: d.id })

const outDir = resolve(options.outDir ?? 'dist-static')
const distDir = options.distDir ?? d.cli?.distDir
if (!distDir)
const distSource = options.distDir ?? d.cli?.distDir
if (!distSource)
throw new Error(`[devframe] createBuild: no distDir for "${d.id}". Set \`cli.distDir\` on the definition or pass it as an option.`)

if (existsSync(outDir))
await fs.rm(outDir, { recursive: true })
await fs.mkdir(outDir, { recursive: true })

// Copy author's SPA into the output root.
console.log(c.cyan`[devframe] copying SPA from ${distDir} -> ${outDir}`)
await fs.cp(distDir, outDir, { recursive: true })
const host = createH3DevframeHost({ origin: 'http://localhost', appName: d.id })

// A static deploy must be self-contained: a local dir (or a remote source
// backed by a locally installed package) is copied; an uninstalled remote
// source materializes every listed file from the provider.
const resolved = resolveStaticAssetsSource(distSource, host.getStorageDir('project'))
if (typeof resolved === 'string') {
console.log(c.cyan`[devframe] copying SPA from ${resolved} -> ${outDir}`)
await fs.cp(resolved, outDir, { recursive: true })
}
else {
console.log(c.cyan`[devframe] materializing SPA from ${resolved.assets.package}@${resolved.assets.version} -> ${outDir}`)
await resolved.materialize(outDir)
}

const ctx = await createHostContext({
cwd: process.cwd(),
mode: 'build',
host: createH3DevframeHost({ origin: 'http://localhost', appName: d.id }),
host,
})
await d.setup(ctx)

Expand Down
3 changes: 2 additions & 1 deletion packages/devframe/src/adapters/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { DevframeRpcConnection, WsOriginRegistry } from 'devframe/rpc/trans
import type { DevframeAuthHandler } from '../node/auth/handler'
import type { StartedServer } from '../node/instance-shell'
import type { DevframeDefinition, DevframeSseOptions, DevframeWsOptions, McpRouteOptions } from '../types/devframe'
import type { StaticAssetsSource } from '../types/remote-assets'
import type { DevframeNodeRpcSession, DevframeNodeRpcSessionMeta } from '../types/rpc'
import { createServer } from 'node:http'
import { open } from 'devframe/utils/open'
Expand Down Expand Up @@ -37,7 +38,7 @@ export interface CreateDevServerOptions {
* is expected to be hosted elsewhere (e.g. by a parent Vite/Nuxt
* dev server via `devframeViteBridge` from `@devframes/vite`).
*/
distDir?: string
distDir?: StaticAssetsSource
/**
* Override the SPA mount path. Defaults to
* `resolveBasePath(def, 'standalone')` (i.e. `def.basePath` or `/`).
Expand Down
Loading
Loading