wensity 0.1.4 → 0.1.6
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/LICENSE +21 -0
- package/README.md +49 -11
- package/dist/index.js +1757 -159
- package/package.json +12 -8
package/dist/index.js
CHANGED
|
@@ -1,12 +1,809 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/commands.ts
|
|
4
|
-
import { mkdir as
|
|
5
|
-
import { dirname as
|
|
4
|
+
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
5
|
+
import { dirname as dirname6, join as join6 } from "path";
|
|
6
|
+
|
|
7
|
+
// ../wensity-preset/src/constants.ts
|
|
8
|
+
var ICON_LIBRARIES = ["tabler", "lucide"];
|
|
9
|
+
var RADIUS_PRESETS = ["none", "small", "medium", "large"];
|
|
10
|
+
var BUILTIN_FONT_IDS = ["site", "system-sans", "system-mono"];
|
|
11
|
+
var RADIUS_PRESET_CSS = {
|
|
12
|
+
none: "0px",
|
|
13
|
+
small: "0.5rem",
|
|
14
|
+
medium: "0.875rem",
|
|
15
|
+
large: "1.25rem"
|
|
16
|
+
};
|
|
17
|
+
var FONT_ID_EXPANSIONS = {
|
|
18
|
+
"site:sans": "var(--font-sans)",
|
|
19
|
+
"site:display": "var(--font-display)",
|
|
20
|
+
"site:mono": "var(--font-mono)",
|
|
21
|
+
"system-sans": "ui-sans-serif, system-ui, sans-serif",
|
|
22
|
+
"system-mono": "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace"
|
|
23
|
+
};
|
|
24
|
+
var CONTROL_SOLID_COLOR_KEYS = [
|
|
25
|
+
"controlSolid",
|
|
26
|
+
"controlSolidHover",
|
|
27
|
+
"controlSolidActive",
|
|
28
|
+
"controlSolidForeground"
|
|
29
|
+
];
|
|
30
|
+
var CONTROL_SOLID_CSS_VARS = {
|
|
31
|
+
controlSolid: "--primitive-control-solid",
|
|
32
|
+
controlSolidHover: "--primitive-control-solid-hover",
|
|
33
|
+
controlSolidActive: "--primitive-control-solid-active",
|
|
34
|
+
controlSolidForeground: "--primitive-control-solid-foreground"
|
|
35
|
+
};
|
|
36
|
+
var STYLE_OBJECT_KEYS = [
|
|
37
|
+
"iconLibrary",
|
|
38
|
+
"radius",
|
|
39
|
+
"fonts",
|
|
40
|
+
"colors",
|
|
41
|
+
"presetCode"
|
|
42
|
+
];
|
|
43
|
+
var FONT_ROLE_KEYS = ["sans", "display", "mono"];
|
|
44
|
+
var COLOR_MODE_KEYS = ["light", "dark"];
|
|
45
|
+
var HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
46
|
+
var CUSTOM_FONT_PREFIX = "custom:";
|
|
47
|
+
var CUSTOM_FONT_STACK_MAX_LENGTH = 120;
|
|
48
|
+
var PRESET_CODEC_PREFIX = "wsty1";
|
|
49
|
+
var PRESET_CODEC_MAX_DECODED_JSON_BYTES = 4 * 1024;
|
|
50
|
+
var PRESET_CODEC_MAX_CODE_LENGTH = 6 * 1024;
|
|
51
|
+
var CONFIG_SCHEMA_VERSION = 1;
|
|
52
|
+
function isBuiltinFontId(value) {
|
|
53
|
+
return BUILTIN_FONT_IDS.includes(value);
|
|
54
|
+
}
|
|
55
|
+
function isIconLibrary(value) {
|
|
56
|
+
return ICON_LIBRARIES.includes(value);
|
|
57
|
+
}
|
|
58
|
+
function isRadiusPreset(value) {
|
|
59
|
+
return RADIUS_PRESETS.includes(value);
|
|
60
|
+
}
|
|
61
|
+
function isFontRoleValue(value) {
|
|
62
|
+
if (isBuiltinFontId(value)) return true;
|
|
63
|
+
return value.startsWith(CUSTOM_FONT_PREFIX) && value.length > CUSTOM_FONT_PREFIX.length;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ../wensity-preset/src/defaults.ts
|
|
67
|
+
var DEFAULT_STYLE_FONTS = {
|
|
68
|
+
sans: "site",
|
|
69
|
+
display: "site",
|
|
70
|
+
mono: "system-mono"
|
|
71
|
+
};
|
|
72
|
+
var DEFAULT_CONTROL_SOLID_LIGHT = {
|
|
73
|
+
controlSolid: "#24292d",
|
|
74
|
+
controlSolidHover: "#2c3237",
|
|
75
|
+
controlSolidActive: "#1e2326",
|
|
76
|
+
controlSolidForeground: "#ffffff"
|
|
77
|
+
};
|
|
78
|
+
var DEFAULT_CONTROL_SOLID_DARK = {
|
|
79
|
+
controlSolid: "#f5f3ee",
|
|
80
|
+
controlSolidHover: "#ffffff",
|
|
81
|
+
controlSolidActive: "#e8e4da",
|
|
82
|
+
controlSolidForeground: "#151719"
|
|
83
|
+
};
|
|
84
|
+
var DEFAULT_STYLE_CONFIG = {
|
|
85
|
+
iconLibrary: "tabler",
|
|
86
|
+
radius: "medium",
|
|
87
|
+
fonts: { ...DEFAULT_STYLE_FONTS },
|
|
88
|
+
colors: {
|
|
89
|
+
light: { ...DEFAULT_CONTROL_SOLID_LIGHT },
|
|
90
|
+
dark: { ...DEFAULT_CONTROL_SOLID_DARK }
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
function createDefaultStyleConfig() {
|
|
94
|
+
return {
|
|
95
|
+
iconLibrary: DEFAULT_STYLE_CONFIG.iconLibrary,
|
|
96
|
+
radius: DEFAULT_STYLE_CONFIG.radius,
|
|
97
|
+
fonts: { ...DEFAULT_STYLE_CONFIG.fonts },
|
|
98
|
+
colors: {
|
|
99
|
+
light: { ...DEFAULT_CONTROL_SOLID_LIGHT },
|
|
100
|
+
dark: { ...DEFAULT_CONTROL_SOLID_DARK }
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ../wensity-preset/src/schema.ts
|
|
106
|
+
var fontRoleSchema = {
|
|
107
|
+
oneOf: [
|
|
108
|
+
{ type: "string", enum: [...BUILTIN_FONT_IDS] },
|
|
109
|
+
{
|
|
110
|
+
type: "string",
|
|
111
|
+
pattern: `^${CUSTOM_FONT_PREFIX}.+`,
|
|
112
|
+
minLength: CUSTOM_FONT_PREFIX.length + 1
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
};
|
|
116
|
+
var controlSolidColorsSchema = {
|
|
117
|
+
type: "object",
|
|
118
|
+
additionalProperties: false,
|
|
119
|
+
properties: Object.fromEntries(
|
|
120
|
+
CONTROL_SOLID_COLOR_KEYS.map((key) => [
|
|
121
|
+
key,
|
|
122
|
+
{ type: "string", pattern: HEX_COLOR_PATTERN.source }
|
|
123
|
+
])
|
|
124
|
+
)
|
|
125
|
+
};
|
|
126
|
+
var WENSITY_CONFIG_SCHEMA = {
|
|
127
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
128
|
+
$id: "https://ui.wensity.com/schema/wensity.json",
|
|
129
|
+
title: "Wensity CLI configuration",
|
|
130
|
+
type: "object",
|
|
131
|
+
additionalProperties: false,
|
|
132
|
+
properties: {
|
|
133
|
+
$schema: { type: "string" },
|
|
134
|
+
version: { type: "integer", const: 1 },
|
|
135
|
+
registry: { type: "string", format: "uri" },
|
|
136
|
+
components: { type: "string", minLength: 1 },
|
|
137
|
+
utils: { type: "string", minLength: 1 },
|
|
138
|
+
tailwind: {
|
|
139
|
+
type: "object",
|
|
140
|
+
additionalProperties: false,
|
|
141
|
+
properties: {
|
|
142
|
+
config: { type: "string" },
|
|
143
|
+
css: { type: "string" }
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
aliases: {
|
|
147
|
+
type: "object",
|
|
148
|
+
additionalProperties: false,
|
|
149
|
+
properties: {
|
|
150
|
+
components: { type: "string", minLength: 1 },
|
|
151
|
+
utils: { type: "string", minLength: 1 }
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
style: {
|
|
155
|
+
type: "object",
|
|
156
|
+
additionalProperties: false,
|
|
157
|
+
properties: {
|
|
158
|
+
iconLibrary: { type: "string", enum: [...ICON_LIBRARIES] },
|
|
159
|
+
radius: { type: "string", enum: [...RADIUS_PRESETS] },
|
|
160
|
+
fonts: {
|
|
161
|
+
type: "object",
|
|
162
|
+
additionalProperties: false,
|
|
163
|
+
properties: {
|
|
164
|
+
sans: fontRoleSchema,
|
|
165
|
+
display: fontRoleSchema,
|
|
166
|
+
mono: fontRoleSchema
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
colors: {
|
|
170
|
+
type: "object",
|
|
171
|
+
additionalProperties: false,
|
|
172
|
+
properties: {
|
|
173
|
+
light: controlSolidColorsSchema,
|
|
174
|
+
dark: controlSolidColorsSchema
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
presetCode: {
|
|
178
|
+
type: "string",
|
|
179
|
+
minLength: 1,
|
|
180
|
+
maxLength: PRESET_CODEC_MAX_CODE_LENGTH
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
required: ["registry", "components", "utils", "aliases"]
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// ../wensity-preset/src/fonts.ts
|
|
189
|
+
var ALLOWED_UNQUOTED_FONT_TOKENS = [
|
|
190
|
+
"ui-sans-serif",
|
|
191
|
+
"system-ui",
|
|
192
|
+
"sans-serif",
|
|
193
|
+
"monospace",
|
|
194
|
+
"ui-monospace",
|
|
195
|
+
"SFMono-Regular",
|
|
196
|
+
"Menlo",
|
|
197
|
+
"Monaco",
|
|
198
|
+
"Consolas",
|
|
199
|
+
"var(--font-sans)",
|
|
200
|
+
"var(--font-display)",
|
|
201
|
+
"var(--font-mono)"
|
|
202
|
+
];
|
|
203
|
+
var ALLOWED_UNQUOTED_SET = new Set(ALLOWED_UNQUOTED_FONT_TOKENS);
|
|
204
|
+
var QUOTED_FAMILY_PATTERN = /^(["'])([A-Za-z0-9 -]+)\1$/;
|
|
205
|
+
var CONTROL_OR_BREAKOUT_PATTERN = /[\u0000-\u001f\u007f;{}\\\n\r<>]|url\s*\(|@import|expression\s*\(|<\//i;
|
|
206
|
+
function validateCustomFontStack(value, path) {
|
|
207
|
+
if (!value.startsWith(CUSTOM_FONT_PREFIX)) {
|
|
208
|
+
throw new PresetValidationError(`Expected custom font stack at ${path}.`, path);
|
|
209
|
+
}
|
|
210
|
+
const stack = value.slice(CUSTOM_FONT_PREFIX.length);
|
|
211
|
+
if (stack.length === 0) {
|
|
212
|
+
throw new PresetValidationError(`Empty custom font stack at ${path}.`, path);
|
|
213
|
+
}
|
|
214
|
+
if (stack.length > CUSTOM_FONT_STACK_MAX_LENGTH) {
|
|
215
|
+
throw new PresetValidationError(
|
|
216
|
+
`Custom font stack at ${path} exceeds ${CUSTOM_FONT_STACK_MAX_LENGTH} characters.`,
|
|
217
|
+
path
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (CONTROL_OR_BREAKOUT_PATTERN.test(stack)) {
|
|
221
|
+
throw new PresetValidationError(
|
|
222
|
+
`Custom font stack at ${path} contains forbidden characters or CSS breakout syntax.`,
|
|
223
|
+
path
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
const tokens = splitFontStackTokens(stack);
|
|
227
|
+
if (tokens.length === 0) {
|
|
228
|
+
throw new PresetValidationError(`Custom font stack at ${path} has no tokens.`, path);
|
|
229
|
+
}
|
|
230
|
+
for (const token of tokens) {
|
|
231
|
+
validateFontStackToken(token, path);
|
|
232
|
+
}
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
function splitFontStackTokens(stack) {
|
|
236
|
+
const tokens = [];
|
|
237
|
+
let current = "";
|
|
238
|
+
let quote = null;
|
|
239
|
+
for (let i = 0; i < stack.length; i += 1) {
|
|
240
|
+
const ch = stack[i];
|
|
241
|
+
if (quote) {
|
|
242
|
+
current += ch;
|
|
243
|
+
if (ch === quote) quote = null;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (ch === '"' || ch === "'") {
|
|
247
|
+
quote = ch;
|
|
248
|
+
current += ch;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (ch === ",") {
|
|
252
|
+
const trimmed2 = current.trim();
|
|
253
|
+
if (trimmed2) tokens.push(trimmed2);
|
|
254
|
+
current = "";
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
current += ch;
|
|
258
|
+
}
|
|
259
|
+
if (quote) {
|
|
260
|
+
throw new PresetValidationError("Unterminated quoted font family in custom stack.", "fonts");
|
|
261
|
+
}
|
|
262
|
+
const trimmed = current.trim();
|
|
263
|
+
if (trimmed) tokens.push(trimmed);
|
|
264
|
+
return tokens;
|
|
265
|
+
}
|
|
266
|
+
function validateFontStackToken(token, path) {
|
|
267
|
+
if (QUOTED_FAMILY_PATTERN.test(token)) return;
|
|
268
|
+
if (ALLOWED_UNQUOTED_SET.has(token)) return;
|
|
269
|
+
throw new PresetValidationError(
|
|
270
|
+
`Invalid font token "${token}" at ${path}. Use a quoted family or an allowlisted keyword.`,
|
|
271
|
+
path
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
function expandFontRole(value, role) {
|
|
275
|
+
if (value.startsWith(CUSTOM_FONT_PREFIX)) {
|
|
276
|
+
validateCustomFontStack(value, `fonts.${role}`);
|
|
277
|
+
return value.slice(CUSTOM_FONT_PREFIX.length);
|
|
278
|
+
}
|
|
279
|
+
if (value === "site") {
|
|
280
|
+
if (role === "sans") return FONT_ID_EXPANSIONS["site:sans"];
|
|
281
|
+
if (role === "display") return FONT_ID_EXPANSIONS["site:display"];
|
|
282
|
+
return FONT_ID_EXPANSIONS["site:mono"];
|
|
283
|
+
}
|
|
284
|
+
if (value === "system-sans") return FONT_ID_EXPANSIONS["system-sans"];
|
|
285
|
+
if (value === "system-mono") return FONT_ID_EXPANSIONS["system-mono"];
|
|
286
|
+
if (isBuiltinFontId(value)) {
|
|
287
|
+
throw new PresetValidationError(`Unhandled built-in font id "${value}".`, `fonts.${role}`);
|
|
288
|
+
}
|
|
289
|
+
throw new PresetValidationError(`Invalid font role value at fonts.${role}.`, `fonts.${role}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ../wensity-preset/src/validate.ts
|
|
293
|
+
var FORBIDDEN_OBJECT_KEYS = ["__proto__", "constructor", "prototype"];
|
|
294
|
+
var PresetValidationError = class extends Error {
|
|
295
|
+
path;
|
|
296
|
+
constructor(message, path = "style") {
|
|
297
|
+
super(message);
|
|
298
|
+
this.name = "PresetValidationError";
|
|
299
|
+
this.path = path;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
function assertNoForbiddenObjectKeys(value, path) {
|
|
303
|
+
for (const key of FORBIDDEN_OBJECT_KEYS) {
|
|
304
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
305
|
+
throw new PresetValidationError(`Forbidden key at ${path}.`, path);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function assertPlainObject(value, path) {
|
|
310
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
311
|
+
throw new PresetValidationError(`Expected an object at ${path}.`, path);
|
|
312
|
+
}
|
|
313
|
+
assertNoForbiddenObjectKeys(value, path);
|
|
314
|
+
}
|
|
315
|
+
function assertAllowedKeys(value, allowed, path) {
|
|
316
|
+
const allowedSet = new Set(allowed);
|
|
317
|
+
for (const key of Object.keys(value)) {
|
|
318
|
+
if (!allowedSet.has(key)) {
|
|
319
|
+
throw new PresetValidationError(`Unknown key "${key}" at ${path}.`, `${path}.${key}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function validateHexColor(value, path) {
|
|
324
|
+
if (typeof value !== "string" || !HEX_COLOR_PATTERN.test(value)) {
|
|
325
|
+
throw new PresetValidationError(
|
|
326
|
+
`Invalid color at ${path}. Expected #RGB, #RRGGBB, or #RRGGBBAA.`,
|
|
327
|
+
path
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
return value;
|
|
331
|
+
}
|
|
332
|
+
function validateFontRole(value, path) {
|
|
333
|
+
if (typeof value !== "string" || !isFontRoleValue(value)) {
|
|
334
|
+
throw new PresetValidationError(
|
|
335
|
+
`Invalid font at ${path}. Expected site | system-sans | system-mono | custom:<stack>.`,
|
|
336
|
+
path
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
if (value.startsWith(CUSTOM_FONT_PREFIX)) {
|
|
340
|
+
validateCustomFontStack(value, path);
|
|
341
|
+
}
|
|
342
|
+
return value;
|
|
343
|
+
}
|
|
344
|
+
function validateControlSolidPartial(value, path) {
|
|
345
|
+
assertPlainObject(value, path);
|
|
346
|
+
assertAllowedKeys(value, CONTROL_SOLID_COLOR_KEYS, path);
|
|
347
|
+
const result = {};
|
|
348
|
+
for (const key of CONTROL_SOLID_COLOR_KEYS) {
|
|
349
|
+
if (key in value) {
|
|
350
|
+
result[key] = validateHexColor(value[key], `${path}.${key}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return result;
|
|
354
|
+
}
|
|
355
|
+
function validateFontsPartial(value, path) {
|
|
356
|
+
assertPlainObject(value, path);
|
|
357
|
+
assertAllowedKeys(value, FONT_ROLE_KEYS, path);
|
|
358
|
+
const result = {};
|
|
359
|
+
for (const key of FONT_ROLE_KEYS) {
|
|
360
|
+
if (key in value) {
|
|
361
|
+
result[key] = validateFontRole(value[key], `${path}.${key}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return result;
|
|
365
|
+
}
|
|
366
|
+
function validateColorsPartial(value, path) {
|
|
367
|
+
assertPlainObject(value, path);
|
|
368
|
+
assertAllowedKeys(value, COLOR_MODE_KEYS, path);
|
|
369
|
+
const result = {};
|
|
370
|
+
for (const mode of COLOR_MODE_KEYS) {
|
|
371
|
+
if (mode in value) {
|
|
372
|
+
result[mode] = validateControlSolidPartial(value[mode], `${path}.${mode}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return result;
|
|
376
|
+
}
|
|
377
|
+
function validateStyleConfigPartial(value) {
|
|
378
|
+
if (value === void 0 || value === null) return {};
|
|
379
|
+
assertPlainObject(value, "style");
|
|
380
|
+
assertAllowedKeys(value, STYLE_OBJECT_KEYS, "style");
|
|
381
|
+
const result = {};
|
|
382
|
+
if ("iconLibrary" in value) {
|
|
383
|
+
if (typeof value.iconLibrary !== "string" || !isIconLibrary(value.iconLibrary)) {
|
|
384
|
+
throw new PresetValidationError(
|
|
385
|
+
"Invalid style.iconLibrary. Expected tabler | lucide.",
|
|
386
|
+
"style.iconLibrary"
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
result.iconLibrary = value.iconLibrary;
|
|
390
|
+
}
|
|
391
|
+
if ("radius" in value) {
|
|
392
|
+
if (typeof value.radius !== "string" || !isRadiusPreset(value.radius)) {
|
|
393
|
+
throw new PresetValidationError(
|
|
394
|
+
"Invalid style.radius. Expected none | small | medium | large.",
|
|
395
|
+
"style.radius"
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
result.radius = value.radius;
|
|
399
|
+
}
|
|
400
|
+
if ("fonts" in value) {
|
|
401
|
+
result.fonts = validateFontsPartial(value.fonts, "style.fonts");
|
|
402
|
+
}
|
|
403
|
+
if ("colors" in value) {
|
|
404
|
+
result.colors = validateColorsPartial(value.colors, "style.colors");
|
|
405
|
+
}
|
|
406
|
+
if ("presetCode" in value) {
|
|
407
|
+
if (typeof value.presetCode !== "string" || value.presetCode.length === 0) {
|
|
408
|
+
throw new PresetValidationError(
|
|
409
|
+
"Invalid style.presetCode. Expected a non-empty string.",
|
|
410
|
+
"style.presetCode"
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
if (value.presetCode.length > PRESET_CODEC_MAX_CODE_LENGTH) {
|
|
414
|
+
throw new PresetValidationError(
|
|
415
|
+
`Invalid style.presetCode. Exceeds ${PRESET_CODEC_MAX_CODE_LENGTH} characters.`,
|
|
416
|
+
"style.presetCode"
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
result.presetCode = value.presetCode;
|
|
420
|
+
}
|
|
421
|
+
return result;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ../wensity-preset/src/normalize.ts
|
|
425
|
+
function mergeControlSolid(base, partial) {
|
|
426
|
+
if (!partial) return { ...base };
|
|
427
|
+
const merged = { ...base };
|
|
428
|
+
for (const key of CONTROL_SOLID_COLOR_KEYS) {
|
|
429
|
+
const value = partial[key];
|
|
430
|
+
if (value !== void 0) merged[key] = value;
|
|
431
|
+
}
|
|
432
|
+
return merged;
|
|
433
|
+
}
|
|
434
|
+
function mergeFonts(base, partial) {
|
|
435
|
+
if (!partial) return { ...base };
|
|
436
|
+
const merged = { ...base };
|
|
437
|
+
for (const key of FONT_ROLE_KEYS) {
|
|
438
|
+
const value = partial[key];
|
|
439
|
+
if (value !== void 0) merged[key] = value;
|
|
440
|
+
}
|
|
441
|
+
return merged;
|
|
442
|
+
}
|
|
443
|
+
function mergeColors(base, partial) {
|
|
444
|
+
return {
|
|
445
|
+
light: mergeControlSolid(base.light, partial?.light),
|
|
446
|
+
dark: mergeControlSolid(base.dark, partial?.dark)
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function normalizeStyleConfig(partial) {
|
|
450
|
+
const validated = validateStyleConfigPartial(partial ?? void 0);
|
|
451
|
+
const defaults = createDefaultStyleConfig();
|
|
452
|
+
const normalized = {
|
|
453
|
+
iconLibrary: validated.iconLibrary ?? defaults.iconLibrary,
|
|
454
|
+
radius: validated.radius ?? defaults.radius,
|
|
455
|
+
fonts: mergeFonts(defaults.fonts, validated.fonts),
|
|
456
|
+
colors: mergeColors(defaults.colors, validated.colors)
|
|
457
|
+
};
|
|
458
|
+
if (validated.presetCode !== void 0) {
|
|
459
|
+
normalized.presetCode = validated.presetCode;
|
|
460
|
+
}
|
|
461
|
+
return normalized;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ../wensity-preset/src/codec.ts
|
|
465
|
+
var CODEC_PAYLOAD_KEYS = ["v", "iconLibrary", "radius", "fonts", "colors"];
|
|
466
|
+
function assertPlainObject2(value, path) {
|
|
467
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
468
|
+
throw new PresetValidationError(`Expected an object at ${path}.`, path);
|
|
469
|
+
}
|
|
470
|
+
assertNoForbiddenObjectKeys(value, path);
|
|
471
|
+
}
|
|
472
|
+
function assertAllowedKeys2(value, allowed, path) {
|
|
473
|
+
const allowedSet = new Set(allowed);
|
|
474
|
+
for (const key of Object.keys(value)) {
|
|
475
|
+
if (!allowedSet.has(key)) {
|
|
476
|
+
throw new PresetValidationError(`Unknown key "${key}" at ${path}.`, `${path}.${key}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function utf8ByteLength(text) {
|
|
481
|
+
return new TextEncoder().encode(text).byteLength;
|
|
482
|
+
}
|
|
483
|
+
var BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
484
|
+
function toBase64Url(bytes) {
|
|
485
|
+
let out = "";
|
|
486
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
487
|
+
const a = bytes[i];
|
|
488
|
+
const b = i + 1 < bytes.length ? bytes[i + 1] : 0;
|
|
489
|
+
const c = i + 2 < bytes.length ? bytes[i + 2] : 0;
|
|
490
|
+
const triple = a << 16 | b << 8 | c;
|
|
491
|
+
out += BASE64URL_ALPHABET[triple >> 18 & 63];
|
|
492
|
+
out += BASE64URL_ALPHABET[triple >> 12 & 63];
|
|
493
|
+
if (i + 1 < bytes.length) out += BASE64URL_ALPHABET[triple >> 6 & 63];
|
|
494
|
+
if (i + 2 < bytes.length) out += BASE64URL_ALPHABET[triple & 63];
|
|
495
|
+
}
|
|
496
|
+
return out;
|
|
497
|
+
}
|
|
498
|
+
function fromBase64Url(code) {
|
|
499
|
+
if (code.length === 0 || /[^A-Za-z0-9_-]/.test(code)) {
|
|
500
|
+
throw new PresetValidationError("Preset code is not valid base64url.", "preset");
|
|
501
|
+
}
|
|
502
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
503
|
+
for (let i = 0; i < BASE64URL_ALPHABET.length; i++) {
|
|
504
|
+
lookup.set(BASE64URL_ALPHABET[i], i);
|
|
505
|
+
}
|
|
506
|
+
const out = new Uint8Array(Math.floor(code.length * 3 / 4));
|
|
507
|
+
let outIndex = 0;
|
|
508
|
+
for (let i = 0; i < code.length; i += 4) {
|
|
509
|
+
const c0 = lookup.get(code[i]);
|
|
510
|
+
const c1 = lookup.get(code[i + 1]);
|
|
511
|
+
const c2 = i + 2 < code.length ? lookup.get(code[i + 2]) : 0;
|
|
512
|
+
const c3 = i + 3 < code.length ? lookup.get(code[i + 3]) : 0;
|
|
513
|
+
if (c0 === void 0 || c1 === void 0 || c2 === void 0 || c3 === void 0) {
|
|
514
|
+
throw new PresetValidationError("Preset code is not valid base64url.", "preset");
|
|
515
|
+
}
|
|
516
|
+
const triple = c0 << 18 | c1 << 12 | c2 << 6 | c3;
|
|
517
|
+
if (outIndex < out.length) out[outIndex++] = triple >> 16 & 255;
|
|
518
|
+
if (i + 2 < code.length && outIndex < out.length) out[outIndex++] = triple >> 8 & 255;
|
|
519
|
+
if (i + 3 < code.length && outIndex < out.length) out[outIndex++] = triple & 255;
|
|
520
|
+
}
|
|
521
|
+
return outIndex === out.length ? out : out.subarray(0, outIndex);
|
|
522
|
+
}
|
|
523
|
+
function stableStringifyCodecPayload(payload) {
|
|
524
|
+
const parts = [
|
|
525
|
+
`"v":${payload.v}`,
|
|
526
|
+
`"iconLibrary":${JSON.stringify(payload.iconLibrary)}`,
|
|
527
|
+
`"radius":${JSON.stringify(payload.radius)}`,
|
|
528
|
+
`"fonts":${stableStringifyFonts(payload.fonts)}`
|
|
529
|
+
];
|
|
530
|
+
if (payload.colors !== void 0) {
|
|
531
|
+
parts.push(`"colors":${stableStringifyColors(payload.colors)}`);
|
|
532
|
+
}
|
|
533
|
+
return `{${parts.join(",")}}`;
|
|
534
|
+
}
|
|
535
|
+
function stableStringifyFonts(fonts) {
|
|
536
|
+
return `{"sans":${JSON.stringify(fonts.sans)},"display":${JSON.stringify(fonts.display)},"mono":${JSON.stringify(fonts.mono)}}`;
|
|
537
|
+
}
|
|
538
|
+
function stableStringifyColors(colors) {
|
|
539
|
+
const modeParts = [];
|
|
540
|
+
for (const mode of COLOR_MODE_KEYS) {
|
|
541
|
+
const partial = colors[mode];
|
|
542
|
+
if (!partial) continue;
|
|
543
|
+
const keyParts = [];
|
|
544
|
+
for (const key of CONTROL_SOLID_COLOR_KEYS) {
|
|
545
|
+
const value = partial[key];
|
|
546
|
+
if (value !== void 0) {
|
|
547
|
+
keyParts.push(`${JSON.stringify(key)}:${JSON.stringify(value)}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (keyParts.length > 0) {
|
|
551
|
+
modeParts.push(`${JSON.stringify(mode)}:{${keyParts.join(",")}}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return `{${modeParts.join(",")}}`;
|
|
555
|
+
}
|
|
556
|
+
function validateCodecFonts(value, path) {
|
|
557
|
+
assertPlainObject2(value, path);
|
|
558
|
+
assertAllowedKeys2(value, FONT_ROLE_KEYS, path);
|
|
559
|
+
for (const key of FONT_ROLE_KEYS) {
|
|
560
|
+
if (!(key in value)) {
|
|
561
|
+
throw new PresetValidationError(`Missing required font role "${key}" at ${path}.`, `${path}.${key}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const fonts = {};
|
|
565
|
+
for (const key of FONT_ROLE_KEYS) {
|
|
566
|
+
const raw = value[key];
|
|
567
|
+
if (typeof raw !== "string" || !isFontRoleValue(raw)) {
|
|
568
|
+
throw new PresetValidationError(
|
|
569
|
+
`Invalid font at ${path}.${key}. Expected site | system-sans | system-mono | custom:<stack>.`,
|
|
570
|
+
`${path}.${key}`
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
if (raw.startsWith(CUSTOM_FONT_PREFIX)) {
|
|
574
|
+
validateCustomFontStack(raw, `${path}.${key}`);
|
|
575
|
+
}
|
|
576
|
+
fonts[key] = raw;
|
|
577
|
+
}
|
|
578
|
+
return fonts;
|
|
579
|
+
}
|
|
580
|
+
function validateCodecColors(value, path) {
|
|
581
|
+
const partial = validateStyleConfigPartial({ colors: value });
|
|
582
|
+
if (!partial.colors) {
|
|
583
|
+
throw new PresetValidationError(`Invalid colors at ${path}.`, path);
|
|
584
|
+
}
|
|
585
|
+
return partial.colors;
|
|
586
|
+
}
|
|
587
|
+
function validateCodecPayload(value) {
|
|
588
|
+
assertPlainObject2(value, "preset");
|
|
589
|
+
assertAllowedKeys2(value, CODEC_PAYLOAD_KEYS, "preset");
|
|
590
|
+
if (value.v !== 1) {
|
|
591
|
+
throw new PresetValidationError(
|
|
592
|
+
typeof value.v === "number" ? `Unsupported preset codec version ${value.v}. Expected v: 1.` : "Preset payload missing v: 1.",
|
|
593
|
+
"preset.v"
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
if (typeof value.iconLibrary !== "string" || !isIconLibrary(value.iconLibrary)) {
|
|
597
|
+
throw new PresetValidationError(
|
|
598
|
+
"Invalid preset.iconLibrary. Expected tabler | lucide.",
|
|
599
|
+
"preset.iconLibrary"
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
if (typeof value.radius !== "string" || !isRadiusPreset(value.radius)) {
|
|
603
|
+
throw new PresetValidationError(
|
|
604
|
+
"Invalid preset.radius. Expected none | small | medium | large.",
|
|
605
|
+
"preset.radius"
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
if (!("fonts" in value)) {
|
|
609
|
+
throw new PresetValidationError("Preset payload missing fonts.", "preset.fonts");
|
|
610
|
+
}
|
|
611
|
+
const payload = {
|
|
612
|
+
v: 1,
|
|
613
|
+
iconLibrary: value.iconLibrary,
|
|
614
|
+
radius: value.radius,
|
|
615
|
+
fonts: validateCodecFonts(value.fonts, "preset.fonts")
|
|
616
|
+
};
|
|
617
|
+
if ("colors" in value) {
|
|
618
|
+
payload.colors = validateCodecColors(value.colors, "preset.colors");
|
|
619
|
+
}
|
|
620
|
+
return payload;
|
|
621
|
+
}
|
|
622
|
+
function codecPayloadToStyleInput(payload) {
|
|
623
|
+
const input = {
|
|
624
|
+
iconLibrary: payload.iconLibrary,
|
|
625
|
+
radius: payload.radius,
|
|
626
|
+
fonts: payload.fonts
|
|
627
|
+
};
|
|
628
|
+
if (payload.colors) {
|
|
629
|
+
input.colors = payload.colors;
|
|
630
|
+
}
|
|
631
|
+
return input;
|
|
632
|
+
}
|
|
633
|
+
function encodePreset(style) {
|
|
634
|
+
const normalized = normalizeStyleConfig(style);
|
|
635
|
+
for (const role of FONT_ROLE_KEYS) {
|
|
636
|
+
const value = normalized.fonts[role];
|
|
637
|
+
if (value.startsWith(CUSTOM_FONT_PREFIX)) {
|
|
638
|
+
validateCustomFontStack(value, `style.fonts.${role}`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
const colors = normalized.colors ? {
|
|
642
|
+
light: pickDefinedColors(normalized.colors.light),
|
|
643
|
+
dark: pickDefinedColors(normalized.colors.dark)
|
|
644
|
+
} : void 0;
|
|
645
|
+
const hasColors = colors && (colors.light && Object.keys(colors.light).length > 0 || colors.dark && Object.keys(colors.dark).length > 0);
|
|
646
|
+
const payload = {
|
|
647
|
+
v: 1,
|
|
648
|
+
iconLibrary: normalized.iconLibrary,
|
|
649
|
+
radius: normalized.radius,
|
|
650
|
+
fonts: { ...normalized.fonts },
|
|
651
|
+
...hasColors ? { colors } : {}
|
|
652
|
+
};
|
|
653
|
+
const json = stableStringifyCodecPayload(payload);
|
|
654
|
+
const bytes = new TextEncoder().encode(json);
|
|
655
|
+
if (bytes.byteLength > PRESET_CODEC_MAX_DECODED_JSON_BYTES) {
|
|
656
|
+
throw new PresetValidationError(
|
|
657
|
+
`Preset JSON exceeds ${PRESET_CODEC_MAX_DECODED_JSON_BYTES} bytes.`,
|
|
658
|
+
"preset"
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
const code = `${PRESET_CODEC_PREFIX}.${toBase64Url(bytes)}`;
|
|
662
|
+
if (code.length > PRESET_CODEC_MAX_CODE_LENGTH) {
|
|
663
|
+
throw new PresetValidationError(
|
|
664
|
+
`Preset code exceeds ${PRESET_CODEC_MAX_CODE_LENGTH} characters.`,
|
|
665
|
+
"preset"
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
return code;
|
|
669
|
+
}
|
|
670
|
+
function pickDefinedColors(partial) {
|
|
671
|
+
if (!partial) return void 0;
|
|
672
|
+
const out = {};
|
|
673
|
+
for (const key of CONTROL_SOLID_COLOR_KEYS) {
|
|
674
|
+
if (partial[key] !== void 0) out[key] = partial[key];
|
|
675
|
+
}
|
|
676
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
677
|
+
}
|
|
678
|
+
function decodePreset(code) {
|
|
679
|
+
if (typeof code !== "string" || code.length === 0) {
|
|
680
|
+
throw new PresetValidationError("Preset code is required.", "preset");
|
|
681
|
+
}
|
|
682
|
+
if (code.length > PRESET_CODEC_MAX_CODE_LENGTH) {
|
|
683
|
+
throw new PresetValidationError(
|
|
684
|
+
`Preset code exceeds ${PRESET_CODEC_MAX_CODE_LENGTH} characters.`,
|
|
685
|
+
"preset"
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
const prefix = `${PRESET_CODEC_PREFIX}.`;
|
|
689
|
+
if (!code.startsWith(prefix)) {
|
|
690
|
+
throw new PresetValidationError(
|
|
691
|
+
`Unsupported or missing preset prefix. Expected codes starting with "${PRESET_CODEC_PREFIX}".`,
|
|
692
|
+
"preset"
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
const encoded = code.slice(prefix.length);
|
|
696
|
+
if (!encoded) {
|
|
697
|
+
throw new PresetValidationError("Preset code payload is empty.", "preset");
|
|
698
|
+
}
|
|
699
|
+
if (encoded.includes("=") || /[^A-Za-z0-9_-]/.test(encoded)) {
|
|
700
|
+
throw new PresetValidationError("Preset code payload is not valid unpadded base64url.", "preset");
|
|
701
|
+
}
|
|
702
|
+
const bytes = fromBase64Url(encoded);
|
|
703
|
+
if (bytes.byteLength === 0) {
|
|
704
|
+
throw new PresetValidationError("Preset code decodes to empty payload.", "preset");
|
|
705
|
+
}
|
|
706
|
+
if (bytes.byteLength > PRESET_CODEC_MAX_DECODED_JSON_BYTES) {
|
|
707
|
+
throw new PresetValidationError(
|
|
708
|
+
`Decoded preset JSON exceeds ${PRESET_CODEC_MAX_DECODED_JSON_BYTES} bytes.`,
|
|
709
|
+
"preset"
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
let jsonText;
|
|
713
|
+
try {
|
|
714
|
+
jsonText = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
715
|
+
} catch {
|
|
716
|
+
throw new PresetValidationError("Preset payload is not valid UTF-8.", "preset");
|
|
717
|
+
}
|
|
718
|
+
if (utf8ByteLength(jsonText) > PRESET_CODEC_MAX_DECODED_JSON_BYTES) {
|
|
719
|
+
throw new PresetValidationError(
|
|
720
|
+
`Decoded preset JSON exceeds ${PRESET_CODEC_MAX_DECODED_JSON_BYTES} bytes.`,
|
|
721
|
+
"preset"
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
let parsed;
|
|
725
|
+
try {
|
|
726
|
+
parsed = JSON.parse(jsonText);
|
|
727
|
+
} catch {
|
|
728
|
+
throw new PresetValidationError("Preset payload is not valid JSON.", "preset");
|
|
729
|
+
}
|
|
730
|
+
return validateCodecPayload(parsed);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ../wensity-preset/src/icon-map.ts
|
|
734
|
+
var ICON_TRANSFORM_EXCLUSIONS = [
|
|
735
|
+
"IconFileTypeCsv",
|
|
736
|
+
"IconFileTypeDoc",
|
|
737
|
+
"IconFileTypePdf",
|
|
738
|
+
"IconFileTypePpt",
|
|
739
|
+
"IconFileTypeXls",
|
|
740
|
+
"IconPointFilled",
|
|
741
|
+
"IconLoader2"
|
|
742
|
+
];
|
|
743
|
+
var TABLER_TO_LUCIDE = {
|
|
744
|
+
IconAlertTriangle: "AlertTriangle",
|
|
745
|
+
IconArrowDownRight: "ArrowDownRight",
|
|
746
|
+
IconArrowLeft: "ArrowLeft",
|
|
747
|
+
IconArrowUpRight: "ArrowUpRight",
|
|
748
|
+
IconCalendar: "Calendar",
|
|
749
|
+
IconCamera: "Camera",
|
|
750
|
+
IconCheck: "Check",
|
|
751
|
+
IconChevronDown: "ChevronDown",
|
|
752
|
+
IconChevronLeft: "ChevronLeft",
|
|
753
|
+
IconChevronRight: "ChevronRight",
|
|
754
|
+
IconChevronUp: "ChevronUp",
|
|
755
|
+
IconChevronsLeft: "ChevronsLeft",
|
|
756
|
+
IconChevronsRight: "ChevronsRight",
|
|
757
|
+
IconCircleX: "CircleX",
|
|
758
|
+
IconClock: "Clock",
|
|
759
|
+
IconCornerDownLeft: "CornerDownLeft",
|
|
760
|
+
IconDots: "Ellipsis",
|
|
761
|
+
IconDownload: "Download",
|
|
762
|
+
IconFile: "File",
|
|
763
|
+
IconFileMusic: "FileAudio",
|
|
764
|
+
IconFileText: "FileText",
|
|
765
|
+
IconFileZip: "FileArchive",
|
|
766
|
+
IconInfoCircle: "Info",
|
|
767
|
+
IconLayoutSidebarLeftCollapse: "PanelLeftClose",
|
|
768
|
+
IconLayoutSidebarLeftExpand: "PanelLeftOpen",
|
|
769
|
+
IconMinus: "Minus",
|
|
770
|
+
IconPhoto: "Image",
|
|
771
|
+
IconPlus: "Plus",
|
|
772
|
+
IconRefresh: "RefreshCw",
|
|
773
|
+
IconSearch: "Search",
|
|
774
|
+
IconSelector: "ChevronsUpDown",
|
|
775
|
+
IconSlash: "Slash",
|
|
776
|
+
IconUser: "User",
|
|
777
|
+
IconVideo: "Video",
|
|
778
|
+
IconX: "X"
|
|
779
|
+
};
|
|
780
|
+
var EXCLUSION_SET = new Set(ICON_TRANSFORM_EXCLUSIONS);
|
|
781
|
+
function isIconTransformExclusion(name) {
|
|
782
|
+
return EXCLUSION_SET.has(name);
|
|
783
|
+
}
|
|
784
|
+
function isMappedTablerIcon(name) {
|
|
785
|
+
return Object.prototype.hasOwnProperty.call(TABLER_TO_LUCIDE, name);
|
|
786
|
+
}
|
|
787
|
+
function resolveLucideExport(tablerName) {
|
|
788
|
+
if (isIconTransformExclusion(tablerName)) {
|
|
789
|
+
throw new Error(
|
|
790
|
+
`Icon "${tablerName}" is a Phase 0 exclusion and must not be transformed. Use internal SVG/CSS geometry instead.`
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
if (!isMappedTablerIcon(tablerName)) {
|
|
794
|
+
throw new Error(
|
|
795
|
+
`No approved Lucide mapping for Tabler icon "${tablerName}". Add an audited map entry or convert it to internal geometry.`
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
return TABLER_TO_LUCIDE[tablerName];
|
|
799
|
+
}
|
|
800
|
+
var LUCIDE_TO_TABLER = Object.fromEntries(
|
|
801
|
+
Object.entries(TABLER_TO_LUCIDE).map(([tabler, lucide]) => [lucide, tabler])
|
|
802
|
+
);
|
|
6
803
|
|
|
7
804
|
// src/constants.ts
|
|
8
805
|
var CLI_NAME = "wensity";
|
|
9
|
-
var CLI_VERSION = "0.1.
|
|
806
|
+
var CLI_VERSION = "0.1.6";
|
|
10
807
|
var CONFIG_FILE_NAME = "wensity.json";
|
|
11
808
|
var DEFAULT_API_ORIGIN = "https://ui.wensity.com";
|
|
12
809
|
var DEFAULT_REGISTRY_PATH = "/api/registry";
|
|
@@ -14,10 +811,11 @@ var DEFAULT_COMPONENTS_DIR = "src/components/wensity";
|
|
|
14
811
|
var DEFAULT_UTILS_PATH = "src/lib/utils";
|
|
15
812
|
var DEFAULT_COMPONENTS_ALIAS = "@/components/wensity";
|
|
16
813
|
var DEFAULT_UTILS_ALIAS = "@/lib/utils";
|
|
814
|
+
var INIT_PRIMITIVE_CSS_SLUG = "button";
|
|
17
815
|
var HELP_TEXT = `Wensity CLI ${CLI_VERSION}
|
|
18
816
|
|
|
19
817
|
Usage:
|
|
20
|
-
wensity init [options]
|
|
818
|
+
wensity init [--preset <code>] [--preset-file <path>] [options]
|
|
21
819
|
wensity login [options]
|
|
22
820
|
wensity logout
|
|
23
821
|
wensity whoami [--json]
|
|
@@ -27,7 +825,7 @@ Usage:
|
|
|
27
825
|
wensity version
|
|
28
826
|
|
|
29
827
|
Commands:
|
|
30
|
-
init Create wensity.json and local helper files
|
|
828
|
+
init Create wensity.json and local helper files (optional --preset applies style + CSS)
|
|
31
829
|
login Save a Wensity API token for premium component access
|
|
32
830
|
logout Remove saved CLI credentials
|
|
33
831
|
whoami Verify the saved token and print the authenticated account
|
|
@@ -41,12 +839,21 @@ Options:
|
|
|
41
839
|
--registry <url> Override the registry endpoint
|
|
42
840
|
--type <type> Select component or block, defaults to component
|
|
43
841
|
--token <token> Provide a token non-interactively for login
|
|
842
|
+
--preset <code> Apply a shareable Wensity style preset (wsty1.\u2026); merges style when config exists
|
|
843
|
+
--preset-file <path> Read a preset code or JSON payload from a file
|
|
44
844
|
--force Overwrite generated files when adding or initializing
|
|
45
845
|
--no-install Write files without running the package manager
|
|
46
846
|
--dry-run Print planned writes and installs without changing files
|
|
47
847
|
--json Print machine-readable output where supported
|
|
48
848
|
--help Show help
|
|
49
849
|
--version Print the CLI version
|
|
850
|
+
|
|
851
|
+
Preset notes:
|
|
852
|
+
Generate codes at ${DEFAULT_API_ORIGIN}/docs/primitives/setup
|
|
853
|
+
Plain init refuses an existing wensity.json unless --force
|
|
854
|
+
init --preset merges style + primitive CSS; does not rewrite installed source
|
|
855
|
+
add/update read style.iconLibrary for UI primitives only (Tabler|Lucide)
|
|
856
|
+
Invalid/expired/unsupported codes exit non-zero with an actionable message
|
|
50
857
|
`;
|
|
51
858
|
|
|
52
859
|
// src/errors.ts
|
|
@@ -108,7 +915,7 @@ function hasFlag(args, name) {
|
|
|
108
915
|
return args.options.get(name) === true;
|
|
109
916
|
}
|
|
110
917
|
function expectsValue(name) {
|
|
111
|
-
return ["api-url", "registry", "token", "type"].includes(name);
|
|
918
|
+
return ["api-url", "registry", "token", "type", "preset", "preset-file"].includes(name);
|
|
112
919
|
}
|
|
113
920
|
function normalizeOptionName(name) {
|
|
114
921
|
if (name === "dry") return "dry-run";
|
|
@@ -205,7 +1012,20 @@ async function loadConfig(cwd, overrides = {}) {
|
|
|
205
1012
|
try {
|
|
206
1013
|
const parsed = JSON.parse(await readFile(foundPath, "utf8"));
|
|
207
1014
|
return {
|
|
208
|
-
config: normalizeConfig(
|
|
1015
|
+
config: normalizeConfig(
|
|
1016
|
+
{
|
|
1017
|
+
...defaults,
|
|
1018
|
+
...parsed,
|
|
1019
|
+
aliases: {
|
|
1020
|
+
...defaults.aliases,
|
|
1021
|
+
...parsed.aliases
|
|
1022
|
+
},
|
|
1023
|
+
tailwind: parsed.tailwind ? { ...defaults.tailwind, ...parsed.tailwind } : defaults.tailwind,
|
|
1024
|
+
style: parsed.style,
|
|
1025
|
+
version: parsed.version ?? defaults.version
|
|
1026
|
+
},
|
|
1027
|
+
overrides
|
|
1028
|
+
),
|
|
209
1029
|
configPath,
|
|
210
1030
|
projectRoot,
|
|
211
1031
|
exists: true
|
|
@@ -214,6 +1034,9 @@ async function loadConfig(cwd, overrides = {}) {
|
|
|
214
1034
|
if (error instanceof SyntaxError) {
|
|
215
1035
|
throw new CliError(`${CONFIG_FILE_NAME} contains invalid JSON.`);
|
|
216
1036
|
}
|
|
1037
|
+
if (error instanceof PresetValidationError) {
|
|
1038
|
+
throw new CliError(`${CONFIG_FILE_NAME}: ${error.message}`);
|
|
1039
|
+
}
|
|
217
1040
|
throw error;
|
|
218
1041
|
}
|
|
219
1042
|
}
|
|
@@ -221,6 +1044,7 @@ function createDefaultConfig(projectRoot, apiUrl = DEFAULT_API_ORIGIN) {
|
|
|
221
1044
|
const apiOrigin = normalizeUrlOrigin(apiUrl);
|
|
222
1045
|
return {
|
|
223
1046
|
$schema: joinUrl(apiOrigin, "/schema/wensity.json"),
|
|
1047
|
+
version: CONFIG_SCHEMA_VERSION,
|
|
224
1048
|
registry: joinUrl(apiOrigin, DEFAULT_REGISTRY_PATH),
|
|
225
1049
|
components: DEFAULT_COMPONENTS_DIR,
|
|
226
1050
|
utils: DEFAULT_UTILS_PATH,
|
|
@@ -231,7 +1055,8 @@ function createDefaultConfig(projectRoot, apiUrl = DEFAULT_API_ORIGIN) {
|
|
|
231
1055
|
aliases: {
|
|
232
1056
|
components: DEFAULT_COMPONENTS_ALIAS,
|
|
233
1057
|
utils: DEFAULT_UTILS_ALIAS
|
|
234
|
-
}
|
|
1058
|
+
},
|
|
1059
|
+
style: createDefaultStyleConfig()
|
|
235
1060
|
};
|
|
236
1061
|
}
|
|
237
1062
|
async function writeConfigFile(path, config, force) {
|
|
@@ -242,6 +1067,26 @@ async function writeConfigFile(path, config, force) {
|
|
|
242
1067
|
await writeFile(path, `${JSON.stringify(config, null, 2)}
|
|
243
1068
|
`, "utf8");
|
|
244
1069
|
}
|
|
1070
|
+
async function mergeStyleIntoConfigFile(configPath, style) {
|
|
1071
|
+
const raw = JSON.parse(await readFile(configPath, "utf8"));
|
|
1072
|
+
const merged = {
|
|
1073
|
+
...raw,
|
|
1074
|
+
version: raw.version ?? CONFIG_SCHEMA_VERSION,
|
|
1075
|
+
registry: raw.registry ?? "",
|
|
1076
|
+
components: raw.components ?? DEFAULT_COMPONENTS_DIR,
|
|
1077
|
+
utils: raw.utils ?? DEFAULT_UTILS_PATH,
|
|
1078
|
+
aliases: {
|
|
1079
|
+
components: raw.aliases?.components ?? DEFAULT_COMPONENTS_ALIAS,
|
|
1080
|
+
utils: raw.aliases?.utils ?? DEFAULT_UTILS_ALIAS
|
|
1081
|
+
},
|
|
1082
|
+
style
|
|
1083
|
+
};
|
|
1084
|
+
if (raw.$schema) merged.$schema = raw.$schema;
|
|
1085
|
+
if (raw.tailwind) merged.tailwind = raw.tailwind;
|
|
1086
|
+
await writeFile(configPath, `${JSON.stringify(merged, null, 2)}
|
|
1087
|
+
`, "utf8");
|
|
1088
|
+
return normalizeConfig(merged, {});
|
|
1089
|
+
}
|
|
245
1090
|
function resolveConfiguredFilePath(projectRoot, relativePath, extension = "ts") {
|
|
246
1091
|
try {
|
|
247
1092
|
return resolveInside(projectRoot, withExtension(relativePath, extension));
|
|
@@ -271,23 +1116,39 @@ async function findUp(start, fileName) {
|
|
|
271
1116
|
}
|
|
272
1117
|
function normalizeConfig(config, overrides) {
|
|
273
1118
|
const apiOrigin = overrides.apiUrl ? normalizeUrlOrigin(overrides.apiUrl) : void 0;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
1119
|
+
try {
|
|
1120
|
+
return {
|
|
1121
|
+
...config,
|
|
1122
|
+
version: config.version ?? CONFIG_SCHEMA_VERSION,
|
|
1123
|
+
registry: overrides.registry ?? (apiOrigin ? joinUrl(apiOrigin, DEFAULT_REGISTRY_PATH) : config.registry),
|
|
1124
|
+
components: config.components || DEFAULT_COMPONENTS_DIR,
|
|
1125
|
+
utils: config.utils || DEFAULT_UTILS_PATH,
|
|
1126
|
+
aliases: {
|
|
1127
|
+
components: config.aliases?.components || DEFAULT_COMPONENTS_ALIAS,
|
|
1128
|
+
utils: config.aliases?.utils || DEFAULT_UTILS_ALIAS
|
|
1129
|
+
},
|
|
1130
|
+
// In-memory only: missing style fields resolve to defaults without rewriting disk.
|
|
1131
|
+
style: normalizeStyleConfig(config.style)
|
|
1132
|
+
};
|
|
1133
|
+
} catch (error) {
|
|
1134
|
+
if (error instanceof PresetValidationError) {
|
|
1135
|
+
throw new CliError(`${CONFIG_FILE_NAME}: ${error.message}`);
|
|
282
1136
|
}
|
|
283
|
-
|
|
1137
|
+
throw error;
|
|
1138
|
+
}
|
|
284
1139
|
}
|
|
285
1140
|
function detectTailwindConfig(projectRoot) {
|
|
286
1141
|
const candidates = ["tailwind.config.ts", "tailwind.config.js", "tailwind.config.mjs"];
|
|
287
1142
|
return candidates.find((candidate) => existsSync(join2(projectRoot, candidate))) ?? "tailwind.config.ts";
|
|
288
1143
|
}
|
|
289
1144
|
function detectGlobalCss(projectRoot) {
|
|
290
|
-
const candidates = [
|
|
1145
|
+
const candidates = [
|
|
1146
|
+
"src/app/globals.css",
|
|
1147
|
+
"src/styles/globals.css",
|
|
1148
|
+
"app/globals.css",
|
|
1149
|
+
"src/index.css",
|
|
1150
|
+
"styles/globals.css"
|
|
1151
|
+
];
|
|
291
1152
|
return candidates.find((candidate) => existsSync(join2(projectRoot, candidate))) ?? "src/app/globals.css";
|
|
292
1153
|
}
|
|
293
1154
|
function withExtension(path, extension) {
|
|
@@ -396,63 +1257,13 @@ function requiredHome(env) {
|
|
|
396
1257
|
throw new CliError("HOME is not set, so the CLI cannot locate ~/.config/wensity.");
|
|
397
1258
|
}
|
|
398
1259
|
|
|
399
|
-
// src/http.ts
|
|
400
|
-
async function requestJson(url, options = {}) {
|
|
401
|
-
const headers = new Headers(options.headers);
|
|
402
|
-
headers.set("accept", "application/json");
|
|
403
|
-
headers.set("user-agent", `${CLI_NAME}/${CLI_VERSION}`);
|
|
404
|
-
if (options.body && !headers.has("content-type")) {
|
|
405
|
-
headers.set("content-type", "application/json");
|
|
406
|
-
}
|
|
407
|
-
if (options.token) {
|
|
408
|
-
headers.set("authorization", `Bearer ${options.token}`);
|
|
409
|
-
}
|
|
410
|
-
let response;
|
|
411
|
-
try {
|
|
412
|
-
response = await fetch(url, { ...options, headers });
|
|
413
|
-
} catch (error) {
|
|
414
|
-
const message = error instanceof Error ? error.message : "Unknown network error";
|
|
415
|
-
throw new CliError(`Could not reach ${url}: ${message}`);
|
|
416
|
-
}
|
|
417
|
-
const text = await response.text();
|
|
418
|
-
const payload = parseJson(text);
|
|
419
|
-
if (!response.ok) {
|
|
420
|
-
const message = formatErrorMessage(payload) ?? response.statusText;
|
|
421
|
-
throw new CliError(`${response.status} ${message}`);
|
|
422
|
-
}
|
|
423
|
-
return payload;
|
|
424
|
-
}
|
|
425
|
-
function parseJson(text) {
|
|
426
|
-
if (!text.trim()) return null;
|
|
427
|
-
try {
|
|
428
|
-
return JSON.parse(text);
|
|
429
|
-
} catch {
|
|
430
|
-
throw new CliError("Registry returned invalid JSON.");
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
function formatErrorMessage(payload) {
|
|
434
|
-
if (!payload || typeof payload !== "object") return void 0;
|
|
435
|
-
const record = payload;
|
|
436
|
-
const code = typeof record.error === "string" ? record.error : void 0;
|
|
437
|
-
const message = typeof record.message === "string" ? record.message : code;
|
|
438
|
-
if (code === "pro_license_required") {
|
|
439
|
-
const base = message ?? "This component requires Wensity Pro.";
|
|
440
|
-
return `${base}
|
|
441
|
-
Upgrade here: https://ui.wensity.com/pricing`;
|
|
442
|
-
}
|
|
443
|
-
return message;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// src/package-manager.ts
|
|
447
|
-
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
448
|
-
import { spawnSync } from "child_process";
|
|
449
|
-
import { join as join4 } from "path";
|
|
450
|
-
|
|
451
1260
|
// src/output.ts
|
|
452
1261
|
import boxen from "boxen";
|
|
453
1262
|
import Table from "cli-table3";
|
|
454
1263
|
import ora from "ora";
|
|
455
1264
|
import pc from "picocolors";
|
|
1265
|
+
var BRAND_RED = "#cd1c18";
|
|
1266
|
+
var PRICING_BLUE = "#2563eb";
|
|
456
1267
|
var LOGO = [
|
|
457
1268
|
"\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557",
|
|
458
1269
|
"\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D",
|
|
@@ -475,33 +1286,46 @@ function printJson(value) {
|
|
|
475
1286
|
function formatList(items) {
|
|
476
1287
|
return items.map((item) => ` ${pc.dim("\u2022")} ${item}`).join("\n");
|
|
477
1288
|
}
|
|
1289
|
+
function brandRed(message) {
|
|
1290
|
+
return color(message, BRAND_RED);
|
|
1291
|
+
}
|
|
1292
|
+
function pricingBlue(message) {
|
|
1293
|
+
return color(message, PRICING_BLUE);
|
|
1294
|
+
}
|
|
478
1295
|
function brandBanner(subtitle) {
|
|
479
|
-
const lines = [
|
|
480
|
-
|
|
1296
|
+
const lines = [
|
|
1297
|
+
"",
|
|
1298
|
+
...LOGO.map((line) => brandRed(centerLine(line))),
|
|
1299
|
+
pc.bold(centerLine("Wensity CLI"))
|
|
1300
|
+
];
|
|
1301
|
+
if (subtitle) lines.push(pc.dim(centerLine(subtitle)));
|
|
481
1302
|
return lines.join("\n");
|
|
482
1303
|
}
|
|
483
1304
|
function printBrand(subtitle) {
|
|
484
1305
|
print(brandBanner(subtitle));
|
|
485
1306
|
}
|
|
1307
|
+
function printTitle(title, subtitle) {
|
|
1308
|
+
print(`${brandRed("\u25C6")} ${pc.bold(title)}${subtitle ? ` ${pc.dim(subtitle)}` : ""}`);
|
|
1309
|
+
}
|
|
486
1310
|
function success(message) {
|
|
487
1311
|
return `${pc.green("\u2713")} ${message}`;
|
|
488
1312
|
}
|
|
489
1313
|
function failure(message) {
|
|
490
|
-
return `${
|
|
1314
|
+
return `${brandRed("\u2715")} ${message}`;
|
|
491
1315
|
}
|
|
492
1316
|
function action(message) {
|
|
493
|
-
return `${
|
|
1317
|
+
return `${brandRed("\u25C6")} ${message}`;
|
|
494
1318
|
}
|
|
495
1319
|
function badge(label, tone = "muted") {
|
|
496
1320
|
const text = ` ${label.toUpperCase()} `;
|
|
497
|
-
if (tone === "pro") return
|
|
1321
|
+
if (tone === "pro") return bgBrandRed(pc.white(pc.bold(text)));
|
|
498
1322
|
if (tone === "free") return pc.bgGreen(pc.black(pc.bold(text)));
|
|
499
1323
|
return pc.inverse(text);
|
|
500
1324
|
}
|
|
501
1325
|
function panel(title, body) {
|
|
502
1326
|
const content = Array.isArray(body) ? body.join("\n") : body;
|
|
503
1327
|
return boxen(content, {
|
|
504
|
-
title:
|
|
1328
|
+
title: brandRed(pc.bold(title)),
|
|
505
1329
|
titleAlignment: "left",
|
|
506
1330
|
borderColor: "red",
|
|
507
1331
|
borderStyle: "round",
|
|
@@ -512,12 +1336,12 @@ function panel(title, body) {
|
|
|
512
1336
|
function nextSteps(steps) {
|
|
513
1337
|
return panel(
|
|
514
1338
|
"Next steps",
|
|
515
|
-
steps.map((step) => `${
|
|
1339
|
+
steps.map((step) => `${brandRed("\u203A")} ${pc.bold(step)}`)
|
|
516
1340
|
);
|
|
517
1341
|
}
|
|
518
1342
|
function table(head, rows) {
|
|
519
1343
|
const output = new Table({
|
|
520
|
-
head: head.map((item) =>
|
|
1344
|
+
head: head.map((item) => brandRed(pc.bold(item))),
|
|
521
1345
|
chars: {
|
|
522
1346
|
top: "\u2500",
|
|
523
1347
|
"top-mid": "\u252C",
|
|
@@ -566,10 +1390,351 @@ async function withSpinner(text, task, successText = text) {
|
|
|
566
1390
|
function formatCliError(message) {
|
|
567
1391
|
return `${failure(message)}
|
|
568
1392
|
|
|
569
|
-
${pc.dim("Need help?")} ${
|
|
1393
|
+
${pc.dim("Need help?")} ${pricingBlue("https://ui.wensity.com/docs")}`;
|
|
1394
|
+
}
|
|
1395
|
+
function formatProRequiredError(resourceName, resourceType) {
|
|
1396
|
+
return [
|
|
1397
|
+
`${resourceName} is a Pro ${resourceType}.`,
|
|
1398
|
+
brandRed("Create an API token from the Wensity dashboard, then run:"),
|
|
1399
|
+
` ${brandRed("npx wensity login --token <token>")}`,
|
|
1400
|
+
`Pricing: ${pricingBlue("https://ui.wensity.com/pricing")}`
|
|
1401
|
+
].join("\n");
|
|
1402
|
+
}
|
|
1403
|
+
function color(message, hex) {
|
|
1404
|
+
if (!shouldColor()) return message;
|
|
1405
|
+
const [red, green, blue] = hexToRgb(hex);
|
|
1406
|
+
return `\x1B[38;2;${red};${green};${blue}m${message}\x1B[39m`;
|
|
1407
|
+
}
|
|
1408
|
+
function bgBrandRed(message) {
|
|
1409
|
+
if (!shouldColor()) return message;
|
|
1410
|
+
const [red, green, blue] = hexToRgb(BRAND_RED);
|
|
1411
|
+
return `\x1B[48;2;${red};${green};${blue}m${message}\x1B[49m`;
|
|
1412
|
+
}
|
|
1413
|
+
function shouldColor() {
|
|
1414
|
+
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") return true;
|
|
1415
|
+
return process.env.NO_COLOR === void 0 && process.env.FORCE_COLOR !== "0";
|
|
1416
|
+
}
|
|
1417
|
+
function hexToRgb(hex) {
|
|
1418
|
+
const value = hex.replace("#", "");
|
|
1419
|
+
return [
|
|
1420
|
+
Number.parseInt(value.slice(0, 2), 16),
|
|
1421
|
+
Number.parseInt(value.slice(2, 4), 16),
|
|
1422
|
+
Number.parseInt(value.slice(4, 6), 16)
|
|
1423
|
+
];
|
|
1424
|
+
}
|
|
1425
|
+
function centerLine(line) {
|
|
1426
|
+
const columns = Math.min(process.stdout.columns || 80, 100);
|
|
1427
|
+
const padding = Math.max(0, Math.floor((columns - line.length) / 2));
|
|
1428
|
+
return `${" ".repeat(padding)}${line}`;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// src/http.ts
|
|
1432
|
+
async function requestJson(url, options = {}) {
|
|
1433
|
+
const headers = new Headers(options.headers);
|
|
1434
|
+
headers.set("accept", "application/json");
|
|
1435
|
+
headers.set("user-agent", `${CLI_NAME}/${CLI_VERSION}`);
|
|
1436
|
+
if (options.body && !headers.has("content-type")) {
|
|
1437
|
+
headers.set("content-type", "application/json");
|
|
1438
|
+
}
|
|
1439
|
+
if (options.token) {
|
|
1440
|
+
headers.set("authorization", `Bearer ${options.token}`);
|
|
1441
|
+
}
|
|
1442
|
+
let response;
|
|
1443
|
+
try {
|
|
1444
|
+
response = await fetch(url, { ...options, headers });
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
const message = error instanceof Error ? error.message : "Unknown network error";
|
|
1447
|
+
throw new CliError(`Could not reach ${url}: ${message}`);
|
|
1448
|
+
}
|
|
1449
|
+
const text = await response.text();
|
|
1450
|
+
const payload = parseJson(text);
|
|
1451
|
+
if (!response.ok) {
|
|
1452
|
+
const message = formatErrorMessage(payload) ?? response.statusText;
|
|
1453
|
+
throw new CliError(`${response.status} ${message}`);
|
|
1454
|
+
}
|
|
1455
|
+
return payload;
|
|
1456
|
+
}
|
|
1457
|
+
function parseJson(text) {
|
|
1458
|
+
if (!text.trim()) return null;
|
|
1459
|
+
try {
|
|
1460
|
+
return JSON.parse(text);
|
|
1461
|
+
} catch {
|
|
1462
|
+
throw new CliError("Registry returned invalid JSON.");
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
function formatErrorMessage(payload) {
|
|
1466
|
+
if (!payload || typeof payload !== "object") return void 0;
|
|
1467
|
+
const record = payload;
|
|
1468
|
+
const code = typeof record.error === "string" ? record.error : void 0;
|
|
1469
|
+
const message = typeof record.message === "string" ? record.message : code;
|
|
1470
|
+
if (code === "pro_license_required") {
|
|
1471
|
+
const base = message ?? "This component requires Wensity Pro.";
|
|
1472
|
+
return `${base}
|
|
1473
|
+
Upgrade here: ${pricingBlue("https://ui.wensity.com/pricing")}`;
|
|
1474
|
+
}
|
|
1475
|
+
return message;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// src/icon-transform.ts
|
|
1479
|
+
var UI_PRIMITIVES_CATEGORY = "UI Primitives";
|
|
1480
|
+
var TABLER_ICONS_PACKAGE = "@tabler/icons-react";
|
|
1481
|
+
var LUCIDE_ICONS_PACKAGE = "lucide-react";
|
|
1482
|
+
var EMPTY_SWAP = { remove: [], add: [] };
|
|
1483
|
+
var NATIVE_SVG_TAGS = /* @__PURE__ */ new Set([
|
|
1484
|
+
"svg",
|
|
1485
|
+
"path",
|
|
1486
|
+
"circle",
|
|
1487
|
+
"rect",
|
|
1488
|
+
"line",
|
|
1489
|
+
"polyline",
|
|
1490
|
+
"polygon",
|
|
1491
|
+
"g",
|
|
1492
|
+
"defs",
|
|
1493
|
+
"use",
|
|
1494
|
+
"symbol",
|
|
1495
|
+
"clipPath",
|
|
1496
|
+
"mask",
|
|
1497
|
+
"linearGradient",
|
|
1498
|
+
"radialGradient",
|
|
1499
|
+
"stop",
|
|
1500
|
+
"text",
|
|
1501
|
+
"tspan",
|
|
1502
|
+
"foreignObject"
|
|
1503
|
+
]);
|
|
1504
|
+
function createTablerAdapter() {
|
|
1505
|
+
return {
|
|
1506
|
+
id: "tabler",
|
|
1507
|
+
packageName: TABLER_ICONS_PACKAGE,
|
|
1508
|
+
oppositePackageName: LUCIDE_ICONS_PACKAGE,
|
|
1509
|
+
resolveExportFromTabler: (tablerName) => tablerName
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
function createLucideAdapter() {
|
|
1513
|
+
return {
|
|
1514
|
+
id: "lucide",
|
|
1515
|
+
packageName: LUCIDE_ICONS_PACKAGE,
|
|
1516
|
+
oppositePackageName: TABLER_ICONS_PACKAGE,
|
|
1517
|
+
resolveExportFromTabler: (tablerName, context) => {
|
|
1518
|
+
try {
|
|
1519
|
+
return resolveLucideExport(tablerName);
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1522
|
+
throw new CliError(`${context}: ${message}`);
|
|
1523
|
+
}
|
|
1524
|
+
},
|
|
1525
|
+
strokePropTo: "strokeWidth"
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
var ICON_LIBRARY_ADAPTERS = {
|
|
1529
|
+
tabler: createTablerAdapter,
|
|
1530
|
+
lucide: createLucideAdapter
|
|
1531
|
+
};
|
|
1532
|
+
function getIconLibraryAdapter(library) {
|
|
1533
|
+
return ICON_LIBRARY_ADAPTERS[library]();
|
|
1534
|
+
}
|
|
1535
|
+
function isPrimitivePayloadCategory(category) {
|
|
1536
|
+
return category === UI_PRIMITIVES_CATEGORY;
|
|
1537
|
+
}
|
|
1538
|
+
function transformIconLibrary(content, options) {
|
|
1539
|
+
if (!options.isPrimitiveCategory) {
|
|
1540
|
+
return { content, dependencySwap: EMPTY_SWAP };
|
|
1541
|
+
}
|
|
1542
|
+
const adapter = getIconLibraryAdapter(options.iconLibrary);
|
|
1543
|
+
const context = options.slug ? `primitive "${options.slug}"` : "primitive source";
|
|
1544
|
+
const hasTabler = hasPackageImport(content, TABLER_ICONS_PACKAGE);
|
|
1545
|
+
const hasLucide = hasPackageImport(content, LUCIDE_ICONS_PACKAGE);
|
|
1546
|
+
if (adapter.id === "lucide") {
|
|
1547
|
+
if (hasTabler) {
|
|
1548
|
+
const transformed = rewriteTablerImportsToTarget(content, adapter, context);
|
|
1549
|
+
return {
|
|
1550
|
+
content: rewriteStrokeProps(transformed.content, transformed.localBindings),
|
|
1551
|
+
dependencySwap: {
|
|
1552
|
+
remove: [TABLER_ICONS_PACKAGE],
|
|
1553
|
+
add: [LUCIDE_ICONS_PACKAGE]
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
if (hasLucide) {
|
|
1558
|
+
return {
|
|
1559
|
+
content,
|
|
1560
|
+
dependencySwap: {
|
|
1561
|
+
remove: [TABLER_ICONS_PACKAGE],
|
|
1562
|
+
add: [LUCIDE_ICONS_PACKAGE]
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
return { content, dependencySwap: EMPTY_SWAP };
|
|
1567
|
+
}
|
|
1568
|
+
if (hasLucide && !hasTabler) {
|
|
1569
|
+
return {
|
|
1570
|
+
content,
|
|
1571
|
+
dependencySwap: {
|
|
1572
|
+
remove: [LUCIDE_ICONS_PACKAGE],
|
|
1573
|
+
add: [TABLER_ICONS_PACKAGE]
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
if (hasTabler) {
|
|
1578
|
+
assertNoUnmappedTablerImports(content, context);
|
|
1579
|
+
return {
|
|
1580
|
+
content,
|
|
1581
|
+
dependencySwap: {
|
|
1582
|
+
remove: [LUCIDE_ICONS_PACKAGE],
|
|
1583
|
+
add: [TABLER_ICONS_PACKAGE]
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
return { content, dependencySwap: EMPTY_SWAP };
|
|
1588
|
+
}
|
|
1589
|
+
function aggregateIconDependencySwaps(swaps) {
|
|
1590
|
+
const remove = /* @__PURE__ */ new Set();
|
|
1591
|
+
const add = /* @__PURE__ */ new Set();
|
|
1592
|
+
for (const swap of swaps) {
|
|
1593
|
+
for (const pkg of swap.remove) remove.add(pkg);
|
|
1594
|
+
for (const pkg of swap.add) add.add(pkg);
|
|
1595
|
+
}
|
|
1596
|
+
return {
|
|
1597
|
+
remove: [...remove],
|
|
1598
|
+
add: [...add]
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
function applyIconDependencySwap(declared, swap) {
|
|
1602
|
+
const remove = new Set(swap.remove);
|
|
1603
|
+
const next = declared.filter((dep) => !remove.has(dep));
|
|
1604
|
+
for (const pkg of swap.add) {
|
|
1605
|
+
if (!next.includes(pkg)) next.push(pkg);
|
|
1606
|
+
}
|
|
1607
|
+
return next;
|
|
1608
|
+
}
|
|
1609
|
+
function warnOppositeIconPackageMessage(iconLibrary, installedPackages) {
|
|
1610
|
+
const selected = iconLibrary === "lucide" ? LUCIDE_ICONS_PACKAGE : TABLER_ICONS_PACKAGE;
|
|
1611
|
+
const opposite = iconLibrary === "lucide" ? TABLER_ICONS_PACKAGE : LUCIDE_ICONS_PACKAGE;
|
|
1612
|
+
if (installedPackages.has(opposite) && !installedPackages.has(selected)) {
|
|
1613
|
+
return `Project package.json still lists ${opposite} while style.iconLibrary is "${iconLibrary}". Wensity will install ${selected} for new primitive installs but will not uninstall ${opposite} (it may be used outside Wensity).`;
|
|
1614
|
+
}
|
|
1615
|
+
if (installedPackages.has(opposite) && installedPackages.has(selected)) {
|
|
1616
|
+
return `Project package.json lists both ${selected} and ${opposite}. Wensity primitives use only ${selected} (style.iconLibrary="${iconLibrary}"); consider removing ${opposite} if unused.`;
|
|
1617
|
+
}
|
|
1618
|
+
return void 0;
|
|
1619
|
+
}
|
|
1620
|
+
function hasPackageImport(source, packageName) {
|
|
1621
|
+
const escaped = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1622
|
+
return new RegExp(`from\\s+["']${escaped}["']`).test(source);
|
|
1623
|
+
}
|
|
1624
|
+
function rewriteTablerImportsToTarget(content, adapter, context) {
|
|
1625
|
+
const localBindings = /* @__PURE__ */ new Set();
|
|
1626
|
+
const importPattern = /^(?<indent>[ \t]*)(?<typeOnly>import\s+type\s+|import\s+)\{(?<clause>[^}]*)\}\s*from\s+(?<quote>["'])@tabler\/icons-react\k<quote>;?[ \t]*$/gm;
|
|
1627
|
+
const next = content.replace(
|
|
1628
|
+
importPattern,
|
|
1629
|
+
(full, indent, typePrefix, clause, quote) => {
|
|
1630
|
+
const specs = parseNamedImportClause(clause);
|
|
1631
|
+
if (specs.length === 0) return full;
|
|
1632
|
+
const rewritten = [];
|
|
1633
|
+
for (const spec of specs) {
|
|
1634
|
+
if (spec.isTypeOnly || typePrefix.includes("type")) {
|
|
1635
|
+
if (spec.imported === "Icon" || isIconTransformExclusion(spec.imported)) {
|
|
1636
|
+
throw new CliError(
|
|
1637
|
+
`${context}: unexpected type-only icon import "${spec.imported}" from ${TABLER_ICONS_PACKAGE}.`
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
if (isIconTransformExclusion(spec.imported)) {
|
|
1642
|
+
throw new CliError(
|
|
1643
|
+
`${context}: "${spec.imported}" is excluded from the icon map (Phase 0 internal geometry). It must not appear as a library import.`
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
const targetExport = adapter.resolveExportFromTabler(spec.imported, context);
|
|
1647
|
+
localBindings.add(spec.local);
|
|
1648
|
+
if (adapter.id === "lucide") {
|
|
1649
|
+
if (targetExport === spec.local) {
|
|
1650
|
+
rewritten.push(targetExport);
|
|
1651
|
+
} else {
|
|
1652
|
+
rewritten.push(`${targetExport} as ${spec.local}`);
|
|
1653
|
+
}
|
|
1654
|
+
} else {
|
|
1655
|
+
rewritten.push(
|
|
1656
|
+
spec.imported === spec.local ? spec.imported : `${spec.imported} as ${spec.local}`
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
const prefix = typePrefix.startsWith("import type") ? "import type " : "import ";
|
|
1661
|
+
return `${indent}${prefix}{ ${rewritten.join(", ")} } from ${quote}${adapter.packageName}${quote};`;
|
|
1662
|
+
}
|
|
1663
|
+
);
|
|
1664
|
+
if (hasPackageImport(next, TABLER_ICONS_PACKAGE)) {
|
|
1665
|
+
throw new CliError(
|
|
1666
|
+
`${context}: failed to rewrite all ${TABLER_ICONS_PACKAGE} imports for ${adapter.id}.`
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
if (/(?:import|export)\s+\*\s+as\s+\w+\s+from\s+["']@tabler\/icons-react["']/.test(content)) {
|
|
1670
|
+
throw new CliError(
|
|
1671
|
+
`${context}: namespace imports from ${TABLER_ICONS_PACKAGE} are not supported. Use named imports.`
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
return { content: next, localBindings };
|
|
1675
|
+
}
|
|
1676
|
+
function assertNoUnmappedTablerImports(content, context) {
|
|
1677
|
+
const importPattern = /\{([^}]*)\}\s*from\s+["']@tabler\/icons-react["']/g;
|
|
1678
|
+
for (const match of content.matchAll(importPattern)) {
|
|
1679
|
+
const specs = parseNamedImportClause(match[1] ?? "");
|
|
1680
|
+
for (const spec of specs) {
|
|
1681
|
+
if (isIconTransformExclusion(spec.imported)) {
|
|
1682
|
+
throw new CliError(
|
|
1683
|
+
`${context}: "${spec.imported}" must not appear as a library import (Phase 0 exclusion).`
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
if (spec.imported.startsWith("Icon")) {
|
|
1687
|
+
resolveLucideExport(spec.imported);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
function parseNamedImportClause(clause) {
|
|
1693
|
+
const specs = [];
|
|
1694
|
+
for (const raw of clause.split(",")) {
|
|
1695
|
+
const part = raw.trim();
|
|
1696
|
+
if (!part) continue;
|
|
1697
|
+
const typeOnly = /^type\s+/.test(part);
|
|
1698
|
+
const body = typeOnly ? part.replace(/^type\s+/, "").trim() : part;
|
|
1699
|
+
const asMatch = body.match(/^(\w+)\s+as\s+(\w+)$/);
|
|
1700
|
+
if (asMatch) {
|
|
1701
|
+
specs.push({
|
|
1702
|
+
imported: asMatch[1],
|
|
1703
|
+
local: asMatch[2],
|
|
1704
|
+
isTypeOnly: typeOnly
|
|
1705
|
+
});
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
if (/^\w+$/.test(body)) {
|
|
1709
|
+
specs.push({ imported: body, local: body, isTypeOnly: typeOnly });
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
return specs;
|
|
1713
|
+
}
|
|
1714
|
+
function rewriteStrokeProps(content, localBindings) {
|
|
1715
|
+
if (localBindings.size === 0) return content;
|
|
1716
|
+
const tags = new Set(localBindings);
|
|
1717
|
+
if (localBindings.size > 0 && /\bFileIcon\b/.test(content) && /\bpackIconMap\b/.test(content)) {
|
|
1718
|
+
tags.add("FileIcon");
|
|
1719
|
+
}
|
|
1720
|
+
const tagAlternation = [...tags].map((tag) => tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
1721
|
+
if (!tagAlternation) return content;
|
|
1722
|
+
const openTagPattern = new RegExp(`<(${tagAlternation})(\\s[\\s\\S]*?)(/?)>`, "g");
|
|
1723
|
+
return content.replace(openTagPattern, (full, tagName, attrs, selfClose) => {
|
|
1724
|
+
if (NATIVE_SVG_TAGS.has(tagName)) return full;
|
|
1725
|
+
if (!attrs) return full;
|
|
1726
|
+
const nextAttrs = attrs.replace(
|
|
1727
|
+
/(^|[\s])stroke(\s*=\s*(?:\{[^}]*\}|"[^"]*"|'[^']*'))/g,
|
|
1728
|
+
"$1strokeWidth$2"
|
|
1729
|
+
);
|
|
1730
|
+
return `<${tagName}${nextAttrs}${selfClose}>`;
|
|
1731
|
+
});
|
|
570
1732
|
}
|
|
571
1733
|
|
|
572
1734
|
// src/package-manager.ts
|
|
1735
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
1736
|
+
import { spawnSync } from "child_process";
|
|
1737
|
+
import { join as join4 } from "path";
|
|
573
1738
|
function detectPackageManager(projectRoot) {
|
|
574
1739
|
if (existsSync2(join4(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
|
|
575
1740
|
if (existsSync2(join4(projectRoot, "bun.lockb")) || existsSync2(join4(projectRoot, "bun.lock"))) return "bun";
|
|
@@ -578,16 +1743,22 @@ function detectPackageManager(projectRoot) {
|
|
|
578
1743
|
return "npm";
|
|
579
1744
|
}
|
|
580
1745
|
function missingDependencies(projectRoot, dependencies) {
|
|
1746
|
+
const installed = readInstalledPackageNames(projectRoot);
|
|
1747
|
+
if (!installed) return dependencies;
|
|
1748
|
+
return dependencies.filter((dependency) => !installed.has(dependency));
|
|
1749
|
+
}
|
|
1750
|
+
function readInstalledPackageNames(projectRoot) {
|
|
581
1751
|
const packageJsonPath = join4(projectRoot, "package.json");
|
|
582
|
-
if (!existsSync2(packageJsonPath)) return
|
|
1752
|
+
if (!existsSync2(packageJsonPath)) return void 0;
|
|
583
1753
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
1754
|
+
return new Set(
|
|
1755
|
+
Object.keys({
|
|
1756
|
+
...packageJson.dependencies,
|
|
1757
|
+
...packageJson.devDependencies,
|
|
1758
|
+
...packageJson.peerDependencies,
|
|
1759
|
+
...packageJson.optionalDependencies
|
|
1760
|
+
})
|
|
1761
|
+
);
|
|
591
1762
|
}
|
|
592
1763
|
function runInstall(projectRoot, dependencies) {
|
|
593
1764
|
if (dependencies.length === 0) return;
|
|
@@ -623,9 +1794,179 @@ function installArgs(packageManager, dependencies) {
|
|
|
623
1794
|
}
|
|
624
1795
|
}
|
|
625
1796
|
|
|
626
|
-
// src/
|
|
1797
|
+
// src/preset.ts
|
|
1798
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1799
|
+
async function resolvePresetStyle(input) {
|
|
1800
|
+
if (input.presetCode && input.presetFile) {
|
|
1801
|
+
throw new CliError("Use either --preset or --preset-file, not both.");
|
|
1802
|
+
}
|
|
1803
|
+
let code = input.presetCode;
|
|
1804
|
+
if (input.presetFile) {
|
|
1805
|
+
try {
|
|
1806
|
+
const raw = await readFile3(input.presetFile, "utf8");
|
|
1807
|
+
const trimmed = raw.trim();
|
|
1808
|
+
if (trimmed.startsWith("{")) {
|
|
1809
|
+
const parsed = JSON.parse(trimmed);
|
|
1810
|
+
if (parsed && typeof parsed === "object" && "v" in parsed) {
|
|
1811
|
+
const payload = validateCodecPayload(parsed);
|
|
1812
|
+
const json = stableStringifyCodecPayload(payload);
|
|
1813
|
+
code = `${PRESET_CODEC_PREFIX}.${Buffer.from(json, "utf8").toString("base64url")}`;
|
|
1814
|
+
} else {
|
|
1815
|
+
code = encodePreset(parsed);
|
|
1816
|
+
}
|
|
1817
|
+
} else {
|
|
1818
|
+
code = trimmed;
|
|
1819
|
+
}
|
|
1820
|
+
} catch (error) {
|
|
1821
|
+
if (error instanceof PresetValidationError) {
|
|
1822
|
+
throw new CliError(`Invalid preset file: ${error.message}`);
|
|
1823
|
+
}
|
|
1824
|
+
if (error instanceof SyntaxError) {
|
|
1825
|
+
throw new CliError(`Preset file contains invalid JSON: ${error.message}`);
|
|
1826
|
+
}
|
|
1827
|
+
if (error instanceof CliError) throw error;
|
|
1828
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1829
|
+
throw new CliError(`Failed to read preset file: ${message}`);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
if (!code) {
|
|
1833
|
+
throw new CliError("Missing preset. Pass --preset <code> or --preset-file <path>.");
|
|
1834
|
+
}
|
|
1835
|
+
try {
|
|
1836
|
+
const trimmed = code.trim();
|
|
1837
|
+
const payload = decodePreset(trimmed);
|
|
1838
|
+
const style = normalizeStyleConfig({
|
|
1839
|
+
...codecPayloadToStyleInput(payload),
|
|
1840
|
+
presetCode: trimmed
|
|
1841
|
+
});
|
|
1842
|
+
return { style, presetCode: trimmed };
|
|
1843
|
+
} catch (error) {
|
|
1844
|
+
if (error instanceof PresetValidationError) {
|
|
1845
|
+
throw new CliError(`Invalid preset: ${error.message}`);
|
|
1846
|
+
}
|
|
1847
|
+
throw error;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
// src/preset-init-writes.ts
|
|
627
1852
|
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
628
|
-
import { dirname as dirname4
|
|
1853
|
+
import { dirname as dirname4 } from "path";
|
|
1854
|
+
async function commitPresetInitWrites(input) {
|
|
1855
|
+
let cssWritten = false;
|
|
1856
|
+
try {
|
|
1857
|
+
if (input.cssChanged) {
|
|
1858
|
+
await mkdir3(dirname4(input.cssAbsolute), { recursive: true });
|
|
1859
|
+
await writeFile3(input.cssAbsolute, input.nextCss, "utf8");
|
|
1860
|
+
cssWritten = true;
|
|
1861
|
+
}
|
|
1862
|
+
if (input.writeUtility) {
|
|
1863
|
+
await mkdir3(dirname4(input.utilityAbsolutePath), { recursive: true });
|
|
1864
|
+
await writeFile3(input.utilityAbsolutePath, input.utilityContent, "utf8");
|
|
1865
|
+
}
|
|
1866
|
+
await input.commitConfig();
|
|
1867
|
+
} catch (error) {
|
|
1868
|
+
if (cssWritten) {
|
|
1869
|
+
try {
|
|
1870
|
+
await writeFile3(input.cssAbsolute, input.existingCss, "utf8");
|
|
1871
|
+
} catch {
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
throw error;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// src/style-css.ts
|
|
1879
|
+
var ROOT_SELECTOR = ":root";
|
|
1880
|
+
var DARK_SELECTOR = ".dark";
|
|
1881
|
+
function applyStyleToPrimitiveCss(block, styleInput) {
|
|
1882
|
+
const style = normalizeStyleConfig(styleInput);
|
|
1883
|
+
let next = block;
|
|
1884
|
+
const radiusValue = RADIUS_PRESET_CSS[style.radius];
|
|
1885
|
+
next = replaceDeclInSelector(next, ROOT_SELECTOR, "--primitive-radius", radiusValue);
|
|
1886
|
+
next = replaceDeclInSelector(next, DARK_SELECTOR, "--primitive-radius", radiusValue);
|
|
1887
|
+
for (const role of FONT_ROLE_KEYS) {
|
|
1888
|
+
const cssVar = `--primitive-font-${role}`;
|
|
1889
|
+
const cssValue = expandFontRole(style.fonts[role], role);
|
|
1890
|
+
next = replaceDeclInSelector(next, ROOT_SELECTOR, cssVar, cssValue);
|
|
1891
|
+
next = replaceDeclInSelector(next, DARK_SELECTOR, cssVar, cssValue);
|
|
1892
|
+
}
|
|
1893
|
+
if (style.colors?.light) {
|
|
1894
|
+
for (const key of CONTROL_SOLID_COLOR_KEYS) {
|
|
1895
|
+
const value = style.colors.light[key];
|
|
1896
|
+
if (value === void 0) continue;
|
|
1897
|
+
next = replaceDeclInSelector(next, ROOT_SELECTOR, CONTROL_SOLID_CSS_VARS[key], value);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
if (style.colors?.dark) {
|
|
1901
|
+
for (const key of CONTROL_SOLID_COLOR_KEYS) {
|
|
1902
|
+
const value = style.colors.dark[key];
|
|
1903
|
+
if (value === void 0) continue;
|
|
1904
|
+
next = replaceDeclInSelector(next, DARK_SELECTOR, CONTROL_SOLID_CSS_VARS[key], value);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
return next;
|
|
1908
|
+
}
|
|
1909
|
+
function replaceDeclInSelector(css, selector, property, value) {
|
|
1910
|
+
const block = findTopLevelRule(css, selector);
|
|
1911
|
+
if (!block) return css;
|
|
1912
|
+
const declPattern = new RegExp(
|
|
1913
|
+
`(${escapeRegExp(property)}\\s*:\\s*)([^;]+)(;)`
|
|
1914
|
+
);
|
|
1915
|
+
if (!declPattern.test(block.body)) {
|
|
1916
|
+
return css;
|
|
1917
|
+
}
|
|
1918
|
+
const updatedBody = block.body.replace(declPattern, `$1${value}$3`);
|
|
1919
|
+
if (updatedBody === block.body) return css;
|
|
1920
|
+
return `${css.slice(0, block.bodyStart)}${updatedBody}${css.slice(block.bodyEnd)}`;
|
|
1921
|
+
}
|
|
1922
|
+
function findTopLevelRule(css, selector) {
|
|
1923
|
+
const needle = `${selector} {`;
|
|
1924
|
+
let searchFrom = 0;
|
|
1925
|
+
while (searchFrom < css.length) {
|
|
1926
|
+
const start = css.indexOf(needle, searchFrom);
|
|
1927
|
+
if (start === -1) return void 0;
|
|
1928
|
+
const bodyStart = start + needle.length;
|
|
1929
|
+
const bodyEnd = findMatchingBrace(css, bodyStart - 1);
|
|
1930
|
+
if (bodyEnd === -1) return void 0;
|
|
1931
|
+
if (isLikelyTopLevel(css, start)) {
|
|
1932
|
+
return {
|
|
1933
|
+
bodyStart,
|
|
1934
|
+
bodyEnd,
|
|
1935
|
+
body: css.slice(bodyStart, bodyEnd)
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
searchFrom = bodyEnd + 1;
|
|
1939
|
+
}
|
|
1940
|
+
return void 0;
|
|
1941
|
+
}
|
|
1942
|
+
function isLikelyTopLevel(css, index) {
|
|
1943
|
+
let depth = 0;
|
|
1944
|
+
for (let i = 0; i < index; i += 1) {
|
|
1945
|
+
const ch = css[i];
|
|
1946
|
+
if (ch === "{") depth += 1;
|
|
1947
|
+
else if (ch === "}") depth -= 1;
|
|
1948
|
+
}
|
|
1949
|
+
return depth === 0;
|
|
1950
|
+
}
|
|
1951
|
+
function findMatchingBrace(css, openIndex) {
|
|
1952
|
+
let depth = 0;
|
|
1953
|
+
for (let i = openIndex; i < css.length; i += 1) {
|
|
1954
|
+
const ch = css[i];
|
|
1955
|
+
if (ch === "{") depth += 1;
|
|
1956
|
+
else if (ch === "}") {
|
|
1957
|
+
depth -= 1;
|
|
1958
|
+
if (depth === 0) return i;
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
return -1;
|
|
1962
|
+
}
|
|
1963
|
+
function escapeRegExp(value) {
|
|
1964
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
// src/component-install.ts
|
|
1968
|
+
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
1969
|
+
import { dirname as dirname5, join as join5, relative as relative3 } from "path";
|
|
629
1970
|
|
|
630
1971
|
// src/templates.ts
|
|
631
1972
|
function cnUtilityTemplate() {
|
|
@@ -782,27 +2123,46 @@ export function WensitySecondaryButton({
|
|
|
782
2123
|
|
|
783
2124
|
// src/component-install.ts
|
|
784
2125
|
var ALWAYS_PRESENT_PACKAGES = /* @__PURE__ */ new Set(["react", "react-dom", "next"]);
|
|
785
|
-
|
|
2126
|
+
var OVERLAY_HELPER_PATH = "components/wensity/overlay.tsx";
|
|
2127
|
+
var PRIMITIVE_CSS_START_MARKER = "/* \u2500\u2500 Wensity Primitive Tokens";
|
|
2128
|
+
var PRIMITIVE_CSS_END_MARKER = "/* \u2500\u2500 End Wensity Primitive Tokens \u2500\u2500 */";
|
|
2129
|
+
async function prepareInstall(projectRoot, config, payload, options = {}) {
|
|
786
2130
|
const fallbackFileName = componentFileName(payload.slug);
|
|
787
2131
|
const files = [];
|
|
788
2132
|
const componentContents = [];
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
2133
|
+
const iconSwaps = [];
|
|
2134
|
+
const style = normalizeStyleConfig(config.style);
|
|
2135
|
+
const isPrimitiveCategory = isPrimitivePayloadCategory(payload.category);
|
|
2136
|
+
const payloadHelperNeeds = normalizePayloadHelperNeeds(
|
|
2137
|
+
"helperNeeds" in payload ? payload.helperNeeds : void 0
|
|
2138
|
+
);
|
|
2139
|
+
const inferredHelperNeeds = { utils: false, primitives: false, overlay: false };
|
|
2140
|
+
const hasGraphHelperFiles = payload.files.some((file) => isGraphHelperFile(file));
|
|
2141
|
+
for (const [index, payloadFile] of payload.files.entries()) {
|
|
2142
|
+
const payloadFileKind = getPayloadFileKind(payloadFile);
|
|
2143
|
+
const relativeOutputPath = getPayloadOutputPath(payloadFile, fallbackFileName, config);
|
|
2144
|
+
const absoluteOutputPath = resolveInside(projectRoot, relativeOutputPath);
|
|
2145
|
+
if (!payloadHelperNeeds) mergeHelperNeeds(inferredHelperNeeds, inferHelperNeeds(payloadFile.content));
|
|
2146
|
+
const transformed = transformComponentSource(payloadFile.content, config, {
|
|
2147
|
+
isPrimitiveCategory,
|
|
2148
|
+
iconLibrary: style.iconLibrary,
|
|
2149
|
+
slug: payload.slug
|
|
803
2150
|
});
|
|
2151
|
+
componentContents.push(transformed.content);
|
|
2152
|
+
iconSwaps.push(transformed.dependencySwap);
|
|
2153
|
+
const isPrimaryFile = index === 0 && payloadFileKind === "component";
|
|
2154
|
+
const shouldWriteFile = isPrimaryFile || options.overwriteExistingDependencyFiles || !await fileExists(absoluteOutputPath);
|
|
2155
|
+
if (shouldWriteFile) {
|
|
2156
|
+
files.push({
|
|
2157
|
+
absolutePath: absoluteOutputPath,
|
|
2158
|
+
relativePath: relativeOutputPath,
|
|
2159
|
+
content: transformed.content,
|
|
2160
|
+
kind: payloadFileKind
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
804
2163
|
}
|
|
805
|
-
|
|
2164
|
+
const helperNeeds = payloadHelperNeeds ?? inferredHelperNeeds;
|
|
2165
|
+
if (!hasGraphHelperFiles && helperNeeds.utils) {
|
|
806
2166
|
const utilityPath = withSourceExtension(config.utils, "ts");
|
|
807
2167
|
if (!await fileExists(resolveInside(projectRoot, utilityPath))) {
|
|
808
2168
|
files.push({
|
|
@@ -813,7 +2173,7 @@ async function prepareInstall(projectRoot, config, payload) {
|
|
|
813
2173
|
});
|
|
814
2174
|
}
|
|
815
2175
|
}
|
|
816
|
-
if (
|
|
2176
|
+
if (!hasGraphHelperFiles && helperNeeds.primitives) {
|
|
817
2177
|
const primitivesPath = toPosixPath(join5(config.components, "primitives.tsx"));
|
|
818
2178
|
if (!await fileExists(resolveInside(projectRoot, primitivesPath))) {
|
|
819
2179
|
const primitives = primitivesTemplate(config.aliases.utils);
|
|
@@ -826,17 +2186,69 @@ async function prepareInstall(projectRoot, config, payload) {
|
|
|
826
2186
|
componentContents.push(primitives);
|
|
827
2187
|
}
|
|
828
2188
|
}
|
|
2189
|
+
const iconDependencySwap = aggregateIconDependencySwaps(iconSwaps);
|
|
2190
|
+
const declaredAfterSwap = applyIconDependencySwap(payload.dependencies ?? [], iconDependencySwap);
|
|
829
2191
|
const dependencies = collectDependencies({
|
|
830
|
-
declared:
|
|
2192
|
+
declared: declaredAfterSwap,
|
|
831
2193
|
source: componentContents.join("\n"),
|
|
832
|
-
needsUtility
|
|
2194
|
+
needsUtility: helperNeeds.utils
|
|
833
2195
|
});
|
|
834
|
-
|
|
2196
|
+
const cssFile = await planPrimitiveCssFile(projectRoot, config, payload);
|
|
2197
|
+
return {
|
|
2198
|
+
files,
|
|
2199
|
+
dependencies,
|
|
2200
|
+
...cssFile ? { cssFile } : {},
|
|
2201
|
+
...isPrimitiveCategory ? { iconDependencySwap } : {}
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
async function planPrimitiveCssFile(projectRoot, config, payload) {
|
|
2205
|
+
if (!("primitiveCss" in payload) || typeof payload.primitiveCss !== "string") {
|
|
2206
|
+
return void 0;
|
|
2207
|
+
}
|
|
2208
|
+
let primitiveCss = payload.primitiveCss.trim();
|
|
2209
|
+
if (!primitiveCss) return void 0;
|
|
2210
|
+
const style = normalizeStyleConfig(config.style);
|
|
2211
|
+
primitiveCss = applyStyleToPrimitiveCss(primitiveCss, style).trim();
|
|
2212
|
+
const cssRelative = config.tailwind?.css;
|
|
2213
|
+
if (!cssRelative) return void 0;
|
|
2214
|
+
const absolutePath = resolveInside(projectRoot, cssRelative);
|
|
2215
|
+
const existing = await fileExists(absolutePath) ? await readFile4(absolutePath, "utf8") : "";
|
|
2216
|
+
const next = upsertPrimitiveCssBlock(existing, primitiveCss);
|
|
2217
|
+
if (next === existing) return void 0;
|
|
2218
|
+
return {
|
|
2219
|
+
absolutePath,
|
|
2220
|
+
relativePath: toPosixPath(cssRelative),
|
|
2221
|
+
content: next,
|
|
2222
|
+
kind: "config"
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
function upsertPrimitiveCssBlock(existingCss, block) {
|
|
2226
|
+
const normalizedBlock = block.trim();
|
|
2227
|
+
const start = existingCss.indexOf(PRIMITIVE_CSS_START_MARKER);
|
|
2228
|
+
const end = existingCss.indexOf(PRIMITIVE_CSS_END_MARKER);
|
|
2229
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
2230
|
+
const before = existingCss.slice(0, start).replace(/\s*$/, "\n\n");
|
|
2231
|
+
const after = existingCss.slice(end + PRIMITIVE_CSS_END_MARKER.length).replace(/^\s*/, "\n");
|
|
2232
|
+
return `${before}${normalizedBlock}${after}`.replace(/\n{3,}/g, "\n\n");
|
|
2233
|
+
}
|
|
2234
|
+
if (start !== -1 || end !== -1) {
|
|
2235
|
+
throw new CliError(
|
|
2236
|
+
"Configured Tailwind CSS has a partial Wensity Primitive Tokens block. Remove the broken markers and re-run install."
|
|
2237
|
+
);
|
|
2238
|
+
}
|
|
2239
|
+
const trimmed = existingCss.replace(/\s*$/, "");
|
|
2240
|
+
if (!trimmed) return `${normalizedBlock}
|
|
2241
|
+
`;
|
|
2242
|
+
return `${trimmed}
|
|
2243
|
+
|
|
2244
|
+
${normalizedBlock}
|
|
2245
|
+
`;
|
|
835
2246
|
}
|
|
836
2247
|
async function assertNoConflicts(files, force) {
|
|
837
2248
|
if (force) return;
|
|
838
2249
|
const conflicts = [];
|
|
839
2250
|
for (const file of files) {
|
|
2251
|
+
if (file.kind === "config") continue;
|
|
840
2252
|
if (await fileExists(file.absolutePath)) conflicts.push(file.relativePath);
|
|
841
2253
|
}
|
|
842
2254
|
if (conflicts.length > 0) {
|
|
@@ -846,27 +2258,93 @@ ${conflicts.map((path) => ` - ${path}`).join("\n")}`);
|
|
|
846
2258
|
}
|
|
847
2259
|
async function writePlannedFiles(files) {
|
|
848
2260
|
for (const file of files) {
|
|
849
|
-
await
|
|
850
|
-
await
|
|
2261
|
+
await mkdir4(dirname5(file.absolutePath), { recursive: true });
|
|
2262
|
+
await writeFile4(file.absolutePath, file.content, "utf8");
|
|
851
2263
|
}
|
|
852
2264
|
}
|
|
853
2265
|
function summarizeFiles(projectRoot, files) {
|
|
854
2266
|
return files.map((file) => toPosixPath(relative3(projectRoot, file.absolutePath)));
|
|
855
2267
|
}
|
|
856
|
-
function transformComponentSource(content, config) {
|
|
857
|
-
const needsUtility = content.includes("@wensity/ui/lib") || importsFrom(content, "@/lib/utils");
|
|
858
|
-
const needsPrimitives = content.includes("@wensity/ui/components") || importsFrom(content, "@/components/wensity/primitives");
|
|
2268
|
+
function transformComponentSource(content, config, iconOptions) {
|
|
859
2269
|
let transformed = content;
|
|
860
2270
|
transformed = transformed.replace(/from\s+["']@wensity\/ui\/lib["']/g, `from "${config.aliases.utils}"`);
|
|
861
2271
|
transformed = transformed.replace(/from\s+["']@\/lib\/utils["']/g, `from "${config.aliases.utils}"`);
|
|
862
|
-
transformed = transformed.replace(/from\s+["']@wensity\/ui\/components["']/g, `from "
|
|
863
|
-
transformed = transformed
|
|
864
|
-
|
|
2272
|
+
transformed = transformed.replace(/from\s+["']@wensity\/ui\/components["']/g, `from "@/components/wensity/primitives"`);
|
|
2273
|
+
transformed = rewriteWensityComponentAlias(transformed, config.aliases.components);
|
|
2274
|
+
const iconResult = transformIconLibrary(transformed, {
|
|
2275
|
+
isPrimitiveCategory: iconOptions.isPrimitiveCategory,
|
|
2276
|
+
iconLibrary: iconOptions.iconLibrary,
|
|
2277
|
+
slug: iconOptions.slug
|
|
2278
|
+
});
|
|
2279
|
+
return { content: iconResult.content, dependencySwap: iconResult.dependencySwap };
|
|
2280
|
+
}
|
|
2281
|
+
function getPayloadFileKind(payloadFile) {
|
|
2282
|
+
if ("kind" in payloadFile && payloadFile.kind === "helper") return "helper";
|
|
2283
|
+
return isGraphHelperFile(payloadFile) ? "helper" : "component";
|
|
2284
|
+
}
|
|
2285
|
+
function getPayloadOutputPath(payloadFile, fallbackFileName, config) {
|
|
2286
|
+
if (isUtilsHelperFile(payloadFile)) return withSourceExtension(config.utils, "ts");
|
|
2287
|
+
if (isOverlayHelperFile(payloadFile)) return toPosixPath(join5(config.components, "overlay.tsx"));
|
|
2288
|
+
if (isPrimitivesHelperFile(payloadFile)) return toPosixPath(join5(config.components, "primitives.tsx"));
|
|
2289
|
+
if (payloadFile.kind === "helper") {
|
|
2290
|
+
throw new CliError(`Unsupported registry helper file path: ${payloadFile.path}`);
|
|
2291
|
+
}
|
|
2292
|
+
const safePayloadPath = sanitizePayloadPath(payloadFile.path, fallbackFileName);
|
|
2293
|
+
return toPosixPath(join5(config.components, safePayloadPath));
|
|
2294
|
+
}
|
|
2295
|
+
function isGraphHelperFile(payloadFile) {
|
|
2296
|
+
return isUtilsHelperFile(payloadFile) || isOverlayHelperFile(payloadFile) || isPrimitivesHelperFile(payloadFile);
|
|
2297
|
+
}
|
|
2298
|
+
function isUtilsHelperFile(payloadFile) {
|
|
2299
|
+
const normalized = payloadFile.path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
2300
|
+
return payloadFile.kind === "helper" && (normalized === "lib/utils.ts" || normalized === "src/lib/utils.ts");
|
|
2301
|
+
}
|
|
2302
|
+
function isOverlayHelperFile(payloadFile) {
|
|
2303
|
+
const normalized = payloadFile.path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
2304
|
+
return payloadFile.kind === "helper" && (normalized === OVERLAY_HELPER_PATH || normalized === `src/${OVERLAY_HELPER_PATH}`);
|
|
2305
|
+
}
|
|
2306
|
+
function isPrimitivesHelperFile(payloadFile) {
|
|
2307
|
+
const normalized = payloadFile.path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
2308
|
+
return payloadFile.kind === "helper" && (normalized === "components/wensity/primitives.tsx" || normalized === "src/components/wensity/primitives.tsx");
|
|
2309
|
+
}
|
|
2310
|
+
function rewriteWensityComponentAlias(source, componentsAlias) {
|
|
2311
|
+
const replacement = componentsAlias.replace(/\/$/, "");
|
|
2312
|
+
return source.replace(
|
|
2313
|
+
/(from\s+["'])@\/components\/wensity(\/[^"']*)?(["'])/g,
|
|
2314
|
+
(_match, prefix, suffix, quote) => `${prefix}${replacement}${suffix ?? ""}${quote}`
|
|
2315
|
+
).replace(
|
|
2316
|
+
/(import\(\s*["'])@\/components\/wensity(\/[^"']*)?(["']\s*\))/g,
|
|
2317
|
+
(_match, prefix, suffix, suffixQuote) => `${prefix}${replacement}${suffix ?? ""}${suffixQuote}`
|
|
2318
|
+
);
|
|
865
2319
|
}
|
|
866
2320
|
function importsFrom(source, specifier) {
|
|
867
2321
|
const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
868
2322
|
return new RegExp(`from\\s+["']${escaped}["']`).test(source);
|
|
869
2323
|
}
|
|
2324
|
+
function normalizePayloadHelperNeeds(value) {
|
|
2325
|
+
if (!value || typeof value !== "object") return void 0;
|
|
2326
|
+
const helperNeeds = value;
|
|
2327
|
+
if (typeof helperNeeds.utils !== "boolean" || typeof helperNeeds.primitives !== "boolean") {
|
|
2328
|
+
return void 0;
|
|
2329
|
+
}
|
|
2330
|
+
const overlay = helperNeeds.overlay === true;
|
|
2331
|
+
return {
|
|
2332
|
+
utils: helperNeeds.primitives || overlay || helperNeeds.utils,
|
|
2333
|
+
primitives: helperNeeds.primitives,
|
|
2334
|
+
overlay
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
function inferHelperNeeds(source) {
|
|
2338
|
+
const overlay = importsFrom(source, "@/components/wensity/overlay");
|
|
2339
|
+
const primitives = source.includes("@wensity/ui/components") || importsFrom(source, "@/components/wensity/primitives");
|
|
2340
|
+
const utils = overlay || primitives || source.includes("@wensity/ui/lib") || importsFrom(source, "@/lib/utils");
|
|
2341
|
+
return { utils, primitives, overlay };
|
|
2342
|
+
}
|
|
2343
|
+
function mergeHelperNeeds(target, source) {
|
|
2344
|
+
target.overlay ||= source.overlay === true;
|
|
2345
|
+
target.primitives ||= source.primitives;
|
|
2346
|
+
target.utils ||= source.utils || source.primitives || source.overlay === true;
|
|
2347
|
+
}
|
|
870
2348
|
function collectDependencies(input) {
|
|
871
2349
|
const dependencies = /* @__PURE__ */ new Set();
|
|
872
2350
|
for (const dependency of input.declared) {
|
|
@@ -960,13 +2438,28 @@ async function initCommand(context, parsed) {
|
|
|
960
2438
|
const force = hasFlag(parsed, "force");
|
|
961
2439
|
const install = !hasFlag(parsed, "no-install");
|
|
962
2440
|
const apiUrl = getStringOption(parsed, "api-url") ?? context.env.WENSITY_API_URL ?? DEFAULT_API_ORIGIN;
|
|
2441
|
+
const presetCode = getStringOption(parsed, "preset");
|
|
2442
|
+
const presetFile = getStringOption(parsed, "preset-file");
|
|
2443
|
+
const hasPreset = Boolean(presetCode || presetFile);
|
|
2444
|
+
const dependencies = ["clsx", "tailwind-merge"];
|
|
2445
|
+
if (hasPreset) {
|
|
2446
|
+
await initWithPreset(context, {
|
|
2447
|
+
dryRun,
|
|
2448
|
+
force,
|
|
2449
|
+
install,
|
|
2450
|
+
apiUrl,
|
|
2451
|
+
presetCode,
|
|
2452
|
+
presetFile,
|
|
2453
|
+
dependencies
|
|
2454
|
+
});
|
|
2455
|
+
return;
|
|
2456
|
+
}
|
|
963
2457
|
const config = createDefaultConfig(context.cwd, apiUrl);
|
|
964
2458
|
const configPath = join6(context.cwd, CONFIG_FILE_NAME);
|
|
965
2459
|
const utilityPath = resolveConfiguredFilePath(context.cwd, config.utils);
|
|
966
2460
|
const utilityRelativePath = toPosixPath(config.utils.endsWith(".ts") ? config.utils : `${config.utils}.ts`);
|
|
967
|
-
const dependencies = ["clsx", "tailwind-merge"];
|
|
968
2461
|
if (dryRun) {
|
|
969
|
-
|
|
2462
|
+
printTitle("Project initialization preview");
|
|
970
2463
|
print(action("Would write:"));
|
|
971
2464
|
print(formatList([CONFIG_FILE_NAME, utilityRelativePath]));
|
|
972
2465
|
if (install) print(action(`Would install: ${dependencies.join(" ")}`));
|
|
@@ -979,16 +2472,114 @@ async function initCommand(context, parsed) {
|
|
|
979
2472
|
if (install) {
|
|
980
2473
|
runInstall(context.cwd, missingDependencies(context.cwd, dependencies));
|
|
981
2474
|
}
|
|
982
|
-
|
|
2475
|
+
printTitle("Project initialized");
|
|
983
2476
|
print(panel("Created", [
|
|
984
2477
|
success(CONFIG_FILE_NAME),
|
|
985
2478
|
success(utilityRelativePath),
|
|
986
2479
|
success(`registry: ${apiUrl}`)
|
|
987
2480
|
]));
|
|
988
2481
|
print(nextSteps([
|
|
989
|
-
"npx wensity list",
|
|
990
|
-
"npx wensity
|
|
991
|
-
"npx wensity
|
|
2482
|
+
"Browse available components: npx wensity list",
|
|
2483
|
+
"Browse premium blocks: npx wensity list --type block",
|
|
2484
|
+
"Install your chosen slug: npx wensity add <component-slug>"
|
|
2485
|
+
]));
|
|
2486
|
+
}
|
|
2487
|
+
async function initWithPreset(context, options) {
|
|
2488
|
+
const { style, presetCode } = await resolvePresetStyle({
|
|
2489
|
+
presetCode: options.presetCode,
|
|
2490
|
+
presetFile: options.presetFile
|
|
2491
|
+
});
|
|
2492
|
+
const configPath = join6(context.cwd, CONFIG_FILE_NAME);
|
|
2493
|
+
const configExists = await fileExists(configPath);
|
|
2494
|
+
let config;
|
|
2495
|
+
if (configExists && !options.force) {
|
|
2496
|
+
const loaded = await loadConfig(context.cwd, { apiUrl: options.apiUrl });
|
|
2497
|
+
config = {
|
|
2498
|
+
...loaded.config,
|
|
2499
|
+
style
|
|
2500
|
+
};
|
|
2501
|
+
} else {
|
|
2502
|
+
config = {
|
|
2503
|
+
...createDefaultConfig(context.cwd, options.apiUrl),
|
|
2504
|
+
style
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
const utilityPath = resolveConfiguredFilePath(context.cwd, config.utils);
|
|
2508
|
+
const utilityRelativePath = toPosixPath(config.utils.endsWith(".ts") ? config.utils : `${config.utils}.ts`);
|
|
2509
|
+
const registry = config.registry;
|
|
2510
|
+
let primitiveCss;
|
|
2511
|
+
try {
|
|
2512
|
+
const payload = await withSpinner(
|
|
2513
|
+
`Fetching primitive tokens (${INIT_PRIMITIVE_CSS_SLUG})`,
|
|
2514
|
+
() => fetchComponentPayload({ slug: INIT_PRIMITIVE_CSS_SLUG, registry }),
|
|
2515
|
+
"Fetched primitive tokens"
|
|
2516
|
+
);
|
|
2517
|
+
if (typeof payload.primitiveCss !== "string" || !payload.primitiveCss.trim()) {
|
|
2518
|
+
throw new CliError(
|
|
2519
|
+
`Pinned slug "${INIT_PRIMITIVE_CSS_SLUG}" did not return primitiveCss. Cannot apply preset styles.`
|
|
2520
|
+
);
|
|
2521
|
+
}
|
|
2522
|
+
primitiveCss = applyStyleToPrimitiveCss(payload.primitiveCss, style).trim();
|
|
2523
|
+
} catch (error) {
|
|
2524
|
+
if (error instanceof CliError) throw error;
|
|
2525
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2526
|
+
throw new CliError(`Failed to fetch primitive CSS for preset init: ${message}`);
|
|
2527
|
+
}
|
|
2528
|
+
const cssRelative = config.tailwind?.css;
|
|
2529
|
+
if (!cssRelative) {
|
|
2530
|
+
throw new CliError("No Tailwind CSS path configured. Set tailwind.css in wensity.json.");
|
|
2531
|
+
}
|
|
2532
|
+
const cssAbsolute = resolveInside(context.cwd, cssRelative);
|
|
2533
|
+
const existingCss = await fileExists(cssAbsolute) ? await readFile5(cssAbsolute, "utf8") : "";
|
|
2534
|
+
const nextCss = upsertPrimitiveCssBlock(existingCss, primitiveCss);
|
|
2535
|
+
const cssChanged = nextCss !== existingCss;
|
|
2536
|
+
if (options.dryRun) {
|
|
2537
|
+
printTitle("Project initialization preview (preset)");
|
|
2538
|
+
print(action("Would write:"));
|
|
2539
|
+
const planned = [CONFIG_FILE_NAME, utilityRelativePath];
|
|
2540
|
+
if (cssChanged) planned.push(toPosixPath(cssRelative));
|
|
2541
|
+
print(formatList(planned));
|
|
2542
|
+
print(action(`Preset: ${presetCode.slice(0, 24)}\u2026`));
|
|
2543
|
+
print(action(`Style: icon=${style.iconLibrary} radius=${style.radius}`));
|
|
2544
|
+
if (options.install) print(action(`Would install: ${options.dependencies.join(" ")}`));
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
const shouldWriteUtility = options.force || !await fileExists(utilityPath);
|
|
2548
|
+
await commitPresetInitWrites({
|
|
2549
|
+
cssAbsolute,
|
|
2550
|
+
existingCss,
|
|
2551
|
+
nextCss,
|
|
2552
|
+
cssChanged,
|
|
2553
|
+
writeUtility: shouldWriteUtility,
|
|
2554
|
+
utilityAbsolutePath: utilityPath,
|
|
2555
|
+
utilityContent: cnUtilityTemplate(),
|
|
2556
|
+
commitConfig: async () => {
|
|
2557
|
+
if (configExists && !options.force) {
|
|
2558
|
+
await mergeStyleIntoConfigFile(configPath, style);
|
|
2559
|
+
} else {
|
|
2560
|
+
await writeConfigFile(configPath, config, options.force);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
});
|
|
2564
|
+
if (options.install) {
|
|
2565
|
+
runInstall(context.cwd, missingDependencies(context.cwd, options.dependencies));
|
|
2566
|
+
}
|
|
2567
|
+
printTitle("Project initialized");
|
|
2568
|
+
const created = [
|
|
2569
|
+
success(CONFIG_FILE_NAME),
|
|
2570
|
+
success(utilityRelativePath),
|
|
2571
|
+
success(`registry: ${options.apiUrl}`),
|
|
2572
|
+
success(`preset applied (${style.iconLibrary}, radius=${style.radius})`)
|
|
2573
|
+
];
|
|
2574
|
+
if (cssChanged) {
|
|
2575
|
+
created.push(success(`CSS tokens: ${toPosixPath(cssRelative)}`));
|
|
2576
|
+
} else {
|
|
2577
|
+
created.push(action(`CSS tokens unchanged: ${toPosixPath(cssRelative)}`));
|
|
2578
|
+
}
|
|
2579
|
+
print(panel("Created", created));
|
|
2580
|
+
print(nextSteps([
|
|
2581
|
+
"Browse available components: npx wensity list",
|
|
2582
|
+
"Install a primitive: npx wensity add button"
|
|
992
2583
|
]));
|
|
993
2584
|
}
|
|
994
2585
|
async function loginCommand(context, parsed) {
|
|
@@ -1011,17 +2602,19 @@ async function loginCommand(context, parsed) {
|
|
|
1011
2602
|
printBrand("API token verified");
|
|
1012
2603
|
print(formatAccountPanel(whoami, path));
|
|
1013
2604
|
print(nextSteps(whoami.plan?.tier === "pro" ? [
|
|
1014
|
-
"npx wensity
|
|
1015
|
-
"npx wensity
|
|
1016
|
-
"npx wensity
|
|
2605
|
+
"Browse Pro components: npx wensity list",
|
|
2606
|
+
"Browse Pro blocks: npx wensity list --type block",
|
|
2607
|
+
"Install a component: npx wensity add <component-slug>",
|
|
2608
|
+
"Install a block: npx wensity add <block-slug> --type block"
|
|
1017
2609
|
] : [
|
|
1018
|
-
"npx wensity
|
|
1019
|
-
"
|
|
2610
|
+
"Browse free components: npx wensity list",
|
|
2611
|
+
"Install any Free slug from the table: npx wensity add <slug>",
|
|
2612
|
+
`Upgrade to unlock Pro components and blocks: ${pricingBlue("https://ui.wensity.com/pricing")}`
|
|
1020
2613
|
]));
|
|
1021
2614
|
}
|
|
1022
2615
|
async function logoutCommand(context) {
|
|
1023
2616
|
const removed = await deleteCredentials(context.env);
|
|
1024
|
-
|
|
2617
|
+
printTitle("Session");
|
|
1025
2618
|
print(removed ? success("Logged out of Wensity.") : action("No Wensity credentials were stored."));
|
|
1026
2619
|
}
|
|
1027
2620
|
async function whoamiCommand(context, parsed) {
|
|
@@ -1056,8 +2649,9 @@ async function listCommand(context, parsed) {
|
|
|
1056
2649
|
printBrand("Premium blocks");
|
|
1057
2650
|
print(formatBlockGroup(response2.blocks));
|
|
1058
2651
|
print(nextSteps([
|
|
1059
|
-
"
|
|
1060
|
-
|
|
2652
|
+
brandRed("Create an API token in the dashboard if you have Pro"),
|
|
2653
|
+
brandRed("Save the token: npx wensity login --token <token>"),
|
|
2654
|
+
"Install your chosen block: npx wensity add <block-slug> --type block"
|
|
1061
2655
|
]));
|
|
1062
2656
|
return;
|
|
1063
2657
|
}
|
|
@@ -1075,9 +2669,9 @@ async function listCommand(context, parsed) {
|
|
|
1075
2669
|
print(formatComponentGroup("Free", response.components.filter((component) => component.access !== "pro")));
|
|
1076
2670
|
print(formatComponentGroup("Pro", response.components.filter((component) => component.access === "pro")));
|
|
1077
2671
|
print(nextSteps([
|
|
1078
|
-
"npx wensity add
|
|
1079
|
-
"npx wensity login --token <
|
|
1080
|
-
"npx wensity list --type block"
|
|
2672
|
+
"Choose any Free slug above: npx wensity add <component-slug>",
|
|
2673
|
+
brandRed("For Pro slugs, save an API token: npx wensity login --token <token>"),
|
|
2674
|
+
"Browse layout blocks: npx wensity list --type block"
|
|
1081
2675
|
]));
|
|
1082
2676
|
}
|
|
1083
2677
|
function formatComponentGroup(label, components) {
|
|
@@ -1116,37 +2710,51 @@ Usage: wensity ${updateMode ? "update" : "add"} <slug>${resourceType === "block"
|
|
|
1116
2710
|
const loaded = await loadConfig(context.cwd, { registry: registryOverride, apiUrl });
|
|
1117
2711
|
const credentials = await readCredentials(context.env);
|
|
1118
2712
|
const registry = registryUrlForType(loaded.config.registry, resourceType, registryOverride);
|
|
1119
|
-
const payload = resourceType === "block" ? await fetchBlockPayload({ slug, registry, credentials }) : await fetchComponentPayload({ slug, registry, credentials });
|
|
1120
|
-
const prepared = await prepareInstall(loaded.projectRoot, loaded.config, payload);
|
|
1121
2713
|
const force = updateMode || hasFlag(parsed, "force");
|
|
2714
|
+
const payload = resourceType === "block" ? await fetchBlockPayload({ slug, registry, credentials }) : await fetchComponentPayload({ slug, registry, credentials });
|
|
2715
|
+
const prepared = await prepareInstall(loaded.projectRoot, loaded.config, payload, {
|
|
2716
|
+
overwriteExistingDependencyFiles: force
|
|
2717
|
+
});
|
|
1122
2718
|
const dryRun = hasFlag(parsed, "dry-run");
|
|
1123
2719
|
const install = !hasFlag(parsed, "no-install");
|
|
1124
|
-
|
|
1125
|
-
|
|
2720
|
+
const plannedInstallFiles = prepared.cssFile ? [...prepared.files, prepared.cssFile] : prepared.files;
|
|
2721
|
+
await assertNoConflicts(plannedInstallFiles, force || dryRun);
|
|
2722
|
+
const plannedFiles = summarizeFiles(loaded.projectRoot, plannedInstallFiles);
|
|
1126
2723
|
const missing = missingDependencies(loaded.projectRoot, prepared.dependencies);
|
|
2724
|
+
const style = normalizeStyleConfig(loaded.config.style);
|
|
2725
|
+
const installedPackages = readInstalledPackageNames(loaded.projectRoot);
|
|
2726
|
+
const oppositeIconWarning = installedPackages && prepared.iconDependencySwap ? warnOppositeIconPackageMessage(style.iconLibrary, installedPackages) : void 0;
|
|
1127
2727
|
if (dryRun) {
|
|
1128
|
-
|
|
2728
|
+
printTitle(updateMode ? "Update preview" : "Install preview");
|
|
1129
2729
|
print(action("Would write:"));
|
|
1130
2730
|
print(formatList(plannedFiles));
|
|
2731
|
+
if (prepared.cssFile) {
|
|
2732
|
+
print(action(`Would update CSS tokens in: ${prepared.cssFile.relativePath}`));
|
|
2733
|
+
}
|
|
1131
2734
|
if (install && missing.length > 0) print(action(`Would install: ${missing.join(" ")}`));
|
|
2735
|
+
if (oppositeIconWarning) warn(oppositeIconWarning);
|
|
1132
2736
|
return;
|
|
1133
2737
|
}
|
|
1134
|
-
await writePlannedFiles(
|
|
2738
|
+
await writePlannedFiles(plannedInstallFiles);
|
|
1135
2739
|
if (install) {
|
|
1136
2740
|
runInstall(loaded.projectRoot, missing);
|
|
1137
2741
|
}
|
|
1138
|
-
|
|
2742
|
+
printTitle(updateMode ? "Component updated" : "Component installed");
|
|
1139
2743
|
print(panel(payload.name, [
|
|
1140
2744
|
success(`${updateMode ? "Updated" : "Added"} ${payload.slug}`),
|
|
1141
2745
|
...plannedFiles.map((file) => success(file))
|
|
1142
2746
|
]));
|
|
2747
|
+
if (oppositeIconWarning) warn(oppositeIconWarning);
|
|
1143
2748
|
if (!install && missing.length > 0) {
|
|
1144
2749
|
warn(`Dependencies were not installed: ${missing.join(" ")}`);
|
|
1145
|
-
print(nextSteps([
|
|
2750
|
+
print(nextSteps([
|
|
2751
|
+
`Install skipped dependencies: npm install ${missing.join(" ")}`,
|
|
2752
|
+
"Run without --no-install next time to let Wensity install them automatically"
|
|
2753
|
+
]));
|
|
1146
2754
|
return;
|
|
1147
2755
|
}
|
|
1148
2756
|
print(nextSteps([
|
|
1149
|
-
"Import the
|
|
2757
|
+
"Import the installed file from your Wensity components folder",
|
|
1150
2758
|
resourceType === "block" ? "Customize the block copy, colors, and sections" : "Customize props and styling for your app"
|
|
1151
2759
|
]));
|
|
1152
2760
|
}
|
|
@@ -1172,12 +2780,7 @@ async function fetchComponentPayload(input) {
|
|
|
1172
2780
|
}
|
|
1173
2781
|
}
|
|
1174
2782
|
if (metadata.access === "pro") {
|
|
1175
|
-
throw new CliError(
|
|
1176
|
-
`${metadata.name} is a Pro component.`,
|
|
1177
|
-
`Create an API token from the Wensity dashboard, then run:`,
|
|
1178
|
-
` npx wensity login --token <token>`,
|
|
1179
|
-
`Upgrade: https://ui.wensity.com/pricing`
|
|
1180
|
-
].join("\n"));
|
|
2783
|
+
throw new CliError(formatProRequiredError(metadata.name, "component"));
|
|
1181
2784
|
}
|
|
1182
2785
|
const publicSourceEndpoint = metadata.endpoints?.registry ?? registrySlugUrl(input.registry, input.slug);
|
|
1183
2786
|
const endpoint = publicSourceEndpoint.startsWith("http") ? publicSourceEndpoint : joinUrl(registryOrigin(input.registry), publicSourceEndpoint);
|
|
@@ -1197,12 +2800,7 @@ async function fetchBlockPayload(input) {
|
|
|
1197
2800
|
`Found ${input.slug}`
|
|
1198
2801
|
);
|
|
1199
2802
|
if (!input.credentials) {
|
|
1200
|
-
throw new CliError(
|
|
1201
|
-
`${metadata.name} is a Pro block.`,
|
|
1202
|
-
`Create an API token from the Wensity dashboard, then run:`,
|
|
1203
|
-
` npx wensity login --token <token>`,
|
|
1204
|
-
`Upgrade: https://ui.wensity.com/pricing`
|
|
1205
|
-
].join("\n"));
|
|
2803
|
+
throw new CliError(formatProRequiredError(metadata.name, "block"));
|
|
1206
2804
|
}
|
|
1207
2805
|
const credentials = input.credentials;
|
|
1208
2806
|
const cliEndpoint = metadata.endpoints?.cli ?? `/api/cli/blocks/${input.slug}`;
|
|
@@ -1226,7 +2824,7 @@ function registryUrlForType(componentRegistry, resourceType, registryOverride) {
|
|
|
1226
2824
|
async function requireCredentials(env) {
|
|
1227
2825
|
const credentials = await readCredentials(env);
|
|
1228
2826
|
if (!credentials) {
|
|
1229
|
-
throw new CliError(
|
|
2827
|
+
throw new CliError(`Not logged in. ${brandRed("Run wensity login with an API token from the Wensity dashboard.")}`);
|
|
1230
2828
|
}
|
|
1231
2829
|
return credentials;
|
|
1232
2830
|
}
|
|
@@ -1251,7 +2849,7 @@ function formatHelp() {
|
|
|
1251
2849
|
nextSteps([
|
|
1252
2850
|
"npx wensity init",
|
|
1253
2851
|
"npx wensity list",
|
|
1254
|
-
"npx wensity add
|
|
2852
|
+
"npx wensity add <component-slug>"
|
|
1255
2853
|
])
|
|
1256
2854
|
].join("\n");
|
|
1257
2855
|
}
|
|
@@ -1269,8 +2867,8 @@ function formatAccountPanel(whoami, savedPath) {
|
|
|
1269
2867
|
return panel(isPro ? "Wensity Pro" : "Wensity Free", lines);
|
|
1270
2868
|
}
|
|
1271
2869
|
async function writeTextFile(path, content) {
|
|
1272
|
-
await
|
|
1273
|
-
await
|
|
2870
|
+
await mkdir5(dirname6(path), { recursive: true });
|
|
2871
|
+
await writeFile5(path, content, "utf8");
|
|
1274
2872
|
}
|
|
1275
2873
|
|
|
1276
2874
|
// src/index.ts
|