@svadmin/create 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/index.js +1397 -91
- package/package.json +4 -3
- package/scaffold-manifest.json +11 -11
package/dist/index.js
CHANGED
|
@@ -5023,10 +5023,10 @@ var require_picocolors = __commonJS(function(exports, module) {
|
|
|
5023
5023
|
});
|
|
5024
5024
|
|
|
5025
5025
|
// src/index.ts
|
|
5026
|
-
var
|
|
5027
|
-
var
|
|
5028
|
-
import
|
|
5029
|
-
import
|
|
5026
|
+
var import_prompts2 = __toESM(require_prompts3(), 1);
|
|
5027
|
+
var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
5028
|
+
import fs2 from "node:fs";
|
|
5029
|
+
import path2 from "node:path";
|
|
5030
5030
|
import { fileURLToPath } from "node:url";
|
|
5031
5031
|
import { createRequire as createRequire2 } from "node:module";
|
|
5032
5032
|
import { spawnSync } from "node:child_process";
|
|
@@ -5127,6 +5127,1310 @@ function createProjectPackageJson(scaffold, options) {
|
|
|
5127
5127
|
};
|
|
5128
5128
|
}
|
|
5129
5129
|
|
|
5130
|
+
// src/infer-command.ts
|
|
5131
|
+
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
5132
|
+
var import_prompts = __toESM(require_prompts3(), 1);
|
|
5133
|
+
import fs from "node:fs";
|
|
5134
|
+
import path from "node:path";
|
|
5135
|
+
|
|
5136
|
+
// ../core/src/inferencer-openapi.ts
|
|
5137
|
+
function isRef(obj) {
|
|
5138
|
+
return obj !== null && typeof obj === "object" && "$ref" in obj && typeof obj.$ref === "string";
|
|
5139
|
+
}
|
|
5140
|
+
function resolveRef(ref, root) {
|
|
5141
|
+
const parts = ref.replace("#/", "").split("/");
|
|
5142
|
+
let current = root;
|
|
5143
|
+
for (const part of parts) {
|
|
5144
|
+
if (current === null || typeof current !== "object")
|
|
5145
|
+
return null;
|
|
5146
|
+
current = current[part];
|
|
5147
|
+
if (!current)
|
|
5148
|
+
return null;
|
|
5149
|
+
}
|
|
5150
|
+
return current;
|
|
5151
|
+
}
|
|
5152
|
+
function resolveSchema(schemaOrRef, root) {
|
|
5153
|
+
if (isRef(schemaOrRef)) {
|
|
5154
|
+
return resolveRef(schemaOrRef.$ref, root);
|
|
5155
|
+
}
|
|
5156
|
+
return schemaOrRef;
|
|
5157
|
+
}
|
|
5158
|
+
function mapOpenAPITypeToFieldType(key, schema, root) {
|
|
5159
|
+
const { type, format } = schema;
|
|
5160
|
+
if (schema.enum && schema.enum.length > 0)
|
|
5161
|
+
return "select";
|
|
5162
|
+
if (type === "array") {
|
|
5163
|
+
if (schema.items) {
|
|
5164
|
+
const itemSchema = resolveSchema(schema.items, root);
|
|
5165
|
+
if (itemSchema?.type === "string") {
|
|
5166
|
+
const lk = key.toLowerCase();
|
|
5167
|
+
if (lk.includes("image") || lk.includes("photo"))
|
|
5168
|
+
return "images";
|
|
5169
|
+
return "tags";
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
5172
|
+
return "tags";
|
|
5173
|
+
}
|
|
5174
|
+
if (type === "object")
|
|
5175
|
+
return "json";
|
|
5176
|
+
if (type === "boolean")
|
|
5177
|
+
return "boolean";
|
|
5178
|
+
if (type === "number" || type === "integer")
|
|
5179
|
+
return "number";
|
|
5180
|
+
if (type === "string") {
|
|
5181
|
+
if (format === "date" || format === "date-time")
|
|
5182
|
+
return "date";
|
|
5183
|
+
if (format === "email")
|
|
5184
|
+
return "email";
|
|
5185
|
+
if (format === "uri" || format === "url")
|
|
5186
|
+
return "url";
|
|
5187
|
+
if (format === "binary" || format === "byte")
|
|
5188
|
+
return "image";
|
|
5189
|
+
const lk = key.toLowerCase();
|
|
5190
|
+
if (lk.includes("email"))
|
|
5191
|
+
return "email";
|
|
5192
|
+
if (lk.includes("phone") || lk.includes("tel"))
|
|
5193
|
+
return "phone";
|
|
5194
|
+
if (lk.includes("url") || lk.includes("link") || lk.includes("website"))
|
|
5195
|
+
return "url";
|
|
5196
|
+
if (lk.includes("avatar") || lk.includes("image") || lk.includes("photo") || lk.includes("logo"))
|
|
5197
|
+
return "image";
|
|
5198
|
+
if (lk.includes("color") || lk.includes("colour"))
|
|
5199
|
+
return "color";
|
|
5200
|
+
if (lk.includes("description") || lk.includes("content") || lk.includes("body") || lk.includes("bio"))
|
|
5201
|
+
return "textarea";
|
|
5202
|
+
if (lk === "created_at" || lk === "updated_at" || lk.endsWith("_at") || lk.endsWith("_date"))
|
|
5203
|
+
return "date";
|
|
5204
|
+
return "text";
|
|
5205
|
+
}
|
|
5206
|
+
return "text";
|
|
5207
|
+
}
|
|
5208
|
+
function inferFromOpenAPI(spec, options = {}) {
|
|
5209
|
+
const { primaryKey = "id", include, exclude } = options;
|
|
5210
|
+
const schemas = spec.components?.schemas ?? {};
|
|
5211
|
+
const resources = [];
|
|
5212
|
+
const resourcePaths = detectResourcePaths(spec);
|
|
5213
|
+
for (const [schemaName, schemaObj] of Object.entries(schemas)) {
|
|
5214
|
+
if (include && !include.includes(schemaName))
|
|
5215
|
+
continue;
|
|
5216
|
+
if (exclude && exclude.includes(schemaName))
|
|
5217
|
+
continue;
|
|
5218
|
+
const resolved = resolveSchema(schemaObj, spec);
|
|
5219
|
+
if (!resolved || resolved.type !== "object" || !resolved.properties)
|
|
5220
|
+
continue;
|
|
5221
|
+
const fields = [];
|
|
5222
|
+
const requiredFields = new Set(resolved.required ?? []);
|
|
5223
|
+
for (const [propName, propSchemaOrRef] of Object.entries(resolved.properties)) {
|
|
5224
|
+
const propSchema = resolveSchema(propSchemaOrRef, spec);
|
|
5225
|
+
if (!propSchema)
|
|
5226
|
+
continue;
|
|
5227
|
+
const fieldType = mapOpenAPITypeToFieldType(propName, propSchema, spec);
|
|
5228
|
+
let relatedResource = null;
|
|
5229
|
+
if (isRef(propSchemaOrRef)) {
|
|
5230
|
+
const refName = propSchemaOrRef.$ref.split("/").pop() ?? "";
|
|
5231
|
+
relatedResource = refName.toLowerCase() + "s";
|
|
5232
|
+
} else if (propName.endsWith("_id") || propName.endsWith("Id")) {
|
|
5233
|
+
const base = propName.replace(/_id$|Id$/, "");
|
|
5234
|
+
relatedResource = base.endsWith("s") ? base : base + "s";
|
|
5235
|
+
}
|
|
5236
|
+
const field = {
|
|
5237
|
+
key: propName,
|
|
5238
|
+
label: humanize(propName),
|
|
5239
|
+
type: relatedResource ? "relation" : fieldType,
|
|
5240
|
+
required: requiredFields.has(propName),
|
|
5241
|
+
sortable: ["text", "number", "date"].includes(fieldType),
|
|
5242
|
+
searchable: ["text", "email"].includes(fieldType),
|
|
5243
|
+
showInList: propName !== primaryKey && fieldType !== "textarea" && fieldType !== "json",
|
|
5244
|
+
showInForm: propName !== primaryKey
|
|
5245
|
+
};
|
|
5246
|
+
if (relatedResource) {
|
|
5247
|
+
field.resource = relatedResource;
|
|
5248
|
+
field.optionLabel = "name";
|
|
5249
|
+
field.optionValue = "id";
|
|
5250
|
+
}
|
|
5251
|
+
if (propSchema.enum) {
|
|
5252
|
+
field.options = propSchema.enum.map((v) => ({
|
|
5253
|
+
label: String(v).charAt(0).toUpperCase() + String(v).slice(1),
|
|
5254
|
+
value: v
|
|
5255
|
+
}));
|
|
5256
|
+
}
|
|
5257
|
+
fields.push(field);
|
|
5258
|
+
}
|
|
5259
|
+
fields.sort((a, b) => {
|
|
5260
|
+
if (a.key === primaryKey)
|
|
5261
|
+
return -1;
|
|
5262
|
+
if (b.key === primaryKey)
|
|
5263
|
+
return 1;
|
|
5264
|
+
return 0;
|
|
5265
|
+
});
|
|
5266
|
+
const baseName = schemaName.toLowerCase();
|
|
5267
|
+
const resourceName = baseName.endsWith("s") ? baseName : baseName + "s";
|
|
5268
|
+
const pathInfo = resourcePaths.get(baseName) ?? resourcePaths.get(resourceName);
|
|
5269
|
+
resources.push({
|
|
5270
|
+
name: resourceName,
|
|
5271
|
+
label: schemaName,
|
|
5272
|
+
primaryKey,
|
|
5273
|
+
fields,
|
|
5274
|
+
canCreate: pathInfo?.hasPost ?? true,
|
|
5275
|
+
canEdit: pathInfo?.hasPut ?? true,
|
|
5276
|
+
canDelete: pathInfo?.hasDelete ?? true,
|
|
5277
|
+
canShow: pathInfo?.hasGet ?? true
|
|
5278
|
+
});
|
|
5279
|
+
}
|
|
5280
|
+
return resources;
|
|
5281
|
+
}
|
|
5282
|
+
function detectResourcePaths(spec) {
|
|
5283
|
+
const result = new Map;
|
|
5284
|
+
if (!spec.paths)
|
|
5285
|
+
return result;
|
|
5286
|
+
for (const [path, methods] of Object.entries(spec.paths)) {
|
|
5287
|
+
const segments = path.split("/").filter(Boolean);
|
|
5288
|
+
const resourceSegment = segments.find((s) => !s.startsWith("{") && s !== "api" && s !== "v1" && s !== "v2");
|
|
5289
|
+
if (!resourceSegment)
|
|
5290
|
+
continue;
|
|
5291
|
+
const name = resourceSegment.toLowerCase();
|
|
5292
|
+
const existing = result.get(name) ?? { hasGet: false, hasPost: false, hasPut: false, hasDelete: false };
|
|
5293
|
+
if (methods.get)
|
|
5294
|
+
existing.hasGet = true;
|
|
5295
|
+
if (methods.post)
|
|
5296
|
+
existing.hasPost = true;
|
|
5297
|
+
if (methods.put || methods.patch)
|
|
5298
|
+
existing.hasPut = true;
|
|
5299
|
+
if (methods.delete)
|
|
5300
|
+
existing.hasDelete = true;
|
|
5301
|
+
result.set(name, existing);
|
|
5302
|
+
}
|
|
5303
|
+
return result;
|
|
5304
|
+
}
|
|
5305
|
+
function humanize(key) {
|
|
5306
|
+
return key.replace(/_/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
5307
|
+
}
|
|
5308
|
+
// ../core/src/inferencer-graphql.ts
|
|
5309
|
+
var GRAPHQL_INTROSPECTION_QUERY = `query IntrospectionQuery {
|
|
5310
|
+
__schema {
|
|
5311
|
+
queryType { name }
|
|
5312
|
+
mutationType { name }
|
|
5313
|
+
subscriptionType { name }
|
|
5314
|
+
types {
|
|
5315
|
+
kind
|
|
5316
|
+
name
|
|
5317
|
+
description
|
|
5318
|
+
fields(includeDeprecated: true) {
|
|
5319
|
+
name
|
|
5320
|
+
description
|
|
5321
|
+
type {
|
|
5322
|
+
kind
|
|
5323
|
+
name
|
|
5324
|
+
ofType {
|
|
5325
|
+
kind
|
|
5326
|
+
name
|
|
5327
|
+
ofType {
|
|
5328
|
+
kind
|
|
5329
|
+
name
|
|
5330
|
+
ofType {
|
|
5331
|
+
kind
|
|
5332
|
+
name
|
|
5333
|
+
ofType {
|
|
5334
|
+
kind
|
|
5335
|
+
name
|
|
5336
|
+
}
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5339
|
+
}
|
|
5340
|
+
}
|
|
5341
|
+
}
|
|
5342
|
+
enumValues(includeDeprecated: true) {
|
|
5343
|
+
name
|
|
5344
|
+
description
|
|
5345
|
+
}
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5348
|
+
}`;
|
|
5349
|
+
function unwrapType(typeRef) {
|
|
5350
|
+
let current = typeRef;
|
|
5351
|
+
let isRequired = false;
|
|
5352
|
+
let isList = false;
|
|
5353
|
+
if (current.kind === "NON_NULL") {
|
|
5354
|
+
isRequired = true;
|
|
5355
|
+
if (current.ofType)
|
|
5356
|
+
current = current.ofType;
|
|
5357
|
+
}
|
|
5358
|
+
if (current.kind === "LIST") {
|
|
5359
|
+
isList = true;
|
|
5360
|
+
if (current.ofType) {
|
|
5361
|
+
current = current.ofType;
|
|
5362
|
+
if (current.kind === "NON_NULL") {
|
|
5363
|
+
if (current.ofType)
|
|
5364
|
+
current = current.ofType;
|
|
5365
|
+
}
|
|
5366
|
+
}
|
|
5367
|
+
}
|
|
5368
|
+
return {
|
|
5369
|
+
baseName: current.name ?? "String",
|
|
5370
|
+
kind: current.kind,
|
|
5371
|
+
isList,
|
|
5372
|
+
isRequired
|
|
5373
|
+
};
|
|
5374
|
+
}
|
|
5375
|
+
function mapGraphQLTypeToFieldType(key, typeInfo, enumMap) {
|
|
5376
|
+
const { baseName, isList } = typeInfo;
|
|
5377
|
+
const lk = key.toLowerCase();
|
|
5378
|
+
if (enumMap.has(baseName))
|
|
5379
|
+
return "select";
|
|
5380
|
+
if (isList) {
|
|
5381
|
+
if (lk.includes("image") || lk.includes("photo") || lk.includes("avatar") || lk.includes("picture")) {
|
|
5382
|
+
return "images";
|
|
5383
|
+
}
|
|
5384
|
+
return "tags";
|
|
5385
|
+
}
|
|
5386
|
+
switch (baseName) {
|
|
5387
|
+
case "Int":
|
|
5388
|
+
case "Float":
|
|
5389
|
+
case "Decimal":
|
|
5390
|
+
case "BigInt":
|
|
5391
|
+
return "number";
|
|
5392
|
+
case "Boolean":
|
|
5393
|
+
return "boolean";
|
|
5394
|
+
case "Date":
|
|
5395
|
+
case "DateTime":
|
|
5396
|
+
case "Time":
|
|
5397
|
+
case "Timestamp":
|
|
5398
|
+
return "date";
|
|
5399
|
+
case "JSON":
|
|
5400
|
+
case "JsonObject":
|
|
5401
|
+
case "JSONObject":
|
|
5402
|
+
case "Json":
|
|
5403
|
+
return "json";
|
|
5404
|
+
case "ID":
|
|
5405
|
+
return "text";
|
|
5406
|
+
case "String":
|
|
5407
|
+
default:
|
|
5408
|
+
break;
|
|
5409
|
+
}
|
|
5410
|
+
if (lk.includes("email"))
|
|
5411
|
+
return "email";
|
|
5412
|
+
if (lk.includes("phone") || lk.includes("tel") || lk.includes("mobile"))
|
|
5413
|
+
return "phone";
|
|
5414
|
+
if (lk.includes("url") || lk.includes("link") || lk.includes("website"))
|
|
5415
|
+
return "url";
|
|
5416
|
+
if (lk.includes("avatar") || lk.includes("image") || lk.includes("photo") || lk.includes("thumbnail") || lk.includes("logo"))
|
|
5417
|
+
return "image";
|
|
5418
|
+
if (lk.includes("color") || lk.includes("colour"))
|
|
5419
|
+
return "color";
|
|
5420
|
+
if (lk.includes("description") || lk.includes("content") || lk.includes("body") || lk.includes("bio") || lk.includes("summary"))
|
|
5421
|
+
return "textarea";
|
|
5422
|
+
if (lk === "created_at" || lk === "updated_at" || lk.endsWith("_at") || lk.endsWith("_date") || /\bdate\b/.test(lk))
|
|
5423
|
+
return "date";
|
|
5424
|
+
return "text";
|
|
5425
|
+
}
|
|
5426
|
+
function parseGraphQLSDL(sdl) {
|
|
5427
|
+
const types = [];
|
|
5428
|
+
const queryType = { name: "Query" };
|
|
5429
|
+
const mutationType = { name: "Mutation" };
|
|
5430
|
+
const cleanSDL = sdl.replace(/#[^\n\r]*/g, "");
|
|
5431
|
+
const enumRegex = /enum\s+([A-Za-z0-9_]+)\s*\{([^}]+)\}/g;
|
|
5432
|
+
let match;
|
|
5433
|
+
while ((match = enumRegex.exec(cleanSDL)) !== null) {
|
|
5434
|
+
const enumName = match[1];
|
|
5435
|
+
const rawValues = match[2].split(/\s+/).filter(Boolean);
|
|
5436
|
+
types.push({
|
|
5437
|
+
kind: "ENUM",
|
|
5438
|
+
name: enumName,
|
|
5439
|
+
enumValues: rawValues.map((v) => ({ name: v }))
|
|
5440
|
+
});
|
|
5441
|
+
}
|
|
5442
|
+
const typeRegex = /type\s+([A-Za-z0-9_]+)\s*(?:implements\s+[A-Za-z0-9_&,\s]+)?\s*\{([^}]+)\}/g;
|
|
5443
|
+
while ((match = typeRegex.exec(cleanSDL)) !== null) {
|
|
5444
|
+
const typeName = match[1];
|
|
5445
|
+
const fieldsBody = match[2];
|
|
5446
|
+
const fieldLines = fieldsBody.split(`
|
|
5447
|
+
`).map((l) => l.trim()).filter(Boolean);
|
|
5448
|
+
const fields = [];
|
|
5449
|
+
for (const line of fieldLines) {
|
|
5450
|
+
const fieldMatch = /^([A-Za-z0-9_]+)(?:\([^)]*\))?\s*:\s*([[\]A-Za-z0-9_!]+)/.exec(line);
|
|
5451
|
+
if (!fieldMatch)
|
|
5452
|
+
continue;
|
|
5453
|
+
const fName = fieldMatch[1];
|
|
5454
|
+
const rawType = fieldMatch[2];
|
|
5455
|
+
const isNonNullable = rawType.endsWith("!");
|
|
5456
|
+
const cleanType = isNonNullable ? rawType.slice(0, -1) : rawType;
|
|
5457
|
+
const isList = cleanType.startsWith("[") && cleanType.endsWith("]");
|
|
5458
|
+
const innerType = isList ? cleanType.slice(1, -1).replace(/!$/, "") : cleanType;
|
|
5459
|
+
let typeRef = {
|
|
5460
|
+
kind: "SCALAR",
|
|
5461
|
+
name: innerType
|
|
5462
|
+
};
|
|
5463
|
+
if (isList) {
|
|
5464
|
+
typeRef = {
|
|
5465
|
+
kind: "LIST",
|
|
5466
|
+
ofType: typeRef
|
|
5467
|
+
};
|
|
5468
|
+
}
|
|
5469
|
+
if (isNonNullable) {
|
|
5470
|
+
typeRef = {
|
|
5471
|
+
kind: "NON_NULL",
|
|
5472
|
+
ofType: typeRef
|
|
5473
|
+
};
|
|
5474
|
+
}
|
|
5475
|
+
fields.push({
|
|
5476
|
+
name: fName,
|
|
5477
|
+
type: typeRef
|
|
5478
|
+
});
|
|
5479
|
+
}
|
|
5480
|
+
types.push({
|
|
5481
|
+
kind: "OBJECT",
|
|
5482
|
+
name: typeName,
|
|
5483
|
+
fields
|
|
5484
|
+
});
|
|
5485
|
+
}
|
|
5486
|
+
return {
|
|
5487
|
+
queryType,
|
|
5488
|
+
mutationType,
|
|
5489
|
+
types
|
|
5490
|
+
};
|
|
5491
|
+
}
|
|
5492
|
+
function inferFromGraphQL(schemaOrIntrospection, options = {}) {
|
|
5493
|
+
const { primaryKey = "id", include, exclude } = options;
|
|
5494
|
+
let schemaBody;
|
|
5495
|
+
if (typeof schemaOrIntrospection === "string") {
|
|
5496
|
+
schemaBody = parseGraphQLSDL(schemaOrIntrospection);
|
|
5497
|
+
} else if (schemaOrIntrospection && typeof schemaOrIntrospection === "object" && "data" in schemaOrIntrospection && schemaOrIntrospection.data?.__schema) {
|
|
5498
|
+
schemaBody = schemaOrIntrospection.data.__schema;
|
|
5499
|
+
} else if (schemaOrIntrospection && typeof schemaOrIntrospection === "object" && "__schema" in schemaOrIntrospection && schemaOrIntrospection.__schema) {
|
|
5500
|
+
schemaBody = schemaOrIntrospection.__schema;
|
|
5501
|
+
} else if (schemaOrIntrospection && typeof schemaOrIntrospection === "object" && "types" in schemaOrIntrospection && Array.isArray(schemaOrIntrospection.types)) {
|
|
5502
|
+
schemaBody = schemaOrIntrospection;
|
|
5503
|
+
} else {
|
|
5504
|
+
return [];
|
|
5505
|
+
}
|
|
5506
|
+
const types = schemaBody.types ?? [];
|
|
5507
|
+
const queryTypeName = schemaBody.queryType?.name ?? "Query";
|
|
5508
|
+
const mutationTypeName = schemaBody.mutationType?.name ?? "Mutation";
|
|
5509
|
+
const subscriptionTypeName = schemaBody.subscriptionType?.name ?? "Subscription";
|
|
5510
|
+
const enumMap = new Map;
|
|
5511
|
+
for (const t of types) {
|
|
5512
|
+
if (t.kind === "ENUM" && t.name) {
|
|
5513
|
+
const values = (t.enumValues ?? []).map((ev) => ev.name);
|
|
5514
|
+
enumMap.set(t.name, values);
|
|
5515
|
+
}
|
|
5516
|
+
}
|
|
5517
|
+
const objectTypeMap = new Map;
|
|
5518
|
+
for (const t of types) {
|
|
5519
|
+
if (t.kind === "OBJECT" && t.name) {
|
|
5520
|
+
objectTypeMap.set(t.name, t);
|
|
5521
|
+
}
|
|
5522
|
+
}
|
|
5523
|
+
const queryFields = new Set;
|
|
5524
|
+
const queryType = objectTypeMap.get(queryTypeName);
|
|
5525
|
+
if (queryType?.fields) {
|
|
5526
|
+
for (const f of queryType.fields)
|
|
5527
|
+
queryFields.add(f.name.toLowerCase());
|
|
5528
|
+
}
|
|
5529
|
+
const mutationFields = new Set;
|
|
5530
|
+
const mutationType = objectTypeMap.get(mutationTypeName);
|
|
5531
|
+
if (mutationType?.fields) {
|
|
5532
|
+
for (const f of mutationType.fields)
|
|
5533
|
+
mutationFields.add(f.name.toLowerCase());
|
|
5534
|
+
}
|
|
5535
|
+
const resources = [];
|
|
5536
|
+
for (const typeObj of types) {
|
|
5537
|
+
const typeName = typeObj.name;
|
|
5538
|
+
if (!typeName)
|
|
5539
|
+
continue;
|
|
5540
|
+
if (typeName.startsWith("__") || typeName === queryTypeName || typeName === mutationTypeName || typeName === subscriptionTypeName || typeObj.kind !== "OBJECT" || !typeObj.fields || typeObj.fields.length === 0) {
|
|
5541
|
+
continue;
|
|
5542
|
+
}
|
|
5543
|
+
if (include && !include.includes(typeName))
|
|
5544
|
+
continue;
|
|
5545
|
+
if (exclude && exclude.includes(typeName))
|
|
5546
|
+
continue;
|
|
5547
|
+
const baseName = typeName.toLowerCase();
|
|
5548
|
+
const resourceName = baseName.endsWith("s") ? baseName : baseName + "s";
|
|
5549
|
+
const fields = [];
|
|
5550
|
+
for (const fieldDesc of typeObj.fields) {
|
|
5551
|
+
const fieldName = fieldDesc.name;
|
|
5552
|
+
const typeInfo = unwrapType(fieldDesc.type);
|
|
5553
|
+
let fieldType = mapGraphQLTypeToFieldType(fieldName, typeInfo, enumMap);
|
|
5554
|
+
let relatedResource = null;
|
|
5555
|
+
if (objectTypeMap.has(typeInfo.baseName) && typeInfo.baseName !== typeName) {
|
|
5556
|
+
const target = typeInfo.baseName.toLowerCase();
|
|
5557
|
+
relatedResource = target.endsWith("s") ? target : target + "s";
|
|
5558
|
+
fieldType = "relation";
|
|
5559
|
+
} else if (fieldName.endsWith("_id") || fieldName.endsWith("Id")) {
|
|
5560
|
+
const base = fieldName.replace(/_id$|Id$/, "");
|
|
5561
|
+
relatedResource = base.endsWith("s") ? base : base + "s";
|
|
5562
|
+
fieldType = "relation";
|
|
5563
|
+
}
|
|
5564
|
+
const field = {
|
|
5565
|
+
key: fieldName,
|
|
5566
|
+
label: humanize2(fieldName),
|
|
5567
|
+
type: relatedResource ? "relation" : fieldType,
|
|
5568
|
+
required: typeInfo.isRequired,
|
|
5569
|
+
sortable: ["text", "number", "date"].includes(fieldType),
|
|
5570
|
+
searchable: ["text", "email"].includes(fieldType),
|
|
5571
|
+
showInList: fieldName !== primaryKey && fieldType !== "textarea" && fieldType !== "json",
|
|
5572
|
+
showInForm: fieldName !== primaryKey
|
|
5573
|
+
};
|
|
5574
|
+
if (relatedResource) {
|
|
5575
|
+
field.resource = relatedResource;
|
|
5576
|
+
field.optionLabel = "name";
|
|
5577
|
+
field.optionValue = "id";
|
|
5578
|
+
}
|
|
5579
|
+
if (enumMap.has(typeInfo.baseName)) {
|
|
5580
|
+
const enumValues = enumMap.get(typeInfo.baseName) ?? [];
|
|
5581
|
+
field.options = enumValues.map((val) => ({
|
|
5582
|
+
label: humanize2(val),
|
|
5583
|
+
value: val
|
|
5584
|
+
}));
|
|
5585
|
+
}
|
|
5586
|
+
fields.push(field);
|
|
5587
|
+
}
|
|
5588
|
+
fields.sort((a, b) => {
|
|
5589
|
+
if (a.key === primaryKey)
|
|
5590
|
+
return -1;
|
|
5591
|
+
if (b.key === primaryKey)
|
|
5592
|
+
return 1;
|
|
5593
|
+
return a.key.localeCompare(b.key);
|
|
5594
|
+
});
|
|
5595
|
+
const canShow = queryFields.size === 0 || queryFields.has(baseName) || queryFields.has(resourceName) || queryFields.has(`get${baseName}`) || queryFields.has(`${baseName}byid`) || queryFields.has(`find${baseName}`);
|
|
5596
|
+
const canCreate = mutationFields.size === 0 || mutationFields.has(`create${baseName}`) || mutationFields.has(`insert${baseName}`) || mutationFields.has(`add${baseName}`) || mutationFields.has(`insert_${resourceName}`) || mutationFields.has(`create_${baseName}`);
|
|
5597
|
+
const canEdit = mutationFields.size === 0 || mutationFields.has(`update${baseName}`) || mutationFields.has(`edit${baseName}`) || mutationFields.has(`update_${resourceName}`) || mutationFields.has(`update_${baseName}`) || mutationFields.has(`save${baseName}`);
|
|
5598
|
+
const canDelete = mutationFields.size === 0 || mutationFields.has(`delete${baseName}`) || mutationFields.has(`remove${baseName}`) || mutationFields.has(`delete_${resourceName}`) || mutationFields.has(`delete_${baseName}`);
|
|
5599
|
+
resources.push({
|
|
5600
|
+
name: resourceName,
|
|
5601
|
+
label: typeName,
|
|
5602
|
+
primaryKey,
|
|
5603
|
+
fields,
|
|
5604
|
+
canCreate,
|
|
5605
|
+
canEdit,
|
|
5606
|
+
canDelete,
|
|
5607
|
+
canShow
|
|
5608
|
+
});
|
|
5609
|
+
}
|
|
5610
|
+
return resources;
|
|
5611
|
+
}
|
|
5612
|
+
function humanize2(key) {
|
|
5613
|
+
return key.replace(/_/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
5614
|
+
}
|
|
5615
|
+
|
|
5616
|
+
// ../core/src/inferencer.ts
|
|
5617
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5618
|
+
var URL_RE = /^https?:\/\//;
|
|
5619
|
+
var IMAGE_RE = /\.(png|jpe?g|gif|svg|webp|avif|ico)(\?.*)?$/i;
|
|
5620
|
+
var PHONE_RE = /^\+?[\d\s\-()]{7,}$/;
|
|
5621
|
+
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2})?/;
|
|
5622
|
+
var COLOR_RE = /^#([0-9a-fA-F]{3,8})$/;
|
|
5623
|
+
function inferFieldType(key, value) {
|
|
5624
|
+
if (value === null || value === undefined)
|
|
5625
|
+
return "text";
|
|
5626
|
+
if (typeof value === "boolean")
|
|
5627
|
+
return "boolean";
|
|
5628
|
+
if (typeof value === "number")
|
|
5629
|
+
return "number";
|
|
5630
|
+
if (Array.isArray(value)) {
|
|
5631
|
+
if (value.length > 0 && typeof value[0] === "string") {
|
|
5632
|
+
if (value.every((v) => typeof v === "string" && IMAGE_RE.test(v)))
|
|
5633
|
+
return "images";
|
|
5634
|
+
return "tags";
|
|
5635
|
+
}
|
|
5636
|
+
if (value.length > 0 && typeof value[0] === "number")
|
|
5637
|
+
return "tags";
|
|
5638
|
+
return "json";
|
|
5639
|
+
}
|
|
5640
|
+
if (typeof value === "object")
|
|
5641
|
+
return "json";
|
|
5642
|
+
if (typeof value === "string") {
|
|
5643
|
+
if (COLOR_RE.test(value))
|
|
5644
|
+
return "color";
|
|
5645
|
+
if (EMAIL_RE.test(value))
|
|
5646
|
+
return "email";
|
|
5647
|
+
if (IMAGE_RE.test(value))
|
|
5648
|
+
return "image";
|
|
5649
|
+
if (URL_RE.test(value))
|
|
5650
|
+
return "url";
|
|
5651
|
+
if (PHONE_RE.test(value))
|
|
5652
|
+
return "phone";
|
|
5653
|
+
if (ISO_DATE_RE.test(value))
|
|
5654
|
+
return "date";
|
|
5655
|
+
if (value.length > 200)
|
|
5656
|
+
return "textarea";
|
|
5657
|
+
const lk = key.toLowerCase();
|
|
5658
|
+
if (lk.includes("email"))
|
|
5659
|
+
return "email";
|
|
5660
|
+
if (lk.includes("phone") || lk.includes("tel") || lk.includes("mobile"))
|
|
5661
|
+
return "phone";
|
|
5662
|
+
if (lk.includes("url") || lk.includes("link") || lk.includes("website"))
|
|
5663
|
+
return "url";
|
|
5664
|
+
if (lk.includes("avatar") || lk.includes("image") || lk.includes("photo") || lk.includes("thumbnail") || lk.includes("logo"))
|
|
5665
|
+
return "image";
|
|
5666
|
+
if (lk.includes("color") || lk.includes("colour"))
|
|
5667
|
+
return "color";
|
|
5668
|
+
if (lk.includes("description") || lk.includes("content") || lk.includes("body") || lk.includes("bio") || lk.includes("summary"))
|
|
5669
|
+
return "textarea";
|
|
5670
|
+
if (lk === "created_at" || lk === "updated_at" || lk.endsWith("_at") || lk.endsWith("_date") || /\bdate\b/.test(lk))
|
|
5671
|
+
return "date";
|
|
5672
|
+
return "text";
|
|
5673
|
+
}
|
|
5674
|
+
return "text";
|
|
5675
|
+
}
|
|
5676
|
+
var RELATION_SUFFIXES = ["_id", "Id", "_ID"];
|
|
5677
|
+
function isLikelyRelation(key) {
|
|
5678
|
+
for (const suffix of RELATION_SUFFIXES) {
|
|
5679
|
+
if (key.endsWith(suffix)) {
|
|
5680
|
+
const resource = key.slice(0, -suffix.length);
|
|
5681
|
+
return resource.endsWith("s") ? resource : resource + "s";
|
|
5682
|
+
}
|
|
5683
|
+
}
|
|
5684
|
+
return null;
|
|
5685
|
+
}
|
|
5686
|
+
function inferResource(resourceName, sampleData, options = {}) {
|
|
5687
|
+
const primaryKey = options.primaryKey ?? "id";
|
|
5688
|
+
const label = options.label ?? capitalize(resourceName);
|
|
5689
|
+
if (!sampleData.length) {
|
|
5690
|
+
const emptyResource = { name: resourceName, label, fields: [], primaryKey };
|
|
5691
|
+
return {
|
|
5692
|
+
fields: [],
|
|
5693
|
+
resource: emptyResource,
|
|
5694
|
+
code: `// No data available to infer fields for "${resourceName}".`,
|
|
5695
|
+
typeboxCode: generateTypeBoxSchemaCode(emptyResource),
|
|
5696
|
+
componentCode: {
|
|
5697
|
+
list: generateListPageCode(emptyResource),
|
|
5698
|
+
create: generateCreatePageCode(emptyResource),
|
|
5699
|
+
edit: generateEditPageCode(emptyResource),
|
|
5700
|
+
show: generateShowPageCode(emptyResource)
|
|
5701
|
+
}
|
|
5702
|
+
};
|
|
5703
|
+
}
|
|
5704
|
+
const keySet = new Set;
|
|
5705
|
+
for (const row of sampleData) {
|
|
5706
|
+
for (const k of Object.keys(row))
|
|
5707
|
+
keySet.add(k);
|
|
5708
|
+
}
|
|
5709
|
+
const fields = [];
|
|
5710
|
+
for (const key of keySet) {
|
|
5711
|
+
const values = sampleData.map((r) => r[key]).filter((v) => v !== null && v !== undefined);
|
|
5712
|
+
const sampleValue = values[0];
|
|
5713
|
+
let inferredType = inferFieldType(key, sampleValue);
|
|
5714
|
+
const typeCounts = new Map;
|
|
5715
|
+
for (const v of values) {
|
|
5716
|
+
const t = inferFieldType(key, v);
|
|
5717
|
+
typeCounts.set(t, (typeCounts.get(t) ?? 0) + 1);
|
|
5718
|
+
}
|
|
5719
|
+
let maxCount = 0;
|
|
5720
|
+
for (const [t, count] of typeCounts) {
|
|
5721
|
+
if (count > maxCount) {
|
|
5722
|
+
maxCount = count;
|
|
5723
|
+
inferredType = t;
|
|
5724
|
+
}
|
|
5725
|
+
}
|
|
5726
|
+
const relatedResource = isLikelyRelation(key);
|
|
5727
|
+
const uniqueStrings = new Set(values.filter((v) => typeof v === "string"));
|
|
5728
|
+
const isSelect = inferredType === "text" && uniqueStrings.size > 1 && uniqueStrings.size <= 10 && values.length >= 5;
|
|
5729
|
+
const field = {
|
|
5730
|
+
key,
|
|
5731
|
+
label: humanize3(key),
|
|
5732
|
+
type: relatedResource ? "relation" : isSelect ? "select" : inferredType,
|
|
5733
|
+
sortable: inferredType === "text" || inferredType === "number" || inferredType === "date",
|
|
5734
|
+
searchable: inferredType === "text" || inferredType === "email",
|
|
5735
|
+
showInList: key !== primaryKey && inferredType !== "textarea" && inferredType !== "json" && inferredType !== "richtext",
|
|
5736
|
+
showInForm: key !== primaryKey
|
|
5737
|
+
};
|
|
5738
|
+
if (relatedResource) {
|
|
5739
|
+
field.resource = relatedResource;
|
|
5740
|
+
field.optionLabel = "name";
|
|
5741
|
+
field.optionValue = "id";
|
|
5742
|
+
}
|
|
5743
|
+
if (isSelect) {
|
|
5744
|
+
field.options = [...uniqueStrings].map((v) => ({ label: capitalize(v), value: v }));
|
|
5745
|
+
}
|
|
5746
|
+
fields.push(field);
|
|
5747
|
+
}
|
|
5748
|
+
fields.sort((a, b) => {
|
|
5749
|
+
if (a.key === primaryKey)
|
|
5750
|
+
return -1;
|
|
5751
|
+
if (b.key === primaryKey)
|
|
5752
|
+
return 1;
|
|
5753
|
+
return a.key.localeCompare(b.key);
|
|
5754
|
+
});
|
|
5755
|
+
const resource = {
|
|
5756
|
+
name: resourceName,
|
|
5757
|
+
label,
|
|
5758
|
+
primaryKey,
|
|
5759
|
+
fields,
|
|
5760
|
+
canCreate: true,
|
|
5761
|
+
canEdit: true,
|
|
5762
|
+
canDelete: true,
|
|
5763
|
+
canShow: true
|
|
5764
|
+
};
|
|
5765
|
+
const code = generateCode(resource);
|
|
5766
|
+
const typeboxCode = generateTypeBoxSchemaCode(resource);
|
|
5767
|
+
const componentCode = {
|
|
5768
|
+
list: generateListPageCode(resource),
|
|
5769
|
+
create: generateCreatePageCode(resource),
|
|
5770
|
+
edit: generateEditPageCode(resource),
|
|
5771
|
+
show: generateShowPageCode(resource)
|
|
5772
|
+
};
|
|
5773
|
+
return { fields, resource, code, typeboxCode, componentCode };
|
|
5774
|
+
}
|
|
5775
|
+
function generateCode(resource) {
|
|
5776
|
+
const esc = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
5777
|
+
const fieldsCode = resource.fields.map((f) => {
|
|
5778
|
+
const lines = [
|
|
5779
|
+
` { key: '${esc(f.key)}', label: '${esc(f.label)}', type: '${f.type}'`
|
|
5780
|
+
];
|
|
5781
|
+
if (f.sortable)
|
|
5782
|
+
lines.push(` sortable: true`);
|
|
5783
|
+
if (f.searchable)
|
|
5784
|
+
lines.push(` searchable: true`);
|
|
5785
|
+
if (f.showInList === false)
|
|
5786
|
+
lines.push(` showInList: false`);
|
|
5787
|
+
if (f.showInForm === false)
|
|
5788
|
+
lines.push(` showInForm: false`);
|
|
5789
|
+
if (f.resource) {
|
|
5790
|
+
lines.push(` resource: '${f.resource}'`);
|
|
5791
|
+
lines.push(` optionLabel: '${f.optionLabel}'`);
|
|
5792
|
+
lines.push(` optionValue: '${f.optionValue}'`);
|
|
5793
|
+
}
|
|
5794
|
+
if (f.options) {
|
|
5795
|
+
lines.push(` options: ${JSON.stringify(f.options)}`);
|
|
5796
|
+
}
|
|
5797
|
+
return lines.join(`,
|
|
5798
|
+
`) + " }";
|
|
5799
|
+
}).join(`,
|
|
5800
|
+
`);
|
|
5801
|
+
return `import type { ResourceDefinition } from '@svadmin/core';
|
|
5802
|
+
|
|
5803
|
+
export const ${resource.name}Resource: ResourceDefinition = {
|
|
5804
|
+
name: '${resource.name}',
|
|
5805
|
+
label: '${resource.label}',
|
|
5806
|
+
primaryKey: '${resource.primaryKey ?? "id"}',
|
|
5807
|
+
canCreate: true,
|
|
5808
|
+
canEdit: true,
|
|
5809
|
+
canDelete: true,
|
|
5810
|
+
canShow: true,
|
|
5811
|
+
fields: [
|
|
5812
|
+
${fieldsCode}
|
|
5813
|
+
],
|
|
5814
|
+
};
|
|
5815
|
+
`;
|
|
5816
|
+
}
|
|
5817
|
+
function generateListPageCode(resource) {
|
|
5818
|
+
return `<script lang="ts">
|
|
5819
|
+
import { ListPage, AutoTable } from '@svadmin/ui';
|
|
5820
|
+
</script>
|
|
5821
|
+
|
|
5822
|
+
<ListPage resourceName="${resource.name}">
|
|
5823
|
+
<AutoTable resourceName="${resource.name}" />
|
|
5824
|
+
</ListPage>
|
|
5825
|
+
`;
|
|
5826
|
+
}
|
|
5827
|
+
function generateCreatePageCode(resource) {
|
|
5828
|
+
return `<script lang="ts">
|
|
5829
|
+
import { CreatePage, AutoForm } from '@svadmin/ui';
|
|
5830
|
+
</script>
|
|
5831
|
+
|
|
5832
|
+
<CreatePage resourceName="${resource.name}">
|
|
5833
|
+
<AutoForm resourceName="${resource.name}" mode="create" />
|
|
5834
|
+
</CreatePage>
|
|
5835
|
+
`;
|
|
5836
|
+
}
|
|
5837
|
+
function generateEditPageCode(resource) {
|
|
5838
|
+
return `<script lang="ts">
|
|
5839
|
+
import { EditPage, AutoForm } from '@svadmin/ui';
|
|
5840
|
+
|
|
5841
|
+
interface Props {
|
|
5842
|
+
id?: string | number;
|
|
5843
|
+
}
|
|
5844
|
+
|
|
5845
|
+
let { id }: Props = $props();
|
|
5846
|
+
</script>
|
|
5847
|
+
|
|
5848
|
+
<EditPage resourceName="${resource.name}" {id}>
|
|
5849
|
+
<AutoForm resourceName="${resource.name}" {id} mode="edit" />
|
|
5850
|
+
</EditPage>
|
|
5851
|
+
`;
|
|
5852
|
+
}
|
|
5853
|
+
function generateShowPageCode(resource) {
|
|
5854
|
+
return `<script lang="ts">
|
|
5855
|
+
import { ShowPage, AutoForm } from '@svadmin/ui';
|
|
5856
|
+
|
|
5857
|
+
interface Props {
|
|
5858
|
+
id?: string | number;
|
|
5859
|
+
}
|
|
5860
|
+
|
|
5861
|
+
let { id }: Props = $props();
|
|
5862
|
+
</script>
|
|
5863
|
+
|
|
5864
|
+
<ShowPage resourceName="${resource.name}" {id}>
|
|
5865
|
+
<AutoForm resourceName="${resource.name}" {id} mode="show" />
|
|
5866
|
+
</ShowPage>
|
|
5867
|
+
`;
|
|
5868
|
+
}
|
|
5869
|
+
function generateResourceCode(resource) {
|
|
5870
|
+
return generateCode(resource);
|
|
5871
|
+
}
|
|
5872
|
+
function generateTypeBoxSchemaCode(resource) {
|
|
5873
|
+
const esc = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
5874
|
+
const schemaProps = resource.fields.map((f) => {
|
|
5875
|
+
let typeDef;
|
|
5876
|
+
const isRequired = f.required === true;
|
|
5877
|
+
switch (f.type) {
|
|
5878
|
+
case "number":
|
|
5879
|
+
typeDef = "Type.Number()";
|
|
5880
|
+
break;
|
|
5881
|
+
case "boolean":
|
|
5882
|
+
typeDef = "Type.Boolean()";
|
|
5883
|
+
break;
|
|
5884
|
+
case "date":
|
|
5885
|
+
typeDef = "Type.String({ format: 'date-time' })";
|
|
5886
|
+
break;
|
|
5887
|
+
case "email":
|
|
5888
|
+
typeDef = "Type.String({ format: 'email' })";
|
|
5889
|
+
break;
|
|
5890
|
+
case "url":
|
|
5891
|
+
typeDef = "Type.String({ format: 'uri' })";
|
|
5892
|
+
break;
|
|
5893
|
+
case "phone":
|
|
5894
|
+
typeDef = "Type.String({ pattern: '^\\\\+?[\\\\d\\\\s\\\\-()]{7,}$' })";
|
|
5895
|
+
break;
|
|
5896
|
+
case "tags":
|
|
5897
|
+
typeDef = "Type.Array(Type.String())";
|
|
5898
|
+
break;
|
|
5899
|
+
case "images":
|
|
5900
|
+
typeDef = "Type.Array(Type.String({ format: 'uri' }))";
|
|
5901
|
+
break;
|
|
5902
|
+
case "json":
|
|
5903
|
+
typeDef = "Type.Record(Type.String(), Type.Unknown())";
|
|
5904
|
+
break;
|
|
5905
|
+
case "select":
|
|
5906
|
+
if (f.options && f.options.length > 0) {
|
|
5907
|
+
const literals = f.options.map((o) => `Type.Literal('${esc(String(o.value))}')`).join(", ");
|
|
5908
|
+
typeDef = `Type.Union([${literals}])`;
|
|
5909
|
+
} else {
|
|
5910
|
+
typeDef = "Type.String()";
|
|
5911
|
+
}
|
|
5912
|
+
break;
|
|
5913
|
+
case "relation":
|
|
5914
|
+
typeDef = "Type.Union([Type.String(), Type.Number()])";
|
|
5915
|
+
break;
|
|
5916
|
+
case "textarea":
|
|
5917
|
+
case "richtext":
|
|
5918
|
+
case "text":
|
|
5919
|
+
default:
|
|
5920
|
+
typeDef = "Type.String()";
|
|
5921
|
+
break;
|
|
5922
|
+
}
|
|
5923
|
+
if (!isRequired && f.key !== (resource.primaryKey ?? "id")) {
|
|
5924
|
+
typeDef = `Type.Optional(${typeDef})`;
|
|
5925
|
+
}
|
|
5926
|
+
return ` ${f.key}: ${typeDef},`;
|
|
5927
|
+
}).join(`
|
|
5928
|
+
`);
|
|
5929
|
+
const baseName = resource.name.endsWith("s") ? resource.name.slice(0, -1) : resource.name;
|
|
5930
|
+
const typeName = capitalize(baseName);
|
|
5931
|
+
return `import { Type, type Static } from '@sinclair/typebox';
|
|
5932
|
+
|
|
5933
|
+
export const ${typeName}Schema = Type.Object({
|
|
5934
|
+
${schemaProps}
|
|
5935
|
+
});
|
|
5936
|
+
|
|
5937
|
+
export type ${typeName} = Static<typeof ${typeName}Schema>;
|
|
5938
|
+
`;
|
|
5939
|
+
}
|
|
5940
|
+
function capitalize(s) {
|
|
5941
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
5942
|
+
}
|
|
5943
|
+
function humanize3(key) {
|
|
5944
|
+
return key.replace(/_/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
5945
|
+
}
|
|
5946
|
+
function generateResourceBundle(resource) {
|
|
5947
|
+
return {
|
|
5948
|
+
fields: resource.fields,
|
|
5949
|
+
resource,
|
|
5950
|
+
code: generateResourceCode(resource),
|
|
5951
|
+
typeboxCode: generateTypeBoxSchemaCode(resource),
|
|
5952
|
+
componentCode: {
|
|
5953
|
+
list: generateListPageCode(resource),
|
|
5954
|
+
create: generateCreatePageCode(resource),
|
|
5955
|
+
edit: generateEditPageCode(resource),
|
|
5956
|
+
show: generateShowPageCode(resource)
|
|
5957
|
+
}
|
|
5958
|
+
};
|
|
5959
|
+
}
|
|
5960
|
+
|
|
5961
|
+
// src/infer-command.ts
|
|
5962
|
+
function parseInferArguments(args) {
|
|
5963
|
+
const options = {
|
|
5964
|
+
headers: {},
|
|
5965
|
+
primaryKey: "id",
|
|
5966
|
+
method: "GET",
|
|
5967
|
+
format: "all"
|
|
5968
|
+
};
|
|
5969
|
+
for (let i = 0;i < args.length; i++) {
|
|
5970
|
+
const arg = args[i];
|
|
5971
|
+
if (arg === "--write" || arg === "-w") {
|
|
5972
|
+
options.write = true;
|
|
5973
|
+
} else if (arg === "--dry-run") {
|
|
5974
|
+
options.write = false;
|
|
5975
|
+
} else if (arg === "--url" || arg === "-u") {
|
|
5976
|
+
options.url = args[++i];
|
|
5977
|
+
} else if (arg.startsWith("--url=")) {
|
|
5978
|
+
options.url = arg.slice(6);
|
|
5979
|
+
} else if (arg === "--file" || arg === "-f") {
|
|
5980
|
+
options.file = args[++i];
|
|
5981
|
+
} else if (arg.startsWith("--file=")) {
|
|
5982
|
+
options.file = arg.slice(7);
|
|
5983
|
+
} else if (arg === "--type" || arg === "-t") {
|
|
5984
|
+
options.type = args[++i];
|
|
5985
|
+
} else if (arg.startsWith("--type=")) {
|
|
5986
|
+
options.type = arg.slice(7);
|
|
5987
|
+
} else if (arg === "--resource" || arg === "-r") {
|
|
5988
|
+
options.resource = args[++i];
|
|
5989
|
+
} else if (arg.startsWith("--resource=")) {
|
|
5990
|
+
options.resource = arg.slice(11);
|
|
5991
|
+
} else if (arg === "--out-dir" || arg === "-o" || arg === "--output") {
|
|
5992
|
+
options.outDir = args[++i];
|
|
5993
|
+
} else if (arg.startsWith("--out-dir=")) {
|
|
5994
|
+
options.outDir = arg.slice(10);
|
|
5995
|
+
} else if (arg.startsWith("--output=")) {
|
|
5996
|
+
options.outDir = arg.slice(9);
|
|
5997
|
+
} else if (arg === "--primary-key" || arg === "-k") {
|
|
5998
|
+
options.primaryKey = args[++i];
|
|
5999
|
+
} else if (arg.startsWith("--primary-key=")) {
|
|
6000
|
+
options.primaryKey = arg.slice(14);
|
|
6001
|
+
} else if (arg === "--header" || arg === "-H") {
|
|
6002
|
+
const headerLine = args[++i] ?? "";
|
|
6003
|
+
const colonIndex = headerLine.indexOf(":");
|
|
6004
|
+
if (colonIndex > 0) {
|
|
6005
|
+
const key = headerLine.slice(0, colonIndex).trim();
|
|
6006
|
+
const value = headerLine.slice(colonIndex + 1).trim();
|
|
6007
|
+
if (!options.headers)
|
|
6008
|
+
options.headers = {};
|
|
6009
|
+
options.headers[key] = value;
|
|
6010
|
+
}
|
|
6011
|
+
} else if (arg.startsWith("--header=")) {
|
|
6012
|
+
const headerLine = arg.slice(9);
|
|
6013
|
+
const colonIndex = headerLine.indexOf(":");
|
|
6014
|
+
if (colonIndex > 0) {
|
|
6015
|
+
const key = headerLine.slice(0, colonIndex).trim();
|
|
6016
|
+
const value = headerLine.slice(colonIndex + 1).trim();
|
|
6017
|
+
if (!options.headers)
|
|
6018
|
+
options.headers = {};
|
|
6019
|
+
options.headers[key] = value;
|
|
6020
|
+
}
|
|
6021
|
+
} else if (arg === "--method" || arg === "-m") {
|
|
6022
|
+
options.method = (args[++i] ?? "GET").toUpperCase();
|
|
6023
|
+
} else if (arg.startsWith("--method=")) {
|
|
6024
|
+
options.method = arg.slice(9).toUpperCase();
|
|
6025
|
+
} else if (arg === "--body" || arg === "-b") {
|
|
6026
|
+
options.body = args[++i];
|
|
6027
|
+
} else if (arg.startsWith("--body=")) {
|
|
6028
|
+
options.body = arg.slice(7);
|
|
6029
|
+
} else if (arg === "--format") {
|
|
6030
|
+
options.format = args[++i];
|
|
6031
|
+
} else if (arg.startsWith("--format=")) {
|
|
6032
|
+
options.format = arg.slice(9);
|
|
6033
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
6034
|
+
printInferHelp();
|
|
6035
|
+
process.exit(0);
|
|
6036
|
+
} else if (!arg.startsWith("-") && !options.url && !options.file) {
|
|
6037
|
+
if (arg.startsWith("http://") || arg.startsWith("https://")) {
|
|
6038
|
+
options.url = arg;
|
|
6039
|
+
} else if (fs.existsSync(arg)) {
|
|
6040
|
+
options.file = arg;
|
|
6041
|
+
} else {
|
|
6042
|
+
options.resource = arg;
|
|
6043
|
+
}
|
|
6044
|
+
}
|
|
6045
|
+
}
|
|
6046
|
+
return options;
|
|
6047
|
+
}
|
|
6048
|
+
function printInferHelp() {
|
|
6049
|
+
console.log(`
|
|
6050
|
+
${import_picocolors.default.bold("svadmin infer")} — Automatically infer ResourceDefinitions, TypeBox Schemas, and Svelte 5 CRUD components from REST/GraphQL APIs or OpenAPI specs.
|
|
6051
|
+
|
|
6052
|
+
${import_picocolors.default.bold("USAGE:")}
|
|
6053
|
+
svadmin infer [OPTIONS]
|
|
6054
|
+
svadmin infer <url|file> [OPTIONS]
|
|
6055
|
+
|
|
6056
|
+
${import_picocolors.default.bold("OPTIONS:")}
|
|
6057
|
+
-u, --url <url> REST API endpoint, OpenAPI schema URL, or GraphQL endpoint
|
|
6058
|
+
-f, --file <path> Local OpenAPI JSON/YAML, GraphQL schema (.graphql/JSON), or sample JSON data file
|
|
6059
|
+
-t, --type <type> Explicit source type: rest | openapi | graphql | auto (default: auto)
|
|
6060
|
+
-r, --resource <name> Target resource name filter or explicit resource name for sample data
|
|
6061
|
+
-o, --out-dir <dir> Target directory for generated code (e.g. src/resources)
|
|
6062
|
+
-k, --primary-key <key> Primary key field name (default: id)
|
|
6063
|
+
-H, --header <key:value> Custom HTTP header for fetch requests (can be specified multiple times)
|
|
6064
|
+
-m, --method <GET|POST> HTTP method for REST fetch (default: GET)
|
|
6065
|
+
-b, --body <json> HTTP request body for POST requests
|
|
6066
|
+
-w, --write Write generated files to target directory (default is dry-run when --out-dir is set)
|
|
6067
|
+
--format <all|resource|typebox|components>
|
|
6068
|
+
Output format (default: all)
|
|
6069
|
+
-h, --help Show this help message
|
|
6070
|
+
|
|
6071
|
+
${import_picocolors.default.bold("EXAMPLES:")}
|
|
6072
|
+
svadmin infer --url https://api.example.com/openapi.json --out-dir src/resources --write
|
|
6073
|
+
svadmin infer --url https://api.example.com/graphql --out-dir src/resources --write
|
|
6074
|
+
svadmin infer --url https://api.example.com/api/v1/posts --resource posts
|
|
6075
|
+
svadmin infer --file schema.graphql --out-dir src/resources --write
|
|
6076
|
+
svadmin infer --file sample-posts.json --resource posts --out-dir src/resources --write
|
|
6077
|
+
`);
|
|
6078
|
+
}
|
|
6079
|
+
async function loadSourceData(options, customFetch = fetch) {
|
|
6080
|
+
if (options.file) {
|
|
6081
|
+
const filePath = path.resolve(process.cwd(), options.file);
|
|
6082
|
+
if (!fs.existsSync(filePath)) {
|
|
6083
|
+
throw new Error(`File not found: ${filePath}`);
|
|
6084
|
+
}
|
|
6085
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
6086
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
6087
|
+
const baseName = path.basename(filePath, ext);
|
|
6088
|
+
if (ext === ".graphql" || ext === ".gql") {
|
|
6089
|
+
return {
|
|
6090
|
+
sourceType: "graphql",
|
|
6091
|
+
data: content,
|
|
6092
|
+
derivedResourceName: options.resource || baseName,
|
|
6093
|
+
sourceDescription: options.file
|
|
6094
|
+
};
|
|
6095
|
+
}
|
|
6096
|
+
try {
|
|
6097
|
+
const json = JSON.parse(content);
|
|
6098
|
+
if (typeof json === "object" && json !== null && (("openapi" in json) || ("swagger" in json) || ("paths" in json) && ("components" in json))) {
|
|
6099
|
+
return {
|
|
6100
|
+
sourceType: "openapi",
|
|
6101
|
+
data: json,
|
|
6102
|
+
derivedResourceName: options.resource,
|
|
6103
|
+
sourceDescription: options.file
|
|
6104
|
+
};
|
|
6105
|
+
}
|
|
6106
|
+
if (typeof json === "object" && json !== null && (("__schema" in json) || ("data" in json) && json.data && ("__schema" in json.data) || ("types" in json) && Array.isArray(json.types))) {
|
|
6107
|
+
return {
|
|
6108
|
+
sourceType: "graphql",
|
|
6109
|
+
data: json,
|
|
6110
|
+
derivedResourceName: options.resource,
|
|
6111
|
+
sourceDescription: options.file
|
|
6112
|
+
};
|
|
6113
|
+
}
|
|
6114
|
+
const records = Array.isArray(json) ? json : Array.isArray(json.data) ? json.data : Array.isArray(json.items) ? json.items : [json];
|
|
6115
|
+
return {
|
|
6116
|
+
sourceType: "rest",
|
|
6117
|
+
data: records,
|
|
6118
|
+
derivedResourceName: options.resource || baseName,
|
|
6119
|
+
sourceDescription: options.file
|
|
6120
|
+
};
|
|
6121
|
+
} catch {
|
|
6122
|
+
if (content.includes("type ") || content.includes("enum ") || content.includes("schema ")) {
|
|
6123
|
+
return {
|
|
6124
|
+
sourceType: "graphql",
|
|
6125
|
+
data: content,
|
|
6126
|
+
derivedResourceName: options.resource || baseName,
|
|
6127
|
+
sourceDescription: options.file
|
|
6128
|
+
};
|
|
6129
|
+
}
|
|
6130
|
+
throw new Error(`Unable to parse file ${filePath}. Expected valid JSON or GraphQL SDL schema.`);
|
|
6131
|
+
}
|
|
6132
|
+
}
|
|
6133
|
+
if (options.url) {
|
|
6134
|
+
const url = options.url;
|
|
6135
|
+
const isGraphQL = options.type === "graphql" || url.endsWith("/graphql") || url.includes("/graphql?");
|
|
6136
|
+
if (isGraphQL) {
|
|
6137
|
+
const response2 = await customFetch(url, {
|
|
6138
|
+
method: "POST",
|
|
6139
|
+
headers: {
|
|
6140
|
+
"Content-Type": "application/json",
|
|
6141
|
+
Accept: "application/json",
|
|
6142
|
+
...options.headers ?? {}
|
|
6143
|
+
},
|
|
6144
|
+
body: JSON.stringify({ query: GRAPHQL_INTROSPECTION_QUERY })
|
|
6145
|
+
});
|
|
6146
|
+
if (!response2.ok) {
|
|
6147
|
+
throw new Error(`GraphQL introspection request failed: HTTP ${response2.status} ${response2.statusText}`);
|
|
6148
|
+
}
|
|
6149
|
+
const json2 = await response2.json();
|
|
6150
|
+
return {
|
|
6151
|
+
sourceType: "graphql",
|
|
6152
|
+
data: json2,
|
|
6153
|
+
derivedResourceName: options.resource,
|
|
6154
|
+
sourceDescription: url
|
|
6155
|
+
};
|
|
6156
|
+
}
|
|
6157
|
+
const response = await customFetch(url, {
|
|
6158
|
+
method: options.method ?? "GET",
|
|
6159
|
+
headers: {
|
|
6160
|
+
Accept: "application/json",
|
|
6161
|
+
...options.headers ?? {}
|
|
6162
|
+
},
|
|
6163
|
+
body: options.body
|
|
6164
|
+
});
|
|
6165
|
+
if (!response.ok) {
|
|
6166
|
+
throw new Error(`HTTP request failed: HTTP ${response.status} ${response.statusText}`);
|
|
6167
|
+
}
|
|
6168
|
+
const json = await response.json();
|
|
6169
|
+
if (typeof json === "object" && json !== null && (("openapi" in json) || ("swagger" in json) || ("paths" in json) && ("components" in json))) {
|
|
6170
|
+
return {
|
|
6171
|
+
sourceType: "openapi",
|
|
6172
|
+
data: json,
|
|
6173
|
+
derivedResourceName: options.resource,
|
|
6174
|
+
sourceDescription: url
|
|
6175
|
+
};
|
|
6176
|
+
}
|
|
6177
|
+
if (typeof json === "object" && json !== null && (("__schema" in json) || ("data" in json) && json.data?.__schema)) {
|
|
6178
|
+
return {
|
|
6179
|
+
sourceType: "graphql",
|
|
6180
|
+
data: json,
|
|
6181
|
+
derivedResourceName: options.resource,
|
|
6182
|
+
sourceDescription: url
|
|
6183
|
+
};
|
|
6184
|
+
}
|
|
6185
|
+
let derivedName = options.resource;
|
|
6186
|
+
if (!derivedName) {
|
|
6187
|
+
try {
|
|
6188
|
+
const parsedUrl = new URL(url);
|
|
6189
|
+
const segments = parsedUrl.pathname.split("/").filter(Boolean);
|
|
6190
|
+
derivedName = segments.pop() || "items";
|
|
6191
|
+
} catch {
|
|
6192
|
+
derivedName = "items";
|
|
6193
|
+
}
|
|
6194
|
+
}
|
|
6195
|
+
const records = Array.isArray(json) ? json : Array.isArray(json.data) ? json.data : Array.isArray(json.items) ? json.items : [json];
|
|
6196
|
+
return {
|
|
6197
|
+
sourceType: "rest",
|
|
6198
|
+
data: records,
|
|
6199
|
+
derivedResourceName: derivedName,
|
|
6200
|
+
sourceDescription: url
|
|
6201
|
+
};
|
|
6202
|
+
}
|
|
6203
|
+
throw new Error("Must provide either --url <url> or --file <path>");
|
|
6204
|
+
}
|
|
6205
|
+
function planGeneratedFiles(resources, bundles, format = "all") {
|
|
6206
|
+
const files = [];
|
|
6207
|
+
const exportLines = [];
|
|
6208
|
+
for (const resource of resources) {
|
|
6209
|
+
const bundle = bundles.get(resource.name);
|
|
6210
|
+
if (!bundle)
|
|
6211
|
+
continue;
|
|
6212
|
+
const baseName = resource.name;
|
|
6213
|
+
const pascalName = capitalize2(resource.name.endsWith("s") ? resource.name.slice(0, -1) : resource.name);
|
|
6214
|
+
if (format === "all" || format === "resource") {
|
|
6215
|
+
files.push({
|
|
6216
|
+
relativePath: `${baseName}.resource.ts`,
|
|
6217
|
+
content: bundle.code
|
|
6218
|
+
});
|
|
6219
|
+
exportLines.push(`export * from './${baseName}.resource.js';`);
|
|
6220
|
+
}
|
|
6221
|
+
if (format === "all" || format === "typebox") {
|
|
6222
|
+
files.push({
|
|
6223
|
+
relativePath: `${baseName}.schema.ts`,
|
|
6224
|
+
content: bundle.typeboxCode
|
|
6225
|
+
});
|
|
6226
|
+
exportLines.push(`export * from './${baseName}.schema.js';`);
|
|
6227
|
+
}
|
|
6228
|
+
if (format === "all" || format === "components") {
|
|
6229
|
+
files.push({
|
|
6230
|
+
relativePath: `${baseName}/ListPage.svelte`,
|
|
6231
|
+
content: bundle.componentCode.list
|
|
6232
|
+
});
|
|
6233
|
+
files.push({
|
|
6234
|
+
relativePath: `${baseName}/CreatePage.svelte`,
|
|
6235
|
+
content: bundle.componentCode.create
|
|
6236
|
+
});
|
|
6237
|
+
files.push({
|
|
6238
|
+
relativePath: `${baseName}/EditPage.svelte`,
|
|
6239
|
+
content: bundle.componentCode.edit
|
|
6240
|
+
});
|
|
6241
|
+
files.push({
|
|
6242
|
+
relativePath: `${baseName}/ShowPage.svelte`,
|
|
6243
|
+
content: bundle.componentCode.show
|
|
6244
|
+
});
|
|
6245
|
+
exportLines.push(`export { default as ${pascalName}ListPage } from './${baseName}/ListPage.svelte';`);
|
|
6246
|
+
exportLines.push(`export { default as ${pascalName}CreatePage } from './${baseName}/CreatePage.svelte';`);
|
|
6247
|
+
exportLines.push(`export { default as ${pascalName}EditPage } from './${baseName}/EditPage.svelte';`);
|
|
6248
|
+
exportLines.push(`export { default as ${pascalName}ShowPage } from './${baseName}/ShowPage.svelte';`);
|
|
6249
|
+
}
|
|
6250
|
+
}
|
|
6251
|
+
if (files.length > 0) {
|
|
6252
|
+
files.push({
|
|
6253
|
+
relativePath: "index.ts",
|
|
6254
|
+
content: `// Auto-generated by @svadmin/create infer
|
|
6255
|
+
|
|
6256
|
+
${exportLines.join(`
|
|
6257
|
+
`)}
|
|
6258
|
+
`
|
|
6259
|
+
});
|
|
6260
|
+
}
|
|
6261
|
+
return files;
|
|
6262
|
+
}
|
|
6263
|
+
async function executeInfer(options, customFetch = fetch) {
|
|
6264
|
+
const loaded = await loadSourceData(options, customFetch);
|
|
6265
|
+
const primaryKey = options.primaryKey ?? "id";
|
|
6266
|
+
let resources;
|
|
6267
|
+
const bundles = new Map;
|
|
6268
|
+
if (loaded.sourceType === "openapi") {
|
|
6269
|
+
resources = inferFromOpenAPI(loaded.data, {
|
|
6270
|
+
primaryKey,
|
|
6271
|
+
include: options.resource ? [options.resource] : undefined
|
|
6272
|
+
});
|
|
6273
|
+
for (const res of resources) {
|
|
6274
|
+
bundles.set(res.name, generateResourceBundle(res));
|
|
6275
|
+
}
|
|
6276
|
+
} else if (loaded.sourceType === "graphql") {
|
|
6277
|
+
resources = inferFromGraphQL(loaded.data, {
|
|
6278
|
+
primaryKey,
|
|
6279
|
+
include: options.resource ? [options.resource] : undefined
|
|
6280
|
+
});
|
|
6281
|
+
for (const res of resources) {
|
|
6282
|
+
bundles.set(res.name, generateResourceBundle(res));
|
|
6283
|
+
}
|
|
6284
|
+
} else {
|
|
6285
|
+
const resName = options.resource || loaded.derivedResourceName || "items";
|
|
6286
|
+
const sampleArray = Array.isArray(loaded.data) ? loaded.data : [loaded.data];
|
|
6287
|
+
const inferRes = inferResource(resName, sampleArray, { primaryKey });
|
|
6288
|
+
resources = [inferRes.resource];
|
|
6289
|
+
bundles.set(resName, inferRes);
|
|
6290
|
+
}
|
|
6291
|
+
if (resources.length === 0) {
|
|
6292
|
+
throw new Error(`No resources could be inferred from ${loaded.sourceDescription}`);
|
|
6293
|
+
}
|
|
6294
|
+
const files = planGeneratedFiles(resources, bundles, options.format);
|
|
6295
|
+
let wrote = false;
|
|
6296
|
+
if (options.outDir && options.write) {
|
|
6297
|
+
const targetDir = path.resolve(process.cwd(), options.outDir);
|
|
6298
|
+
for (const file of files) {
|
|
6299
|
+
const fullPath = path.join(targetDir, file.relativePath);
|
|
6300
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
6301
|
+
fs.writeFileSync(fullPath, file.content, "utf-8");
|
|
6302
|
+
}
|
|
6303
|
+
wrote = true;
|
|
6304
|
+
}
|
|
6305
|
+
return {
|
|
6306
|
+
resources,
|
|
6307
|
+
bundles,
|
|
6308
|
+
files,
|
|
6309
|
+
sourceDescription: loaded.sourceDescription,
|
|
6310
|
+
wrote,
|
|
6311
|
+
outDir: options.outDir
|
|
6312
|
+
};
|
|
6313
|
+
}
|
|
6314
|
+
function printInferResult(result) {
|
|
6315
|
+
console.log();
|
|
6316
|
+
console.log(import_picocolors.default.cyan(" ╔═══════════════════════════════════╗"));
|
|
6317
|
+
console.log(import_picocolors.default.cyan(" ║ ") + import_picocolors.default.bold("svadmin infer") + import_picocolors.default.cyan(" ║"));
|
|
6318
|
+
console.log(import_picocolors.default.cyan(" ║ ") + import_picocolors.default.dim("Automated Code & UI Inferencer") + import_picocolors.default.cyan(" ║"));
|
|
6319
|
+
console.log(import_picocolors.default.cyan(" ╚═══════════════════════════════════╝"));
|
|
6320
|
+
console.log();
|
|
6321
|
+
console.log(import_picocolors.default.bold(` Source: ${import_picocolors.default.cyan(result.sourceDescription)}`));
|
|
6322
|
+
console.log(import_picocolors.default.bold(` Inferred: ${import_picocolors.default.green(result.resources.length.toString())} resource(s): ${result.resources.map((r) => r.name).join(", ")}`));
|
|
6323
|
+
console.log();
|
|
6324
|
+
if (result.outDir) {
|
|
6325
|
+
if (result.wrote) {
|
|
6326
|
+
console.log(import_picocolors.default.green(` ✔ Written ${result.files.length} file(s) to ${import_picocolors.default.cyan(result.outDir)}:`));
|
|
6327
|
+
for (const file of result.files) {
|
|
6328
|
+
console.log(` ${import_picocolors.default.green("•")} ${path.join(result.outDir, file.relativePath)}`);
|
|
6329
|
+
}
|
|
6330
|
+
console.log();
|
|
6331
|
+
console.log(import_picocolors.default.dim(" Next step: import and register your generated resources in src/resources.ts"));
|
|
6332
|
+
} else {
|
|
6333
|
+
console.log(import_picocolors.default.yellow(` Dry run plan — ${result.files.length} file(s) planned for ${import_picocolors.default.cyan(result.outDir)}:`));
|
|
6334
|
+
for (const file of result.files) {
|
|
6335
|
+
console.log(` ${import_picocolors.default.cyan("•")} ${path.join(result.outDir, file.relativePath)}`);
|
|
6336
|
+
}
|
|
6337
|
+
console.log();
|
|
6338
|
+
console.log(import_picocolors.default.yellow(" Dry run only; re-run with --write to generate files to disk."));
|
|
6339
|
+
}
|
|
6340
|
+
} else {
|
|
6341
|
+
for (const res of result.resources) {
|
|
6342
|
+
const bundle = result.bundles.get(res.name);
|
|
6343
|
+
console.log(import_picocolors.default.bold(` Resource: ${import_picocolors.default.green(res.name)} (${res.label})`));
|
|
6344
|
+
console.log(` Primary Key: ${import_picocolors.default.cyan(res.primaryKey ?? "id")} | Fields: ${res.fields.length}`);
|
|
6345
|
+
console.log(import_picocolors.default.dim(" Fields:"));
|
|
6346
|
+
for (const f of res.fields) {
|
|
6347
|
+
const badges = [
|
|
6348
|
+
f.type,
|
|
6349
|
+
f.required ? "required" : null,
|
|
6350
|
+
f.resource ? `-> ${f.resource}` : null,
|
|
6351
|
+
f.showInList ? "list" : null,
|
|
6352
|
+
f.showInForm ? "form" : null
|
|
6353
|
+
].filter(Boolean).join(", ");
|
|
6354
|
+
console.log(` ${import_picocolors.default.cyan(f.key)}: ${import_picocolors.default.dim(`(${badges})`)}`);
|
|
6355
|
+
}
|
|
6356
|
+
console.log();
|
|
6357
|
+
if (bundle) {
|
|
6358
|
+
console.log(import_picocolors.default.bold(" TypeBox Schema:"));
|
|
6359
|
+
console.log(import_picocolors.default.dim(" ───────────────────────────────────"));
|
|
6360
|
+
console.log(bundle.typeboxCode.trim());
|
|
6361
|
+
console.log(import_picocolors.default.dim(" ───────────────────────────────────"));
|
|
6362
|
+
console.log();
|
|
6363
|
+
}
|
|
6364
|
+
}
|
|
6365
|
+
console.log(import_picocolors.default.dim(" Tip: Pass --out-dir src/resources --write to save generated files directly."));
|
|
6366
|
+
}
|
|
6367
|
+
console.log();
|
|
6368
|
+
}
|
|
6369
|
+
async function promptInferWizard() {
|
|
6370
|
+
const answers = await import_prompts.default([
|
|
6371
|
+
{
|
|
6372
|
+
type: "select",
|
|
6373
|
+
name: "sourceType",
|
|
6374
|
+
message: "Select API source type:",
|
|
6375
|
+
choices: [
|
|
6376
|
+
{ title: "REST API Endpoint (URL)", value: "rest-url", description: "Fetch sample JSON data from REST endpoint" },
|
|
6377
|
+
{ title: "OpenAPI / Swagger Spec (URL or File)", value: "openapi", description: "OpenAPI 3.x / Swagger schema definition" },
|
|
6378
|
+
{ title: "GraphQL Endpoint (URL or Schema File)", value: "graphql", description: "GraphQL introspection or SDL schema" },
|
|
6379
|
+
{ title: "Local Sample JSON File", value: "rest-file", description: "JSON file with sample data records" }
|
|
6380
|
+
],
|
|
6381
|
+
initial: 0
|
|
6382
|
+
},
|
|
6383
|
+
{
|
|
6384
|
+
type: (prev) => prev === "rest-file" || prev === "openapi" ? "text" : null,
|
|
6385
|
+
name: "pathOrUrl",
|
|
6386
|
+
message: "Enter URL or File path:",
|
|
6387
|
+
validate: (v) => v.trim() ? true : "URL or file path is required"
|
|
6388
|
+
},
|
|
6389
|
+
{
|
|
6390
|
+
type: (prev, values) => values.sourceType === "rest-url" || values.sourceType === "graphql" ? "text" : null,
|
|
6391
|
+
name: "endpointUrl",
|
|
6392
|
+
message: "Enter Endpoint URL:",
|
|
6393
|
+
validate: (v) => v.trim().startsWith("http") ? true : "Must be a valid HTTP(S) URL"
|
|
6394
|
+
},
|
|
6395
|
+
{
|
|
6396
|
+
type: "text",
|
|
6397
|
+
name: "outDir",
|
|
6398
|
+
message: "Target output directory:",
|
|
6399
|
+
initial: "src/resources"
|
|
6400
|
+
},
|
|
6401
|
+
{
|
|
6402
|
+
type: "confirm",
|
|
6403
|
+
name: "write",
|
|
6404
|
+
message: "Write generated files to disk now?",
|
|
6405
|
+
initial: true
|
|
6406
|
+
}
|
|
6407
|
+
]);
|
|
6408
|
+
const targetPath = answers.pathOrUrl || answers.endpointUrl;
|
|
6409
|
+
const isUrl = targetPath?.startsWith("http://") || targetPath?.startsWith("https://");
|
|
6410
|
+
return {
|
|
6411
|
+
url: isUrl ? targetPath : undefined,
|
|
6412
|
+
file: !isUrl ? targetPath : undefined,
|
|
6413
|
+
type: answers.sourceType.startsWith("graphql") ? "graphql" : answers.sourceType.startsWith("openapi") ? "openapi" : "rest",
|
|
6414
|
+
outDir: answers.outDir,
|
|
6415
|
+
write: answers.write
|
|
6416
|
+
};
|
|
6417
|
+
}
|
|
6418
|
+
async function inferCommand(args) {
|
|
6419
|
+
let options = parseInferArguments(args);
|
|
6420
|
+
if (!options.url && !options.file && process.stdin.isTTY) {
|
|
6421
|
+
options = await promptInferWizard();
|
|
6422
|
+
}
|
|
6423
|
+
if (!options.url && !options.file) {
|
|
6424
|
+
printInferHelp();
|
|
6425
|
+
return;
|
|
6426
|
+
}
|
|
6427
|
+
const result = await executeInfer(options);
|
|
6428
|
+
printInferResult(result);
|
|
6429
|
+
}
|
|
6430
|
+
function capitalize2(s) {
|
|
6431
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
6432
|
+
}
|
|
6433
|
+
|
|
5130
6434
|
// src/project-maintenance.ts
|
|
5131
6435
|
import {
|
|
5132
6436
|
constants,
|
|
@@ -5136,10 +6440,10 @@ import {
|
|
|
5136
6440
|
unlinkSync,
|
|
5137
6441
|
writeFileSync
|
|
5138
6442
|
} from "node:fs";
|
|
5139
|
-
function parseDependencyMap(candidate,
|
|
6443
|
+
function parseDependencyMap(candidate, path2) {
|
|
5140
6444
|
if (candidate === undefined)
|
|
5141
6445
|
return;
|
|
5142
|
-
assertStringRecord(candidate,
|
|
6446
|
+
assertStringRecord(candidate, path2);
|
|
5143
6447
|
return { ...candidate };
|
|
5144
6448
|
}
|
|
5145
6449
|
function parseMaintainedPackageJson(packageJsonCandidate) {
|
|
@@ -5415,15 +6719,15 @@ function writeProjectPackageJsonUpgrade(packagePath, scaffold, backupDate) {
|
|
|
5415
6719
|
|
|
5416
6720
|
// src/index.ts
|
|
5417
6721
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
5418
|
-
var __dirname2 =
|
|
6722
|
+
var __dirname2 = path2.dirname(__filename2);
|
|
5419
6723
|
function loadShippedScaffoldManifest() {
|
|
5420
|
-
return loadScaffoldManifest(
|
|
6724
|
+
return loadScaffoldManifest(path2.join(__dirname2, "..", "scaffold-manifest.json"));
|
|
5421
6725
|
}
|
|
5422
6726
|
function projectDirectoryFromArguments(positional) {
|
|
5423
6727
|
if (positional.length > 1) {
|
|
5424
6728
|
throw new Error(`Expected at most one project directory, received: ${positional.join(", ")}`);
|
|
5425
6729
|
}
|
|
5426
|
-
return
|
|
6730
|
+
return path2.resolve(process.cwd(), positional[0] ?? ".");
|
|
5427
6731
|
}
|
|
5428
6732
|
function doctorProjectDirectory(args) {
|
|
5429
6733
|
const unknownOption = args.find((argument) => argument.startsWith("-"));
|
|
@@ -5459,52 +6763,52 @@ function upgradeChangeMessage(change) {
|
|
|
5459
6763
|
return `update ${change.packageName} from ${change.from ?? "missing"} to ${change.to}`;
|
|
5460
6764
|
}
|
|
5461
6765
|
function printDoctorIssue(issue) {
|
|
5462
|
-
const marker = issue.kind === "drift" || issue.kind === "section" ?
|
|
6766
|
+
const marker = issue.kind === "drift" || issue.kind === "section" ? import_picocolors2.default.yellow(" ⚠") : import_picocolors2.default.red(" ✗");
|
|
5463
6767
|
console.log(`${marker} ${doctorIssueMessage(issue)}`);
|
|
5464
|
-
console.log(
|
|
6768
|
+
console.log(import_picocolors2.default.dim(` → ${issue.action}`));
|
|
5465
6769
|
}
|
|
5466
6770
|
function printDoctorReport(report, projectDirectory) {
|
|
5467
6771
|
console.log();
|
|
5468
|
-
console.log(
|
|
6772
|
+
console.log(import_picocolors2.default.bold(`svadmin doctor — ${projectDirectory}`));
|
|
5469
6773
|
if (report.status === "clean") {
|
|
5470
|
-
console.log(
|
|
6774
|
+
console.log(import_picocolors2.default.green(" ✔ Dependencies match the shipped svadmin scaffold."));
|
|
5471
6775
|
} else {
|
|
5472
6776
|
for (const issue of report.issues)
|
|
5473
6777
|
printDoctorIssue(issue);
|
|
5474
6778
|
console.log();
|
|
5475
|
-
console.log(
|
|
6779
|
+
console.log(import_picocolors2.default.yellow(` ${report.issues.length} actionable issue(s) found.`));
|
|
5476
6780
|
}
|
|
5477
6781
|
console.log();
|
|
5478
6782
|
}
|
|
5479
6783
|
function doctor(args) {
|
|
5480
6784
|
const projectDirectory = doctorProjectDirectory(args);
|
|
5481
|
-
const project = readMaintainedPackageJson(
|
|
6785
|
+
const project = readMaintainedPackageJson(path2.join(projectDirectory, "package.json"));
|
|
5482
6786
|
const report = doctorProjectPackageJson(project, loadShippedScaffoldManifest());
|
|
5483
6787
|
printDoctorReport(report, projectDirectory);
|
|
5484
6788
|
process.exitCode = report.exitCode;
|
|
5485
6789
|
}
|
|
5486
6790
|
function printUpgradeChanges(upgradeExecution) {
|
|
5487
6791
|
for (const change of upgradeExecution.plan.changes) {
|
|
5488
|
-
console.log(` ${
|
|
6792
|
+
console.log(` ${import_picocolors2.default.cyan("•")} ${upgradeChangeMessage(change)}`);
|
|
5489
6793
|
}
|
|
5490
6794
|
console.log();
|
|
5491
6795
|
}
|
|
5492
6796
|
function printUpgradeOutcome(upgradeExecution, packagePath) {
|
|
5493
6797
|
if (upgradeExecution.wrote) {
|
|
5494
|
-
console.log(
|
|
5495
|
-
console.log(` Backup: ${
|
|
5496
|
-
console.log(` Restore by copying the backup over: ${
|
|
6798
|
+
console.log(import_picocolors2.default.green(" ✔ package.json updated."));
|
|
6799
|
+
console.log(` Backup: ${import_picocolors2.default.cyan(upgradeExecution.backupPath)}`);
|
|
6800
|
+
console.log(` Restore by copying the backup over: ${import_picocolors2.default.cyan(packagePath)}`);
|
|
5497
6801
|
} else {
|
|
5498
|
-
console.log(
|
|
6802
|
+
console.log(import_picocolors2.default.yellow(" Dry run only; package.json was not changed."));
|
|
5499
6803
|
console.log(" Re-run this command with --write to apply the plan.");
|
|
5500
6804
|
}
|
|
5501
6805
|
console.log();
|
|
5502
6806
|
}
|
|
5503
6807
|
function printUpgradeExecution(upgradeExecution, projectDirectory, packagePath) {
|
|
5504
6808
|
console.log();
|
|
5505
|
-
console.log(
|
|
6809
|
+
console.log(import_picocolors2.default.bold(`svadmin upgrade — ${projectDirectory}`));
|
|
5506
6810
|
if (upgradeExecution.plan.changes.length === 0) {
|
|
5507
|
-
console.log(
|
|
6811
|
+
console.log(import_picocolors2.default.green(" ✔ package.json already matches the shipped scaffold."));
|
|
5508
6812
|
console.log();
|
|
5509
6813
|
return;
|
|
5510
6814
|
}
|
|
@@ -5513,60 +6817,60 @@ function printUpgradeExecution(upgradeExecution, projectDirectory, packagePath)
|
|
|
5513
6817
|
}
|
|
5514
6818
|
function upgrade(args) {
|
|
5515
6819
|
const commandArguments = parseUpgradeArguments(args);
|
|
5516
|
-
const packagePath =
|
|
6820
|
+
const packagePath = path2.join(commandArguments.projectDirectory, "package.json");
|
|
5517
6821
|
const scaffoldManifest = loadShippedScaffoldManifest();
|
|
5518
6822
|
const upgradeExecution = commandArguments.write ? writeProjectPackageJsonUpgrade(packagePath, scaffoldManifest, new Date) : planProjectPackageFileUpgrade(packagePath, scaffoldManifest);
|
|
5519
6823
|
printUpgradeExecution(upgradeExecution, commandArguments.projectDirectory, packagePath);
|
|
5520
6824
|
}
|
|
5521
6825
|
var GUIDANCE_FILES = ["DESIGN.md", "AGENTS.md"];
|
|
5522
6826
|
function missingGuidanceFiles(projectDirectory) {
|
|
5523
|
-
return GUIDANCE_FILES.filter((fileName) => !
|
|
6827
|
+
return GUIDANCE_FILES.filter((fileName) => !fs2.existsSync(path2.join(projectDirectory, fileName)));
|
|
5524
6828
|
}
|
|
5525
6829
|
function printGuidancePlan(projectDirectory, missingFiles) {
|
|
5526
6830
|
console.log();
|
|
5527
|
-
console.log(
|
|
6831
|
+
console.log(import_picocolors2.default.bold(`svadmin guidance — ${projectDirectory}`));
|
|
5528
6832
|
for (const fileName of missingFiles) {
|
|
5529
|
-
console.log(` ${
|
|
6833
|
+
console.log(` ${import_picocolors2.default.cyan("•")} add ${fileName}`);
|
|
5530
6834
|
}
|
|
5531
6835
|
console.log();
|
|
5532
6836
|
}
|
|
5533
6837
|
function installMissingGuidanceFiles(guidanceDirectory, projectDirectory, missingFiles) {
|
|
5534
6838
|
for (const fileName of missingFiles) {
|
|
5535
|
-
|
|
6839
|
+
fs2.copyFileSync(path2.join(guidanceDirectory, fileName), path2.join(projectDirectory, fileName));
|
|
5536
6840
|
}
|
|
5537
6841
|
}
|
|
5538
6842
|
function guidance(args) {
|
|
5539
6843
|
const { projectDirectory, write } = parseUpgradeArguments(args);
|
|
5540
|
-
const guidanceDirectory =
|
|
5541
|
-
if (!
|
|
6844
|
+
const guidanceDirectory = path2.join(__dirname2, "..", "guidance");
|
|
6845
|
+
if (!fs2.existsSync(projectDirectory))
|
|
5542
6846
|
throw new Error(`Project directory does not exist: ${projectDirectory}`);
|
|
5543
|
-
if (!
|
|
6847
|
+
if (!fs2.existsSync(guidanceDirectory))
|
|
5544
6848
|
throw new Error("Shipped svadmin guidance files are missing");
|
|
5545
6849
|
const missingFiles = missingGuidanceFiles(projectDirectory);
|
|
5546
6850
|
if (missingFiles.length === 0) {
|
|
5547
|
-
console.log(
|
|
6851
|
+
console.log(import_picocolors2.default.green(`
|
|
5548
6852
|
✔ DESIGN.md and AGENTS.md already exist; nothing was changed.
|
|
5549
6853
|
`));
|
|
5550
6854
|
return;
|
|
5551
6855
|
}
|
|
5552
6856
|
printGuidancePlan(projectDirectory, missingFiles);
|
|
5553
6857
|
if (!write) {
|
|
5554
|
-
console.log(
|
|
6858
|
+
console.log(import_picocolors2.default.yellow(` Dry run only; re-run with --write to add missing guidance files.
|
|
5555
6859
|
`));
|
|
5556
6860
|
return;
|
|
5557
6861
|
}
|
|
5558
6862
|
installMissingGuidanceFiles(guidanceDirectory, projectDirectory, missingFiles);
|
|
5559
|
-
console.log(
|
|
6863
|
+
console.log(import_picocolors2.default.green(` ✔ Added ${missingFiles.length} guidance file(s); existing files were preserved.`));
|
|
5560
6864
|
console.log();
|
|
5561
6865
|
}
|
|
5562
6866
|
async function init() {
|
|
5563
6867
|
console.log();
|
|
5564
|
-
console.log(
|
|
5565
|
-
console.log(
|
|
5566
|
-
console.log(
|
|
5567
|
-
console.log(
|
|
6868
|
+
console.log(import_picocolors2.default.cyan(" ╔═══════════════════════════════════╗"));
|
|
6869
|
+
console.log(import_picocolors2.default.cyan(" ║ ") + import_picocolors2.default.bold("create-svadmin") + import_picocolors2.default.cyan(" ║"));
|
|
6870
|
+
console.log(import_picocolors2.default.cyan(" ║ ") + import_picocolors2.default.dim("Headless Admin for Svelte 5") + import_picocolors2.default.cyan(" ║"));
|
|
6871
|
+
console.log(import_picocolors2.default.cyan(" ╚═══════════════════════════════════╝"));
|
|
5568
6872
|
console.log();
|
|
5569
|
-
const response = await
|
|
6873
|
+
const response = await import_prompts2.default([
|
|
5570
6874
|
{
|
|
5571
6875
|
type: "text",
|
|
5572
6876
|
name: "projectName",
|
|
@@ -5575,7 +6879,7 @@ async function init() {
|
|
|
5575
6879
|
validate: (value) => {
|
|
5576
6880
|
if (!value.trim())
|
|
5577
6881
|
return "Project name is required";
|
|
5578
|
-
if (
|
|
6882
|
+
if (fs2.existsSync(value.trim()) && fs2.readdirSync(value.trim()).length > 0) {
|
|
5579
6883
|
return "Directory already exists and is not empty";
|
|
5580
6884
|
}
|
|
5581
6885
|
return true;
|
|
@@ -5613,61 +6917,61 @@ async function init() {
|
|
|
5613
6917
|
}
|
|
5614
6918
|
]);
|
|
5615
6919
|
if (!response.projectName) {
|
|
5616
|
-
console.log(
|
|
6920
|
+
console.log(import_picocolors2.default.red(`
|
|
5617
6921
|
Operation cancelled.
|
|
5618
6922
|
`));
|
|
5619
6923
|
return;
|
|
5620
6924
|
}
|
|
5621
|
-
const projectDir =
|
|
5622
|
-
if (!
|
|
5623
|
-
|
|
6925
|
+
const projectDir = path2.resolve(process.cwd(), response.projectName.trim());
|
|
6926
|
+
if (!fs2.existsSync(projectDir)) {
|
|
6927
|
+
fs2.mkdirSync(projectDir, { recursive: true });
|
|
5624
6928
|
}
|
|
5625
6929
|
console.log(`
|
|
5626
|
-
${
|
|
6930
|
+
${import_picocolors2.default.bold("Scaffolding")} project in ${import_picocolors2.default.green(projectDir)}...
|
|
5627
6931
|
`);
|
|
5628
|
-
const templateDir =
|
|
5629
|
-
const guidanceDir =
|
|
6932
|
+
const templateDir = path2.join(__dirname2, "..", "template");
|
|
6933
|
+
const guidanceDir = path2.join(__dirname2, "..", "guidance");
|
|
5630
6934
|
const scaffoldManifest = loadShippedScaffoldManifest();
|
|
5631
6935
|
function copyDir(src, dest) {
|
|
5632
|
-
|
|
5633
|
-
const entries =
|
|
6936
|
+
fs2.mkdirSync(dest, { recursive: true });
|
|
6937
|
+
const entries = fs2.readdirSync(src, { withFileTypes: true });
|
|
5634
6938
|
for (const entry of entries) {
|
|
5635
|
-
const srcPath =
|
|
5636
|
-
const destPath =
|
|
6939
|
+
const srcPath = path2.join(src, entry.name);
|
|
6940
|
+
const destPath = path2.join(dest, entry.name === "_gitignore" ? ".gitignore" : entry.name);
|
|
5637
6941
|
if (entry.isDirectory()) {
|
|
5638
6942
|
copyDir(srcPath, destPath);
|
|
5639
6943
|
} else {
|
|
5640
|
-
|
|
6944
|
+
fs2.copyFileSync(srcPath, destPath);
|
|
5641
6945
|
}
|
|
5642
6946
|
}
|
|
5643
6947
|
}
|
|
5644
|
-
if (
|
|
6948
|
+
if (fs2.existsSync(templateDir)) {
|
|
5645
6949
|
copyDir(templateDir, projectDir);
|
|
5646
|
-
console.log(
|
|
6950
|
+
console.log(import_picocolors2.default.green(" ✔") + " Template files copied");
|
|
5647
6951
|
}
|
|
5648
|
-
if (
|
|
6952
|
+
if (fs2.existsSync(guidanceDir)) {
|
|
5649
6953
|
copyDir(guidanceDir, projectDir);
|
|
5650
|
-
console.log(
|
|
6954
|
+
console.log(import_picocolors2.default.green(" ✔") + " AI and design guidance copied");
|
|
5651
6955
|
}
|
|
5652
6956
|
const packageJson = createProjectPackageJson(scaffoldManifest, {
|
|
5653
6957
|
projectName: response.projectName,
|
|
5654
6958
|
dataProvider: response.dataProvider,
|
|
5655
6959
|
authProvider: response.authProvider
|
|
5656
6960
|
});
|
|
5657
|
-
|
|
6961
|
+
fs2.writeFileSync(path2.join(projectDir, "package.json"), `${JSON.stringify(packageJson, null, 2)}
|
|
5658
6962
|
`);
|
|
5659
|
-
console.log(
|
|
5660
|
-
|
|
6963
|
+
console.log(import_picocolors2.default.green(" ✔") + " package.json generated");
|
|
6964
|
+
fs2.writeFileSync(path2.join(projectDir, ".gitignore"), `node_modules
|
|
5661
6965
|
dist
|
|
5662
6966
|
.svelte-kit
|
|
5663
6967
|
.env
|
|
5664
6968
|
.env.local
|
|
5665
6969
|
*.local
|
|
5666
6970
|
`);
|
|
5667
|
-
console.log(
|
|
6971
|
+
console.log(import_picocolors2.default.green(" ✔") + " .gitignore generated");
|
|
5668
6972
|
const dpLabel = response.dataProvider === "simple-rest" ? "Simple REST" : response.dataProvider === "supabase" ? "Supabase" : response.dataProvider === "graphql" ? "GraphQL" : "Custom";
|
|
5669
6973
|
const authLabel = response.authProvider === "mock" ? "Mock (demo)" : response.authProvider === "jwt" ? "JWT" : response.authProvider === "supabase" ? "Supabase Auth" : "None";
|
|
5670
|
-
|
|
6974
|
+
fs2.writeFileSync(path2.join(projectDir, "README.md"), `# ${response.projectName}
|
|
5671
6975
|
|
|
5672
6976
|
Built with [svadmin](https://github.com/vibeunion/svadmin) — Headless Admin Framework for Svelte 5.
|
|
5673
6977
|
|
|
@@ -5685,30 +6989,30 @@ bun run dev
|
|
|
5685
6989
|
- **Auth**: ${authLabel}
|
|
5686
6990
|
- **State**: TanStack Query v6
|
|
5687
6991
|
`);
|
|
5688
|
-
console.log(
|
|
6992
|
+
console.log(import_picocolors2.default.green(" ✔") + " README.md generated");
|
|
5689
6993
|
if (response.installDeps) {
|
|
5690
6994
|
console.log(`
|
|
5691
|
-
${
|
|
6995
|
+
${import_picocolors2.default.bold("Installing dependencies...")}
|
|
5692
6996
|
`);
|
|
5693
6997
|
const bunInstall = spawnSync("bun", ["install"], { cwd: projectDir, stdio: "inherit" });
|
|
5694
6998
|
if (bunInstall.status !== 0) {
|
|
5695
6999
|
const npmInstall = spawnSync("npm", ["install"], { cwd: projectDir, stdio: "inherit" });
|
|
5696
7000
|
if (npmInstall.status !== 0) {
|
|
5697
|
-
console.log(
|
|
7001
|
+
console.log(import_picocolors2.default.yellow("\n ⚠ Auto-install failed. Run `bun install` or `npm install` manually."));
|
|
5698
7002
|
}
|
|
5699
7003
|
}
|
|
5700
7004
|
}
|
|
5701
7005
|
console.log();
|
|
5702
|
-
console.log(
|
|
7006
|
+
console.log(import_picocolors2.default.green(import_picocolors2.default.bold(" ✔ Project ready!")));
|
|
5703
7007
|
console.log();
|
|
5704
7008
|
console.log(" Next steps:");
|
|
5705
|
-
console.log(` ${
|
|
7009
|
+
console.log(` ${import_picocolors2.default.cyan(`cd ${response.projectName}`)}`);
|
|
5706
7010
|
if (!response.installDeps) {
|
|
5707
|
-
console.log(` ${
|
|
7011
|
+
console.log(` ${import_picocolors2.default.cyan("bun install")}`);
|
|
5708
7012
|
}
|
|
5709
|
-
console.log(` ${
|
|
7013
|
+
console.log(` ${import_picocolors2.default.cyan("bun run dev")}`);
|
|
5710
7014
|
console.log();
|
|
5711
|
-
console.log(` Docs: ${
|
|
7015
|
+
console.log(` Docs: ${import_picocolors2.default.blue("https://github.com/vibeunion/svadmin")}`);
|
|
5712
7016
|
console.log();
|
|
5713
7017
|
}
|
|
5714
7018
|
var EJECT_COMPONENTS = [
|
|
@@ -5740,64 +7044,64 @@ var EJECT_COMPONENTS = [
|
|
|
5740
7044
|
];
|
|
5741
7045
|
async function eject(args) {
|
|
5742
7046
|
console.log();
|
|
5743
|
-
console.log(
|
|
7047
|
+
console.log(import_picocolors2.default.cyan(" svadmin eject") + import_picocolors2.default.dim(" — copy internal components for deep customization"));
|
|
5744
7048
|
console.log();
|
|
5745
7049
|
const requested = args.filter((a) => !a.startsWith("-"));
|
|
5746
7050
|
const toEject = requested.length > 0 ? requested.filter((name) => {
|
|
5747
7051
|
if (!EJECT_COMPONENTS.includes(name)) {
|
|
5748
|
-
console.log(
|
|
7052
|
+
console.log(import_picocolors2.default.yellow(` ⚠ Unknown component: ${name} (skipped)`));
|
|
5749
7053
|
return false;
|
|
5750
7054
|
}
|
|
5751
7055
|
return true;
|
|
5752
7056
|
}) : [...EJECT_COMPONENTS];
|
|
5753
7057
|
if (toEject.length === 0) {
|
|
5754
|
-
console.log(
|
|
7058
|
+
console.log(import_picocolors2.default.red(" No valid components to eject."));
|
|
5755
7059
|
console.log(` Available: ${EJECT_COMPONENTS.join(", ")}`);
|
|
5756
7060
|
return;
|
|
5757
7061
|
}
|
|
5758
7062
|
let uiSrcDir;
|
|
5759
7063
|
try {
|
|
5760
7064
|
const require2 = createRequire2(import.meta.url);
|
|
5761
|
-
const uiPkg =
|
|
5762
|
-
uiSrcDir =
|
|
7065
|
+
const uiPkg = path2.dirname(require2.resolve("@svadmin/ui/package.json"));
|
|
7066
|
+
uiSrcDir = path2.join(uiPkg, "src", "components");
|
|
5763
7067
|
} catch {
|
|
5764
|
-
const nm =
|
|
5765
|
-
if (
|
|
7068
|
+
const nm = path2.join(process.cwd(), "node_modules", "@svadmin", "ui", "src", "components");
|
|
7069
|
+
if (fs2.existsSync(nm)) {
|
|
5766
7070
|
uiSrcDir = nm;
|
|
5767
7071
|
} else {
|
|
5768
|
-
console.log(
|
|
7072
|
+
console.log(import_picocolors2.default.red(" ✗ Cannot find @svadmin/ui. Run `bun install` first."));
|
|
5769
7073
|
return;
|
|
5770
7074
|
}
|
|
5771
7075
|
}
|
|
5772
|
-
const destDir =
|
|
5773
|
-
|
|
7076
|
+
const destDir = path2.join(process.cwd(), "src", "components", "svadmin");
|
|
7077
|
+
fs2.mkdirSync(destDir, { recursive: true });
|
|
5774
7078
|
let copied = 0;
|
|
5775
7079
|
for (const name of toEject) {
|
|
5776
|
-
const srcFile =
|
|
5777
|
-
const srcFileAlt =
|
|
5778
|
-
const src =
|
|
7080
|
+
const srcFile = path2.join(uiSrcDir, `${name}.svelte`);
|
|
7081
|
+
const srcFileAlt = path2.join(uiSrcDir, "fields", `${name}.svelte`);
|
|
7082
|
+
const src = fs2.existsSync(srcFile) ? srcFile : fs2.existsSync(srcFileAlt) ? srcFileAlt : null;
|
|
5779
7083
|
if (!src) {
|
|
5780
|
-
console.log(
|
|
7084
|
+
console.log(import_picocolors2.default.yellow(` ⚠ ${name}.svelte not found in @svadmin/ui (skipped)`));
|
|
5781
7085
|
continue;
|
|
5782
7086
|
}
|
|
5783
|
-
let content =
|
|
7087
|
+
let content = fs2.readFileSync(src, "utf-8");
|
|
5784
7088
|
content = content.replace(/from\s+['"]\.\/ui\//g, "from '@svadmin/ui/components/ui/");
|
|
5785
7089
|
content = content.replace(/from\s+['"]\.\/((?!ui\/)[^'"]+)['"]/g, "from './$1'");
|
|
5786
|
-
const destFile =
|
|
5787
|
-
|
|
5788
|
-
console.log(
|
|
7090
|
+
const destFile = path2.join(destDir, `${name}.svelte`);
|
|
7091
|
+
fs2.writeFileSync(destFile, content);
|
|
7092
|
+
console.log(import_picocolors2.default.green(" ✔") + ` ${name}.svelte → src/components/svadmin/`);
|
|
5789
7093
|
copied++;
|
|
5790
7094
|
}
|
|
5791
7095
|
console.log();
|
|
5792
7096
|
if (copied > 0) {
|
|
5793
|
-
console.log(
|
|
7097
|
+
console.log(import_picocolors2.default.green(import_picocolors2.default.bold(` ✔ Ejected ${copied} component(s)`)));
|
|
5794
7098
|
console.log();
|
|
5795
7099
|
console.log(" Usage: import overrides in your AdminApp and pass via `components` prop:");
|
|
5796
7100
|
console.log();
|
|
5797
|
-
console.log(
|
|
5798
|
-
console.log(
|
|
7101
|
+
console.log(import_picocolors2.default.dim(' import CustomLayout from "./components/svadmin/Layout.svelte";'));
|
|
7102
|
+
console.log(import_picocolors2.default.dim(" <AdminApp components={{ Layout: CustomLayout }} ... />"));
|
|
5799
7103
|
} else {
|
|
5800
|
-
console.log(
|
|
7104
|
+
console.log(import_picocolors2.default.yellow(" No components were ejected."));
|
|
5801
7105
|
}
|
|
5802
7106
|
console.log();
|
|
5803
7107
|
}
|
|
@@ -5805,7 +7109,7 @@ var [, , subcommand, ...rest] = process.argv;
|
|
|
5805
7109
|
var runCommand = (command) => {
|
|
5806
7110
|
Promise.resolve().then(command).catch((error) => {
|
|
5807
7111
|
const message = error instanceof Error ? error.message : String(error);
|
|
5808
|
-
console.error(
|
|
7112
|
+
console.error(import_picocolors2.default.red(`
|
|
5809
7113
|
✗ ${message}
|
|
5810
7114
|
`));
|
|
5811
7115
|
process.exitCode = 2;
|
|
@@ -5819,6 +7123,8 @@ if (subcommand === "eject") {
|
|
|
5819
7123
|
runCommand(() => upgrade(rest));
|
|
5820
7124
|
} else if (subcommand === "guidance") {
|
|
5821
7125
|
runCommand(() => guidance(rest));
|
|
7126
|
+
} else if (subcommand === "infer") {
|
|
7127
|
+
runCommand(() => inferCommand(rest));
|
|
5822
7128
|
} else {
|
|
5823
7129
|
runCommand(init);
|
|
5824
7130
|
}
|