velloo 0.1.0 → 0.3.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 +67 -41
- package/canvas/assets/index-CqXW6xA_.js +29 -0
- package/canvas/assets/index-gB0StCrx.css +2 -0
- package/canvas/assets/lucide-all-C_CNpsMs.js +1 -0
- package/canvas/assets/{radix-C3HaOB-d.js → radix-gmpYzWRH.js} +1 -1
- package/canvas/index.html +3 -3
- package/chunk-0zen7bzb.js +3 -0
- package/{chunk-h0c36khx.js → chunk-1jzhw3p4.js} +1 -1
- package/chunk-1n5kmsxb.js +2 -0
- package/{chunk-k6ferbam.js → chunk-2fgfex9b.js} +1 -1
- package/{chunk-t2ttkxe9.js → chunk-3g3dqej3.js} +1 -1
- package/chunk-3tmsrvaj.js +2 -0
- package/chunk-49cgk5s2.js +2 -0
- package/{chunk-e2ag33vd.js → chunk-4nqr089t.js} +2 -2
- package/chunk-52hxv18g.js +6 -0
- package/chunk-5ezzc1h2.js +688 -0
- package/{chunk-74skmg76.js → chunk-5wv5a52y.js} +1 -1
- package/chunk-6nzqhfqq.js +5 -0
- package/{chunk-fgw3hp2v.js → chunk-6zz9kfbd.js} +1 -1
- package/chunk-788t8y5x.js +2 -0
- package/chunk-7bvvbsqy.js +26 -0
- package/{chunk-cep3ktr3.js → chunk-7k7dpj5y.js} +1 -1
- package/{chunk-hs2j6s1v.js → chunk-7njj1qmq.js} +8 -31
- package/{chunk-8ssr8a8w.js → chunk-80fjfyha.js} +3 -6
- package/chunk-94vx1yb0.js +3 -0
- package/{chunk-n5007jcz.js → chunk-9f22qnge.js} +1 -1
- package/chunk-9g4tr0r6.js +2 -0
- package/{chunk-1gx45384.js → chunk-9nhw49vv.js} +7 -8
- package/{chunk-2z0k59hz.js → chunk-as08hzp3.js} +1 -1
- package/chunk-bhx8k09w.js +3 -0
- package/chunk-bpeezw7w.js +3 -0
- package/{chunk-b2ebm1t3.js → chunk-bvmtq0vn.js} +2 -2
- package/{chunk-w5rzk6ms.js → chunk-c4fym47a.js} +64 -20
- package/{chunk-d385435m.js → chunk-c6hywdvq.js} +1 -1
- package/{chunk-jnsfgtn9.js → chunk-cvn689d5.js} +1 -1
- package/chunk-cxzkkn12.js +2 -0
- package/{chunk-56vqqr8s.js → chunk-d9p6s05n.js} +1 -1
- package/{chunk-5js0qw32.js → chunk-dsks52m7.js} +1 -1
- package/{chunk-b7j86xq7.js → chunk-hfzg98c8.js} +1 -1
- package/{chunk-7e39xe21.js → chunk-js6mz7h1.js} +1 -1
- package/chunk-jx11jvq8.js +7 -0
- package/{chunk-2vx420g2.js → chunk-k6n249vb.js} +2 -2
- package/chunk-k7sehkm8.js +3 -0
- package/{chunk-k784090q.js → chunk-k9219kmj.js} +1 -1
- package/{chunk-65wb0g7d.js → chunk-kz4423th.js} +1 -1
- package/{chunk-g02nd9hn.js → chunk-m3czg1tt.js} +1 -1
- package/chunk-maz6z7tz.js +5 -0
- package/{chunk-rksq3w72.js → chunk-n9kcxaj7.js} +2 -2
- package/chunk-n9t36h0q.js +2 -0
- package/chunk-nr3ej3qf.js +5 -0
- package/{chunk-bk8b7f9a.js → chunk-nte0t7v3.js} +4 -3
- package/{chunk-kxk5x7g6.js → chunk-nycs0z7h.js} +1 -1
- package/{chunk-79ykzywe.js → chunk-pjtg3wnm.js} +1 -1
- package/chunk-qdnb2t6a.js +3 -0
- package/chunk-qgwhd4n6.js +86 -0
- package/chunk-sn9vg42q.js +2 -0
- package/{chunk-7p7ha0qk.js → chunk-t5bb30zy.js} +1 -1
- package/chunk-tbmkq7wy.js +6 -0
- package/chunk-tc4mzzfw.js +2 -0
- package/chunk-td5z2sm4.js +6 -0
- package/chunk-wa6qamwh.js +3 -0
- package/chunk-wcqqx6tz.js +2 -0
- package/chunk-wzngjjeb.js +10 -0
- package/chunk-xyfed1j0.js +2 -0
- package/chunk-y5r2w2z3.js +2 -0
- package/chunk-yeh9gb5w.js +3 -0
- package/cli.js +1 -1
- package/launcher.cjs +1 -1
- package/package.json +3 -3
- package/pkgs/helpers/src/descriptors.ts +7 -1
- package/pkgs/provider-antd/src/index.ts +1 -0
- package/pkgs/provider-chakra/src/index.ts +1 -0
- package/pkgs/provider-mui/src/index.ts +1 -0
- package/pkgs/provider-none/src/index.ts +42 -1
- package/pkgs/provider-none/src/intro.ts +2 -2
- package/pkgs/schema/src/config.ts +27 -1
- package/pkgs/schema/src/extension.ts +26 -0
- package/pkgs/schema/src/index.ts +18 -0
- package/pkgs/schema/src/migrate.ts +57 -4
- package/pkgs/schema/src/node.ts +99 -0
- package/pkgs/schema/src/repo.ts +98 -16
- package/pkgs/schema/src/screen.ts +7 -0
- package/pkgs/schema/src/svg-sanitize.ts +472 -59
- package/pkgs/shadcn-snapshot/dist/manifest.json +9 -1
- package/skills/velloo-setup/SKILL.md +70 -48
- package/canvas/assets/index-BCvDZiBo.css +0 -2
- package/canvas/assets/index-BcBJ11d2.js +0 -28
- package/canvas/assets/lucide-all-BdEyryfF.js +0 -1
- package/chunk-1bywxgc5.js +0 -4
- package/chunk-1y50g9vp.js +0 -2
- package/chunk-1yhc252b.js +0 -3
- package/chunk-2mv65qxs.js +0 -5
- package/chunk-329e3phg.js +0 -451
- package/chunk-40657ggd.js +0 -10
- package/chunk-582y60bv.js +0 -2
- package/chunk-5x173xay.js +0 -2
- package/chunk-75wbh438.js +0 -2
- package/chunk-7dggmqwq.js +0 -53
- package/chunk-946pcnt5.js +0 -5
- package/chunk-9vg3yafz.js +0 -3
- package/chunk-aapybh30.js +0 -6
- package/chunk-bjnhhsak.js +0 -2
- package/chunk-dt03awr1.js +0 -2
- package/chunk-erwfk47h.js +0 -3
- package/chunk-mq4znqv2.js +0 -2
- package/chunk-nfr3q7mg.js +0 -5
- package/chunk-nsqs1bqf.js +0 -2
- package/chunk-pphgrsxa.js +0 -2
- package/chunk-qpvj6kzq.js +0 -6
- package/chunk-qwv7xkms.js +0 -2
- package/chunk-tmtzxcs5.js +0 -2
- package/chunk-xr6f94rj.js +0 -2
- package/chunk-y0zxzefg.js +0 -5
- package/chunk-zf3xc1wz.js +0 -2
package/pkgs/schema/src/index.ts
CHANGED
|
@@ -66,6 +66,10 @@ export {
|
|
|
66
66
|
type ExtensionPropDescriptor,
|
|
67
67
|
ExtensionPropDescriptorSchema,
|
|
68
68
|
ExtensionSchema,
|
|
69
|
+
type RepoComponentOverride,
|
|
70
|
+
RepoComponentOverrideSchema,
|
|
71
|
+
type RepoComponentsManifest,
|
|
72
|
+
RepoComponentsManifestSchema,
|
|
69
73
|
} from "./extension.ts";
|
|
70
74
|
export {
|
|
71
75
|
type Frame,
|
|
@@ -77,9 +81,11 @@ export {
|
|
|
77
81
|
export { pascalizeIconName } from "./icon-name.ts";
|
|
78
82
|
export { type ResourceId, ResourceIdSchema } from "./ids.ts";
|
|
79
83
|
export {
|
|
84
|
+
APP_PATH_PREFIX,
|
|
80
85
|
CURRENT_SCHEMA_VERSION,
|
|
81
86
|
FOLDER_MIGRATIONS,
|
|
82
87
|
type FolderMigration,
|
|
88
|
+
type MigrationContext,
|
|
83
89
|
type MigrationRun,
|
|
84
90
|
planMigration,
|
|
85
91
|
schemaVersionOf,
|
|
@@ -88,20 +94,32 @@ export {
|
|
|
88
94
|
type ComponentNode,
|
|
89
95
|
isComponentNode,
|
|
90
96
|
isParamRef,
|
|
97
|
+
isRepoNode,
|
|
91
98
|
isSnippetInstance,
|
|
92
99
|
type Node,
|
|
93
100
|
NodeIdSchema,
|
|
94
101
|
NodeSchema,
|
|
95
102
|
nodeId,
|
|
96
103
|
type ParamRef,
|
|
104
|
+
parseRepoKey,
|
|
105
|
+
type RepoComponentRef,
|
|
106
|
+
RepoComponentRefSchema,
|
|
107
|
+
repoImportIssue,
|
|
108
|
+
repoKey,
|
|
97
109
|
type SnippetInstance,
|
|
98
110
|
} from "./node.ts";
|
|
99
111
|
export {
|
|
112
|
+
DesignNameSchema,
|
|
113
|
+
designNameIssue,
|
|
100
114
|
type FeedbackPrefs,
|
|
101
115
|
FeedbackPrefsSchema,
|
|
116
|
+
isDesignName,
|
|
117
|
+
type NormalizedRepoManifest,
|
|
118
|
+
normalizeRepoManifest,
|
|
102
119
|
REPO_MANIFEST_FILE,
|
|
103
120
|
type RepoManifest,
|
|
104
121
|
RepoManifestSchema,
|
|
122
|
+
toDesignName,
|
|
105
123
|
} from "./repo.ts";
|
|
106
124
|
export { type Screen, ScreenSchema } from "./screen.ts";
|
|
107
125
|
export { type Snippet, type SnippetParam, SnippetParamSchema, SnippetSchema } from "./snippet.ts";
|
|
@@ -24,9 +24,14 @@
|
|
|
24
24
|
* `letterSpacing` records are gone and `typography` parses strictly, so
|
|
25
25
|
* a v2 theme file is now a hard validation failure rather than dead
|
|
26
26
|
* weight — hence the version gate.
|
|
27
|
+
* 4 — designs carry their own `name` (formerly the `velloo.json` project
|
|
28
|
+
* key or a local record's `projectName`), and app-relative paths use the
|
|
29
|
+
* `app:` prefix instead of `project:`.
|
|
27
30
|
*/
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
import { isDesignName, toDesignName } from "./repo.ts";
|
|
33
|
+
|
|
34
|
+
export const CURRENT_SCHEMA_VERSION = 4;
|
|
30
35
|
|
|
31
36
|
/**
|
|
32
37
|
* Read the format version off a raw (unvalidated) config object. Historical
|
|
@@ -41,19 +46,32 @@ export function schemaVersionOf(rawConfig: unknown): number {
|
|
|
41
46
|
|
|
42
47
|
type RawObject = Record<string, unknown>;
|
|
43
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Facts a pure migration can't read for itself. The CLI's upgrade supplies
|
|
51
|
+
* them from the files around the folder.
|
|
52
|
+
*/
|
|
53
|
+
export interface MigrationContext {
|
|
54
|
+
/** The design's name as its registration recorded it. */
|
|
55
|
+
name?: string | undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
export interface FolderMigration {
|
|
45
59
|
/** Migrates from exactly this version to `from + 1`. */
|
|
46
60
|
from: number;
|
|
47
61
|
/** One-line, user-facing description (printed by `velloo upgrade`). */
|
|
48
62
|
summary: string;
|
|
49
63
|
/** Transform the raw config.json object (already shallow-cloned). */
|
|
50
|
-
config?(raw: RawObject): RawObject;
|
|
64
|
+
config?(raw: RawObject, context: MigrationContext): RawObject;
|
|
51
65
|
/** Transform one annotation entry from a `screens/<id>.annotations.json` sidecar. */
|
|
52
66
|
annotation?(raw: RawObject): RawObject;
|
|
53
67
|
/** Transform one `theme/<name>.json` document. */
|
|
54
68
|
theme?(raw: RawObject): RawObject;
|
|
55
69
|
}
|
|
56
70
|
|
|
71
|
+
/** Prefix of a path resolved from the design's application root. */
|
|
72
|
+
export const APP_PATH_PREFIX = "app:";
|
|
73
|
+
const LEGACY_APP_PREFIX = "project:";
|
|
74
|
+
|
|
57
75
|
/** Library id assigned to a legacy single-library config when it is promoted. */
|
|
58
76
|
const LEGACY_LIBRARY_KEY = "default";
|
|
59
77
|
|
|
@@ -194,6 +212,41 @@ export const FOLDER_MIGRATIONS: FolderMigration[] = [
|
|
|
194
212
|
return { ...raw, typography: next };
|
|
195
213
|
},
|
|
196
214
|
},
|
|
215
|
+
{
|
|
216
|
+
from: 3,
|
|
217
|
+
summary: "the design's name moves into its config; `project:` paths become `app:`",
|
|
218
|
+
config(raw, context) {
|
|
219
|
+
const out: RawObject = { ...raw, schemaVersion: 4 };
|
|
220
|
+
if (typeof out.name !== "string" || !isDesignName(out.name)) {
|
|
221
|
+
out.name = (context.name && toDesignName(context.name)) ?? "design";
|
|
222
|
+
}
|
|
223
|
+
const toApp = (path: unknown) =>
|
|
224
|
+
typeof path === "string" && path.startsWith(LEGACY_APP_PREFIX)
|
|
225
|
+
? `${APP_PATH_PREFIX}${path.slice(LEGACY_APP_PREFIX.length)}`
|
|
226
|
+
: path;
|
|
227
|
+
const hostApp = asObject(out.hostApp);
|
|
228
|
+
if (hostApp) out.hostApp = { ...hostApp, root: toApp(hostApp.root) };
|
|
229
|
+
const hostApps = asObject(out.hostApps);
|
|
230
|
+
if (hostApps) {
|
|
231
|
+
out.hostApps = Object.fromEntries(
|
|
232
|
+
Object.entries(hostApps).map(([key, app]) => {
|
|
233
|
+
const entry = asObject(app);
|
|
234
|
+
return [key, entry ? { ...entry, root: toApp(entry.root) } : app];
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const libraries = asObject(out.libraries);
|
|
239
|
+
if (libraries) {
|
|
240
|
+
out.libraries = Object.fromEntries(
|
|
241
|
+
Object.entries(libraries).map(([key, lib]) => {
|
|
242
|
+
const entry = asObject(lib);
|
|
243
|
+
return [key, entry ? { ...entry, componentsPath: toApp(entry.componentsPath) } : lib];
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
},
|
|
249
|
+
},
|
|
197
250
|
];
|
|
198
251
|
|
|
199
252
|
export interface MigrationRun {
|
|
@@ -216,7 +269,7 @@ export interface MigrationRun {
|
|
|
216
269
|
* (version > current) — the caller should tell the user to upgrade velloo —
|
|
217
270
|
* or when the chain has a gap (a bug in this module).
|
|
218
271
|
*/
|
|
219
|
-
export function planMigration(rawConfig: unknown): MigrationRun {
|
|
272
|
+
export function planMigration(rawConfig: unknown, context: MigrationContext = {}): MigrationRun {
|
|
220
273
|
const from = schemaVersionOf(rawConfig);
|
|
221
274
|
if (from > CURRENT_SCHEMA_VERSION) {
|
|
222
275
|
throw new Error(
|
|
@@ -231,7 +284,7 @@ export function planMigration(rawConfig: unknown): MigrationRun {
|
|
|
231
284
|
}
|
|
232
285
|
let config = { ...((rawConfig ?? {}) as RawObject) };
|
|
233
286
|
for (const step of steps) {
|
|
234
|
-
if (step.config) config = step.config(config);
|
|
287
|
+
if (step.config) config = step.config(config, context);
|
|
235
288
|
}
|
|
236
289
|
// Even a no-step run normalizes the version field so `upgrade` is idempotent.
|
|
237
290
|
config.schemaVersion = CURRENT_SCHEMA_VERSION;
|
package/pkgs/schema/src/node.ts
CHANGED
|
@@ -47,6 +47,37 @@ export type ComponentNode = {
|
|
|
47
47
|
* capture → design → emit. Absent ⇒ the node emits as itself.
|
|
48
48
|
*/
|
|
49
49
|
$emitAs?: { name: string; importPath: string } | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Repository component identity: this node IS a component the host app
|
|
52
|
+
* imports (a package export such as Mantine's `Tabs`, or the app's own
|
|
53
|
+
* `StatCard`), not a library component. `$ref` stays the JSX name codegen
|
|
54
|
+
* prints; the identity decides rendering and imports. Takes precedence over
|
|
55
|
+
* any provider component or extension of the same name, so shadowing is
|
|
56
|
+
* explicit on the node rather than a registry-order accident.
|
|
57
|
+
*/
|
|
58
|
+
$repo?: RepoComponentRef | undefined;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Where a repository component comes from. The import form is kept exactly as
|
|
63
|
+
* code generation must print it; the resolved file is runtime cache data and
|
|
64
|
+
* never persisted.
|
|
65
|
+
*/
|
|
66
|
+
export type RepoComponentRef = {
|
|
67
|
+
/**
|
|
68
|
+
* Module specifier. Bare (`@mantine/core`) and aliased (`@/components/card`)
|
|
69
|
+
* specifiers are kept as the app writes them; a `./`-relative one is relative
|
|
70
|
+
* to the host app root, since the importing file varies per call site.
|
|
71
|
+
*/
|
|
72
|
+
importPath: string;
|
|
73
|
+
/** The export binding, or `"default"` for a default export. */
|
|
74
|
+
exportName: string;
|
|
75
|
+
/** Static member path of a compound part: `"List"` for `Tabs.List`. */
|
|
76
|
+
member?: string | undefined;
|
|
77
|
+
/** `config.hostApps` key in a monorepo; absent ⇒ the default host app. */
|
|
78
|
+
app?: string | undefined;
|
|
79
|
+
/** Snippet id drawn in the component's place when it can't render for real. */
|
|
80
|
+
proxy?: string | undefined;
|
|
50
81
|
};
|
|
51
82
|
|
|
52
83
|
export type SnippetInstance = {
|
|
@@ -126,6 +157,42 @@ const EmitAsSchema = z.object({
|
|
|
126
157
|
importPath: z.string().min(1),
|
|
127
158
|
});
|
|
128
159
|
|
|
160
|
+
/**
|
|
161
|
+
* A specifier codegen may print verbatim inside `import … from "…"` and the
|
|
162
|
+
* bundler may resolve against the host app. Conservative charset (no quotes,
|
|
163
|
+
* spaces, semicolons), and no absolute or `..` paths: a design folder is data a
|
|
164
|
+
* cloned repo supplies, and it must not be able to reach outside the app.
|
|
165
|
+
*/
|
|
166
|
+
export function repoImportIssue(importPath: string): string | null {
|
|
167
|
+
if (!/^[\w@./~-]+$/.test(importPath)) return "has characters an import specifier can't carry";
|
|
168
|
+
if (importPath.startsWith("/")) return "must not be an absolute path";
|
|
169
|
+
if (importPath.split("/").includes("..")) return "must not climb out of the host app with ..";
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
174
|
+
|
|
175
|
+
export const RepoComponentRefSchema = z.object({
|
|
176
|
+
importPath: z
|
|
177
|
+
.string()
|
|
178
|
+
.min(1)
|
|
179
|
+
.superRefine((value, ctx) => {
|
|
180
|
+
const issue = repoImportIssue(value);
|
|
181
|
+
if (issue) ctx.addIssue({ code: "custom", message: `importPath ${issue}` });
|
|
182
|
+
}),
|
|
183
|
+
exportName: z
|
|
184
|
+
.string()
|
|
185
|
+
.regex(IDENTIFIER, { message: 'exportName must be an identifier or "default"' }),
|
|
186
|
+
member: z
|
|
187
|
+
.string()
|
|
188
|
+
.regex(/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/, {
|
|
189
|
+
message: 'member must be a dotted identifier path like "List"',
|
|
190
|
+
})
|
|
191
|
+
.optional(),
|
|
192
|
+
app: z.string().min(1).optional(),
|
|
193
|
+
proxy: z.string().min(1).optional(),
|
|
194
|
+
});
|
|
195
|
+
|
|
129
196
|
const ComponentNodeSchema: z.ZodType<ComponentNode> = z.lazy(() =>
|
|
130
197
|
z.object({
|
|
131
198
|
$ref: z.string().min(1),
|
|
@@ -136,9 +203,41 @@ const ComponentNodeSchema: z.ZodType<ComponentNode> = z.lazy(() =>
|
|
|
136
203
|
.transform((items) => items.map(wrapScalarChild))
|
|
137
204
|
.optional(),
|
|
138
205
|
$emitAs: EmitAsSchema.optional(),
|
|
206
|
+
$repo: RepoComponentRefSchema.optional(),
|
|
139
207
|
}),
|
|
140
208
|
);
|
|
141
209
|
|
|
210
|
+
/** True when the node is a repository component (see ComponentNode.$repo). */
|
|
211
|
+
export function isRepoNode(n: Node): n is ComponentNode & { $repo: RepoComponentRef } {
|
|
212
|
+
return isComponentNode(n) && n.$repo !== undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The runtime key a repository component registers under — identity only, so
|
|
217
|
+
* two apps' `Button`s (or Mantine's vs the provider's) never collide.
|
|
218
|
+
*/
|
|
219
|
+
export function repoKey(ref: RepoComponentRef): string {
|
|
220
|
+
const member = ref.member ? `.${ref.member}` : "";
|
|
221
|
+
return `repo:${encodeURIComponent(ref.app ?? "")}:${ref.importPath}#${ref.exportName}${member}`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Invert {@link repoKey}. A bundle URL carries only keys, so the runtime can
|
|
226
|
+
* resolve a screen's components from identity alone — no catalog lookup, which
|
|
227
|
+
* keeps a design renderable while discovery is cold or the app has moved on.
|
|
228
|
+
*/
|
|
229
|
+
export function parseRepoKey(key: string): RepoComponentRef | null {
|
|
230
|
+
const match = /^repo:([^:]*):([^#]+)#([A-Za-z_$][\w$]*)(?:\.(.+))?$/.exec(key);
|
|
231
|
+
if (!match) return null;
|
|
232
|
+
const parsed = RepoComponentRefSchema.safeParse({
|
|
233
|
+
importPath: match[2],
|
|
234
|
+
exportName: match[3],
|
|
235
|
+
...(match[4] ? { member: match[4] } : {}),
|
|
236
|
+
...(match[1] ? { app: decodeURIComponent(match[1]) } : {}),
|
|
237
|
+
});
|
|
238
|
+
return parsed.success ? parsed.data : null;
|
|
239
|
+
}
|
|
240
|
+
|
|
142
241
|
const SnippetInstanceSchema: z.ZodType<SnippetInstance> = z.object({
|
|
143
242
|
$snippet: z.string().min(1),
|
|
144
243
|
$id: NodeIdSchema.optional(),
|
package/pkgs/schema/src/repo.ts
CHANGED
|
@@ -18,35 +18,117 @@ export const FeedbackPrefsSchema = z.object({
|
|
|
18
18
|
|
|
19
19
|
export type FeedbackPrefs = z.infer<typeof FeedbackPrefsSchema>;
|
|
20
20
|
|
|
21
|
-
const
|
|
21
|
+
const MAX_DESIGN_NAME = 80;
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
* Why `name` can't name a design, or null when it can. Almost anything goes —
|
|
25
|
+
* spaces, emoji, any script. The limits are the ones a name typed as a command
|
|
26
|
+
* argument needs: no path separator (a bare argument with one is read as a
|
|
27
|
+
* path), no control characters, nothing that is only whitespace or a dot
|
|
28
|
+
* path, and no padding a shell would silently strip.
|
|
29
|
+
*/
|
|
30
|
+
export function designNameIssue(name: string): string | null {
|
|
31
|
+
if (name.trim() === "") return "a design name can't be empty";
|
|
32
|
+
if (name !== name.trim()) return "a design name can't start or end with spaces";
|
|
33
|
+
if (name === "." || name === "..") return `"${name}" can't be a design name`;
|
|
34
|
+
if (/[/\\]/.test(name)) return "a design name can't contain / or \\";
|
|
35
|
+
if (/\p{Cc}/u.test(name)) return "a design name can't contain control characters";
|
|
36
|
+
if ([...name].length > MAX_DESIGN_NAME)
|
|
37
|
+
return `a design name can't be longer than ${MAX_DESIGN_NAME} characters`;
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isDesignName(name: string): boolean {
|
|
42
|
+
return designNameIssue(name) === null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A Zod string that must be a design name, with the specific reason when it isn't. */
|
|
46
|
+
export const DesignNameSchema = z.string().superRefine((name, ctx) => {
|
|
47
|
+
const issue = designNameIssue(name);
|
|
48
|
+
if (issue) ctx.addIssue({ code: "custom", message: issue });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Coerce free text (a directory name, an app name) into a valid design name,
|
|
53
|
+
* or null when nothing usable is left.
|
|
54
|
+
*/
|
|
55
|
+
export function toDesignName(text: string): string | null {
|
|
56
|
+
const cleaned = [...text.replace(/[/\\\p{Cc}]+/gu, " ").trim()]
|
|
57
|
+
.slice(0, MAX_DESIGN_NAME)
|
|
58
|
+
.join("")
|
|
59
|
+
.trim();
|
|
60
|
+
return cleaned && isDesignName(cleaned) ? cleaned : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The repo-root `velloo.json`. It lists a repo's designs so a monorepo can
|
|
65
|
+
* hold several, and carries the handful of preferences that belong to the
|
|
66
|
+
* repo rather than to any one design — feedback consent is answered once per
|
|
67
|
+
* person, not once per canvas.
|
|
31
68
|
*
|
|
32
|
-
*
|
|
69
|
+
* Only locations live here: each design's name and settings are in its own
|
|
70
|
+
* `.design/config.json`, so nothing here can drift from the design itself.
|
|
71
|
+
* Every entry is a path to a design folder inside the repository. A design
|
|
33
72
|
* kept outside it is recorded only on the machine that has it, never here.
|
|
34
73
|
*/
|
|
35
74
|
export const RepoManifestSchema = z
|
|
36
75
|
.object({
|
|
37
76
|
$schema: z.string().optional(),
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
),
|
|
42
|
-
defaultProject: z.string().optional(),
|
|
77
|
+
designs: z.array(z.string().min(1)),
|
|
78
|
+
/** The name of the design a bare command resolves to when nothing else picks one. */
|
|
79
|
+
defaultDesign: DesignNameSchema.optional(),
|
|
43
80
|
feedback: FeedbackPrefsSchema.optional(),
|
|
44
81
|
})
|
|
45
|
-
.refine((m) =>
|
|
46
|
-
message: "
|
|
82
|
+
.refine((m) => new Set(m.designs).size === m.designs.length, {
|
|
83
|
+
message: "designs lists the same path twice",
|
|
47
84
|
});
|
|
48
85
|
|
|
49
86
|
export type RepoManifest = z.infer<typeof RepoManifestSchema>;
|
|
50
87
|
|
|
51
88
|
/** Filename of the repo-root manifest, resolved by walking up from a folder. */
|
|
52
89
|
export const REPO_MANIFEST_FILE = "velloo.json";
|
|
90
|
+
|
|
91
|
+
export interface NormalizedRepoManifest {
|
|
92
|
+
/** The manifest in the current shape, ready for {@link RepoManifestSchema}. */
|
|
93
|
+
manifest: unknown;
|
|
94
|
+
/**
|
|
95
|
+
* Names the pre-v4 `projects` map gave each path. A folder not yet migrated
|
|
96
|
+
* has no `name` in its config, so these stand in until `velloo upgrade`
|
|
97
|
+
* writes them there.
|
|
98
|
+
*/
|
|
99
|
+
legacyNames: Record<string, string>;
|
|
100
|
+
/** True when the file was in the pre-v4 shape and should be rewritten. */
|
|
101
|
+
legacy: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Read either manifest shape. Before designs carried their own names the file
|
|
106
|
+
* was `{ projects: { name: path }, defaultProject }`; the schema would silently
|
|
107
|
+
* drop those keys, so they are converted here, before parsing.
|
|
108
|
+
*/
|
|
109
|
+
export function normalizeRepoManifest(raw: unknown): NormalizedRepoManifest {
|
|
110
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
111
|
+
return { manifest: raw, legacyNames: {}, legacy: false };
|
|
112
|
+
}
|
|
113
|
+
const { projects, defaultProject, ...rest } = raw as Record<string, unknown>;
|
|
114
|
+
if (projects === undefined || "designs" in rest) {
|
|
115
|
+
return { manifest: raw, legacyNames: {}, legacy: false };
|
|
116
|
+
}
|
|
117
|
+
const legacyNames: Record<string, string> = {};
|
|
118
|
+
const designs: unknown[] = [];
|
|
119
|
+
if (typeof projects === "object" && projects !== null && !Array.isArray(projects)) {
|
|
120
|
+
for (const [name, path] of Object.entries(projects)) {
|
|
121
|
+
designs.push(path);
|
|
122
|
+
if (typeof path === "string") legacyNames[path] = name;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
manifest: {
|
|
127
|
+
...rest,
|
|
128
|
+
designs,
|
|
129
|
+
...(defaultProject !== undefined ? { defaultDesign: defaultProject } : {}),
|
|
130
|
+
},
|
|
131
|
+
legacyNames,
|
|
132
|
+
legacy: true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -24,6 +24,13 @@ export const ScreenSchema = z.object({
|
|
|
24
24
|
* components and extensions resolve against this library's registry.
|
|
25
25
|
*/
|
|
26
26
|
library: z.string().min(1).optional(),
|
|
27
|
+
/**
|
|
28
|
+
* The app route this screen stands for ("/", "/settings/account"). Set by
|
|
29
|
+
* the route scan `init` runs, and editable per screen. The canvas mount
|
|
30
|
+
* hands it to the host framework's router contexts, so a repository nav
|
|
31
|
+
* lights the item the real page would.
|
|
32
|
+
*/
|
|
33
|
+
route: z.string().min(1).optional(),
|
|
27
34
|
tree: NodeSchema,
|
|
28
35
|
});
|
|
29
36
|
|