@unoverse-platform/studio 0.1.30 → 0.1.32
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/bin/studio.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* universe, which is a different job. See docs/architecture/LOCAL_STUDIO.md.
|
|
18
18
|
*/
|
|
19
19
|
import { createServer } from "vite";
|
|
20
|
-
import { dirname, resolve, join } from "path";
|
|
20
|
+
import { dirname, resolve, join, basename } from "path";
|
|
21
21
|
import { fileURLToPath } from "url";
|
|
22
22
|
import { existsSync, readdirSync } from "fs";
|
|
23
23
|
import { createInterface } from "readline/promises";
|
|
@@ -33,8 +33,9 @@ const appRoot = resolve(here, "..");
|
|
|
33
33
|
* which is a tool telling you to do the thing it could do. The first thirty seconds of a
|
|
34
34
|
* new install should end with something on screen, not with a mkdir.
|
|
35
35
|
*
|
|
36
|
-
* It
|
|
37
|
-
*
|
|
36
|
+
* It creates rather than asks. Standing in an empty folder you made and typing this IS the
|
|
37
|
+
* instruction; the folder says where and its name says what. The only question left is the
|
|
38
|
+
* one it cannot answer for you: a folder whose name no project can carry.
|
|
38
39
|
*/
|
|
39
40
|
/** The same walk `catalog.ts` does: the nearest folder above us holding rx/. */
|
|
40
41
|
function findProjectRootFromCwd() {
|
|
@@ -92,45 +93,40 @@ async function scaffoldIfEmpty() {
|
|
|
92
93
|
return;
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
console.log(`\n No project here yet.\n\n ${cwd}\n`);
|
|
96
|
-
|
|
97
96
|
/**
|
|
98
|
-
* THE
|
|
99
|
-
*
|
|
100
|
-
* Studio used to scaffold into the cwd, which made whatever directory you were standing
|
|
101
|
-
* in your repository. Fixing that by naming the folder after the project overcorrected:
|
|
102
|
-
* it produced `sab/rx/sab`, and it said the wrong thing about what the folder IS.
|
|
97
|
+
* THE WORKSPACE IS THE FOLDER YOU ARE STANDING IN, and the project takes its name.
|
|
103
98
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
99
|
+
* Two earlier shapes invented a folder instead of accepting the one the developer had
|
|
100
|
+
* chosen: `rx/<name>/` into ANY cwd (so a scratch folder became a repository), then the
|
|
101
|
+
* typed name owning a folder, then a fixed `studio/` holder — which buried the project
|
|
102
|
+
* one level inside the folder made for it, and asked for a name it could already see.
|
|
103
|
+
* Standing in `~/code/bpp` and typing `unoverse studio` says both things: here, and bpp.
|
|
109
104
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
105
|
+
* A workspace does hold MANY projects (`rx/` is one folder per publishing org), and the
|
|
106
|
+
* folder carrying the first one's name is why the holder was introduced. That is a cost
|
|
107
|
+
* paid at project five, by a developer who can rename the folder; it does not buy the
|
|
108
|
+
* confusion of an extra level in the first thirty seconds.
|
|
112
109
|
*/
|
|
113
|
-
const home =
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
110
|
+
const home = cwd;
|
|
111
|
+
// The folder's own name, when it can be a project name. It becomes an item's `org`
|
|
112
|
+
// column, so `checkProjectName` is the same gate the UI's New project uses.
|
|
113
|
+
const derived = basename(cwd);
|
|
114
|
+
let name = checkProjectName(derived) ? null : derived;
|
|
115
|
+
|
|
116
|
+
if (!name) {
|
|
117
|
+
// NOTHING TO DERIVE — the folder is called something no project can be called
|
|
118
|
+
// (capitals, dots, a leading digit). This is the only case that still asks.
|
|
119
|
+
console.log(`\n No project here yet.\n\n ${cwd}\n`);
|
|
120
|
+
console.log(` "${derived}" cannot be a project name (lowercase letters, numbers and dashes).\n`);
|
|
121
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
122
|
+
for (;;) {
|
|
123
|
+
name = (await rl.question(" Name your project [my-project]: ")).trim() || "my-project";
|
|
124
|
+
const bad = checkProjectName(name);
|
|
125
|
+
if (!bad) break;
|
|
126
|
+
console.log(` ${bad}\n`);
|
|
127
|
+
}
|
|
128
|
+
rl.close();
|
|
132
129
|
}
|
|
133
|
-
rl.close();
|
|
134
130
|
|
|
135
131
|
// The same call the UI's New project makes, so the first project and the fifth are
|
|
136
132
|
// identical (local/scaffold.mjs). rx/<name>/ stays nested: every subfolder of rx/ IS a
|
|
@@ -138,9 +134,7 @@ async function scaffoldIfEmpty() {
|
|
|
138
134
|
// publishes, so a flat rx/ could hold exactly one project and would read `components`
|
|
139
135
|
// and `templates` as orgs.
|
|
140
136
|
createFirstProject(home, name);
|
|
141
|
-
|
|
142
|
-
process.chdir(home);
|
|
143
|
-
console.log(`\n Created studio/ with rx/${name}/, prompts/ and nodes/\n`);
|
|
137
|
+
console.log(`\n Created rx/${name}/, prompts/ and nodes/ in ${cwd}\n`);
|
|
144
138
|
}
|
|
145
139
|
|
|
146
140
|
await scaffoldIfEmpty();
|
package/package.json
CHANGED
|
@@ -39,6 +39,16 @@ const clone = <T,>(v: T): T => JSON.parse(JSON.stringify(v)) as T;
|
|
|
39
39
|
/** `props` remaps the atom's OWN field names to the host's fields, everywhere a field name
|
|
40
40
|
* can appear: binds, both spellings of `visibleWhen`, and `style.when` predicates. */
|
|
41
41
|
function remapFields(node: AnyNode, propMap: Record<string, string>): void {
|
|
42
|
+
// A NESTED `Ref` is remapped through its OWN `props`, whose values name fields in THIS
|
|
43
|
+
// tree's scope. An atom that composes another atom (toggle-row, form-toggle → toggle-switch)
|
|
44
|
+
// otherwise passes the inner one a name the host has renamed away, and the inner control
|
|
45
|
+
// reads an absent key: it draws, it just never reflects anything.
|
|
46
|
+
if (node.type === "Ref" && node.props && typeof node.props === "object") {
|
|
47
|
+
for (const k of Object.keys(node.props as AnyNode)) {
|
|
48
|
+
const v = (node.props as AnyNode)[k];
|
|
49
|
+
if (typeof v === "string" && propMap[v]) (node.props as AnyNode)[k] = propMap[v];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
42
52
|
if (node.bind) for (const k of Object.keys(node.bind)) if (propMap[node.bind[k]]) node.bind[k] = propMap[node.bind[k]];
|
|
43
53
|
if (typeof node.visibleWhen === "string" && propMap[node.visibleWhen]) node.visibleWhen = propMap[node.visibleWhen];
|
|
44
54
|
// The OBJECT form remaps too. A guard is a guard whichever way it is written; carrying
|
|
@@ -135,7 +135,7 @@ The rules, in the order they bite:
|
|
|
135
135
|
of bound fields). Same shape, different values = ONE state, data-driven: seven wizard
|
|
136
136
|
questions sharing one shape are one `question` state whose data changes, never seven
|
|
137
137
|
files (LAYERS §4, §7).
|
|
138
|
-
5. **Atoms** (`rx/atoms/`, via `Ref`) are for shapes shared across components, authoring-time
|
|
138
|
+
5. **Atoms** (`rx/marketplace/atoms/`, via `Ref`) are for shapes shared across components, authoring-time
|
|
139
139
|
only (the server expands them before serving; no Studio view). `Ref props` remaps FIELDS;
|
|
140
140
|
**`Ref with` passes LITERALS**: `{ "type": "Ref", "ref": "button", "with": { "label":
|
|
141
141
|
"Learn more", "icon": "arrowRight" }, "action": { … } }`. A bind whose field is a `with`
|
|
@@ -157,38 +157,83 @@ The rules, in the order they bite:
|
|
|
157
157
|
for surgical refinement). **The schema IS the instruction channel.**
|
|
158
158
|
Write briefs, never side-channel prompts/skills for filling a component. Shape is
|
|
159
159
|
closed + linted. Pairs with the single-state permanent pattern (rule 2) for
|
|
160
|
-
continuously-enriched pages; exemplar: `rx/components/
|
|
160
|
+
continuously-enriched pages; exemplar: `rx/yasisland/components/perfect-day`.
|
|
161
161
|
7. **Tokens only** (LAW 1) + closed style keys + real space-scale steps
|
|
162
162
|
(`docs/design/06`): an invented step is silently broken CSS.
|
|
163
|
-
8. **Lifecycle
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
`
|
|
179
|
-
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
163
|
+
8. **Lifecycle hooks: the component fetches its own data** (full doc:
|
|
164
|
+
`docs/design/lifecycle-hooks.md`; deep law:
|
|
165
|
+
[UNOVERSE_AUTHORING](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_AUTHORING.md) §3c). When a component needs data the
|
|
166
|
+
workflow did not send (not from an AI = brief, not from the workflow = `input` props),
|
|
167
|
+
it declares CALLS that the platform runs at a platform-owned fire point. **No code**:
|
|
168
|
+
a hook is data like everything else in `rx/`. It's for DATA, not styling or logic.
|
|
169
|
+
- **Two phases**: `onStart` (the instance is created: data every copy needs) and
|
|
170
|
+
`onEnterView` (the instance entered a named STATE: data only that state needs). A
|
|
171
|
+
detail view's content is `onEnterView`, NEVER `onStart` — hydrating at creation
|
|
172
|
+
fetches a full document for every card in a grid.
|
|
173
|
+
- **Opt in via the manifest** (authorizes execution; auditable). Nothing undeclared
|
|
174
|
+
runs; an un-opted handler file is a lint error, not an inert file.
|
|
175
|
+
- **The scope key is spelled `layouts:` but its entries are STATE names.**
|
|
176
|
+
`onEnterView` only. Omit it and every state fires.
|
|
177
|
+
- **Name the handler for what it DOES, never for the phase** (`fetchPlaceDetails`, not
|
|
178
|
+
`onEnterView`): the phase already said the moment.
|
|
179
|
+
- **Nothing to wire up.** A component changing its own view writes `view`, and that
|
|
180
|
+
write IS the event the hook reacts to. No signal to author, no action to chain.
|
|
181
|
+
- **Prefer a platform-ready handler.** `getDetail` (zero code) reads the instance's
|
|
182
|
+
`universal_id` and merges the stored record's fields in by name, so authored copy
|
|
183
|
+
never travels through a model. Use it for "fill this card with its own record".
|
|
184
|
+
|
|
185
|
+
```yaml
|
|
186
|
+
# manifest.yaml — the shipped rx/sab/components/product-card
|
|
187
|
+
lifecycle:
|
|
188
|
+
- phase: onEnterView
|
|
189
|
+
layouts: [ detail ] # STATE names; the grid state never wakes it
|
|
190
|
+
handler: getDetail # platform-ready, so this component ships no code
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
- **Beyond the built-ins, DECLARE the calls** in `<handler>.yaml` beside the manifest
|
|
194
|
+
(**filename = the handler, lowercased**: `fetchPlaceDetails` → `fetchplacedetails.yaml`;
|
|
195
|
+
macOS ignores the case difference and Linux does not). `calls:` is the same list an
|
|
196
|
+
`api/run.yaml` holds, run by the same runtime (host allowlist, credentials, retries,
|
|
197
|
+
timeouts, transports); `returns:` projects the results into partial props, merged by
|
|
198
|
+
name. Calls run in order and read each other through `calls.<name>`.
|
|
199
|
+
- **`allowedHosts` in the manifest is NOT optional**: a hook that makes requests
|
|
200
|
+
without it is a lint ERROR (deny by default). Credentials are named by type in the
|
|
201
|
+
manifest and resolved server-side from encrypted storage; **a key never sits in
|
|
202
|
+
`rx/`** (a shared design folder) and never reaches the client.
|
|
203
|
+
- A phase fires once per instance; enrichment streams in (no blocked render); errors
|
|
204
|
+
fail quietly, leaving whatever the card already had.
|
|
205
|
+
|
|
206
|
+
```yaml
|
|
207
|
+
# manifest.yaml
|
|
208
|
+
lifecycle:
|
|
209
|
+
- phase: onEnterView
|
|
210
|
+
layouts: [ focus ]
|
|
211
|
+
handler: fetchPlaceDetails
|
|
212
|
+
allowedHosts: # deny by default — lint error if calls exist without it
|
|
213
|
+
- www.searchapi.io
|
|
214
|
+
credentials:
|
|
215
|
+
- searchapiCredential # resolved server-side, never in this folder
|
|
190
216
|
```
|
|
191
217
|
|
|
218
|
+
```yaml
|
|
219
|
+
# fetchplacedetails.yaml — the handler, lowercased
|
|
220
|
+
calls:
|
|
221
|
+
- name: place
|
|
222
|
+
method: GET
|
|
223
|
+
url: https://www.searchapi.io/api/v1/search
|
|
224
|
+
query: { engine: google_maps, q: "{{ props.title }}" }
|
|
225
|
+
credential: { scheme: apiKeyQuery, param: api_key, value: "{{ credentials.searchapiCredential.apiKey }}" }
|
|
226
|
+
transport: json
|
|
227
|
+
returns: >-
|
|
228
|
+
return ((p) => p ? { rating: p.rating, address: p.address } : {})((calls.place.local_results || [])[0])
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
⚠ A raw `<phase>.js` file beside the manifest (e.g. `onstart.js` with `ctx.env`) is the
|
|
232
|
+
LEGACY form. It still runs for handlers that predate declared calls, but it is **never
|
|
233
|
+
the one to copy**: a script fetches any host with a key from the environment, and the
|
|
234
|
+
calls runtime settles both questions.
|
|
235
|
+
Exemplar: `rx/yasisland/components/restaurant-card/` (maps lookup, declared calls).
|
|
236
|
+
|
|
192
237
|
## Discovery meta: write it to RANK (docs/nodes/node-discoverability.md)
|
|
193
238
|
|
|
194
239
|
A component with a manifest is an **MCP app**: spatial embeds `` `title. whenToUse||description
|