@svadmin/create 0.18.1 → 0.21.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 +45 -0
- package/dist/index.js +1666 -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 fs3 from "node:fs";
|
|
5029
|
+
import path3 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,1571 @@ 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
|
+
|
|
6434
|
+
// src/lite-init.ts
|
|
6435
|
+
import fs2 from "node:fs";
|
|
6436
|
+
import path2 from "node:path";
|
|
6437
|
+
var GENERATED_FILES = {
|
|
6438
|
+
"src/lib/svadmin-lite.ts": `import { dataProvider, resources } from '$lib/admin';
|
|
6439
|
+
import type { ResourceDefinition } from '@svadmin/core';
|
|
6440
|
+
|
|
6441
|
+
export { dataProvider, resources };
|
|
6442
|
+
|
|
6443
|
+
export function getResource(name: string): ResourceDefinition | undefined {
|
|
6444
|
+
return resources.find((resource) => resource.name === name);
|
|
6445
|
+
}
|
|
6446
|
+
`,
|
|
6447
|
+
"src/routes/lite/+layout.ts": `export const ssr = true;
|
|
6448
|
+
export const csr = false;
|
|
6449
|
+
`,
|
|
6450
|
+
"src/routes/lite/+layout.server.ts": `import { resources } from '$lib/svadmin-lite';
|
|
6451
|
+
import type { LayoutServerLoad } from './$types';
|
|
6452
|
+
|
|
6453
|
+
export const load = (({ url }) => {
|
|
6454
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
6455
|
+
const currentResource = segments[1] ?? '';
|
|
6456
|
+
|
|
6457
|
+
return { resources, currentResource };
|
|
6458
|
+
}) satisfies LayoutServerLoad;
|
|
6459
|
+
`,
|
|
6460
|
+
"src/routes/lite/+layout.svelte": `<script lang="ts">
|
|
6461
|
+
import type { Snippet } from 'svelte';
|
|
6462
|
+
import { LiteLayout } from '@svadmin/lite';
|
|
6463
|
+
import '@svadmin/lite/lite.css';
|
|
6464
|
+
import type { LayoutData } from './$types';
|
|
6465
|
+
|
|
6466
|
+
let { data, children }: { data: LayoutData; children: Snippet } = $props();
|
|
6467
|
+
</script>
|
|
6468
|
+
|
|
6469
|
+
<LiteLayout
|
|
6470
|
+
resources={data.resources}
|
|
6471
|
+
currentResource={data.currentResource}
|
|
6472
|
+
brandName="Lite Admin"
|
|
6473
|
+
basePath="/lite"
|
|
6474
|
+
>
|
|
6475
|
+
{@render children()}
|
|
6476
|
+
</LiteLayout>
|
|
6477
|
+
`,
|
|
6478
|
+
"src/routes/lite/+page.server.ts": `import { error, redirect } from '@sveltejs/kit';
|
|
6479
|
+
import { resources } from '$lib/svadmin-lite';
|
|
6480
|
+
import type { PageServerLoad } from './$types';
|
|
6481
|
+
|
|
6482
|
+
export const load = (() => {
|
|
6483
|
+
const firstResource = resources[0];
|
|
6484
|
+
if (!firstResource) throw error(404, 'No Lite resources configured');
|
|
6485
|
+
throw redirect(302, \`/lite/\${firstResource.name}\`);
|
|
6486
|
+
}) satisfies PageServerLoad;
|
|
6487
|
+
`,
|
|
6488
|
+
"src/routes/lite/[resource]/+page.server.ts": `import { error } from '@sveltejs/kit';
|
|
6489
|
+
import { createCrudActions, createListLoader } from '@svadmin/lite';
|
|
6490
|
+
import { dataProvider, getResource } from '$lib/svadmin-lite';
|
|
6491
|
+
import type { Actions, PageServerLoad } from './$types';
|
|
6492
|
+
|
|
6493
|
+
export const load = ((event) => {
|
|
6494
|
+
const resource = getResource(event.params.resource);
|
|
6495
|
+
if (!resource) throw error(404, \`Resource "\${event.params.resource}" not found\`);
|
|
6496
|
+
return createListLoader(dataProvider, resource)(event);
|
|
6497
|
+
}) satisfies PageServerLoad;
|
|
6498
|
+
|
|
6499
|
+
export const actions = {
|
|
6500
|
+
delete: (event) => {
|
|
6501
|
+
const resource = getResource(event.params.resource);
|
|
6502
|
+
if (!resource) throw error(404, \`Resource "\${event.params.resource}" not found\`);
|
|
6503
|
+
return createCrudActions(dataProvider, resource).delete(event);
|
|
6504
|
+
},
|
|
6505
|
+
batchDelete: (event) => {
|
|
6506
|
+
const resource = getResource(event.params.resource);
|
|
6507
|
+
if (!resource) throw error(404, \`Resource "\${event.params.resource}" not found\`);
|
|
6508
|
+
return createCrudActions(dataProvider, resource).batchDelete(event);
|
|
6509
|
+
},
|
|
6510
|
+
} satisfies Actions;
|
|
6511
|
+
`,
|
|
6512
|
+
"src/routes/lite/[resource]/+page.svelte": `<script lang="ts">
|
|
6513
|
+
import { LiteListPage } from '@svadmin/lite';
|
|
6514
|
+
import type { PageProps } from './$types';
|
|
6515
|
+
|
|
6516
|
+
let { data }: PageProps = $props();
|
|
6517
|
+
</script>
|
|
6518
|
+
|
|
6519
|
+
<LiteListPage {...data} basePath="/lite" />
|
|
6520
|
+
`,
|
|
6521
|
+
"src/routes/lite/[resource]/create/+page.server.ts": `import { error, redirect } from '@sveltejs/kit';
|
|
6522
|
+
import { createCrudActions } from '@svadmin/lite';
|
|
6523
|
+
import { dataProvider, getResource } from '$lib/svadmin-lite';
|
|
6524
|
+
import type { Actions, PageServerLoad } from './$types';
|
|
6525
|
+
|
|
6526
|
+
export const load = (({ params }) => {
|
|
6527
|
+
const resource = getResource(params.resource);
|
|
6528
|
+
if (!resource) throw error(404, \`Resource "\${params.resource}" not found\`);
|
|
6529
|
+
return { resource };
|
|
6530
|
+
}) satisfies PageServerLoad;
|
|
6531
|
+
|
|
6532
|
+
export const actions = {
|
|
6533
|
+
create: async (event) => {
|
|
6534
|
+
const resource = getResource(event.params.resource);
|
|
6535
|
+
if (!resource) throw error(404, \`Resource "\${event.params.resource}" not found\`);
|
|
6536
|
+
const result = await createCrudActions(dataProvider, resource).create(event);
|
|
6537
|
+
if (result && 'success' in result && result.success) {
|
|
6538
|
+
throw redirect(303, \`/lite/\${resource.name}\`);
|
|
6539
|
+
}
|
|
6540
|
+
return result;
|
|
6541
|
+
},
|
|
6542
|
+
} satisfies Actions;
|
|
6543
|
+
`,
|
|
6544
|
+
"src/routes/lite/[resource]/create/+page.svelte": `<script lang="ts">
|
|
6545
|
+
import { LiteCreatePage } from '@svadmin/lite';
|
|
6546
|
+
import type { PageProps } from './$types';
|
|
6547
|
+
|
|
6548
|
+
let { data, form }: PageProps = $props();
|
|
6549
|
+
</script>
|
|
6550
|
+
|
|
6551
|
+
<LiteCreatePage
|
|
6552
|
+
resource={data.resource}
|
|
6553
|
+
errors={form?.errors}
|
|
6554
|
+
values={form?.values}
|
|
6555
|
+
basePath="/lite"
|
|
6556
|
+
/>
|
|
6557
|
+
`,
|
|
6558
|
+
"src/routes/lite/[resource]/show/[id]/+page.server.ts": `import { error } from '@sveltejs/kit';
|
|
6559
|
+
import { createDetailLoader } from '@svadmin/lite';
|
|
6560
|
+
import { dataProvider, getResource } from '$lib/svadmin-lite';
|
|
6561
|
+
import type { PageServerLoad } from './$types';
|
|
6562
|
+
|
|
6563
|
+
export const load = ((event) => {
|
|
6564
|
+
const resource = getResource(event.params.resource);
|
|
6565
|
+
if (!resource) throw error(404, \`Resource "\${event.params.resource}" not found\`);
|
|
6566
|
+
return createDetailLoader(dataProvider, resource)(event);
|
|
6567
|
+
}) satisfies PageServerLoad;
|
|
6568
|
+
`,
|
|
6569
|
+
"src/routes/lite/[resource]/show/[id]/+page.svelte": `<script lang="ts">
|
|
6570
|
+
import { LiteShowPage } from '@svadmin/lite';
|
|
6571
|
+
import type { PageProps } from './$types';
|
|
6572
|
+
|
|
6573
|
+
let { data }: PageProps = $props();
|
|
6574
|
+
</script>
|
|
6575
|
+
|
|
6576
|
+
<LiteShowPage resource={data.resource} record={data.record} basePath="/lite" />
|
|
6577
|
+
`,
|
|
6578
|
+
"src/routes/lite/[resource]/edit/[id]/+page.server.ts": `import { error, redirect } from '@sveltejs/kit';
|
|
6579
|
+
import { createCrudActions, createDetailLoader } from '@svadmin/lite';
|
|
6580
|
+
import { dataProvider, getResource } from '$lib/svadmin-lite';
|
|
6581
|
+
import type { Actions, PageServerLoad } from './$types';
|
|
6582
|
+
|
|
6583
|
+
export const load = ((event) => {
|
|
6584
|
+
const resource = getResource(event.params.resource);
|
|
6585
|
+
if (!resource) throw error(404, \`Resource "\${event.params.resource}" not found\`);
|
|
6586
|
+
return createDetailLoader(dataProvider, resource)(event);
|
|
6587
|
+
}) satisfies PageServerLoad;
|
|
6588
|
+
|
|
6589
|
+
export const actions = {
|
|
6590
|
+
update: async (event) => {
|
|
6591
|
+
const resource = getResource(event.params.resource);
|
|
6592
|
+
if (!resource) throw error(404, \`Resource "\${event.params.resource}" not found\`);
|
|
6593
|
+
const result = await createCrudActions(dataProvider, resource).update(event);
|
|
6594
|
+
if (result && 'success' in result && result.success) {
|
|
6595
|
+
throw redirect(303, \`/lite/\${resource.name}/show/\${event.params.id}\`);
|
|
6596
|
+
}
|
|
6597
|
+
return result;
|
|
6598
|
+
},
|
|
6599
|
+
delete: (event) => {
|
|
6600
|
+
const resource = getResource(event.params.resource);
|
|
6601
|
+
if (!resource) throw error(404, \`Resource "\${event.params.resource}" not found\`);
|
|
6602
|
+
return createCrudActions(dataProvider, resource).delete(event);
|
|
6603
|
+
},
|
|
6604
|
+
} satisfies Actions;
|
|
6605
|
+
`,
|
|
6606
|
+
"src/routes/lite/[resource]/edit/[id]/+page.svelte": `<script lang="ts">
|
|
6607
|
+
import { LiteEditPage } from '@svadmin/lite';
|
|
6608
|
+
import type { PageProps } from './$types';
|
|
6609
|
+
|
|
6610
|
+
let { data, form }: PageProps = $props();
|
|
6611
|
+
</script>
|
|
6612
|
+
|
|
6613
|
+
<LiteEditPage
|
|
6614
|
+
resource={data.resource}
|
|
6615
|
+
record={data.record}
|
|
6616
|
+
errors={form?.errors}
|
|
6617
|
+
basePath="/lite"
|
|
6618
|
+
/>
|
|
6619
|
+
`
|
|
6620
|
+
};
|
|
6621
|
+
function parseLiteInitArguments(args) {
|
|
6622
|
+
let write = false;
|
|
6623
|
+
const positional = [];
|
|
6624
|
+
for (const argument of args) {
|
|
6625
|
+
if (argument === "--write")
|
|
6626
|
+
write = true;
|
|
6627
|
+
else if (argument.startsWith("-"))
|
|
6628
|
+
throw new Error(`Unknown option: ${argument}`);
|
|
6629
|
+
else
|
|
6630
|
+
positional.push(argument);
|
|
6631
|
+
}
|
|
6632
|
+
if (positional.length > 1) {
|
|
6633
|
+
throw new Error(`Expected at most one project directory, received: ${positional.join(", ")}`);
|
|
6634
|
+
}
|
|
6635
|
+
return {
|
|
6636
|
+
projectDirectory: path2.resolve(process.cwd(), positional[0] ?? "."),
|
|
6637
|
+
write
|
|
6638
|
+
};
|
|
6639
|
+
}
|
|
6640
|
+
function assertLiteProject(projectDirectory) {
|
|
6641
|
+
if (!fs2.existsSync(projectDirectory)) {
|
|
6642
|
+
throw new Error(`Project directory does not exist: ${projectDirectory}`);
|
|
6643
|
+
}
|
|
6644
|
+
if (!fs2.existsSync(path2.join(projectDirectory, "package.json"))) {
|
|
6645
|
+
throw new Error(`Not a Node project: ${path2.join(projectDirectory, "package.json")} is missing`);
|
|
6646
|
+
}
|
|
6647
|
+
if (!fs2.existsSync(path2.join(projectDirectory, "src", "routes"))) {
|
|
6648
|
+
throw new Error("Lite routes require a SvelteKit project with src/routes. Keep the existing SPA and add a SvelteKit Lite app alongside it.");
|
|
6649
|
+
}
|
|
6650
|
+
const adminModuleExists = ["ts", "js", "svelte"].some((extension) => fs2.existsSync(path2.join(projectDirectory, "src", "lib", `admin.${extension}`)));
|
|
6651
|
+
if (!adminModuleExists) {
|
|
6652
|
+
throw new Error("Lite routes require src/lib/admin.ts (or .js/.svelte) exporting resources and dataProvider.");
|
|
6653
|
+
}
|
|
6654
|
+
}
|
|
6655
|
+
function planLiteInit(projectDirectory) {
|
|
6656
|
+
assertLiteProject(projectDirectory);
|
|
6657
|
+
const entries = Object.entries(GENERATED_FILES).map(([relativePath, content]) => {
|
|
6658
|
+
const filePath = path2.join(projectDirectory, relativePath);
|
|
6659
|
+
return { filePath, relativePath, content, exists: fs2.existsSync(filePath) };
|
|
6660
|
+
});
|
|
6661
|
+
return { projectDirectory, entries };
|
|
6662
|
+
}
|
|
6663
|
+
function writeLiteInit(plan) {
|
|
6664
|
+
const written = [];
|
|
6665
|
+
const preserved = [];
|
|
6666
|
+
for (const entry of plan.entries) {
|
|
6667
|
+
if (entry.exists || fs2.existsSync(entry.filePath)) {
|
|
6668
|
+
preserved.push(entry.relativePath);
|
|
6669
|
+
continue;
|
|
6670
|
+
}
|
|
6671
|
+
fs2.mkdirSync(path2.dirname(entry.filePath), { recursive: true });
|
|
6672
|
+
fs2.writeFileSync(entry.filePath, entry.content);
|
|
6673
|
+
written.push(entry.relativePath);
|
|
6674
|
+
}
|
|
6675
|
+
return { plan, written, preserved };
|
|
6676
|
+
}
|
|
6677
|
+
function liteInitCommand(args) {
|
|
6678
|
+
const options = parseLiteInitArguments(args);
|
|
6679
|
+
const plan = planLiteInit(options.projectDirectory);
|
|
6680
|
+
console.log(`
|
|
6681
|
+
svadmin lite init — ${options.projectDirectory}`);
|
|
6682
|
+
for (const entry of plan.entries) {
|
|
6683
|
+
console.log(` ${entry.exists ? "preserve" : "add"} ${entry.relativePath}`);
|
|
6684
|
+
}
|
|
6685
|
+
if (!options.write) {
|
|
6686
|
+
console.log(`
|
|
6687
|
+
Dry run only; re-run with --write to add missing Lite routes.`);
|
|
6688
|
+
return;
|
|
6689
|
+
}
|
|
6690
|
+
const result = writeLiteInit(plan);
|
|
6691
|
+
console.log(`
|
|
6692
|
+
Written ${result.written.length} file(s); preserved ${result.preserved.length} existing file(s).`);
|
|
6693
|
+
}
|
|
6694
|
+
|
|
5130
6695
|
// src/project-maintenance.ts
|
|
5131
6696
|
import {
|
|
5132
6697
|
constants,
|
|
@@ -5136,10 +6701,10 @@ import {
|
|
|
5136
6701
|
unlinkSync,
|
|
5137
6702
|
writeFileSync
|
|
5138
6703
|
} from "node:fs";
|
|
5139
|
-
function parseDependencyMap(candidate,
|
|
6704
|
+
function parseDependencyMap(candidate, path3) {
|
|
5140
6705
|
if (candidate === undefined)
|
|
5141
6706
|
return;
|
|
5142
|
-
assertStringRecord(candidate,
|
|
6707
|
+
assertStringRecord(candidate, path3);
|
|
5143
6708
|
return { ...candidate };
|
|
5144
6709
|
}
|
|
5145
6710
|
function parseMaintainedPackageJson(packageJsonCandidate) {
|
|
@@ -5415,15 +6980,15 @@ function writeProjectPackageJsonUpgrade(packagePath, scaffold, backupDate) {
|
|
|
5415
6980
|
|
|
5416
6981
|
// src/index.ts
|
|
5417
6982
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
5418
|
-
var __dirname2 =
|
|
6983
|
+
var __dirname2 = path3.dirname(__filename2);
|
|
5419
6984
|
function loadShippedScaffoldManifest() {
|
|
5420
|
-
return loadScaffoldManifest(
|
|
6985
|
+
return loadScaffoldManifest(path3.join(__dirname2, "..", "scaffold-manifest.json"));
|
|
5421
6986
|
}
|
|
5422
6987
|
function projectDirectoryFromArguments(positional) {
|
|
5423
6988
|
if (positional.length > 1) {
|
|
5424
6989
|
throw new Error(`Expected at most one project directory, received: ${positional.join(", ")}`);
|
|
5425
6990
|
}
|
|
5426
|
-
return
|
|
6991
|
+
return path3.resolve(process.cwd(), positional[0] ?? ".");
|
|
5427
6992
|
}
|
|
5428
6993
|
function doctorProjectDirectory(args) {
|
|
5429
6994
|
const unknownOption = args.find((argument) => argument.startsWith("-"));
|
|
@@ -5459,52 +7024,52 @@ function upgradeChangeMessage(change) {
|
|
|
5459
7024
|
return `update ${change.packageName} from ${change.from ?? "missing"} to ${change.to}`;
|
|
5460
7025
|
}
|
|
5461
7026
|
function printDoctorIssue(issue) {
|
|
5462
|
-
const marker = issue.kind === "drift" || issue.kind === "section" ?
|
|
7027
|
+
const marker = issue.kind === "drift" || issue.kind === "section" ? import_picocolors2.default.yellow(" ⚠") : import_picocolors2.default.red(" ✗");
|
|
5463
7028
|
console.log(`${marker} ${doctorIssueMessage(issue)}`);
|
|
5464
|
-
console.log(
|
|
7029
|
+
console.log(import_picocolors2.default.dim(` → ${issue.action}`));
|
|
5465
7030
|
}
|
|
5466
7031
|
function printDoctorReport(report, projectDirectory) {
|
|
5467
7032
|
console.log();
|
|
5468
|
-
console.log(
|
|
7033
|
+
console.log(import_picocolors2.default.bold(`svadmin doctor — ${projectDirectory}`));
|
|
5469
7034
|
if (report.status === "clean") {
|
|
5470
|
-
console.log(
|
|
7035
|
+
console.log(import_picocolors2.default.green(" ✔ Dependencies match the shipped svadmin scaffold."));
|
|
5471
7036
|
} else {
|
|
5472
7037
|
for (const issue of report.issues)
|
|
5473
7038
|
printDoctorIssue(issue);
|
|
5474
7039
|
console.log();
|
|
5475
|
-
console.log(
|
|
7040
|
+
console.log(import_picocolors2.default.yellow(` ${report.issues.length} actionable issue(s) found.`));
|
|
5476
7041
|
}
|
|
5477
7042
|
console.log();
|
|
5478
7043
|
}
|
|
5479
7044
|
function doctor(args) {
|
|
5480
7045
|
const projectDirectory = doctorProjectDirectory(args);
|
|
5481
|
-
const project = readMaintainedPackageJson(
|
|
7046
|
+
const project = readMaintainedPackageJson(path3.join(projectDirectory, "package.json"));
|
|
5482
7047
|
const report = doctorProjectPackageJson(project, loadShippedScaffoldManifest());
|
|
5483
7048
|
printDoctorReport(report, projectDirectory);
|
|
5484
7049
|
process.exitCode = report.exitCode;
|
|
5485
7050
|
}
|
|
5486
7051
|
function printUpgradeChanges(upgradeExecution) {
|
|
5487
7052
|
for (const change of upgradeExecution.plan.changes) {
|
|
5488
|
-
console.log(` ${
|
|
7053
|
+
console.log(` ${import_picocolors2.default.cyan("•")} ${upgradeChangeMessage(change)}`);
|
|
5489
7054
|
}
|
|
5490
7055
|
console.log();
|
|
5491
7056
|
}
|
|
5492
7057
|
function printUpgradeOutcome(upgradeExecution, packagePath) {
|
|
5493
7058
|
if (upgradeExecution.wrote) {
|
|
5494
|
-
console.log(
|
|
5495
|
-
console.log(` Backup: ${
|
|
5496
|
-
console.log(` Restore by copying the backup over: ${
|
|
7059
|
+
console.log(import_picocolors2.default.green(" ✔ package.json updated."));
|
|
7060
|
+
console.log(` Backup: ${import_picocolors2.default.cyan(upgradeExecution.backupPath)}`);
|
|
7061
|
+
console.log(` Restore by copying the backup over: ${import_picocolors2.default.cyan(packagePath)}`);
|
|
5497
7062
|
} else {
|
|
5498
|
-
console.log(
|
|
7063
|
+
console.log(import_picocolors2.default.yellow(" Dry run only; package.json was not changed."));
|
|
5499
7064
|
console.log(" Re-run this command with --write to apply the plan.");
|
|
5500
7065
|
}
|
|
5501
7066
|
console.log();
|
|
5502
7067
|
}
|
|
5503
7068
|
function printUpgradeExecution(upgradeExecution, projectDirectory, packagePath) {
|
|
5504
7069
|
console.log();
|
|
5505
|
-
console.log(
|
|
7070
|
+
console.log(import_picocolors2.default.bold(`svadmin upgrade — ${projectDirectory}`));
|
|
5506
7071
|
if (upgradeExecution.plan.changes.length === 0) {
|
|
5507
|
-
console.log(
|
|
7072
|
+
console.log(import_picocolors2.default.green(" ✔ package.json already matches the shipped scaffold."));
|
|
5508
7073
|
console.log();
|
|
5509
7074
|
return;
|
|
5510
7075
|
}
|
|
@@ -5513,60 +7078,60 @@ function printUpgradeExecution(upgradeExecution, projectDirectory, packagePath)
|
|
|
5513
7078
|
}
|
|
5514
7079
|
function upgrade(args) {
|
|
5515
7080
|
const commandArguments = parseUpgradeArguments(args);
|
|
5516
|
-
const packagePath =
|
|
7081
|
+
const packagePath = path3.join(commandArguments.projectDirectory, "package.json");
|
|
5517
7082
|
const scaffoldManifest = loadShippedScaffoldManifest();
|
|
5518
7083
|
const upgradeExecution = commandArguments.write ? writeProjectPackageJsonUpgrade(packagePath, scaffoldManifest, new Date) : planProjectPackageFileUpgrade(packagePath, scaffoldManifest);
|
|
5519
7084
|
printUpgradeExecution(upgradeExecution, commandArguments.projectDirectory, packagePath);
|
|
5520
7085
|
}
|
|
5521
7086
|
var GUIDANCE_FILES = ["DESIGN.md", "AGENTS.md"];
|
|
5522
7087
|
function missingGuidanceFiles(projectDirectory) {
|
|
5523
|
-
return GUIDANCE_FILES.filter((fileName) => !
|
|
7088
|
+
return GUIDANCE_FILES.filter((fileName) => !fs3.existsSync(path3.join(projectDirectory, fileName)));
|
|
5524
7089
|
}
|
|
5525
7090
|
function printGuidancePlan(projectDirectory, missingFiles) {
|
|
5526
7091
|
console.log();
|
|
5527
|
-
console.log(
|
|
7092
|
+
console.log(import_picocolors2.default.bold(`svadmin guidance — ${projectDirectory}`));
|
|
5528
7093
|
for (const fileName of missingFiles) {
|
|
5529
|
-
console.log(` ${
|
|
7094
|
+
console.log(` ${import_picocolors2.default.cyan("•")} add ${fileName}`);
|
|
5530
7095
|
}
|
|
5531
7096
|
console.log();
|
|
5532
7097
|
}
|
|
5533
7098
|
function installMissingGuidanceFiles(guidanceDirectory, projectDirectory, missingFiles) {
|
|
5534
7099
|
for (const fileName of missingFiles) {
|
|
5535
|
-
|
|
7100
|
+
fs3.copyFileSync(path3.join(guidanceDirectory, fileName), path3.join(projectDirectory, fileName));
|
|
5536
7101
|
}
|
|
5537
7102
|
}
|
|
5538
7103
|
function guidance(args) {
|
|
5539
7104
|
const { projectDirectory, write } = parseUpgradeArguments(args);
|
|
5540
|
-
const guidanceDirectory =
|
|
5541
|
-
if (!
|
|
7105
|
+
const guidanceDirectory = path3.join(__dirname2, "..", "guidance");
|
|
7106
|
+
if (!fs3.existsSync(projectDirectory))
|
|
5542
7107
|
throw new Error(`Project directory does not exist: ${projectDirectory}`);
|
|
5543
|
-
if (!
|
|
7108
|
+
if (!fs3.existsSync(guidanceDirectory))
|
|
5544
7109
|
throw new Error("Shipped svadmin guidance files are missing");
|
|
5545
7110
|
const missingFiles = missingGuidanceFiles(projectDirectory);
|
|
5546
7111
|
if (missingFiles.length === 0) {
|
|
5547
|
-
console.log(
|
|
7112
|
+
console.log(import_picocolors2.default.green(`
|
|
5548
7113
|
✔ DESIGN.md and AGENTS.md already exist; nothing was changed.
|
|
5549
7114
|
`));
|
|
5550
7115
|
return;
|
|
5551
7116
|
}
|
|
5552
7117
|
printGuidancePlan(projectDirectory, missingFiles);
|
|
5553
7118
|
if (!write) {
|
|
5554
|
-
console.log(
|
|
7119
|
+
console.log(import_picocolors2.default.yellow(` Dry run only; re-run with --write to add missing guidance files.
|
|
5555
7120
|
`));
|
|
5556
7121
|
return;
|
|
5557
7122
|
}
|
|
5558
7123
|
installMissingGuidanceFiles(guidanceDirectory, projectDirectory, missingFiles);
|
|
5559
|
-
console.log(
|
|
7124
|
+
console.log(import_picocolors2.default.green(` ✔ Added ${missingFiles.length} guidance file(s); existing files were preserved.`));
|
|
5560
7125
|
console.log();
|
|
5561
7126
|
}
|
|
5562
7127
|
async function init() {
|
|
5563
7128
|
console.log();
|
|
5564
|
-
console.log(
|
|
5565
|
-
console.log(
|
|
5566
|
-
console.log(
|
|
5567
|
-
console.log(
|
|
7129
|
+
console.log(import_picocolors2.default.cyan(" ╔═══════════════════════════════════╗"));
|
|
7130
|
+
console.log(import_picocolors2.default.cyan(" ║ ") + import_picocolors2.default.bold("create-svadmin") + import_picocolors2.default.cyan(" ║"));
|
|
7131
|
+
console.log(import_picocolors2.default.cyan(" ║ ") + import_picocolors2.default.dim("Headless Admin for Svelte 5") + import_picocolors2.default.cyan(" ║"));
|
|
7132
|
+
console.log(import_picocolors2.default.cyan(" ╚═══════════════════════════════════╝"));
|
|
5568
7133
|
console.log();
|
|
5569
|
-
const response = await
|
|
7134
|
+
const response = await import_prompts2.default([
|
|
5570
7135
|
{
|
|
5571
7136
|
type: "text",
|
|
5572
7137
|
name: "projectName",
|
|
@@ -5575,7 +7140,7 @@ async function init() {
|
|
|
5575
7140
|
validate: (value) => {
|
|
5576
7141
|
if (!value.trim())
|
|
5577
7142
|
return "Project name is required";
|
|
5578
|
-
if (
|
|
7143
|
+
if (fs3.existsSync(value.trim()) && fs3.readdirSync(value.trim()).length > 0) {
|
|
5579
7144
|
return "Directory already exists and is not empty";
|
|
5580
7145
|
}
|
|
5581
7146
|
return true;
|
|
@@ -5613,61 +7178,61 @@ async function init() {
|
|
|
5613
7178
|
}
|
|
5614
7179
|
]);
|
|
5615
7180
|
if (!response.projectName) {
|
|
5616
|
-
console.log(
|
|
7181
|
+
console.log(import_picocolors2.default.red(`
|
|
5617
7182
|
Operation cancelled.
|
|
5618
7183
|
`));
|
|
5619
7184
|
return;
|
|
5620
7185
|
}
|
|
5621
|
-
const projectDir =
|
|
5622
|
-
if (!
|
|
5623
|
-
|
|
7186
|
+
const projectDir = path3.resolve(process.cwd(), response.projectName.trim());
|
|
7187
|
+
if (!fs3.existsSync(projectDir)) {
|
|
7188
|
+
fs3.mkdirSync(projectDir, { recursive: true });
|
|
5624
7189
|
}
|
|
5625
7190
|
console.log(`
|
|
5626
|
-
${
|
|
7191
|
+
${import_picocolors2.default.bold("Scaffolding")} project in ${import_picocolors2.default.green(projectDir)}...
|
|
5627
7192
|
`);
|
|
5628
|
-
const templateDir =
|
|
5629
|
-
const guidanceDir =
|
|
7193
|
+
const templateDir = path3.join(__dirname2, "..", "template");
|
|
7194
|
+
const guidanceDir = path3.join(__dirname2, "..", "guidance");
|
|
5630
7195
|
const scaffoldManifest = loadShippedScaffoldManifest();
|
|
5631
7196
|
function copyDir(src, dest) {
|
|
5632
|
-
|
|
5633
|
-
const entries =
|
|
7197
|
+
fs3.mkdirSync(dest, { recursive: true });
|
|
7198
|
+
const entries = fs3.readdirSync(src, { withFileTypes: true });
|
|
5634
7199
|
for (const entry of entries) {
|
|
5635
|
-
const srcPath =
|
|
5636
|
-
const destPath =
|
|
7200
|
+
const srcPath = path3.join(src, entry.name);
|
|
7201
|
+
const destPath = path3.join(dest, entry.name === "_gitignore" ? ".gitignore" : entry.name);
|
|
5637
7202
|
if (entry.isDirectory()) {
|
|
5638
7203
|
copyDir(srcPath, destPath);
|
|
5639
7204
|
} else {
|
|
5640
|
-
|
|
7205
|
+
fs3.copyFileSync(srcPath, destPath);
|
|
5641
7206
|
}
|
|
5642
7207
|
}
|
|
5643
7208
|
}
|
|
5644
|
-
if (
|
|
7209
|
+
if (fs3.existsSync(templateDir)) {
|
|
5645
7210
|
copyDir(templateDir, projectDir);
|
|
5646
|
-
console.log(
|
|
7211
|
+
console.log(import_picocolors2.default.green(" ✔") + " Template files copied");
|
|
5647
7212
|
}
|
|
5648
|
-
if (
|
|
7213
|
+
if (fs3.existsSync(guidanceDir)) {
|
|
5649
7214
|
copyDir(guidanceDir, projectDir);
|
|
5650
|
-
console.log(
|
|
7215
|
+
console.log(import_picocolors2.default.green(" ✔") + " AI and design guidance copied");
|
|
5651
7216
|
}
|
|
5652
7217
|
const packageJson = createProjectPackageJson(scaffoldManifest, {
|
|
5653
7218
|
projectName: response.projectName,
|
|
5654
7219
|
dataProvider: response.dataProvider,
|
|
5655
7220
|
authProvider: response.authProvider
|
|
5656
7221
|
});
|
|
5657
|
-
|
|
7222
|
+
fs3.writeFileSync(path3.join(projectDir, "package.json"), `${JSON.stringify(packageJson, null, 2)}
|
|
5658
7223
|
`);
|
|
5659
|
-
console.log(
|
|
5660
|
-
|
|
7224
|
+
console.log(import_picocolors2.default.green(" ✔") + " package.json generated");
|
|
7225
|
+
fs3.writeFileSync(path3.join(projectDir, ".gitignore"), `node_modules
|
|
5661
7226
|
dist
|
|
5662
7227
|
.svelte-kit
|
|
5663
7228
|
.env
|
|
5664
7229
|
.env.local
|
|
5665
7230
|
*.local
|
|
5666
7231
|
`);
|
|
5667
|
-
console.log(
|
|
7232
|
+
console.log(import_picocolors2.default.green(" ✔") + " .gitignore generated");
|
|
5668
7233
|
const dpLabel = response.dataProvider === "simple-rest" ? "Simple REST" : response.dataProvider === "supabase" ? "Supabase" : response.dataProvider === "graphql" ? "GraphQL" : "Custom";
|
|
5669
7234
|
const authLabel = response.authProvider === "mock" ? "Mock (demo)" : response.authProvider === "jwt" ? "JWT" : response.authProvider === "supabase" ? "Supabase Auth" : "None";
|
|
5670
|
-
|
|
7235
|
+
fs3.writeFileSync(path3.join(projectDir, "README.md"), `# ${response.projectName}
|
|
5671
7236
|
|
|
5672
7237
|
Built with [svadmin](https://github.com/vibeunion/svadmin) — Headless Admin Framework for Svelte 5.
|
|
5673
7238
|
|
|
@@ -5685,30 +7250,30 @@ bun run dev
|
|
|
5685
7250
|
- **Auth**: ${authLabel}
|
|
5686
7251
|
- **State**: TanStack Query v6
|
|
5687
7252
|
`);
|
|
5688
|
-
console.log(
|
|
7253
|
+
console.log(import_picocolors2.default.green(" ✔") + " README.md generated");
|
|
5689
7254
|
if (response.installDeps) {
|
|
5690
7255
|
console.log(`
|
|
5691
|
-
${
|
|
7256
|
+
${import_picocolors2.default.bold("Installing dependencies...")}
|
|
5692
7257
|
`);
|
|
5693
7258
|
const bunInstall = spawnSync("bun", ["install"], { cwd: projectDir, stdio: "inherit" });
|
|
5694
7259
|
if (bunInstall.status !== 0) {
|
|
5695
7260
|
const npmInstall = spawnSync("npm", ["install"], { cwd: projectDir, stdio: "inherit" });
|
|
5696
7261
|
if (npmInstall.status !== 0) {
|
|
5697
|
-
console.log(
|
|
7262
|
+
console.log(import_picocolors2.default.yellow("\n ⚠ Auto-install failed. Run `bun install` or `npm install` manually."));
|
|
5698
7263
|
}
|
|
5699
7264
|
}
|
|
5700
7265
|
}
|
|
5701
7266
|
console.log();
|
|
5702
|
-
console.log(
|
|
7267
|
+
console.log(import_picocolors2.default.green(import_picocolors2.default.bold(" ✔ Project ready!")));
|
|
5703
7268
|
console.log();
|
|
5704
7269
|
console.log(" Next steps:");
|
|
5705
|
-
console.log(` ${
|
|
7270
|
+
console.log(` ${import_picocolors2.default.cyan(`cd ${response.projectName}`)}`);
|
|
5706
7271
|
if (!response.installDeps) {
|
|
5707
|
-
console.log(` ${
|
|
7272
|
+
console.log(` ${import_picocolors2.default.cyan("bun install")}`);
|
|
5708
7273
|
}
|
|
5709
|
-
console.log(` ${
|
|
7274
|
+
console.log(` ${import_picocolors2.default.cyan("bun run dev")}`);
|
|
5710
7275
|
console.log();
|
|
5711
|
-
console.log(` Docs: ${
|
|
7276
|
+
console.log(` Docs: ${import_picocolors2.default.blue("https://github.com/vibeunion/svadmin")}`);
|
|
5712
7277
|
console.log();
|
|
5713
7278
|
}
|
|
5714
7279
|
var EJECT_COMPONENTS = [
|
|
@@ -5740,64 +7305,64 @@ var EJECT_COMPONENTS = [
|
|
|
5740
7305
|
];
|
|
5741
7306
|
async function eject(args) {
|
|
5742
7307
|
console.log();
|
|
5743
|
-
console.log(
|
|
7308
|
+
console.log(import_picocolors2.default.cyan(" svadmin eject") + import_picocolors2.default.dim(" — copy internal components for deep customization"));
|
|
5744
7309
|
console.log();
|
|
5745
7310
|
const requested = args.filter((a) => !a.startsWith("-"));
|
|
5746
7311
|
const toEject = requested.length > 0 ? requested.filter((name) => {
|
|
5747
7312
|
if (!EJECT_COMPONENTS.includes(name)) {
|
|
5748
|
-
console.log(
|
|
7313
|
+
console.log(import_picocolors2.default.yellow(` ⚠ Unknown component: ${name} (skipped)`));
|
|
5749
7314
|
return false;
|
|
5750
7315
|
}
|
|
5751
7316
|
return true;
|
|
5752
7317
|
}) : [...EJECT_COMPONENTS];
|
|
5753
7318
|
if (toEject.length === 0) {
|
|
5754
|
-
console.log(
|
|
7319
|
+
console.log(import_picocolors2.default.red(" No valid components to eject."));
|
|
5755
7320
|
console.log(` Available: ${EJECT_COMPONENTS.join(", ")}`);
|
|
5756
7321
|
return;
|
|
5757
7322
|
}
|
|
5758
7323
|
let uiSrcDir;
|
|
5759
7324
|
try {
|
|
5760
7325
|
const require2 = createRequire2(import.meta.url);
|
|
5761
|
-
const uiPkg =
|
|
5762
|
-
uiSrcDir =
|
|
7326
|
+
const uiPkg = path3.dirname(require2.resolve("@svadmin/ui/package.json"));
|
|
7327
|
+
uiSrcDir = path3.join(uiPkg, "src", "components");
|
|
5763
7328
|
} catch {
|
|
5764
|
-
const nm =
|
|
5765
|
-
if (
|
|
7329
|
+
const nm = path3.join(process.cwd(), "node_modules", "@svadmin", "ui", "src", "components");
|
|
7330
|
+
if (fs3.existsSync(nm)) {
|
|
5766
7331
|
uiSrcDir = nm;
|
|
5767
7332
|
} else {
|
|
5768
|
-
console.log(
|
|
7333
|
+
console.log(import_picocolors2.default.red(" ✗ Cannot find @svadmin/ui. Run `bun install` first."));
|
|
5769
7334
|
return;
|
|
5770
7335
|
}
|
|
5771
7336
|
}
|
|
5772
|
-
const destDir =
|
|
5773
|
-
|
|
7337
|
+
const destDir = path3.join(process.cwd(), "src", "components", "svadmin");
|
|
7338
|
+
fs3.mkdirSync(destDir, { recursive: true });
|
|
5774
7339
|
let copied = 0;
|
|
5775
7340
|
for (const name of toEject) {
|
|
5776
|
-
const srcFile =
|
|
5777
|
-
const srcFileAlt =
|
|
5778
|
-
const src =
|
|
7341
|
+
const srcFile = path3.join(uiSrcDir, `${name}.svelte`);
|
|
7342
|
+
const srcFileAlt = path3.join(uiSrcDir, "fields", `${name}.svelte`);
|
|
7343
|
+
const src = fs3.existsSync(srcFile) ? srcFile : fs3.existsSync(srcFileAlt) ? srcFileAlt : null;
|
|
5779
7344
|
if (!src) {
|
|
5780
|
-
console.log(
|
|
7345
|
+
console.log(import_picocolors2.default.yellow(` ⚠ ${name}.svelte not found in @svadmin/ui (skipped)`));
|
|
5781
7346
|
continue;
|
|
5782
7347
|
}
|
|
5783
|
-
let content =
|
|
7348
|
+
let content = fs3.readFileSync(src, "utf-8");
|
|
5784
7349
|
content = content.replace(/from\s+['"]\.\/ui\//g, "from '@svadmin/ui/components/ui/");
|
|
5785
7350
|
content = content.replace(/from\s+['"]\.\/((?!ui\/)[^'"]+)['"]/g, "from './$1'");
|
|
5786
|
-
const destFile =
|
|
5787
|
-
|
|
5788
|
-
console.log(
|
|
7351
|
+
const destFile = path3.join(destDir, `${name}.svelte`);
|
|
7352
|
+
fs3.writeFileSync(destFile, content);
|
|
7353
|
+
console.log(import_picocolors2.default.green(" ✔") + ` ${name}.svelte → src/components/svadmin/`);
|
|
5789
7354
|
copied++;
|
|
5790
7355
|
}
|
|
5791
7356
|
console.log();
|
|
5792
7357
|
if (copied > 0) {
|
|
5793
|
-
console.log(
|
|
7358
|
+
console.log(import_picocolors2.default.green(import_picocolors2.default.bold(` ✔ Ejected ${copied} component(s)`)));
|
|
5794
7359
|
console.log();
|
|
5795
7360
|
console.log(" Usage: import overrides in your AdminApp and pass via `components` prop:");
|
|
5796
7361
|
console.log();
|
|
5797
|
-
console.log(
|
|
5798
|
-
console.log(
|
|
7362
|
+
console.log(import_picocolors2.default.dim(' import CustomLayout from "./components/svadmin/Layout.svelte";'));
|
|
7363
|
+
console.log(import_picocolors2.default.dim(" <AdminApp components={{ Layout: CustomLayout }} ... />"));
|
|
5799
7364
|
} else {
|
|
5800
|
-
console.log(
|
|
7365
|
+
console.log(import_picocolors2.default.yellow(" No components were ejected."));
|
|
5801
7366
|
}
|
|
5802
7367
|
console.log();
|
|
5803
7368
|
}
|
|
@@ -5805,7 +7370,7 @@ var [, , subcommand, ...rest] = process.argv;
|
|
|
5805
7370
|
var runCommand = (command) => {
|
|
5806
7371
|
Promise.resolve().then(command).catch((error) => {
|
|
5807
7372
|
const message = error instanceof Error ? error.message : String(error);
|
|
5808
|
-
console.error(
|
|
7373
|
+
console.error(import_picocolors2.default.red(`
|
|
5809
7374
|
✗ ${message}
|
|
5810
7375
|
`));
|
|
5811
7376
|
process.exitCode = 2;
|
|
@@ -5819,6 +7384,16 @@ if (subcommand === "eject") {
|
|
|
5819
7384
|
runCommand(() => upgrade(rest));
|
|
5820
7385
|
} else if (subcommand === "guidance") {
|
|
5821
7386
|
runCommand(() => guidance(rest));
|
|
7387
|
+
} else if (subcommand === "infer") {
|
|
7388
|
+
runCommand(() => inferCommand(rest));
|
|
7389
|
+
} else if (subcommand === "lite") {
|
|
7390
|
+
if (rest[0] !== "init") {
|
|
7391
|
+
runCommand(() => {
|
|
7392
|
+
throw new Error("Usage: create-svadmin lite init [project-directory] [--write]");
|
|
7393
|
+
});
|
|
7394
|
+
} else {
|
|
7395
|
+
runCommand(() => liteInitCommand(rest.slice(1)));
|
|
7396
|
+
}
|
|
5822
7397
|
} else {
|
|
5823
7398
|
runCommand(init);
|
|
5824
7399
|
}
|