@lepsto/sdk-app 84.0.0
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/README.md +233 -0
- package/dist/_types/gen/analytics/index.d.ts +2 -0
- package/dist/_types/gen/analytics/queryOptions.gen.d.ts +74 -0
- package/dist/_types/gen/bindings.gen.d.ts +3 -0
- package/dist/_types/gen/brain/index.d.ts +2 -0
- package/dist/_types/gen/brain/queryOptions.gen.d.ts +166 -0
- package/dist/_types/gen/client.gen.d.ts +1087 -0
- package/dist/_types/gen/consent/index.d.ts +2 -0
- package/dist/_types/gen/consent/queryOptions.gen.d.ts +62 -0
- package/dist/_types/gen/content/index.d.ts +2 -0
- package/dist/_types/gen/content/queryOptions.gen.d.ts +126 -0
- package/dist/_types/gen/deployment/index.d.ts +2 -0
- package/dist/_types/gen/deployment/queryOptions.gen.d.ts +438 -0
- package/dist/_types/gen/mail/index.d.ts +2 -0
- package/dist/_types/gen/mail/queryOptions.gen.d.ts +206 -0
- package/dist/_types/gen/manifest.gen.d.ts +4 -0
- package/dist/_types/gen/observe/index.d.ts +2 -0
- package/dist/_types/gen/observe/queryOptions.gen.d.ts +142 -0
- package/dist/_types/gen/organization/index.d.ts +2 -0
- package/dist/_types/gen/organization/queryOptions.gen.d.ts +362 -0
- package/dist/_types/gen/realtime/index.d.ts +2 -0
- package/dist/_types/gen/realtime/queryOptions.gen.d.ts +114 -0
- package/dist/_types/gen/support/index.d.ts +2 -0
- package/dist/_types/gen/support/queryOptions.gen.d.ts +206 -0
- package/dist/_types/gen/tracking/index.d.ts +2 -0
- package/dist/_types/gen/tracking/queryOptions.gen.d.ts +150 -0
- package/dist/_types/gen/types.gen.d.ts +126011 -0
- package/dist/_types/gen/users/index.d.ts +2 -0
- package/dist/_types/gen/users/queryOptions.gen.d.ts +182 -0
- package/dist/_types/gen/waitlist/index.d.ts +2 -0
- package/dist/_types/gen/waitlist/queryOptions.gen.d.ts +30 -0
- package/dist/_types/gen/workflow/index.d.ts +2 -0
- package/dist/_types/gen/workflow/queryOptions.gen.d.ts +166 -0
- package/dist/_types/index.d.ts +11 -0
- package/dist/_types/package.json +1 -0
- package/dist/_types/react/index.d.ts +33 -0
- package/dist/_types/runtime/access-reason.d.ts +27 -0
- package/dist/_types/runtime/access.d.ts +40 -0
- package/dist/_types/runtime/compositions.d.ts +16 -0
- package/dist/_types/runtime/connectStream.d.ts +15 -0
- package/dist/_types/runtime/createLesslyApp.d.ts +10 -0
- package/dist/_types/runtime/csrf.d.ts +1 -0
- package/dist/_types/runtime/errors.d.ts +18 -0
- package/dist/_types/runtime/name.d.ts +26 -0
- package/dist/_types/runtime/operations.d.ts +13 -0
- package/dist/_types/runtime/permission-match.d.ts +62 -0
- package/dist/_types/runtime/request.d.ts +9 -0
- package/dist/_types/runtime/types.d.ts +129 -0
- package/dist/analytics/index.cjs +96 -0
- package/dist/analytics/index.cjs.map +1 -0
- package/dist/analytics/index.d.cts +1 -0
- package/dist/analytics/index.d.ts +1 -0
- package/dist/analytics/index.js +77 -0
- package/dist/analytics/index.js.map +1 -0
- package/dist/brain/index.cjs +211 -0
- package/dist/brain/index.cjs.map +1 -0
- package/dist/brain/index.d.cts +1 -0
- package/dist/brain/index.d.ts +1 -0
- package/dist/brain/index.js +169 -0
- package/dist/brain/index.js.map +1 -0
- package/dist/consent/index.cjs +81 -0
- package/dist/consent/index.cjs.map +1 -0
- package/dist/consent/index.d.cts +1 -0
- package/dist/consent/index.d.ts +1 -0
- package/dist/consent/index.js +65 -0
- package/dist/consent/index.js.map +1 -0
- package/dist/content/index.cjs +161 -0
- package/dist/content/index.cjs.map +1 -0
- package/dist/content/index.d.cts +1 -0
- package/dist/content/index.d.ts +1 -0
- package/dist/content/index.js +129 -0
- package/dist/content/index.js.map +1 -0
- package/dist/deployment/index.cjs +551 -0
- package/dist/deployment/index.cjs.map +1 -0
- package/dist/deployment/index.d.cts +1 -0
- package/dist/deployment/index.d.ts +1 -0
- package/dist/deployment/index.js +441 -0
- package/dist/deployment/index.js.map +1 -0
- package/dist/index.cjs +12565 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12557 -0
- package/dist/index.js.map +1 -0
- package/dist/mail/index.cjs +261 -0
- package/dist/mail/index.cjs.map +1 -0
- package/dist/mail/index.d.cts +1 -0
- package/dist/mail/index.d.ts +1 -0
- package/dist/mail/index.js +209 -0
- package/dist/mail/index.js.map +1 -0
- package/dist/observe/index.cjs +181 -0
- package/dist/observe/index.cjs.map +1 -0
- package/dist/observe/index.d.cts +1 -0
- package/dist/observe/index.d.ts +1 -0
- package/dist/observe/index.js +145 -0
- package/dist/observe/index.js.map +1 -0
- package/dist/organization/index.cjs +456 -0
- package/dist/organization/index.cjs.map +1 -0
- package/dist/organization/index.d.cts +1 -0
- package/dist/organization/index.d.ts +1 -0
- package/dist/organization/index.js +365 -0
- package/dist/organization/index.js.map +1 -0
- package/dist/react/index.cjs +29 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +1 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +26 -0
- package/dist/react/index.js.map +1 -0
- package/dist/realtime/index.cjs +146 -0
- package/dist/realtime/index.cjs.map +1 -0
- package/dist/realtime/index.d.cts +1 -0
- package/dist/realtime/index.d.ts +1 -0
- package/dist/realtime/index.js +117 -0
- package/dist/realtime/index.js.map +1 -0
- package/dist/support/index.cjs +261 -0
- package/dist/support/index.cjs.map +1 -0
- package/dist/support/index.d.cts +1 -0
- package/dist/support/index.d.ts +1 -0
- package/dist/support/index.js +209 -0
- package/dist/support/index.js.map +1 -0
- package/dist/tracking/index.cjs +191 -0
- package/dist/tracking/index.cjs.map +1 -0
- package/dist/tracking/index.d.cts +1 -0
- package/dist/tracking/index.d.ts +1 -0
- package/dist/tracking/index.js +153 -0
- package/dist/tracking/index.js.map +1 -0
- package/dist/users/index.cjs +231 -0
- package/dist/users/index.cjs.map +1 -0
- package/dist/users/index.d.cts +1 -0
- package/dist/users/index.d.ts +1 -0
- package/dist/users/index.js +185 -0
- package/dist/users/index.js.map +1 -0
- package/dist/waitlist/index.cjs +41 -0
- package/dist/waitlist/index.cjs.map +1 -0
- package/dist/waitlist/index.d.cts +1 -0
- package/dist/waitlist/index.d.ts +1 -0
- package/dist/waitlist/index.js +33 -0
- package/dist/waitlist/index.js.map +1 -0
- package/dist/workflow/index.cjs +211 -0
- package/dist/workflow/index.cjs.map +1 -0
- package/dist/workflow/index.d.cts +1 -0
- package/dist/workflow/index.d.ts +1 -0
- package/dist/workflow/index.js +169 -0
- package/dist/workflow/index.js.map +1 -0
- package/docs/README.md +89 -0
- package/docs/recipes/access.md +781 -0
- package/docs/recipes/federation.md +285 -0
- package/docs/recipes/local-dev.md +111 -0
- package/docs/recipes/sdk-usage.md +194 -0
- package/docs/rules.md +522 -0
- package/package.json +212 -0
- package/src/gen/analytics/index.ts +3 -0
- package/src/gen/analytics/queryOptions.gen.ts +124 -0
- package/src/gen/bindings.gen.ts +11330 -0
- package/src/gen/brain/index.ts +3 -0
- package/src/gen/brain/queryOptions.gen.ts +278 -0
- package/src/gen/client.gen.ts +2288 -0
- package/src/gen/consent/index.ts +3 -0
- package/src/gen/consent/queryOptions.gen.ts +102 -0
- package/src/gen/content/index.ts +3 -0
- package/src/gen/content/queryOptions.gen.ts +202 -0
- package/src/gen/deployment/index.ts +3 -0
- package/src/gen/deployment/queryOptions.gen.ts +707 -0
- package/src/gen/mail/index.ts +3 -0
- package/src/gen/mail/queryOptions.gen.ts +331 -0
- package/src/gen/manifest.gen.ts +780 -0
- package/src/gen/observe/index.ts +3 -0
- package/src/gen/observe/queryOptions.gen.ts +235 -0
- package/src/gen/organization/index.ts +3 -0
- package/src/gen/organization/queryOptions.gen.ts +578 -0
- package/src/gen/realtime/index.ts +3 -0
- package/src/gen/realtime/queryOptions.gen.ts +185 -0
- package/src/gen/support/index.ts +3 -0
- package/src/gen/support/queryOptions.gen.ts +331 -0
- package/src/gen/tracking/index.ts +3 -0
- package/src/gen/tracking/queryOptions.gen.ts +249 -0
- package/src/gen/types.gen.ts +119506 -0
- package/src/gen/users/index.ts +3 -0
- package/src/gen/users/queryOptions.gen.ts +295 -0
- package/src/gen/waitlist/index.ts +3 -0
- package/src/gen/waitlist/queryOptions.gen.ts +50 -0
- package/src/gen/workflow/index.ts +3 -0
- package/src/gen/workflow/queryOptions.gen.ts +274 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# Recipe: Module Federation setup
|
|
2
|
+
|
|
3
|
+
How to wire the Module Federation contract an App must implement (APP-004),
|
|
4
|
+
and the dual-mode CSS graph that goes with it (APP-008, APP-009, APP-010).
|
|
5
|
+
This page shows the *how*; for exact wording, rationale, and examples of the
|
|
6
|
+
shared rules, see the referenced `FED-*` IDs in the toolkit-guide — this
|
|
7
|
+
recipe does not restate them.
|
|
8
|
+
|
|
9
|
+
## Expose `./App` — and nothing else
|
|
10
|
+
|
|
11
|
+
Your app's federation config exposes exactly one module: a React component at
|
|
12
|
+
`./App` (FED-001).
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// vite.config.ts (federation mode only)
|
|
16
|
+
federation({
|
|
17
|
+
name: APP_NAME, // MUST equal the manifest `id` (FED-004)
|
|
18
|
+
filename: 'remoteEntry.js',
|
|
19
|
+
manifest: true,
|
|
20
|
+
exposes: {
|
|
21
|
+
'./App': './src/federation/app-remote.tsx',
|
|
22
|
+
},
|
|
23
|
+
shared: SHARED,
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
There is no `./navigation` module. FED-011 — the shell's extension-menu
|
|
28
|
+
contract — is **RETIRED (2026-08)**: the shell never registers, preloads or
|
|
29
|
+
calls it, there is no `subscribeNavItems` (v1 or v2) and no
|
|
30
|
+
`navContractVersion` negotiation, and the menu the shell renders while your app
|
|
31
|
+
is open comes from **your own** `lessly.app.yaml` — the `nav.sections` block
|
|
32
|
+
(APP-011) — not from your App code, and not from a toolkit manifest.
|
|
33
|
+
|
|
34
|
+
Still mapping the expose is inert, not an error — you just ship a chunk nobody
|
|
35
|
+
fetches. If your app has one, the cleanup is: delete
|
|
36
|
+
`src/federation/navigation.ts`, drop the `./navigation` entry from `exposes`,
|
|
37
|
+
and delete any `navContractVersion` / `notify()` plumbing `./App` called. What
|
|
38
|
+
the module used to feed the menu — environment lists, tool groups, live badges,
|
|
39
|
+
status dots, a settings gear — belongs **in your own pages**, rendered by
|
|
40
|
+
`./App` under its relative routes (FED-003). Deep links, refresh and the back
|
|
41
|
+
button are unaffected: the dynamic items only ever fed the menu, never the
|
|
42
|
+
router.
|
|
43
|
+
|
|
44
|
+
## The menu moves to the manifest, not to a module
|
|
45
|
+
|
|
46
|
+
The rows the module used to publish are now data in your App's manifest:
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
# lessly.app.yaml
|
|
50
|
+
nav:
|
|
51
|
+
icon: flask-conical
|
|
52
|
+
sections:
|
|
53
|
+
- id: overview
|
|
54
|
+
items:
|
|
55
|
+
- label: Dashboard
|
|
56
|
+
path: '' # '' = your App's root
|
|
57
|
+
match: exact
|
|
58
|
+
icon: gauge
|
|
59
|
+
- label: Activity
|
|
60
|
+
path: activity
|
|
61
|
+
icon: activity
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`path` is relative to your App's base path (`/<productSlug>/~/<id>`), so these
|
|
65
|
+
are the same relative routes `./App` already renders under (FED-003). The full
|
|
66
|
+
field constraints, the worked example, what happens when you ship no block, and
|
|
67
|
+
how a change reaches production are in APP-011.
|
|
68
|
+
|
|
69
|
+
## Shared singletons: `>=` floors, not carets, on 0.x packages
|
|
70
|
+
|
|
71
|
+
`react`, `react-dom`, `react-router`, and `@lessly/ui` MUST be declared as
|
|
72
|
+
shared singletons, never bundled (FED-005), pinned via `@module-federation/vite`
|
|
73
|
+
at exactly `1.16.12` (FED-006):
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const SHARED = {
|
|
77
|
+
react: { singleton: true, strictVersion: false, requiredVersion: '^19' },
|
|
78
|
+
'react-dom': { singleton: true, strictVersion: false, requiredVersion: '^19' },
|
|
79
|
+
'react-router': { singleton: true, strictVersion: false, requiredVersion: '^7.18' },
|
|
80
|
+
// 0.x singletons use a `>=` floor, NOT a caret: `^0.x` resolves to `<0.(x+1).0`,
|
|
81
|
+
// so a routine shell ui/icon bump would fail Module Federation version negotiation.
|
|
82
|
+
'@lessly/ui': { singleton: true, strictVersion: false, requiredVersion: '>=0.1.0' },
|
|
83
|
+
};
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`@lessly/ui` is pre-1.0. A caret range on a 0.x package (`^0.1.0`) resolves to
|
|
87
|
+
`<0.2.0`, so the next time the shell bumps `@lessly/ui`'s minor version, MF
|
|
88
|
+
version negotiation fails and your remote won't load under the shell. Use a
|
|
89
|
+
`>=` floor for every 0.x shared singleton instead — it only guarantees a
|
|
90
|
+
minimum, so it survives minor bumps upstream. Any other 0.x singleton you add
|
|
91
|
+
(e.g. an icon package) follows the same rule.
|
|
92
|
+
|
|
93
|
+
## Dual build: standalone + federation, merged into one `dist/`
|
|
94
|
+
|
|
95
|
+
The build MUST produce one merged dual-mode `dist/` covering both standalone
|
|
96
|
+
and federation artifacts (FED-007): a standalone build (plain SPA, `base: '/'`)
|
|
97
|
+
and a federation build (`base: '/~/<id>/'`, emits `remoteEntry.js` +
|
|
98
|
+
`mf-manifest.json`), merged so both coexist:
|
|
99
|
+
|
|
100
|
+
```jsonc
|
|
101
|
+
// package.json scripts
|
|
102
|
+
{
|
|
103
|
+
"build": "tsc -b && pnpm build:standalone && pnpm build:federation && pnpm build:merge",
|
|
104
|
+
"build:standalone": "vite build",
|
|
105
|
+
"build:federation": "vite build --mode federation",
|
|
106
|
+
"build:merge": "node scripts/merge-federation-dist.mjs"
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// vite.config.ts
|
|
112
|
+
export default defineConfig(({ mode }) => {
|
|
113
|
+
const isFederation = mode === 'federation';
|
|
114
|
+
return {
|
|
115
|
+
base: isFederation ? `/~/${APP_NAME}/` : '/',
|
|
116
|
+
build: {
|
|
117
|
+
outDir: isFederation ? 'dist-federation' : 'dist',
|
|
118
|
+
emptyOutDir: true,
|
|
119
|
+
},
|
|
120
|
+
// ...
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The merge step copies `dist-federation/*` into `dist/`, keeping the
|
|
126
|
+
**standalone** `index.html` (not the federation one) and failing loudly on any
|
|
127
|
+
filename collision with differing content, then removes `dist-federation/`.
|
|
128
|
+
The result: one `dist/` that serves the standalone SPA at `/` and the
|
|
129
|
+
federation remote entry + assets at `/~/<id>/`.
|
|
130
|
+
|
|
131
|
+
## Absolute asset base in federation mode
|
|
132
|
+
|
|
133
|
+
The asset base in federation mode is the absolute path `/~/<id>/` — not a
|
|
134
|
+
relative path, and `<id>` MUST match the manifest `id` and the MF `name`
|
|
135
|
+
(FED-004). Getting this wrong breaks asset resolution once the shell mounts
|
|
136
|
+
your remote at that path.
|
|
137
|
+
|
|
138
|
+
## Styles: two entries, one vocabulary
|
|
139
|
+
|
|
140
|
+
Federation mode and standalone mode enter the CSS graph through **different**
|
|
141
|
+
stylesheets, and that split is the whole point. Standalone is your app's own
|
|
142
|
+
page, so it owns the base layer, the fonts and the full kit stylesheet.
|
|
143
|
+
Federation mounts your subtree inside the shell's already-painted page, so the
|
|
144
|
+
federation entry must add nothing global (APP-008), must take its scales from
|
|
145
|
+
the shell's preset (APP-009), and must park its utilities in
|
|
146
|
+
`layer(remote-utilities)` (APP-010).
|
|
147
|
+
|
|
148
|
+
Four files, ready to copy.
|
|
149
|
+
|
|
150
|
+
```css
|
|
151
|
+
/* src/index.federation.css — the federation entry, imported by ./App.
|
|
152
|
+
Utilities only: no preflight, no fonts, no base layer, no @theme, and no
|
|
153
|
+
tailwindcss/theme.css at all. */
|
|
154
|
+
|
|
155
|
+
/* Layer order first, so positions are fixed before Tailwind emits its own.
|
|
156
|
+
Mirrors the shell's declaration in lessly-workspace:apps/web/src/index.css. */
|
|
157
|
+
@layer theme, base, components, remote-utilities, utilities;
|
|
158
|
+
|
|
159
|
+
@config '../tailwind.config.ts';
|
|
160
|
+
@import 'tailwindcss/utilities.css' layer(remote-utilities);
|
|
161
|
+
@import '@lessly/ui/styles-federated.css';
|
|
162
|
+
|
|
163
|
+
/* Tailwind must scan the kit's compiled output, or the classes @lessly/ui
|
|
164
|
+
components use are never generated. */
|
|
165
|
+
@source '../node_modules/@lessly/ui/dist/index.js';
|
|
166
|
+
|
|
167
|
+
/* ...and must NOT scan your tests: a utility class named as a test fixture is
|
|
168
|
+
just a string to Tailwind, and would be compiled into the shipped sheet. */
|
|
169
|
+
@source not '../src/**/*.test.ts';
|
|
170
|
+
@source not '../src/**/*.test.tsx';
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
```css
|
|
174
|
+
/* src/index.css — the standalone entry, imported by src/main.tsx. Your app
|
|
175
|
+
owns the page here: full tailwindcss (preflight included), the fonts, the
|
|
176
|
+
full kit stylesheet, and the base layer. Same @config as the federation
|
|
177
|
+
entry — one vocabulary on both (APP-009). */
|
|
178
|
+
@config '../tailwind.config.ts';
|
|
179
|
+
@import 'tailwindcss';
|
|
180
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter');
|
|
181
|
+
@import '@lessly/ui/styles.css';
|
|
182
|
+
@import './base.css';
|
|
183
|
+
|
|
184
|
+
/* Same reason as in the federation entry: Tailwind generates utilities per
|
|
185
|
+
entry, and neither entry's scan reaches node_modules on its own. */
|
|
186
|
+
@source '../node_modules/@lessly/ui/dist/index.js';
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```css
|
|
190
|
+
/* src/base.css — the global base layer. Imported by the STANDALONE entry
|
|
191
|
+
ONLY; it must never be reachable from src/index.federation.css. */
|
|
192
|
+
@layer base {
|
|
193
|
+
* { @apply border-border-subtle; }
|
|
194
|
+
body { @apply bg-bg-primary text-text-primary; font-family: var(--font-sans); }
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
// tailwind.config.ts — the single source of scale vocabulary, for both entries
|
|
200
|
+
import { lesslyPreset } from '@lessly/ui/tailwind-preset';
|
|
201
|
+
|
|
202
|
+
export default {
|
|
203
|
+
presets: [lesslyPreset],
|
|
204
|
+
content: ['./index.html', './src/**/*.{ts,tsx}'],
|
|
205
|
+
};
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Wire them up:
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
// src/federation/app-remote.tsx
|
|
212
|
+
import '../index.federation.css';
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
// src/main.tsx
|
|
217
|
+
import './index.css';
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Three things that are easy to get wrong
|
|
221
|
+
|
|
222
|
+
**`theme.css` is optional, and it is not a token bridge.** There is no
|
|
223
|
+
`src/theme.css` in the layout above, and an app with no tokens of its own does
|
|
224
|
+
not need one. The kit's tokens come from `@lessly/ui/styles-federated.css` in
|
|
225
|
+
federation and `@lessly/ui/styles.css` in standalone; re-declaring them locally
|
|
226
|
+
is a hand-copied set that drifts from the kit silently, and an `@theme` block
|
|
227
|
+
restating the kit's scales is exactly what APP-009 forbids.
|
|
228
|
+
|
|
229
|
+
Add `src/theme.css` only for tokens **your app owns and the kit does not** —
|
|
230
|
+
`--hover-bg` and the like — and then it holds **variable declarations only**.
|
|
231
|
+
Both entries import it, so a `@layer base { body { … } }` block in it is
|
|
232
|
+
reachable from the federation entry and will re-font the shell's chrome after
|
|
233
|
+
first paint. That exact block is what shipped the production font jump; it
|
|
234
|
+
belongs in `src/base.css`, standalone-only.
|
|
235
|
+
|
|
236
|
+
**The sanctioned federated export, not a hand-rolled filter.**
|
|
237
|
+
`@lessly/ui/styles-federated.css` is `styles.css` minus the `@font-face` blocks
|
|
238
|
+
and the `@layer base` — tokens, `--color-*` aliases, `.link`, the `.motion-*`
|
|
239
|
+
helpers and the compiled tw-animate-css layer are all still there. A prebuild
|
|
240
|
+
script that reads the kit's stylesheet out of `node_modules` and strips the
|
|
241
|
+
forbidden blocks itself breaks on any restructure of a file you don't own.
|
|
242
|
+
Depend on `@lessly/ui` at `^4.0.0`, the range the shell runs.
|
|
243
|
+
|
|
244
|
+
**`layer(remote-utilities)` only after the shell declares the order.** The
|
|
245
|
+
shell already ships `@layer theme, base, components, remote-utilities,
|
|
246
|
+
utilities;` (`lessly-workspace:apps/web/src/index.css`). Adopt the layer
|
|
247
|
+
against a shell that hasn't, and `remote-utilities` becomes the last-declared —
|
|
248
|
+
and therefore highest-priority — layer, so every one of your utilities beats
|
|
249
|
+
every shell utility. Verify the shell first; adopting early is a regression,
|
|
250
|
+
not a head start.
|
|
251
|
+
|
|
252
|
+
### Guard it
|
|
253
|
+
|
|
254
|
+
A CSS regression here is invisible in a standalone build — it only shows once
|
|
255
|
+
your app is composed into a live shell. Keep a standing test over the **built**
|
|
256
|
+
federation CSS (`dist-federation/`), not over the entry's import list, since an
|
|
257
|
+
imported stylesheet can carry what the entry looks clean of. What it must
|
|
258
|
+
assert, and the two shapes it must not confuse:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
# No fonts, and no base-layer BLOCK. Note the `{`: `@layer base, components;`
|
|
262
|
+
# is a harmless order declaration and must not trip the guard.
|
|
263
|
+
grep -REn "@font-face|@layer[[:space:]]+base[[:space:]]*\{" dist-federation/
|
|
264
|
+
|
|
265
|
+
# A global rule is a violation only if it declares more than custom properties:
|
|
266
|
+
# styles-federated.css's `*, ::before, ::after { --tw-*: … }` is legal.
|
|
267
|
+
grep -REn "(^|[,}])[[:space:]]*(html|body|\*)[^{]*\{" dist-federation/
|
|
268
|
+
grep -R -- "-webkit-text-size-adjust" dist-federation/ && echo "preflight leaked"
|
|
269
|
+
|
|
270
|
+
# Utilities in the app's own layer, never the shell's.
|
|
271
|
+
grep -REn "@layer[[:space:]]+utilities[[:space:]]*\{" dist-federation/ \
|
|
272
|
+
&& echo "APP-010 — utilities still in the shell's layer"
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Fast Refresh under a local shell
|
|
276
|
+
|
|
277
|
+
When developing your remote composed under a local shell (`vite --mode
|
|
278
|
+
federation`), point `reactRefreshHost` at the shell's dev origin so React Fast
|
|
279
|
+
Refresh works through the federation boundary:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
react(isFederation ? { reactRefreshHost: shellDevOrigin } : {})
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
See `recipes/local-dev.md` for how to run this composed setup end to end.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Recipe: running an App locally
|
|
2
|
+
|
|
3
|
+
How to run an App standalone and, when needed, composed under a local shell.
|
|
4
|
+
Auth stays ambient in both modes (APP-005) — you never implement login or
|
|
5
|
+
token handling yourself.
|
|
6
|
+
|
|
7
|
+
## Standalone: `pnpm dev`
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This starts the app on its own, outside any shell, proxied through
|
|
14
|
+
`@lessly/app-dev`'s local dev server. Platform API calls made through your
|
|
15
|
+
`sdk` (created with `createLesslyApp`, see `recipes/sdk-usage.md`) are
|
|
16
|
+
forwarded through a bearer proxy at exactly this path:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
/lessly-api
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`@lessly/app-dev` attaches your locally stored bearer token to everything it
|
|
23
|
+
proxies through `/lessly-api`, so your SDK client just talks to that path in
|
|
24
|
+
dev — it never sees or handles the token itself.
|
|
25
|
+
|
|
26
|
+
So in dev you pass `baseUrl: '/lessly-api'` — a page-relative `baseUrl` is
|
|
27
|
+
supported and resolved against the current document (browser-only); see
|
|
28
|
+
`recipes/sdk-usage.md`.
|
|
29
|
+
|
|
30
|
+
### Device-code login
|
|
31
|
+
|
|
32
|
+
The first time you run `pnpm dev` (or once your local session expires),
|
|
33
|
+
`@lessly/app-dev` walks you through a device-code login: it prints a
|
|
34
|
+
verification URL and a short code, opens your browser, and polls until you
|
|
35
|
+
approve. Once approved, it stores the resulting bearer token locally and the
|
|
36
|
+
`/lessly-api` proxy starts attaching it automatically. You don't write any of
|
|
37
|
+
this — it's ambient, per APP-005: an App MUST NOT implement its own login,
|
|
38
|
+
token handling, or refresh.
|
|
39
|
+
|
|
40
|
+
### `.env.local`
|
|
41
|
+
|
|
42
|
+
Set which product your local session targets:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
# .env.local
|
|
46
|
+
VITE_PRODUCT_ID=prod_123
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Vite does not load `.env.example` — copy it to `.env.local` and edit the
|
|
50
|
+
value. `.env.local` is per-developer and untracked; don't commit it.
|
|
51
|
+
|
|
52
|
+
### The stub `user` prop
|
|
53
|
+
|
|
54
|
+
In dev-standalone, the `user` prop your app receives is a **stub**:
|
|
55
|
+
`{ id: <from the local dev JWT>, displayName: 'Dev User' }`. It is not a real
|
|
56
|
+
session — it exists only so you have *something* to render while developing
|
|
57
|
+
outside a shell. Per APP-005, don't build any behavior on `user` beyond
|
|
58
|
+
display (a name badge, an avatar fallback). The real, shell-authenticated
|
|
59
|
+
user only arrives once your app is composed via federation, through the
|
|
60
|
+
`./App` props — code that branches on `user` for anything beyond display will
|
|
61
|
+
work in dev-standalone and misbehave once composed under a real shell.
|
|
62
|
+
|
|
63
|
+
## Composed: running under a local shell
|
|
64
|
+
|
|
65
|
+
To see your app the way it will actually render — composed under the shell,
|
|
66
|
+
receiving real federation props — run:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
pnpm dev:shell
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This builds/serves your remote in federation mode (`vite --mode federation`,
|
|
73
|
+
see `recipes/federation.md`) and points it at a local or staging shell for
|
|
74
|
+
Fast Refresh (`reactRefreshHost`).
|
|
75
|
+
|
|
76
|
+
### Pointing a staging shell at your local remote
|
|
77
|
+
|
|
78
|
+
To make a **staging** shell load your app from your local dev server instead
|
|
79
|
+
of its deployed remote, set this in the shell's browser `localStorage`
|
|
80
|
+
(**staging only** — this override does not exist in production):
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
localStorage['lessly:dev-remote'] = "<slug>=<origin>"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`<slug>` is your app's manifest `id`; `<origin>` is your local dev server's
|
|
87
|
+
origin (e.g. `http://localhost:5173`). For example:
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
localStorage['lessly:dev-remote'] = 'my-app=http://localhost:5173';
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Reload the staging shell after setting this — it reads the override at
|
|
94
|
+
startup and fetches your remote entry from `<origin>` instead of the deployed
|
|
95
|
+
one for the app matching `<slug>`. Remove the key (or clear `localStorage`) to
|
|
96
|
+
go back to the deployed remote.
|
|
97
|
+
|
|
98
|
+
## Keeping your local setup current
|
|
99
|
+
|
|
100
|
+
Update the SDK and `@lessly/app-dev` with the `@latest` form, never `npm
|
|
101
|
+
update`:
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
npm i @lessly/sdk-app@latest
|
|
105
|
+
npm i @lessly/app-dev@latest
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`@lessly/sdk-app` and `@lessly/app-dev` are pre-1.0 and pinned with caret
|
|
109
|
+
ranges, so `npm update` never crosses a minor version — and since the SDK
|
|
110
|
+
bumps its minor version whenever the platform ships a new tool, `npm update`
|
|
111
|
+
would never actually deliver those updates (see APP-007).
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Recipe: calling the platform API
|
|
2
|
+
|
|
3
|
+
How to reach Lessly platform data from an App. The normative rule is APP-001
|
|
4
|
+
(all platform data access goes through `@lessly/sdk-app`) and APP-006 (prefer
|
|
5
|
+
the generated option factories) — this page shows the *how*.
|
|
6
|
+
|
|
7
|
+
## Create the client
|
|
8
|
+
|
|
9
|
+
Create one client with `createLesslyApp` and call operations through the
|
|
10
|
+
Proxy namespace tree:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { createLesslyApp } from '@lessly/sdk-app';
|
|
14
|
+
|
|
15
|
+
const sdk = createLesslyApp({
|
|
16
|
+
baseUrl: 'https://api.lessly.dev',
|
|
17
|
+
productId: 'prod_123',
|
|
18
|
+
// getCsrfToken defaults to reading the `lessly_csrf` cookie; override for SSR/tests.
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const connectors = await sdk.organization.connectors.list({ productId: 'prod_123' });
|
|
22
|
+
await sdk.organization.product.create({ name: 'Acme' });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
> The exact namespaces you see depend on the live catalog — see
|
|
26
|
+
> [Namespaces and subpath imports](#namespaces-and-subpath-imports). The calls
|
|
27
|
+
> above use `organization`; substitute the namespace/resource you need.
|
|
28
|
+
|
|
29
|
+
### `baseUrl`: absolute or page-relative
|
|
30
|
+
|
|
31
|
+
`baseUrl` may be either form:
|
|
32
|
+
|
|
33
|
+
- **Absolute** — `'https://api.lessly.dev'`. Works anywhere, including SSR and
|
|
34
|
+
tests.
|
|
35
|
+
- **Page-relative** — `'/lessly-api'`, the path `pnpm dev` proxies through
|
|
36
|
+
(see `recipes/local-dev.md`). It is resolved against the current document via
|
|
37
|
+
`globalThis.location`, so it is **browser-only**: outside a browser the SDK
|
|
38
|
+
throws a named error (`@lessly/sdk-app: relative baseUrl "/lessly-api" cannot
|
|
39
|
+
be resolved outside a browser`) instead of an opaque `Invalid URL`. Tests and
|
|
40
|
+
SSR must pass an absolute base.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const sdk = createLesslyApp({
|
|
44
|
+
baseUrl: import.meta.env.DEV ? '/lessly-api' : 'https://api.lessly.dev',
|
|
45
|
+
productId: 'prod_123',
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`productId` is the active product — read it from your `./App` props or the
|
|
50
|
+
`X-Product-Id` header (see APP-005), never decoded from a token.
|
|
51
|
+
|
|
52
|
+
## Handle errors with `LesslyApiError`
|
|
53
|
+
|
|
54
|
+
Every failed call throws a typed `LesslyApiError` with `status`, `code`, and
|
|
55
|
+
`body`. Branch on `code` (stable) rather than `status` alone when you need to
|
|
56
|
+
distinguish platform-defined failure reasons:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { LesslyApiError } from '@lessly/sdk-app';
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await sdk.organization.product.create({ name: 'Acme' });
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (err instanceof LesslyApiError) {
|
|
65
|
+
if (err.status === 409) {
|
|
66
|
+
// err.code / err.body carry the platform's structured reason
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Missing CSRF cookie
|
|
74
|
+
|
|
75
|
+
Mutating calls (`POST`, `PUT`, `PATCH`, `DELETE`) require the `lessly_csrf`
|
|
76
|
+
cookie. If it is absent, the SDK throws before any network call:
|
|
77
|
+
`LesslyApiError` with `status: 0` and `code: 'csrf_cookie_missing'`. There is no
|
|
78
|
+
retry and no hidden token refresh — re-establish the session, or supply
|
|
79
|
+
`getCsrfToken` for SSR/tests. Read-only calls are unaffected.
|
|
80
|
+
|
|
81
|
+
## Prefer the generated query/mutation option factories
|
|
82
|
+
|
|
83
|
+
Each namespace subpath (`@lessly/sdk-app/<namespace>`) ships framework-agnostic
|
|
84
|
+
query/mutation **option factories** — plain objects, not hooks — usable with
|
|
85
|
+
any TanStack Query adapter (React, Solid, Vue, Svelte). You pass the `sdk`
|
|
86
|
+
instance explicitly:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// React example — works the same with any @tanstack/*-query adapter.
|
|
90
|
+
import { useQuery, useMutation } from '@tanstack/react-query';
|
|
91
|
+
import {
|
|
92
|
+
organizationConnectorsListQueryOptions,
|
|
93
|
+
organizationProductCreateMutationOptions,
|
|
94
|
+
} from '@lessly/sdk-app/organization';
|
|
95
|
+
|
|
96
|
+
function Connectors() {
|
|
97
|
+
const { data } = useQuery(organizationConnectorsListQueryOptions(sdk, { productId: 'prod_123' }));
|
|
98
|
+
const create = useMutation(organizationProductCreateMutationOptions(sdk));
|
|
99
|
+
// create.mutate({ name: 'Acme' })
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The factories return `{ queryKey, queryFn }` (and `{ mutationKey, mutationFn }`),
|
|
104
|
+
so they also work directly with `queryClient.ensureQueryData(...)` and friends
|
|
105
|
+
— useful for prefetching outside a component, e.g. in a route loader:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
await queryClient.ensureQueryData(organizationConnectorsListQueryOptions(sdk, { productId: 'prod_123' }));
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Prefer these factories over hand-rolling calls against `sdk.<namespace>...`
|
|
112
|
+
directly (APP-006): they keep query keys, request shapes, and cache
|
|
113
|
+
invalidation consistent with the published API surface, and stay in sync with
|
|
114
|
+
it automatically when you run `npm i @lessly/sdk-app@latest`.
|
|
115
|
+
|
|
116
|
+
## Namespaces and subpath imports
|
|
117
|
+
|
|
118
|
+
The SDK is organized by namespace (currently `organization` and `playground`).
|
|
119
|
+
Call operations off `sdk.<namespace>.<resource>.<action>(...)`, and import that
|
|
120
|
+
namespace's option factories from the matching subpath,
|
|
121
|
+
`@lessly/sdk-app/<namespace>` — not the package root. This keeps each App's
|
|
122
|
+
bundle limited to the namespaces it actually uses.
|
|
123
|
+
|
|
124
|
+
Some operations belong to the namespace itself rather than to a resource under
|
|
125
|
+
it — listing the organizations you can see, for example, has no entity to hang
|
|
126
|
+
off. Those are called directly on the namespace, `sdk.<namespace>.<action>(...)`,
|
|
127
|
+
and their query keys drop the resource segment to match. Both forms are
|
|
128
|
+
generated from the live catalog; check `client.gen.ts` (or your editor's
|
|
129
|
+
autocomplete) for which shape a given operation takes.
|
|
130
|
+
|
|
131
|
+
The available namespaces track the live catalog and **grow as platform
|
|
132
|
+
extensions migrate to `@lessly-platform/tools`** — a namespace appears here only once its
|
|
133
|
+
tools carry a REST binding. Run `npm i @lessly/sdk-app@latest` to pick up newly
|
|
134
|
+
migrated namespaces. To see exactly what a given version exposes, check its
|
|
135
|
+
subpaths in `package.json` `exports`, or the `manifest.gen.ts` namespace list.
|
|
136
|
+
|
|
137
|
+
## Streaming (`*Connect` factories)
|
|
138
|
+
|
|
139
|
+
Some catalog tools are ws-bound: instead of a request/response pair they carry
|
|
140
|
+
a `ws` binding, and the generator emits a `<toolId>Connect` factory for them
|
|
141
|
+
next to the option factories. That factory is the **only** sanctioned way to
|
|
142
|
+
stream from an App (APP-001) — it derives `ws(s)://` from your `baseUrl`, opens
|
|
143
|
+
the socket, and hands back a small handle:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { createLesslyApp } from '@lessly/sdk-app';
|
|
147
|
+
import { playgroundWsEchoConnect } from '@lessly/sdk-app/playground';
|
|
148
|
+
|
|
149
|
+
const sdk = createLesslyApp({ baseUrl: 'https://api.lessly.dev', productId: 'prod_123' });
|
|
150
|
+
|
|
151
|
+
const stream = playgroundWsEchoConnect(sdk, { room: 'lobby' });
|
|
152
|
+
const off = stream.onMessage((frame) => console.log(frame));
|
|
153
|
+
stream.onClose(({ code, reason, wasClean }) => console.log('closed', code, reason, wasClean));
|
|
154
|
+
stream.send('hello'); // queued until the socket is OPEN, then flushed in order
|
|
155
|
+
// later
|
|
156
|
+
off();
|
|
157
|
+
stream.close(1000, 'done');
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
What the handle does and does not do:
|
|
161
|
+
|
|
162
|
+
- **Session cookies ride the upgrade.** No CSRF token and no product header are
|
|
163
|
+
sent — a browser `WebSocket` cannot set headers at all, and the gateway
|
|
164
|
+
authenticates the upgrade from the session.
|
|
165
|
+
- **Frames are raw.** `onMessage` delivers `MessageEvent.data` exactly as it
|
|
166
|
+
arrived (`string`, `ArrayBuffer` or `Blob`); the SDK never parses it. Framing
|
|
167
|
+
is protocol-specific and is documented by the tool's own description.
|
|
168
|
+
- **No reconnection.** A dropped connection surfaces once, through `onClose`
|
|
169
|
+
with the close code (`1006` for an abnormal drop). Whether and how to
|
|
170
|
+
reconnect is the App's decision — the SDK will not retry behind your back.
|
|
171
|
+
- **Params are placed like a GET.** `:token` segments are filled from the input,
|
|
172
|
+
everything else becomes a query parameter.
|
|
173
|
+
|
|
174
|
+
The `*Connect` factory is the App-facing surface, and the only one APP-001
|
|
175
|
+
sanctions. The lower-level pieces it is built on — `sdk.openStream(binding, …)`
|
|
176
|
+
and the `connectStream` runtime — are the SDK's own primitive: they are exported
|
|
177
|
+
so the generated code can use them and so tests can substitute a socket, not so
|
|
178
|
+
that App code can address a route by hand. Call the factory.
|
|
179
|
+
|
|
180
|
+
Streaming needs a browser: outside one (SSR, node tests) there is no
|
|
181
|
+
`WebSocket`, and the SDK says so by name rather than failing opaquely. Pass
|
|
182
|
+
`options.WebSocket` to supply an implementation in tests.
|
|
183
|
+
|
|
184
|
+
## What not to do
|
|
185
|
+
|
|
186
|
+
- Don't open a `WebSocket` to a platform host, and don't hand-build a
|
|
187
|
+
`ws(s)://` URL from `baseUrl` (APP-001) — use the generated `*Connect`
|
|
188
|
+
factory. A tool without a `ws` binding has no sanctioned stream.
|
|
189
|
+
- Don't `fetch` a platform host directly, and don't use any other HTTP client
|
|
190
|
+
for platform data (APP-001) — always go through `sdk` or the generated
|
|
191
|
+
factories.
|
|
192
|
+
- Don't hand-roll request paths against the client when a
|
|
193
|
+
`*QueryOptions`/`*MutationOptions` factory already exists for that operation
|
|
194
|
+
(APP-006).
|