@assembly-js/app-foundation 0.0.19 → 0.0.20
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/CLAUDE.md +7 -8
- package/bin/check-next-config.mjs +71 -0
- package/dist/next.d.ts +2 -0
- package/dist/next.js +25 -0
- package/package.json +5 -1
package/CLAUDE.md
CHANGED
|
@@ -188,15 +188,14 @@ Because the app runs in an iframe inside the parent dashboard, several normal Ne
|
|
|
188
188
|
- **Action buttons go in the header — set via the `useAssemblyHeader` hook.** Submit, Save, Edit, Create — any primary page-level action MUST live in the header (`primaryCta`), not a `<Button>` in the page body. Secondary actions use `secondaryCta`; overflow/tertiary actions use `actionsMenu`. Drive all of it through the `useAssemblyHeader` hook (`app/components/use-assembly-header.ts`) — **never call `AssemblyBridge.header.set*` from your own `useEffect`** (that hand-rolling is what makes header buttons vanish after a re-render). See `.claude/skills/assembly-app-bridge-header/SKILL.md` for the full API.
|
|
189
189
|
**Exception: the notification surface (`/n`).** The Notification Center embeds the `/n` route in a plain `<iframe>` without app bridge header support. `useAssemblyHeader` calls are silently ignored — header CTAs never appear. On the notification surface, render primary actions as inline `<Button>` elements in the page body. Detect the notification surface by the route — code under `app/n/` IS the surface; for a shared component pass an explicit prop from the `/n` page rather than inferring from `payload.notificationId`, which is best-effort under the session-exchange flow. This exception applies ONLY to the `/n` route; all other surfaces MUST use the header. **Do not reuse a component that calls `useAssemblyHeader` on the `/n` surface** — if it also has an inline action button, both the header CTA and the inline button will render on non-notification surfaces (duplicate CTAs). Either create a separate component for `/n` or accept a prop that switches between header and inline CTAs.
|
|
190
190
|
- **Theme + fonts.** The portal pushes the workspace's theme to the iframe via `app-bridge`. Don't hardcode brand colors; use neutrals + Tailwind defaults and let the parent's theme show through where appropriate.
|
|
191
|
-
- **`
|
|
191
|
+
- **`next.config.ts` is platform-owned — never edit it. Your Next config lives in `app.config.ts`.** The wrapper in `next.config.ts` (`withAssemblyDev` from `@assembly-js/app-foundation/next`) spreads `app.config.ts` and applies the iframe-preview invariants on top: `allowedDevOrigins` for the sandbox host (without it the app renders but has zero interactivity — hydration JavaScript is blocked cross-origin) and disabling the React debug channel (without it the preview hangs forever for users on WebSocket-blocking networks). Put your configuration in `app.config.ts`:
|
|
192
192
|
```ts
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
allowedDevOrigins: ['sb-XXXX.vercel.run'], // use $SANDBOX_HOST value
|
|
193
|
+
// app.config.ts — app-owned; the platform wrapper applies invariants on top
|
|
194
|
+
const appConfig: NextConfig = {
|
|
195
|
+
serverExternalPackages: ["sharp", "canvas", "@napi-rs/canvas"],
|
|
197
196
|
};
|
|
198
197
|
```
|
|
199
|
-
|
|
198
|
+
`bun run build` fails (`check:next-config`) if `next.config.ts` is rewritten away from the wrapper form.
|
|
200
199
|
- **Leave `instrumentation-client.ts` in place.** It's a dev-only preview helper (hides Next's red error overlay and relays build status to the builder preview) that no-ops in production. Don't remove or repurpose it.
|
|
201
200
|
- **`localStorage` works** but is scoped per-origin — the app's own Vercel domain. Useful for caching, not for sharing state with the parent.
|
|
202
201
|
- **Mobile-first layout with appropriate width.** The app renders in the iframe and must work down to 320 px. Design mobile-first: start with a single-column layout and scale up with `sm:`/`md:`/`lg:` breakpoints. **Content width depends on the app type:** most apps (tables, dashboards, boards) should use the full available width. Form-centric, single-column, or text-heavy apps that would look sparse at full width should center their content in a narrower column (`max-w-3xl mx-auto`). If the task description or plan specifies a content width, follow it; otherwise choose whichever looks better for the content. **When narrowing, place the `max-w-*` wrapper as the outermost page-level container** — outside `Tabs`, filter bars, and all other page chrome — so everything aligns to the same column. Do not narrow only the inner content of each tab panel while leaving `TabsList` or other siblings full-width.
|
|
@@ -327,7 +326,7 @@ Server components, route handlers, and server actions run in **Node on Vercel**
|
|
|
327
326
|
|
|
328
327
|
- **Visual work** (draw a chart, render on a canvas) is **client** work — load the library with `next/dynamic` + `ssr: false`, never in server code.
|
|
329
328
|
- **Fuzzy extraction** (pull fields from a document, classify, summarize) is usually better done through the **AI proxy** (see `assembly-ai`) than a parsing lib — no native binding, no serverless hazard.
|
|
330
|
-
- **Genuine server-side data work** (transform an image, parse bytes) must use a Node-native library: add native/CommonJS packages (e.g. `sharp`) to `serverExternalPackages` in `
|
|
329
|
+
- **Genuine server-side data work** (transform an image, parse bytes) must use a Node-native library: add native/CommonJS packages (e.g. `sharp`) to `serverExternalPackages` in `app.config.ts` (bundling breaks the binding), and give any dep that's loaded dynamically or transitively a static, string-literal import in server code — the deploy tracer can't follow dynamic requires, so such a package silently won't exist in the deployed function even though preview works.
|
|
331
330
|
- **Browser-only packages** (touch `window`/`document`/`DOMMatrix`/`HTMLCanvasElement` at import *or during operations*) can't run server-side at all — `serverExternalPackages` doesn't help and dynamic `import()` only defers the crash. Use a Node-native alternative or move the work client-side.
|
|
332
331
|
|
|
333
332
|
---
|
|
@@ -1124,7 +1123,7 @@ Use `AssemblyBridge.header.*` for breadcrumbs, CTAs, and actions menu. See `.cla
|
|
|
1124
1123
|
- ❌ Don't hand-roll UI components (`<button>`, `<input>`, `<dialog>`, `<table>`, etc.) when a shadcn component exists. See the Design system section for install instructions. Using `Link`/`<a className={buttonVariants(...)}>` for a button-looking link is the intentional exception.
|
|
1125
1124
|
|
|
1126
1125
|
- ❌ Don't add a manual close button (a `<DialogClose>` render or any X button) inside `DialogContent`. `DialogContent` already ships a built-in close button (`showCloseButton` defaults to `true`); adding another produces two overlapping X icons in the top-right corner. Only pass `showCloseButton={false}` when a custom affordance genuinely replaces the built-in one.
|
|
1127
|
-
- ❌ Don't
|
|
1126
|
+
- ❌ Don't edit `next.config.ts` — it's the platform-owned wrapper that keeps the preview working (cross-origin hydration, WebSocket-blocked networks), and `bun run build` fails if it's rewritten. Your Next configuration belongs in `app.config.ts`.
|
|
1128
1127
|
- ❌ Don't import a native or browser-only library into server code without the serverless setup — `bun run build` passes but the deployed route crashes at request time. Decide the layer first per "Server runs in serverless Node" above.
|
|
1129
1128
|
- ❌ Don't use `Select` or `Combobox` to pick any Assembly person or organization record (clients, internal users, team members, staff, companies). Always use `UserCompanySelector` — in forms, filters, dropdowns, "assign to" controls, everywhere.
|
|
1130
1129
|
- ❌ Don't use `Combobox` when the user asks for a "dropdown" or "select" — use `Select`. Only use `Combobox` when the user explicitly needs type-to-search/filter functionality for non-user/non-company data.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Fails the build when next.config has been rewritten away from the
|
|
3
|
+
// platform-owned wrapper form. Dependency-free on purpose: it runs inside the
|
|
4
|
+
// generated app's sandbox before any deps beyond Node exist.
|
|
5
|
+
//
|
|
6
|
+
// next.config.ts is platform boilerplate: `export default withAssemblyDev(appConfig)`.
|
|
7
|
+
// The wrapper (from @assembly-js/app-foundation/next) applies the dev-preview
|
|
8
|
+
// invariants — allowedDevOrigins from the sandbox host, and disabling the React
|
|
9
|
+
// debug channel, whose loss silently hangs hydration forever on Next 16.3+ when
|
|
10
|
+
// the dev WebSocket can't connect. Agents legitimately need Next config
|
|
11
|
+
// (serverExternalPackages etc.) — that lives in app.config.ts, which the
|
|
12
|
+
// wrapper spreads. Prose rules get skipped; a failing build doesn't.
|
|
13
|
+
//
|
|
14
|
+
// An app without app.config.ts predates the wrapper split (cloned from an older
|
|
15
|
+
// foundation). There the wrapper form was never the contract, so this check
|
|
16
|
+
// exits 0 — those apps are protected by the boot/restart patch scripts instead.
|
|
17
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
|
|
20
|
+
const ROOT = process.cwd();
|
|
21
|
+
const CONFIG_FILES = ['next.config.ts', 'next.config.mjs', 'next.config.js'];
|
|
22
|
+
const APP_CONFIG_FILES = ['app.config.ts', 'app.config.mjs', 'app.config.js'];
|
|
23
|
+
|
|
24
|
+
if (!APP_CONFIG_FILES.some((f) => existsSync(join(ROOT, f)))) {
|
|
25
|
+
process.exit(0); // pre-wrapper app: not this check's contract
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const present = CONFIG_FILES.filter((f) => existsSync(join(ROOT, f)));
|
|
29
|
+
|
|
30
|
+
// A second config file shadows the wrapper: Next loads only one of them, and
|
|
31
|
+
// its pick may be the unwrapped copy — so validating just the canonical file
|
|
32
|
+
// would bless a config the server never runs.
|
|
33
|
+
if (present.length > 1) {
|
|
34
|
+
console.error(
|
|
35
|
+
`check-next-config: multiple next.config files found (${present.join(', ')}). ` +
|
|
36
|
+
'next.config.ts is the platform-owned wrapper — delete the other config file(s) ' +
|
|
37
|
+
'and put your Next configuration in app.config.ts instead.',
|
|
38
|
+
);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const configFile = present[0];
|
|
43
|
+
if (!configFile) {
|
|
44
|
+
console.error(
|
|
45
|
+
'check-next-config: no next.config.{ts,mjs,js} found. Restore the platform-owned wrapper:\n\n' +
|
|
46
|
+
' import { withAssemblyDev } from "@assembly-js/app-foundation/next";\n' +
|
|
47
|
+
' import appConfig from "./app.config";\n' +
|
|
48
|
+
' export default withAssemblyDev(appConfig);\n',
|
|
49
|
+
);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const content = readFileSync(join(ROOT, configFile), 'utf8');
|
|
54
|
+
const importsWrapper = /from\s+['"]@assembly-js\/app-foundation\/next['"]/.test(content);
|
|
55
|
+
const exportsWrapped = /export\s+default\s+withAssemblyDev\s*\(/.test(content);
|
|
56
|
+
|
|
57
|
+
if (importsWrapper && exportsWrapped) {
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.error(
|
|
62
|
+
`check-next-config: ${configFile} is platform-owned and must stay in the wrapper form:\n\n` +
|
|
63
|
+
' import { withAssemblyDev } from "@assembly-js/app-foundation/next";\n' +
|
|
64
|
+
' import appConfig from "./app.config";\n' +
|
|
65
|
+
' export default withAssemblyDev(appConfig);\n\n' +
|
|
66
|
+
'Put your Next configuration (serverExternalPackages, experimental flags, …) in app.config.ts — ' +
|
|
67
|
+
'the wrapper spreads it and applies the Assembly dev-preview invariants on top. ' +
|
|
68
|
+
'Editing next.config.ts directly drops those invariants and permanently breaks the preview ' +
|
|
69
|
+
'for users on WebSocket-blocking networks.',
|
|
70
|
+
);
|
|
71
|
+
process.exit(1);
|
package/dist/next.d.ts
ADDED
package/dist/next.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function withAssemblyDev(config = {}) {
|
|
2
|
+
// Production builds (deploys) have no dev server, no HMR socket, and no
|
|
3
|
+
// sandbox host — the wrapper is a pass-through there.
|
|
4
|
+
if (process.env.NODE_ENV === 'production')
|
|
5
|
+
return config;
|
|
6
|
+
const sandboxHost = process.env.SANDBOX_HOST;
|
|
7
|
+
const allowedDevOrigins = sandboxHost
|
|
8
|
+
? [...new Set([sandboxHost, ...(config.allowedDevOrigins ?? [])])]
|
|
9
|
+
: config.allowedDevOrigins;
|
|
10
|
+
// A dev WebSocket that never connects would otherwise hang hydration forever
|
|
11
|
+
// on Next 16.3+. Debug data falls back to the flight stream, so
|
|
12
|
+
// nothing user-visible is lost. Cast because this package type-checks against
|
|
13
|
+
// its Next 15 devDependency, which predates the flag; runtimes that predate
|
|
14
|
+
// it ignore the unknown key.
|
|
15
|
+
const experimental = {
|
|
16
|
+
...config.experimental,
|
|
17
|
+
reactDebugChannel: false,
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
...config,
|
|
21
|
+
...(allowedDevOrigins ? { allowedDevOrigins } : {}),
|
|
22
|
+
devIndicators: config.devIndicators ?? false,
|
|
23
|
+
experimental,
|
|
24
|
+
};
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assembly-js/app-foundation",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"description": "Versioned foundation helpers and build context for Assembly generated apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
"./lib-client": {
|
|
23
23
|
"types": "./dist/lib-client.d.ts",
|
|
24
24
|
"default": "./dist/lib-client.js"
|
|
25
|
+
},
|
|
26
|
+
"./next": {
|
|
27
|
+
"types": "./dist/next.d.ts",
|
|
28
|
+
"default": "./dist/next.js"
|
|
25
29
|
}
|
|
26
30
|
},
|
|
27
31
|
"scripts": {
|