@assembly-js/app-foundation 0.2.7 → 0.3.1
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/CHANGELOG.md +7 -0
- package/CLAUDE.md +3 -0
- package/bin/foundation-exports.mjs +1 -1
- package/dist/bridge-vnext.d.ts +15 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +2 -0
- package/dist/location-prefix.d.ts +19 -0
- package/dist/location-prefix.js +60 -0
- package/dist/location-reporter.d.ts +1 -0
- package/dist/location-reporter.js +46 -0
- package/package.json +2 -1
- package/skills/assembly-app-inner-location/SKILL.md +19 -0
- package/skills/assembly-platform-links/SKILL.md +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -19,6 +19,13 @@ A patch release never changes what an app's build requires; a minor release may
|
|
|
19
19
|
not by hand; see `changes/README.md`. CI fails a PR that changes a check script without a
|
|
20
20
|
`bump: minor` change file.
|
|
21
21
|
|
|
22
|
+
## 0.3.1
|
|
23
|
+
- [guidance] The build agent now upgrades an app's bridge to 2.1.0 and its foundation package instead of skipping a deep link, mounts the location reporter, and deep-links notifications to the specific record.
|
|
24
|
+
- [new] App destinations in `NavigateDestination` now accept an optional `path` for links to an app's inner page.
|
|
25
|
+
|
|
26
|
+
## 0.3.0
|
|
27
|
+
- [new] `LocationReporter` is exported from `@assembly-js/app-foundation/client`, so apps born before the deeplink image can mount it in their layout and report their inner location to the host with the same surface-prefix handling as new apps.
|
|
28
|
+
|
|
22
29
|
## 0.2.7
|
|
23
30
|
- [guidance] The build agent learns to call another app through its generated client and to show an empty state naming that app when the call is refused.
|
|
24
31
|
|
package/CLAUDE.md
CHANGED
|
@@ -48,6 +48,9 @@ Assembly-specific gotchas live in `.claude/skills/<name>/SKILL.md`. Invoke them
|
|
|
48
48
|
<!-- feature:deep-link-bridge-nav -->
|
|
49
49
|
- `assembly-platform-links` — link platform records (client, company, invoice, contract, form, file) to their portal detail page via `linkTo()`; never hand-build a host URL. Also links between workspace apps by install id. Check the app's installed app-bridge first (see the skill) — `navigateTo` doesn't exist on older peers — and only link staff-only destinations (client, company, form, etc.) from the dashboard surface, never the client portal.
|
|
50
50
|
<!-- /feature:deep-link-bridge-nav -->
|
|
51
|
+
<!-- feature:deep-link-app-inner-location -->
|
|
52
|
+
- `assembly-app-inner-location` — mount `<LocationReporter />` so the host mirrors the app's inner page in its address bar; deep-link a notification or link to a specific record with `linkTo({ to: 'app', installId, path })`, never to the app root. Never hand-write location reporting.
|
|
53
|
+
<!-- /feature:deep-link-app-inner-location -->
|
|
51
54
|
<!-- feature:cross-app-apis -->
|
|
52
55
|
- `assembly-app-clients` — calling another workspace app through its generated client in `generated/apps/<appId>/` (`installerClient`, by verb and path), and rendering an empty state that names the app when the call is refused instead of a 500.
|
|
53
56
|
<!-- /feature:cross-app-apis -->
|
|
@@ -27,7 +27,7 @@ export const NON_CLIENT_EXPORTS = new Set([
|
|
|
27
27
|
'ConnectionStatus', 'ConnectorError', 'ContractDocumentError', 'ConnectorErrorCode', 'CookieOutcome', 'CronHandler',
|
|
28
28
|
'DEFAULT_LABELS', 'EXCHANGE_REJECTION_HEADER', 'ExchangeRejectedViewer', 'ExchangedSession',
|
|
29
29
|
'FormatAirtableRecordsOpts', 'GenerateAppClientInput', 'GeneratedAppClient', 'GmailSendArgs', 'GmailSendResult', 'IntegrationClient',
|
|
30
|
-
'InternalUserAccess', 'LEGACY_APP_IDS', 'MintFailure', 'MintOutcome', 'MintSessionOptions', 'MintedAppBearer',
|
|
30
|
+
'InternalUserAccess', 'LEGACY_APP_IDS', 'LocationReporter', 'MintFailure', 'MintOutcome', 'MintSessionOptions', 'MintedAppBearer',
|
|
31
31
|
'MintedBearer', 'NETWORK_CAPTURE_MESSAGE_TYPE', 'NetworkCaptureHeaderFingerprint',
|
|
32
32
|
'NetworkCaptureRecord', 'PAINTED_BEACON_EVENT', 'PAINTED_BEACON_PATH',
|
|
33
33
|
'PORTAL_ORIGIN_COOKIE_NAME', 'PdfText', 'PrincipalAllow', 'PrincipalKind',
|
package/dist/bridge-vnext.d.ts
CHANGED
|
@@ -48,6 +48,7 @@ export type NavigateDestination = {
|
|
|
48
48
|
} | {
|
|
49
49
|
to: 'app';
|
|
50
50
|
installId: string;
|
|
51
|
+
path?: string;
|
|
51
52
|
};
|
|
52
53
|
export type NavigateToOptions = {
|
|
53
54
|
replace?: boolean;
|
|
@@ -71,3 +72,17 @@ declare module '@assembly-js/app-bridge' {
|
|
|
71
72
|
navigateTo(destination: NavigateDestination, options?: NavigateToOptions): Promise<NavigateResult>;
|
|
72
73
|
}
|
|
73
74
|
}
|
|
75
|
+
export type LocationNavigatePayload = {
|
|
76
|
+
path: string;
|
|
77
|
+
search: string;
|
|
78
|
+
replace?: boolean;
|
|
79
|
+
};
|
|
80
|
+
export interface LocationBridge {
|
|
81
|
+
report(path: string, search: string, replace?: boolean): void;
|
|
82
|
+
onNavigate(handler: (payload: LocationNavigatePayload) => void): void;
|
|
83
|
+
}
|
|
84
|
+
declare module '@assembly-js/app-bridge' {
|
|
85
|
+
interface AssemblyBridgeApi {
|
|
86
|
+
location: LocationBridge;
|
|
87
|
+
}
|
|
88
|
+
}
|
package/dist/client.d.ts
CHANGED
package/dist/client.js
CHANGED
|
@@ -23,3 +23,5 @@ export * from './theme.js';
|
|
|
23
23
|
// at compile time, but a value import from ./server would trip the server-only
|
|
24
24
|
// guard for no reason.
|
|
25
25
|
export * from './resource-resolution.js';
|
|
26
|
+
// Browser-safe: the reporter pulls in no Node built-ins, unlike ./next.
|
|
27
|
+
export { LocationReporter } from './location-reporter.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare function getSurfacePrefixes(): string[];
|
|
2
|
+
export declare const SURFACE_PREFIXES: string[];
|
|
3
|
+
/**
|
|
4
|
+
* Detect which surface prefix the given pathname belongs to.
|
|
5
|
+
* Returns the prefix string (e.g. '/c') or '' for the internal surface.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getSurfacePrefix(pathname: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Remove a surface prefix from a pathname, returning an app-local path.
|
|
10
|
+
* '/c/orders/42' with prefix '/c' → '/orders/42'
|
|
11
|
+
* '/c' with prefix '/c' → '/'
|
|
12
|
+
*/
|
|
13
|
+
export declare function stripPrefix(pathname: string, prefix: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Re-attach a surface prefix to an app-local path received from the host.
|
|
16
|
+
* '/orders/42' with prefix '/c' → '/c/orders/42'
|
|
17
|
+
* '/' with prefix '/c' → '/c'
|
|
18
|
+
*/
|
|
19
|
+
export declare function addPrefix(prefix: string, path: string): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const DEFAULT_PREFIXES = ['/c', '/n'];
|
|
2
|
+
// Read surface prefixes from the build-time env value injected by withAssemblyDev.
|
|
3
|
+
// Falls back to the historical /c and /n defaults so apps on an older image
|
|
4
|
+
// continue to work without a rebuild.
|
|
5
|
+
export function getSurfacePrefixes() {
|
|
6
|
+
const raw = process.env.NEXT_PUBLIC_ASSEMBLY_SURFACE_PREFIXES;
|
|
7
|
+
if (!raw)
|
|
8
|
+
return DEFAULT_PREFIXES;
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(raw);
|
|
11
|
+
const prefixes = parsed
|
|
12
|
+
.filter((p) => typeof p === 'string' && p !== '/')
|
|
13
|
+
.map((p) => p.replace(/\/+$/, ''));
|
|
14
|
+
if (prefixes.length === 0)
|
|
15
|
+
return DEFAULT_PREFIXES;
|
|
16
|
+
// Longest first so /clients never matches as /c + "lients".
|
|
17
|
+
return [...prefixes].sort((a, b) => b.length - a.length);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return DEFAULT_PREFIXES;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Kept for backwards compatibility; callers that import this directly still work.
|
|
24
|
+
export const SURFACE_PREFIXES = getSurfacePrefixes();
|
|
25
|
+
/**
|
|
26
|
+
* Detect which surface prefix the given pathname belongs to.
|
|
27
|
+
* Returns the prefix string (e.g. '/c') or '' for the internal surface.
|
|
28
|
+
*/
|
|
29
|
+
export function getSurfacePrefix(pathname) {
|
|
30
|
+
for (const prefix of getSurfacePrefixes()) {
|
|
31
|
+
if (pathname === prefix || pathname.startsWith(`${prefix}/`)) {
|
|
32
|
+
return prefix;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Remove a surface prefix from a pathname, returning an app-local path.
|
|
39
|
+
* '/c/orders/42' with prefix '/c' → '/orders/42'
|
|
40
|
+
* '/c' with prefix '/c' → '/'
|
|
41
|
+
*/
|
|
42
|
+
export function stripPrefix(pathname, prefix) {
|
|
43
|
+
if (!prefix)
|
|
44
|
+
return pathname;
|
|
45
|
+
if (pathname === prefix)
|
|
46
|
+
return '/';
|
|
47
|
+
return pathname.slice(prefix.length) || '/';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Re-attach a surface prefix to an app-local path received from the host.
|
|
51
|
+
* '/orders/42' with prefix '/c' → '/c/orders/42'
|
|
52
|
+
* '/' with prefix '/c' → '/c'
|
|
53
|
+
*/
|
|
54
|
+
export function addPrefix(prefix, path) {
|
|
55
|
+
if (!prefix)
|
|
56
|
+
return path;
|
|
57
|
+
if (path === '/')
|
|
58
|
+
return prefix;
|
|
59
|
+
return `${prefix}${path}`;
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function LocationReporter(): import("react").FunctionComponentElement<import("react").SuspenseProps>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Suspense, createElement, useEffect, useRef } from 'react';
|
|
3
|
+
import { usePathname, useSearchParams, useRouter } from 'next/navigation';
|
|
4
|
+
import { AssemblyBridge } from '@assembly-js/app-bridge';
|
|
5
|
+
import { getSurfacePrefix, stripPrefix, addPrefix } from './location-prefix.js';
|
|
6
|
+
function LocationReporterInner() {
|
|
7
|
+
const pathname = usePathname();
|
|
8
|
+
const searchParams = useSearchParams();
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
// Mutable ref so the onNavigate closure always sees the current prefix
|
|
11
|
+
// without needing to re-register the handler on every pathname change.
|
|
12
|
+
const prefixRef = useRef('');
|
|
13
|
+
const prefix = getSurfacePrefix(pathname);
|
|
14
|
+
prefixRef.current = prefix;
|
|
15
|
+
// Bridges below 2.1 type `location` (see bridge-vnext.ts) but lack it at runtime.
|
|
16
|
+
const bridgeLocation = AssemblyBridge.location;
|
|
17
|
+
// Report location on every client-side route change.
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!bridgeLocation)
|
|
20
|
+
return;
|
|
21
|
+
const path = stripPrefix(pathname, prefix);
|
|
22
|
+
const qs = searchParams.toString();
|
|
23
|
+
bridgeLocation.report(path, qs ? `?${qs}` : '');
|
|
24
|
+
}, [bridgeLocation, pathname, searchParams, prefix]);
|
|
25
|
+
// Register the navigate handler once; rely on prefixRef for the current prefix.
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!bridgeLocation)
|
|
28
|
+
return;
|
|
29
|
+
bridgeLocation.onNavigate(({ path, search, replace }) => {
|
|
30
|
+
const dest = addPrefix(prefixRef.current, path) + (search ?? '');
|
|
31
|
+
if (replace) {
|
|
32
|
+
router.replace(dest);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
router.push(dest);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}, [bridgeLocation, router]);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
// useSearchParams requires a Suspense boundary to avoid de-opting the route
|
|
42
|
+
// to full client-side rendering in Next.js App Router. createElement keeps
|
|
43
|
+
// the package free of a jsx compiler setting.
|
|
44
|
+
export function LocationReporter() {
|
|
45
|
+
return createElement(Suspense, { fallback: null }, createElement(LocationReporterInner));
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assembly-js/app-foundation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Versioned foundation helpers and build context for Assembly generated apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"@neondatabase/serverless": "^1.1.0",
|
|
57
57
|
"@types/bun": "^1.1.0",
|
|
58
58
|
"@types/node": "^22",
|
|
59
|
+
"@types/react": "^19.3.0",
|
|
59
60
|
"drizzle-orm": "^0.45.2",
|
|
60
61
|
"esbuild": "^0.25.12",
|
|
61
62
|
"next": "^15.5.0",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!-- feature:deep-link-app-inner-location -->
|
|
2
|
+
---
|
|
3
|
+
name: assembly-app-inner-location
|
|
4
|
+
description: Use whenever the app has inner pages the host should mirror in its address bar, or when a notification, email or link should open this app on a specific record instead of its root. Covers mounting LocationReporter and pointing notifications at a specific record.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What the reporter does
|
|
8
|
+
|
|
9
|
+
The host mirrors the app's inner page in its own address bar and can open the app straight at an inner page. That only works if the app reports its location, and `<LocationReporter />` from the foundation package does exactly that. The reporter needs `AssemblyBridge.location`, which ships in app-bridge 2.1.0, and a `navigateTo`-only 2.0.0 bridge lacks it. Check with `grep -q onNavigate node_modules/@assembly-js/app-bridge/dist/index.d.ts && echo supported || echo unsupported`; if `unsupported`, run `bun add @assembly-js/app-bridge@2.1.0` (exact stable release, never a prerelease or range), then `bunx tsc --noEmit`, before mounting the reporter.
|
|
10
|
+
|
|
11
|
+
- Mount `<LocationReporter />` once, in `app/layout.tsx`, imported from `@assembly-js/app-foundation/client`. If it is already there, leave it.
|
|
12
|
+
- Never write custom `location.report` or `location.onNavigate` code, and never hand-write a `LocationSync` component. The reporter already handles surface prefixes and route changes.
|
|
13
|
+
|
|
14
|
+
## Notifications deep-link to the record, not the app root
|
|
15
|
+
|
|
16
|
+
When the app sends an email or in-product notification about a specific record (a document request, a submission), the destination is set by the notification's `ctaParams` and surface, not by `linkTo`. Pass `ctaParams` that identify the record (`ctaParams: { requestId: request.id }`) and render that record from an existing inner route of this app, one that works when opened cold. Follow `assembly-notifications` for the send-and-open pattern.
|
|
17
|
+
|
|
18
|
+
To link from a button inside the app to an inner page, use `linkTo` as described in `assembly-platform-links` (the `app` destination takes an optional `path`).
|
|
19
|
+
<!-- /feature:deep-link-app-inner-location -->
|
|
@@ -14,7 +14,13 @@ Before writing any `linkTo()` or `AssemblyBridge.navigateTo()` call, check what
|
|
|
14
14
|
grep -q navigateTo node_modules/@assembly-js/app-bridge/dist/index.d.ts && echo supported || echo unsupported
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
If it reports `unsupported`,
|
|
17
|
+
If it reports `unsupported`, upgrade the app in this same turn instead of skipping the link:
|
|
18
|
+
|
|
19
|
+
1. Confirm the app is on the foundation package: `grep -q app-foundation package.json`. If it is not (vendored `lib/`), say the security update must land first and stop — do not attempt the migration in a feature turn.
|
|
20
|
+
2. Pin the bridge to the exact stable release: `bun add @assembly-js/app-bridge@2.1.0`. Never pin a prerelease (`-beta`, `-rc`, `-next`) or a range.
|
|
21
|
+
3. Bump the foundation dependency listed in `package.json` to `^0.3.0`, the floor that exports `linkTo` and `LocationReporter` (`bun add <that package name>@^0.3.0`).
|
|
22
|
+
4. Run `bunx tsc --noEmit` and `bun run build`, and confirm the existing header and toast bridge calls still type-check. Fix any break before continuing.
|
|
23
|
+
5. Re-run the check above, then proceed with this skill.
|
|
18
24
|
|
|
19
25
|
## When to reach for this skill
|
|
20
26
|
|
|
@@ -74,7 +80,7 @@ The host rejects staff-only destinations when the viewer is a client in the port
|
|
|
74
80
|
| `article` | `articleId` | — | Yes |
|
|
75
81
|
| `files` | — | `channelId` | Yes |
|
|
76
82
|
| `channel` | — | `channelId` | Yes |
|
|
77
|
-
| `app` | `installId` |
|
|
83
|
+
| `app` | `installId` | `path` | Yes, if the target app is installed for that viewer |
|
|
78
84
|
| `home` | — | — | No — staff only |
|
|
79
85
|
| `product` | — | — | No — staff only |
|
|
80
86
|
| `helpdesk` | — | — | Yes |
|