@svadmin/create 0.26.2 → 0.29.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 +2 -2
- package/dist/index.js +289 -161
- package/guidance/AGENTS.md +39 -0
- package/package.json +3 -3
- package/scaffold-manifest.json +6 -9
- package/template/src/app.css +1 -7
- package/template/src/pages/Dashboard.svelte +60 -47
- package/template/src/resource-contracts.ts +29 -0
- package/template/src/resources.ts +5 -0
- package/template/vite.config.ts +0 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The official scaffolding CLI for `headless-admin-svelte` (svadmin).
|
|
4
4
|
|
|
5
|
-
Quickly bootstrap a completely configured, headless admin panel project built on Svelte 5,
|
|
5
|
+
Quickly bootstrap a completely configured, headless admin panel project built on Svelte 5, the native-CSS `@svadmin/ui` components, and TanStack Query.
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
@@ -21,7 +21,7 @@ Follow the interactive prompts to:
|
|
|
21
21
|
|
|
22
22
|
The generated project is pre-configured with:
|
|
23
23
|
- **Svelte 5** + **Vite**
|
|
24
|
-
- **
|
|
24
|
+
- **@svadmin/ui** native CSS, semantic tokens and Bits UI interactions; no host CSS compiler is required
|
|
25
25
|
- **@svadmin/core**: The headless business logic and hooks (useTable, useForm, useAuth, etc.)
|
|
26
26
|
- **@svadmin/ai-elements**: Composable Svelte 5 AI conversation, tool, reasoning, and source components.
|
|
27
27
|
- **@svadmin/ui**: Beautiful default dashboard UI, standalone CRUD buttons, and data tables.
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
19
19
|
return cached;
|
|
20
20
|
}
|
|
21
21
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
22
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
22
|
+
const to = isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
23
23
|
if (mod && typeof mod === "object" || typeof mod === "function") {
|
|
24
24
|
for (let key of __getOwnPropNames(mod))
|
|
25
25
|
if (!__hasOwnProp.call(to, key))
|
|
@@ -259,7 +259,7 @@ var require_clear = __commonJS(function(exports, module) {
|
|
|
259
259
|
if (it)
|
|
260
260
|
o = it;
|
|
261
261
|
var i = 0;
|
|
262
|
-
var F = function
|
|
262
|
+
var F = function F() {};
|
|
263
263
|
return { s: F, n: function n() {
|
|
264
264
|
if (i >= o.length)
|
|
265
265
|
return { done: true };
|
|
@@ -876,7 +876,7 @@ var require_select = __commonJS(function(exports, module) {
|
|
|
876
876
|
else
|
|
877
877
|
this.out.write(clear(this.outputText, this.out.columns));
|
|
878
878
|
super.render();
|
|
879
|
-
let _entriesToDisplay = entriesToDisplay(this.cursor, this.choices.length, this.optionsPerPage),
|
|
879
|
+
let _entriesToDisplay = entriesToDisplay(this.cursor, this.choices.length, this.optionsPerPage), { startIndex, endIndex } = _entriesToDisplay;
|
|
880
880
|
this.outputText = [style.symbol(this.done, this.aborted), color.bold(this.msg), style.delimiter(false), this.done ? this.selection.title : this.selection.disabled ? color.yellow(this.warn) : color.gray(this.hint)].join(" ");
|
|
881
881
|
if (!this.done) {
|
|
882
882
|
this.outputText += `
|
|
@@ -1885,14 +1885,14 @@ Instructions:
|
|
|
1885
1885
|
}
|
|
1886
1886
|
return "";
|
|
1887
1887
|
}
|
|
1888
|
-
renderOption(
|
|
1888
|
+
renderOption(cursor, v, i, arrowIndicator) {
|
|
1889
1889
|
const prefix = (v.selected ? color.green(figures.radioOn) : figures.radioOff) + " " + arrowIndicator + " ";
|
|
1890
1890
|
let title, desc;
|
|
1891
1891
|
if (v.disabled) {
|
|
1892
|
-
title =
|
|
1892
|
+
title = cursor === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);
|
|
1893
1893
|
} else {
|
|
1894
|
-
title =
|
|
1895
|
-
if (
|
|
1894
|
+
title = cursor === i ? color.cyan().underline(v.title) : v.title;
|
|
1895
|
+
if (cursor === i && v.description) {
|
|
1896
1896
|
desc = ` - ${v.description}`;
|
|
1897
1897
|
if (prefix.length + title.length + desc.length >= this.out.columns || v.description.split(/\r?\n/).length > 1) {
|
|
1898
1898
|
desc = `
|
|
@@ -1909,7 +1909,7 @@ Instructions:
|
|
|
1909
1909
|
if (options.length === 0) {
|
|
1910
1910
|
return color.red("No matches for this query.");
|
|
1911
1911
|
}
|
|
1912
|
-
let _entriesToDisplay = entriesToDisplay(this.cursor, options.length, this.optionsPerPage),
|
|
1912
|
+
let _entriesToDisplay = entriesToDisplay(this.cursor, options.length, this.optionsPerPage), { startIndex, endIndex } = _entriesToDisplay;
|
|
1913
1913
|
let prefix, styledOptions = [];
|
|
1914
1914
|
for (let i = startIndex;i < endIndex; i++) {
|
|
1915
1915
|
if (i === startIndex && startIndex > 0) {
|
|
@@ -2217,7 +2217,7 @@ var require_autocomplete = __commonJS(function(exports, module) {
|
|
|
2217
2217
|
else
|
|
2218
2218
|
this.out.write(clear(this.outputText, this.out.columns));
|
|
2219
2219
|
super.render();
|
|
2220
|
-
let _entriesToDisplay = entriesToDisplay(this.select, this.choices.length, this.limit),
|
|
2220
|
+
let _entriesToDisplay = entriesToDisplay(this.select, this.choices.length, this.limit), { startIndex, endIndex } = _entriesToDisplay;
|
|
2221
2221
|
this.outputText = [style.symbol(this.done, this.aborted, this.exited), color.bold(this.msg), style.delimiter(this.completing), this.done && this.suggestions[this.select] ? this.suggestions[this.select].title : this.rendered = this.transform.render(this.input)].join(" ");
|
|
2222
2222
|
if (!this.done) {
|
|
2223
2223
|
const suggestions = this.suggestions.slice(startIndex, endIndex).map((item, i) => this.renderOption(item, this.select === i + startIndex, i === 0 && startIndex > 0, i + startIndex === endIndex - 1 && endIndex < this.choices.length)).join(`
|
|
@@ -2356,12 +2356,12 @@ Instructions:
|
|
|
2356
2356
|
Filtered results for: ${this.inputValue ? this.inputValue : color.gray("Enter something to filter")}
|
|
2357
2357
|
`;
|
|
2358
2358
|
}
|
|
2359
|
-
renderOption(
|
|
2359
|
+
renderOption(cursor, v, i) {
|
|
2360
2360
|
let title;
|
|
2361
2361
|
if (v.disabled)
|
|
2362
|
-
title =
|
|
2362
|
+
title = cursor === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);
|
|
2363
2363
|
else
|
|
2364
|
-
title =
|
|
2364
|
+
title = cursor === i ? color.cyan().underline(v.title) : v.title;
|
|
2365
2365
|
return (v.selected ? color.green(figures.radioOn) : figures.radioOff) + " " + title;
|
|
2366
2366
|
}
|
|
2367
2367
|
renderDoneOrInstructions() {
|
|
@@ -2591,7 +2591,7 @@ var require_dist = __commonJS(function(exports, module) {
|
|
|
2591
2591
|
if (it)
|
|
2592
2592
|
o = it;
|
|
2593
2593
|
var i = 0;
|
|
2594
|
-
var F = function
|
|
2594
|
+
var F = function F() {};
|
|
2595
2595
|
return { s: F, n: function n() {
|
|
2596
2596
|
if (i >= o.length)
|
|
2597
2597
|
return { done: true };
|
|
@@ -2684,17 +2684,17 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
|
|
|
2684
2684
|
onCancel = noop
|
|
2685
2685
|
} = {}) {
|
|
2686
2686
|
const answers = {};
|
|
2687
|
-
const
|
|
2687
|
+
const override = prompt._override || {};
|
|
2688
2688
|
questions = [].concat(questions);
|
|
2689
2689
|
let answer, question, quit, name, type, lastPrompt;
|
|
2690
2690
|
const getFormattedAnswer = /* @__PURE__ */ function() {
|
|
2691
|
-
var _ref = _asyncToGenerator(function* (
|
|
2692
|
-
if (!skipValidation &&
|
|
2691
|
+
var _ref = _asyncToGenerator(function* (question, answer, skipValidation = false) {
|
|
2692
|
+
if (!skipValidation && question.validate && question.validate(answer) !== true) {
|
|
2693
2693
|
return;
|
|
2694
2694
|
}
|
|
2695
|
-
return
|
|
2695
|
+
return question.format ? yield question.format(answer, answers) : answer;
|
|
2696
2696
|
});
|
|
2697
|
-
return function
|
|
2697
|
+
return function getFormattedAnswer(_x, _x2) {
|
|
2698
2698
|
return _ref.apply(this, arguments);
|
|
2699
2699
|
};
|
|
2700
2700
|
}();
|
|
@@ -2727,8 +2727,8 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
|
|
|
2727
2727
|
if (prompts[type] === undefined) {
|
|
2728
2728
|
throw new Error(`prompt type (${type}) is not defined`);
|
|
2729
2729
|
}
|
|
2730
|
-
if (
|
|
2731
|
-
answer = yield getFormattedAnswer(question,
|
|
2730
|
+
if (override[question.name] !== undefined) {
|
|
2731
|
+
answer = yield getFormattedAnswer(question, override[question.name]);
|
|
2732
2732
|
if (answer !== undefined) {
|
|
2733
2733
|
answers[name] = answer;
|
|
2734
2734
|
continue;
|
|
@@ -4870,14 +4870,14 @@ var require_lib = __commonJS(function(exports, module) {
|
|
|
4870
4870
|
var noop = () => {};
|
|
4871
4871
|
async function prompt(questions = [], { onSubmit = noop, onCancel = noop } = {}) {
|
|
4872
4872
|
const answers = {};
|
|
4873
|
-
const
|
|
4873
|
+
const override = prompt._override || {};
|
|
4874
4874
|
questions = [].concat(questions);
|
|
4875
4875
|
let answer, question, quit, name, type, lastPrompt;
|
|
4876
|
-
const getFormattedAnswer = async (
|
|
4877
|
-
if (!skipValidation &&
|
|
4876
|
+
const getFormattedAnswer = async (question, answer, skipValidation = false) => {
|
|
4877
|
+
if (!skipValidation && question.validate && question.validate(answer) !== true) {
|
|
4878
4878
|
return;
|
|
4879
4879
|
}
|
|
4880
|
-
return
|
|
4880
|
+
return question.format ? await question.format(answer, answers) : answer;
|
|
4881
4881
|
};
|
|
4882
4882
|
for (question of questions) {
|
|
4883
4883
|
({ name, type } = question);
|
|
@@ -4901,8 +4901,8 @@ var require_lib = __commonJS(function(exports, module) {
|
|
|
4901
4901
|
if (prompts[type] === undefined) {
|
|
4902
4902
|
throw new Error(`prompt type (${type}) is not defined`);
|
|
4903
4903
|
}
|
|
4904
|
-
if (
|
|
4905
|
-
answer = await getFormattedAnswer(question,
|
|
4904
|
+
if (override[question.name] !== undefined) {
|
|
4905
|
+
answer = await getFormattedAnswer(question, override[question.name]);
|
|
4906
4906
|
if (answer !== undefined) {
|
|
4907
4907
|
answers[name] = answer;
|
|
4908
4908
|
continue;
|
|
@@ -5075,22 +5075,22 @@ function assertReferencedPacksExist(dependencyPacks, selectedPacks) {
|
|
|
5075
5075
|
}
|
|
5076
5076
|
function assertScaffoldManifest(candidate) {
|
|
5077
5077
|
assertJsonObject(candidate, "scaffold manifest");
|
|
5078
|
-
assertNonEmptyString(candidate
|
|
5079
|
-
assertNonEmptyString(candidate
|
|
5080
|
-
if (candidate
|
|
5078
|
+
assertNonEmptyString(candidate["name"], "scaffold manifest.name");
|
|
5079
|
+
assertNonEmptyString(candidate["version"], "scaffold manifest.version");
|
|
5080
|
+
if (candidate["private"] !== true) {
|
|
5081
5081
|
throw new Error("scaffold manifest.private must be true");
|
|
5082
5082
|
}
|
|
5083
|
-
assertNonEmptyString(candidate
|
|
5084
|
-
assertStringRecord(candidate
|
|
5085
|
-
assertStringRecord(candidate
|
|
5086
|
-
assertStringRecord(candidate
|
|
5087
|
-
assertJsonObject(candidate
|
|
5088
|
-
assertDependencyPacks(candidate
|
|
5089
|
-
assertSelectionMap(candidate
|
|
5090
|
-
assertSelectionMap(candidate
|
|
5091
|
-
assertReferencedPacksExist(candidate
|
|
5092
|
-
...Object.values(candidate
|
|
5093
|
-
...Object.values(candidate
|
|
5083
|
+
assertNonEmptyString(candidate["type"], "scaffold manifest.type");
|
|
5084
|
+
assertStringRecord(candidate["scripts"], "scaffold manifest.scripts");
|
|
5085
|
+
assertStringRecord(candidate["dependencies"], "scaffold manifest.dependencies");
|
|
5086
|
+
assertStringRecord(candidate["devDependencies"], "scaffold manifest.devDependencies");
|
|
5087
|
+
assertJsonObject(candidate["svadmin"], "scaffold manifest.svadmin");
|
|
5088
|
+
assertDependencyPacks(candidate["svadmin"]["dependencyPacks"]);
|
|
5089
|
+
assertSelectionMap(candidate["svadmin"]["dataProviders"], DATA_PROVIDER_CHOICES, "scaffold manifest.svadmin.dataProviders");
|
|
5090
|
+
assertSelectionMap(candidate["svadmin"]["authProviders"], AUTH_PROVIDER_CHOICES, "scaffold manifest.svadmin.authProviders");
|
|
5091
|
+
assertReferencedPacksExist(candidate["svadmin"]["dependencyPacks"], [
|
|
5092
|
+
...Object.values(candidate["svadmin"]["dataProviders"]).flat(),
|
|
5093
|
+
...Object.values(candidate["svadmin"]["authProviders"]).flat()
|
|
5094
5094
|
]);
|
|
5095
5095
|
}
|
|
5096
5096
|
function loadScaffoldManifest(manifestPath) {
|
|
@@ -5114,7 +5114,10 @@ function createProjectPackageJson(scaffold, options) {
|
|
|
5114
5114
|
...scaffold.svadmin.authProviders[options.authProvider]
|
|
5115
5115
|
]);
|
|
5116
5116
|
for (const packName of selectedPacks) {
|
|
5117
|
-
|
|
5117
|
+
const pack = scaffold.svadmin.dependencyPacks[packName];
|
|
5118
|
+
if (pack === undefined)
|
|
5119
|
+
throw new Error(`Unknown dependency pack: ${packName}`);
|
|
5120
|
+
mergeDependencyPack(dependencies, pack, packName);
|
|
5118
5121
|
}
|
|
5119
5122
|
return {
|
|
5120
5123
|
name: options.projectName.trim(),
|
|
@@ -5431,8 +5434,10 @@ function parseGraphQLSDL(sdl) {
|
|
|
5431
5434
|
const enumRegex = /enum\s+([A-Za-z0-9_]+)\s*\{([^}]+)\}/g;
|
|
5432
5435
|
let match;
|
|
5433
5436
|
while ((match = enumRegex.exec(cleanSDL)) !== null) {
|
|
5434
|
-
const enumName = match
|
|
5435
|
-
|
|
5437
|
+
const [, enumName, enumBody] = match;
|
|
5438
|
+
if (enumName === undefined || enumBody === undefined)
|
|
5439
|
+
continue;
|
|
5440
|
+
const rawValues = enumBody.split(/\s+/).filter(Boolean);
|
|
5436
5441
|
types.push({
|
|
5437
5442
|
kind: "ENUM",
|
|
5438
5443
|
name: enumName,
|
|
@@ -5441,8 +5446,9 @@ function parseGraphQLSDL(sdl) {
|
|
|
5441
5446
|
}
|
|
5442
5447
|
const typeRegex = /type\s+([A-Za-z0-9_]+)\s*(?:implements\s+[A-Za-z0-9_&,\s]+)?\s*\{([^}]+)\}/g;
|
|
5443
5448
|
while ((match = typeRegex.exec(cleanSDL)) !== null) {
|
|
5444
|
-
const typeName = match
|
|
5445
|
-
|
|
5449
|
+
const [, typeName, fieldsBody] = match;
|
|
5450
|
+
if (typeName === undefined || fieldsBody === undefined)
|
|
5451
|
+
continue;
|
|
5446
5452
|
const fieldLines = fieldsBody.split(`
|
|
5447
5453
|
`).map((l) => l.trim()).filter(Boolean);
|
|
5448
5454
|
const fields = [];
|
|
@@ -5450,8 +5456,9 @@ function parseGraphQLSDL(sdl) {
|
|
|
5450
5456
|
const fieldMatch = /^([A-Za-z0-9_]+)(?:\([^)]*\))?\s*:\s*([[\]A-Za-z0-9_!]+)/.exec(line);
|
|
5451
5457
|
if (!fieldMatch)
|
|
5452
5458
|
continue;
|
|
5453
|
-
const fName = fieldMatch
|
|
5454
|
-
|
|
5459
|
+
const [, fName, rawType] = fieldMatch;
|
|
5460
|
+
if (fName === undefined || rawType === undefined)
|
|
5461
|
+
continue;
|
|
5455
5462
|
const isNonNullable = rawType.endsWith("!");
|
|
5456
5463
|
const cleanType = isNonNullable ? rawType.slice(0, -1) : rawType;
|
|
5457
5464
|
const isList = cleanType.startsWith("[") && cleanType.endsWith("]");
|
|
@@ -5884,6 +5891,13 @@ function generateTypeBoxSchemaCode(resource) {
|
|
|
5884
5891
|
case "date":
|
|
5885
5892
|
typeDef = "Type.String({ format: 'date-time' })";
|
|
5886
5893
|
break;
|
|
5894
|
+
case "datetime":
|
|
5895
|
+
case "time":
|
|
5896
|
+
typeDef = "Type.String()";
|
|
5897
|
+
break;
|
|
5898
|
+
case "daterange":
|
|
5899
|
+
typeDef = "Type.Union([Type.Object({ start: Type.Union([Type.String(), Type.Null()]), end: Type.Union([Type.String(), Type.Null()]) }, { additionalProperties: false }), Type.Null()])";
|
|
5900
|
+
break;
|
|
5887
5901
|
case "email":
|
|
5888
5902
|
typeDef = "Type.String({ format: 'email' })";
|
|
5889
5903
|
break;
|
|
@@ -5966,74 +5980,99 @@ function parseInferArguments(args) {
|
|
|
5966
5980
|
method: "GET",
|
|
5967
5981
|
format: "all"
|
|
5968
5982
|
};
|
|
5969
|
-
|
|
5970
|
-
|
|
5983
|
+
const argumentsIterator = args.values();
|
|
5984
|
+
function requireArgument(flag) {
|
|
5985
|
+
const next = argumentsIterator.next();
|
|
5986
|
+
if (next.done || !next.value.trim() || next.value.startsWith("-")) {
|
|
5987
|
+
throw new Error(`Missing value for ${flag}`);
|
|
5988
|
+
}
|
|
5989
|
+
return next.value;
|
|
5990
|
+
}
|
|
5991
|
+
function sourceType(value) {
|
|
5992
|
+
switch (value) {
|
|
5993
|
+
case "rest":
|
|
5994
|
+
case "openapi":
|
|
5995
|
+
case "graphql":
|
|
5996
|
+
case "auto":
|
|
5997
|
+
return value;
|
|
5998
|
+
default:
|
|
5999
|
+
throw new Error(`Invalid source type: ${value}`);
|
|
6000
|
+
}
|
|
6001
|
+
}
|
|
6002
|
+
function outputFormat(value) {
|
|
6003
|
+
switch (value) {
|
|
6004
|
+
case "all":
|
|
6005
|
+
case "resource":
|
|
6006
|
+
case "typebox":
|
|
6007
|
+
case "components":
|
|
6008
|
+
return value;
|
|
6009
|
+
default:
|
|
6010
|
+
throw new Error(`Invalid output format: ${value}`);
|
|
6011
|
+
}
|
|
6012
|
+
}
|
|
6013
|
+
function addHeader(headerLine) {
|
|
6014
|
+
const colonIndex = headerLine.indexOf(":");
|
|
6015
|
+
const key = headerLine.slice(0, colonIndex).trim();
|
|
6016
|
+
if (colonIndex <= 0 || !key) {
|
|
6017
|
+
throw new Error(`Invalid header: ${headerLine}`);
|
|
6018
|
+
}
|
|
6019
|
+
options.headers ??= {};
|
|
6020
|
+
options.headers[key] = headerLine.slice(colonIndex + 1).trim();
|
|
6021
|
+
}
|
|
6022
|
+
for (const arg of argumentsIterator) {
|
|
6023
|
+
if (arg.startsWith("--") && arg.indexOf("=") === arg.length - 1) {
|
|
6024
|
+
throw new Error(`Missing value for ${arg.slice(0, -1)}`);
|
|
6025
|
+
}
|
|
5971
6026
|
if (arg === "--write" || arg === "-w") {
|
|
5972
6027
|
options.write = true;
|
|
5973
6028
|
} else if (arg === "--dry-run") {
|
|
5974
6029
|
options.write = false;
|
|
5975
6030
|
} else if (arg === "--url" || arg === "-u") {
|
|
5976
|
-
options.url =
|
|
6031
|
+
options.url = requireArgument(arg);
|
|
5977
6032
|
} else if (arg.startsWith("--url=")) {
|
|
5978
6033
|
options.url = arg.slice(6);
|
|
5979
6034
|
} else if (arg === "--file" || arg === "-f") {
|
|
5980
|
-
options.file =
|
|
6035
|
+
options.file = requireArgument(arg);
|
|
5981
6036
|
} else if (arg.startsWith("--file=")) {
|
|
5982
6037
|
options.file = arg.slice(7);
|
|
5983
6038
|
} else if (arg === "--type" || arg === "-t") {
|
|
5984
|
-
options.type =
|
|
6039
|
+
options.type = sourceType(requireArgument(arg));
|
|
5985
6040
|
} else if (arg.startsWith("--type=")) {
|
|
5986
|
-
options.type = arg.slice(7);
|
|
6041
|
+
options.type = sourceType(arg.slice(7));
|
|
5987
6042
|
} else if (arg === "--resource" || arg === "-r") {
|
|
5988
|
-
options.resource =
|
|
6043
|
+
options.resource = requireArgument(arg);
|
|
5989
6044
|
} else if (arg.startsWith("--resource=")) {
|
|
5990
6045
|
options.resource = arg.slice(11);
|
|
5991
6046
|
} else if (arg === "--out-dir" || arg === "-o" || arg === "--output") {
|
|
5992
|
-
options.outDir =
|
|
6047
|
+
options.outDir = requireArgument(arg);
|
|
5993
6048
|
} else if (arg.startsWith("--out-dir=")) {
|
|
5994
6049
|
options.outDir = arg.slice(10);
|
|
5995
6050
|
} else if (arg.startsWith("--output=")) {
|
|
5996
6051
|
options.outDir = arg.slice(9);
|
|
5997
6052
|
} else if (arg === "--primary-key" || arg === "-k") {
|
|
5998
|
-
options.primaryKey =
|
|
6053
|
+
options.primaryKey = requireArgument(arg);
|
|
5999
6054
|
} else if (arg.startsWith("--primary-key=")) {
|
|
6000
6055
|
options.primaryKey = arg.slice(14);
|
|
6001
6056
|
} else if (arg === "--fields") {
|
|
6002
|
-
options.fields =
|
|
6057
|
+
options.fields = requireArgument(arg);
|
|
6003
6058
|
} else if (arg.startsWith("--fields=")) {
|
|
6004
6059
|
options.fields = arg.slice(9);
|
|
6005
6060
|
} else if (arg === "--header" || arg === "-H") {
|
|
6006
|
-
|
|
6007
|
-
const colonIndex = headerLine.indexOf(":");
|
|
6008
|
-
if (colonIndex > 0) {
|
|
6009
|
-
const key = headerLine.slice(0, colonIndex).trim();
|
|
6010
|
-
const value = headerLine.slice(colonIndex + 1).trim();
|
|
6011
|
-
if (!options.headers)
|
|
6012
|
-
options.headers = {};
|
|
6013
|
-
options.headers[key] = value;
|
|
6014
|
-
}
|
|
6061
|
+
addHeader(requireArgument(arg));
|
|
6015
6062
|
} else if (arg.startsWith("--header=")) {
|
|
6016
|
-
|
|
6017
|
-
const colonIndex = headerLine.indexOf(":");
|
|
6018
|
-
if (colonIndex > 0) {
|
|
6019
|
-
const key = headerLine.slice(0, colonIndex).trim();
|
|
6020
|
-
const value = headerLine.slice(colonIndex + 1).trim();
|
|
6021
|
-
if (!options.headers)
|
|
6022
|
-
options.headers = {};
|
|
6023
|
-
options.headers[key] = value;
|
|
6024
|
-
}
|
|
6063
|
+
addHeader(arg.slice(9));
|
|
6025
6064
|
} else if (arg === "--method" || arg === "-m") {
|
|
6026
|
-
options.method = (
|
|
6065
|
+
options.method = requireArgument(arg).toUpperCase();
|
|
6027
6066
|
} else if (arg.startsWith("--method=")) {
|
|
6028
6067
|
options.method = arg.slice(9).toUpperCase();
|
|
6029
6068
|
} else if (arg === "--body" || arg === "-b") {
|
|
6030
|
-
options.body =
|
|
6069
|
+
options.body = requireArgument(arg);
|
|
6031
6070
|
} else if (arg.startsWith("--body=")) {
|
|
6032
6071
|
options.body = arg.slice(7);
|
|
6033
6072
|
} else if (arg === "--format") {
|
|
6034
|
-
options.format =
|
|
6073
|
+
options.format = outputFormat(requireArgument(arg));
|
|
6035
6074
|
} else if (arg.startsWith("--format=")) {
|
|
6036
|
-
options.format = arg.slice(9);
|
|
6075
|
+
options.format = outputFormat(arg.slice(9));
|
|
6037
6076
|
} else if (arg === "--help" || arg === "-h") {
|
|
6038
6077
|
printInferHelp();
|
|
6039
6078
|
process.exit(0);
|
|
@@ -6045,6 +6084,8 @@ function parseInferArguments(args) {
|
|
|
6045
6084
|
} else {
|
|
6046
6085
|
options.resource = arg;
|
|
6047
6086
|
}
|
|
6087
|
+
} else {
|
|
6088
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
6048
6089
|
}
|
|
6049
6090
|
}
|
|
6050
6091
|
return options;
|
|
@@ -6080,6 +6121,22 @@ ${import_picocolors.default.bold("EXAMPLES:")}
|
|
|
6080
6121
|
svadmin infer --file sample-posts.json --resource posts --out-dir src/resources --write
|
|
6081
6122
|
`);
|
|
6082
6123
|
}
|
|
6124
|
+
function isRecord(value) {
|
|
6125
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6126
|
+
}
|
|
6127
|
+
function isOpenAPIDocument(value) {
|
|
6128
|
+
return isRecord(value) && (("openapi" in value) || ("swagger" in value) || ("paths" in value) && ("components" in value));
|
|
6129
|
+
}
|
|
6130
|
+
function isGraphQLDocument(value) {
|
|
6131
|
+
return isRecord(value) && (("__schema" in value) || isRecord(value["data"]) && ("__schema" in value["data"]) || Array.isArray(value["types"]));
|
|
6132
|
+
}
|
|
6133
|
+
function readSampleRecords(value) {
|
|
6134
|
+
const records = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value["data"]) ? value["data"] : isRecord(value) && Array.isArray(value["items"]) ? value["items"] : [value];
|
|
6135
|
+
if (!records.every(isRecord)) {
|
|
6136
|
+
throw new Error("REST samples must be objects or arrays of objects");
|
|
6137
|
+
}
|
|
6138
|
+
return records;
|
|
6139
|
+
}
|
|
6083
6140
|
async function loadSourceData(options, customFetch = fetch) {
|
|
6084
6141
|
if (options.file) {
|
|
6085
6142
|
const filePath = path.resolve(process.cwd(), options.file);
|
|
@@ -6097,31 +6154,9 @@ async function loadSourceData(options, customFetch = fetch) {
|
|
|
6097
6154
|
sourceDescription: options.file
|
|
6098
6155
|
};
|
|
6099
6156
|
}
|
|
6157
|
+
let json;
|
|
6100
6158
|
try {
|
|
6101
|
-
|
|
6102
|
-
if (typeof json === "object" && json !== null && (("openapi" in json) || ("swagger" in json) || ("paths" in json) && ("components" in json))) {
|
|
6103
|
-
return {
|
|
6104
|
-
sourceType: "openapi",
|
|
6105
|
-
data: json,
|
|
6106
|
-
derivedResourceName: options.resource,
|
|
6107
|
-
sourceDescription: options.file
|
|
6108
|
-
};
|
|
6109
|
-
}
|
|
6110
|
-
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))) {
|
|
6111
|
-
return {
|
|
6112
|
-
sourceType: "graphql",
|
|
6113
|
-
data: json,
|
|
6114
|
-
derivedResourceName: options.resource,
|
|
6115
|
-
sourceDescription: options.file
|
|
6116
|
-
};
|
|
6117
|
-
}
|
|
6118
|
-
const records = Array.isArray(json) ? json : Array.isArray(json.data) ? json.data : Array.isArray(json.items) ? json.items : [json];
|
|
6119
|
-
return {
|
|
6120
|
-
sourceType: "rest",
|
|
6121
|
-
data: records,
|
|
6122
|
-
derivedResourceName: options.resource || baseName,
|
|
6123
|
-
sourceDescription: options.file
|
|
6124
|
-
};
|
|
6159
|
+
json = JSON.parse(content);
|
|
6125
6160
|
} catch {
|
|
6126
6161
|
if (content.includes("type ") || content.includes("enum ") || content.includes("schema ")) {
|
|
6127
6162
|
return {
|
|
@@ -6133,12 +6168,35 @@ async function loadSourceData(options, customFetch = fetch) {
|
|
|
6133
6168
|
}
|
|
6134
6169
|
throw new Error(`Unable to parse file ${filePath}. Expected valid JSON or GraphQL SDL schema.`);
|
|
6135
6170
|
}
|
|
6171
|
+
if (isOpenAPIDocument(json)) {
|
|
6172
|
+
return {
|
|
6173
|
+
sourceType: "openapi",
|
|
6174
|
+
data: json,
|
|
6175
|
+
...options.resource === undefined ? {} : { derivedResourceName: options.resource },
|
|
6176
|
+
sourceDescription: options.file
|
|
6177
|
+
};
|
|
6178
|
+
}
|
|
6179
|
+
if (isGraphQLDocument(json)) {
|
|
6180
|
+
return {
|
|
6181
|
+
sourceType: "graphql",
|
|
6182
|
+
data: json,
|
|
6183
|
+
...options.resource === undefined ? {} : { derivedResourceName: options.resource },
|
|
6184
|
+
sourceDescription: options.file
|
|
6185
|
+
};
|
|
6186
|
+
}
|
|
6187
|
+
const records = readSampleRecords(json);
|
|
6188
|
+
return {
|
|
6189
|
+
sourceType: "rest",
|
|
6190
|
+
data: records,
|
|
6191
|
+
derivedResourceName: options.resource || baseName,
|
|
6192
|
+
sourceDescription: options.file
|
|
6193
|
+
};
|
|
6136
6194
|
}
|
|
6137
6195
|
if (options.url) {
|
|
6138
6196
|
const url = options.url;
|
|
6139
6197
|
const isGraphQL = options.type === "graphql" || url.endsWith("/graphql") || url.includes("/graphql?");
|
|
6140
6198
|
if (isGraphQL) {
|
|
6141
|
-
const
|
|
6199
|
+
const response = await customFetch(url, {
|
|
6142
6200
|
method: "POST",
|
|
6143
6201
|
headers: {
|
|
6144
6202
|
"Content-Type": "application/json",
|
|
@@ -6147,14 +6205,14 @@ async function loadSourceData(options, customFetch = fetch) {
|
|
|
6147
6205
|
},
|
|
6148
6206
|
body: JSON.stringify({ query: GRAPHQL_INTROSPECTION_QUERY })
|
|
6149
6207
|
});
|
|
6150
|
-
if (!
|
|
6151
|
-
throw new Error(`GraphQL introspection request failed: HTTP ${
|
|
6208
|
+
if (!response.ok) {
|
|
6209
|
+
throw new Error(`GraphQL introspection request failed: HTTP ${response.status} ${response.statusText}`);
|
|
6152
6210
|
}
|
|
6153
|
-
const
|
|
6211
|
+
const json = await response.json();
|
|
6154
6212
|
return {
|
|
6155
6213
|
sourceType: "graphql",
|
|
6156
|
-
data:
|
|
6157
|
-
derivedResourceName: options.resource,
|
|
6214
|
+
data: json,
|
|
6215
|
+
...options.resource === undefined ? {} : { derivedResourceName: options.resource },
|
|
6158
6216
|
sourceDescription: url
|
|
6159
6217
|
};
|
|
6160
6218
|
}
|
|
@@ -6164,25 +6222,25 @@ async function loadSourceData(options, customFetch = fetch) {
|
|
|
6164
6222
|
Accept: "application/json",
|
|
6165
6223
|
...options.headers ?? {}
|
|
6166
6224
|
},
|
|
6167
|
-
body: options.body
|
|
6225
|
+
...options.body === undefined ? {} : { body: options.body }
|
|
6168
6226
|
});
|
|
6169
6227
|
if (!response.ok) {
|
|
6170
6228
|
throw new Error(`HTTP request failed: HTTP ${response.status} ${response.statusText}`);
|
|
6171
6229
|
}
|
|
6172
6230
|
const json = await response.json();
|
|
6173
|
-
if (
|
|
6231
|
+
if (isOpenAPIDocument(json)) {
|
|
6174
6232
|
return {
|
|
6175
6233
|
sourceType: "openapi",
|
|
6176
6234
|
data: json,
|
|
6177
|
-
derivedResourceName: options.resource,
|
|
6235
|
+
...options.resource === undefined ? {} : { derivedResourceName: options.resource },
|
|
6178
6236
|
sourceDescription: url
|
|
6179
6237
|
};
|
|
6180
6238
|
}
|
|
6181
|
-
if (
|
|
6239
|
+
if (isGraphQLDocument(json)) {
|
|
6182
6240
|
return {
|
|
6183
6241
|
sourceType: "graphql",
|
|
6184
6242
|
data: json,
|
|
6185
|
-
derivedResourceName: options.resource,
|
|
6243
|
+
...options.resource === undefined ? {} : { derivedResourceName: options.resource },
|
|
6186
6244
|
sourceDescription: url
|
|
6187
6245
|
};
|
|
6188
6246
|
}
|
|
@@ -6196,7 +6254,7 @@ async function loadSourceData(options, customFetch = fetch) {
|
|
|
6196
6254
|
derivedName = "items";
|
|
6197
6255
|
}
|
|
6198
6256
|
}
|
|
6199
|
-
const records =
|
|
6257
|
+
const records = readSampleRecords(json);
|
|
6200
6258
|
return {
|
|
6201
6259
|
sourceType: "rest",
|
|
6202
6260
|
data: records,
|
|
@@ -6270,9 +6328,11 @@ async function executeInfer(options, customFetch = fetch) {
|
|
|
6270
6328
|
let resources;
|
|
6271
6329
|
const bundles = new Map;
|
|
6272
6330
|
if (loaded.sourceType === "openapi") {
|
|
6331
|
+
if (!isRecord(loaded.data))
|
|
6332
|
+
throw new Error("OpenAPI source must be an object");
|
|
6273
6333
|
resources = inferFromOpenAPI(loaded.data, {
|
|
6274
6334
|
primaryKey,
|
|
6275
|
-
|
|
6335
|
+
...options.resource ? { include: [options.resource] } : {}
|
|
6276
6336
|
});
|
|
6277
6337
|
for (const res of resources) {
|
|
6278
6338
|
bundles.set(res.name, generateResourceBundle(res));
|
|
@@ -6280,14 +6340,14 @@ async function executeInfer(options, customFetch = fetch) {
|
|
|
6280
6340
|
} else if (loaded.sourceType === "graphql") {
|
|
6281
6341
|
resources = inferFromGraphQL(loaded.data, {
|
|
6282
6342
|
primaryKey,
|
|
6283
|
-
|
|
6343
|
+
...options.resource ? { include: [options.resource] } : {}
|
|
6284
6344
|
});
|
|
6285
6345
|
for (const res of resources) {
|
|
6286
6346
|
bundles.set(res.name, generateResourceBundle(res));
|
|
6287
6347
|
}
|
|
6288
6348
|
} else {
|
|
6289
6349
|
const resName = options.resource || loaded.derivedResourceName || "items";
|
|
6290
|
-
const sampleArray =
|
|
6350
|
+
const sampleArray = readSampleRecords(loaded.data);
|
|
6291
6351
|
const inferRes = inferResource(resName, sampleArray, { primaryKey });
|
|
6292
6352
|
resources = [inferRes.resource];
|
|
6293
6353
|
bundles.set(resName, inferRes);
|
|
@@ -6312,7 +6372,7 @@ async function executeInfer(options, customFetch = fetch) {
|
|
|
6312
6372
|
files,
|
|
6313
6373
|
sourceDescription: loaded.sourceDescription,
|
|
6314
6374
|
wrote,
|
|
6315
|
-
outDir: options.outDir
|
|
6375
|
+
...options.outDir === undefined ? {} : { outDir: options.outDir }
|
|
6316
6376
|
};
|
|
6317
6377
|
}
|
|
6318
6378
|
function printInferResult(result) {
|
|
@@ -6409,14 +6469,33 @@ async function promptInferWizard() {
|
|
|
6409
6469
|
initial: true
|
|
6410
6470
|
}
|
|
6411
6471
|
]);
|
|
6412
|
-
|
|
6413
|
-
|
|
6472
|
+
return parseInferWizardAnswers(answers);
|
|
6473
|
+
}
|
|
6474
|
+
function parseInferWizardAnswers(answers) {
|
|
6475
|
+
if (!isRecord(answers))
|
|
6476
|
+
throw new Error("Invalid inference wizard answers");
|
|
6477
|
+
const sourceType = answers["sourceType"];
|
|
6478
|
+
if (sourceType !== "rest-file" && sourceType !== "rest-url" && sourceType !== "openapi" && sourceType !== "graphql") {
|
|
6479
|
+
throw new Error("Invalid inference wizard source type");
|
|
6480
|
+
}
|
|
6481
|
+
const targetPath = sourceType === "rest-file" || sourceType === "openapi" ? answers["pathOrUrl"] : answers["endpointUrl"];
|
|
6482
|
+
if (typeof targetPath !== "string" || !targetPath.trim()) {
|
|
6483
|
+
throw new Error("Inference wizard requires a URL or file path");
|
|
6484
|
+
}
|
|
6485
|
+
const outDir = answers["outDir"];
|
|
6486
|
+
const write = answers["write"];
|
|
6487
|
+
if (typeof outDir !== "string" || !outDir.trim() || typeof write !== "boolean") {
|
|
6488
|
+
throw new Error("Invalid inference wizard output options");
|
|
6489
|
+
}
|
|
6490
|
+
const isUrl = targetPath.startsWith("http://") || targetPath.startsWith("https://");
|
|
6491
|
+
if ((sourceType === "rest-url" || sourceType === "graphql") && !isUrl) {
|
|
6492
|
+
throw new Error("Inference wizard requires an HTTP(S) endpoint");
|
|
6493
|
+
}
|
|
6414
6494
|
return {
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
write: answers.write
|
|
6495
|
+
...isUrl ? { url: targetPath } : { file: targetPath },
|
|
6496
|
+
type: sourceType === "graphql" ? "graphql" : sourceType === "openapi" ? "openapi" : "rest",
|
|
6497
|
+
outDir,
|
|
6498
|
+
write
|
|
6420
6499
|
};
|
|
6421
6500
|
}
|
|
6422
6501
|
async function inferCommand(args) {
|
|
@@ -6442,34 +6521,81 @@ import path2 from "node:path";
|
|
|
6442
6521
|
function parseGenerateArguments(args) {
|
|
6443
6522
|
return parseInferArguments(args);
|
|
6444
6523
|
}
|
|
6524
|
+
var fieldTypes = {
|
|
6525
|
+
text: true,
|
|
6526
|
+
number: true,
|
|
6527
|
+
boolean: true,
|
|
6528
|
+
date: true,
|
|
6529
|
+
time: true,
|
|
6530
|
+
datetime: true,
|
|
6531
|
+
daterange: true,
|
|
6532
|
+
select: true,
|
|
6533
|
+
multiselect: true,
|
|
6534
|
+
tags: true,
|
|
6535
|
+
textarea: true,
|
|
6536
|
+
richtext: true,
|
|
6537
|
+
image: true,
|
|
6538
|
+
images: true,
|
|
6539
|
+
json: true,
|
|
6540
|
+
relation: true,
|
|
6541
|
+
color: true,
|
|
6542
|
+
url: true,
|
|
6543
|
+
email: true,
|
|
6544
|
+
phone: true,
|
|
6545
|
+
currency: true,
|
|
6546
|
+
percent: true,
|
|
6547
|
+
file: true,
|
|
6548
|
+
markdown: true,
|
|
6549
|
+
password: true,
|
|
6550
|
+
array: true,
|
|
6551
|
+
"tree-select": true,
|
|
6552
|
+
treeselect: true,
|
|
6553
|
+
cascader: true,
|
|
6554
|
+
transfer: true,
|
|
6555
|
+
rate: true,
|
|
6556
|
+
rating: true,
|
|
6557
|
+
avatar: true,
|
|
6558
|
+
copy: true,
|
|
6559
|
+
code: true
|
|
6560
|
+
};
|
|
6561
|
+
function isFieldType(value) {
|
|
6562
|
+
return Object.hasOwn(fieldTypes, value);
|
|
6563
|
+
}
|
|
6564
|
+
function parseManualFields(fields, primaryKey) {
|
|
6565
|
+
const keys = new Set;
|
|
6566
|
+
return fields.split(",").map((field) => {
|
|
6567
|
+
const [rawKey, rawType, extra] = field.split(":");
|
|
6568
|
+
const key = rawKey?.trim();
|
|
6569
|
+
const type = rawType === undefined ? "text" : rawType.trim();
|
|
6570
|
+
if (!key || extra !== undefined)
|
|
6571
|
+
throw new Error(`Invalid field definition: ${field}`);
|
|
6572
|
+
if (!isFieldType(type))
|
|
6573
|
+
throw new Error(`Invalid field type: ${type}`);
|
|
6574
|
+
if (keys.has(key))
|
|
6575
|
+
throw new Error(`Duplicate field: ${key}`);
|
|
6576
|
+
keys.add(key);
|
|
6577
|
+
return {
|
|
6578
|
+
key,
|
|
6579
|
+
label: key.charAt(0).toUpperCase() + key.slice(1),
|
|
6580
|
+
type,
|
|
6581
|
+
required: key === primaryKey
|
|
6582
|
+
};
|
|
6583
|
+
});
|
|
6584
|
+
}
|
|
6445
6585
|
async function generateCommand(args) {
|
|
6446
6586
|
const options = parseGenerateArguments(args);
|
|
6447
6587
|
if (options.resource && options.fields) {
|
|
6448
6588
|
const resourceName = options.resource;
|
|
6449
6589
|
const primaryKey = options.primaryKey ?? "id";
|
|
6450
|
-
const fieldDefs = options.fields
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
});
|
|
6460
|
-
const mockSample = {};
|
|
6461
|
-
for (const f of fieldDefs) {
|
|
6462
|
-
mockSample[f.key] = f.type === "number" ? 1 : f.type === "boolean" ? true : `sample_${f.key}`;
|
|
6463
|
-
}
|
|
6464
|
-
const inferRes = inferResource(resourceName, [mockSample], { primaryKey });
|
|
6465
|
-
const resources = [
|
|
6466
|
-
{
|
|
6467
|
-
name: resourceName,
|
|
6468
|
-
label: resourceName.charAt(0).toUpperCase() + resourceName.slice(1),
|
|
6469
|
-
primaryKey,
|
|
6470
|
-
fields: fieldDefs
|
|
6471
|
-
}
|
|
6472
|
-
];
|
|
6590
|
+
const fieldDefs = parseManualFields(options.fields, primaryKey);
|
|
6591
|
+
const resource = {
|
|
6592
|
+
name: resourceName,
|
|
6593
|
+
label: resourceName.charAt(0).toUpperCase() + resourceName.slice(1),
|
|
6594
|
+
primaryKey,
|
|
6595
|
+
fields: fieldDefs
|
|
6596
|
+
};
|
|
6597
|
+
const resources = [resource];
|
|
6598
|
+
const inferRes = generateResourceBundle(resource);
|
|
6473
6599
|
const bundles = new Map([[resourceName, inferRes]]);
|
|
6474
6600
|
const files = planGeneratedFiles(resources, bundles, options.format);
|
|
6475
6601
|
let wrote = false;
|
|
@@ -6488,7 +6614,7 @@ async function generateCommand(args) {
|
|
|
6488
6614
|
files,
|
|
6489
6615
|
sourceDescription: `manual schema: ${options.fields}`,
|
|
6490
6616
|
wrote,
|
|
6491
|
-
outDir: options.outDir
|
|
6617
|
+
...options.outDir === undefined ? {} : { outDir: options.outDir }
|
|
6492
6618
|
});
|
|
6493
6619
|
return;
|
|
6494
6620
|
}
|
|
@@ -6781,17 +6907,17 @@ import {
|
|
|
6781
6907
|
unlinkSync,
|
|
6782
6908
|
writeFileSync
|
|
6783
6909
|
} from "node:fs";
|
|
6784
|
-
function parseDependencyMap(candidate,
|
|
6910
|
+
function parseDependencyMap(candidate, path) {
|
|
6785
6911
|
if (candidate === undefined)
|
|
6786
6912
|
return;
|
|
6787
|
-
assertStringRecord(candidate,
|
|
6913
|
+
assertStringRecord(candidate, path);
|
|
6788
6914
|
return { ...candidate };
|
|
6789
6915
|
}
|
|
6790
6916
|
function parseMaintainedPackageJson(packageJsonCandidate) {
|
|
6791
6917
|
assertJsonObject(packageJsonCandidate, "package.json");
|
|
6792
6918
|
const packageJson = structuredClone(packageJsonCandidate);
|
|
6793
|
-
const dependencies = parseDependencyMap(packageJsonCandidate
|
|
6794
|
-
const devDependencies = parseDependencyMap(packageJsonCandidate
|
|
6919
|
+
const dependencies = parseDependencyMap(packageJsonCandidate["dependencies"], "package.json.dependencies");
|
|
6920
|
+
const devDependencies = parseDependencyMap(packageJsonCandidate["devDependencies"], "package.json.devDependencies");
|
|
6795
6921
|
if (dependencies !== undefined)
|
|
6796
6922
|
packageJson.dependencies = dependencies;
|
|
6797
6923
|
if (devDependencies !== undefined)
|
|
@@ -6837,6 +6963,8 @@ function desiredDependencies(project, scaffold) {
|
|
|
6837
6963
|
}
|
|
6838
6964
|
for (const packName of selectedDependencyPacks(project, scaffold)) {
|
|
6839
6965
|
const pack = scaffold.svadmin.dependencyPacks[packName];
|
|
6966
|
+
if (pack === undefined)
|
|
6967
|
+
throw new Error(`Unknown dependency pack: ${packName}`);
|
|
6840
6968
|
for (const [packageName, version] of Object.entries(pack)) {
|
|
6841
6969
|
const existing = desired.get(packageName);
|
|
6842
6970
|
if (existing !== undefined && (existing.version !== version || existing.section !== "dependencies")) {
|
|
@@ -6947,12 +7075,12 @@ function duplicateDependencyIssue(desired, state) {
|
|
|
6947
7075
|
function doctorIssueForDependency(project, desired) {
|
|
6948
7076
|
const state = dependencyState(project, desired);
|
|
6949
7077
|
if (state.canonicalVersion === undefined) {
|
|
6950
|
-
return state.alternateVersion === undefined ? missingDependencyIssue(desired) : misplacedDependencyIssue(desired, state);
|
|
7078
|
+
return state.alternateVersion === undefined ? missingDependencyIssue(desired) : misplacedDependencyIssue(desired, { ...state, alternateVersion: state.alternateVersion });
|
|
6951
7079
|
}
|
|
6952
7080
|
if (state.canonicalVersion !== desired.version) {
|
|
6953
7081
|
return versionDependencyIssue(desired, state.canonicalVersion);
|
|
6954
7082
|
}
|
|
6955
|
-
return state.alternateVersion === undefined ? null : duplicateDependencyIssue(desired, state);
|
|
7083
|
+
return state.alternateVersion === undefined ? null : duplicateDependencyIssue(desired, { ...state, alternateVersion: state.alternateVersion });
|
|
6956
7084
|
}
|
|
6957
7085
|
function doctorProjectPackageJson(packageJsonCandidate, scaffold) {
|
|
6958
7086
|
const project = parseMaintainedPackageJson(packageJsonCandidate);
|
|
@@ -6960,10 +7088,10 @@ function doctorProjectPackageJson(packageJsonCandidate, scaffold) {
|
|
|
6960
7088
|
return issues.length === 0 ? { status: "clean", exitCode: 0, issues: [] } : { status: "issues", exitCode: 1, issues };
|
|
6961
7089
|
}
|
|
6962
7090
|
function upgradeChangeForDependency(desired, state) {
|
|
6963
|
-
if (state.canonicalVersion === undefined && state.alternateVersion === undefined) {
|
|
6964
|
-
return { action: "add", packageName: desired.packageName, to: desired.version, section: desired.section };
|
|
6965
|
-
}
|
|
6966
7091
|
if (state.canonicalVersion === undefined) {
|
|
7092
|
+
if (state.alternateVersion === undefined) {
|
|
7093
|
+
return { action: "add", packageName: desired.packageName, to: desired.version, section: desired.section };
|
|
7094
|
+
}
|
|
6967
7095
|
return {
|
|
6968
7096
|
action: "move",
|
|
6969
7097
|
packageName: desired.packageName,
|
|
@@ -6981,7 +7109,7 @@ function upgradeChangeForDependency(desired, state) {
|
|
|
6981
7109
|
from: state.canonicalVersion,
|
|
6982
7110
|
to: desired.version,
|
|
6983
7111
|
section: desired.section,
|
|
6984
|
-
|
|
7112
|
+
...state.alternateVersion === undefined ? {} : { previousSection: state.alternateSection }
|
|
6985
7113
|
};
|
|
6986
7114
|
}
|
|
6987
7115
|
function applyDesiredDependency(updatedPackageJson, desired) {
|
|
@@ -7325,7 +7453,7 @@ bun run dev
|
|
|
7325
7453
|
|
|
7326
7454
|
## Stack
|
|
7327
7455
|
|
|
7328
|
-
- **UI**: Svelte 5 +
|
|
7456
|
+
- **UI**: Svelte 5 + Bits UI + precompiled Panda CSS
|
|
7329
7457
|
- **Data**: ${dpLabel} DataProvider
|
|
7330
7458
|
- **Auth**: ${authLabel}
|
|
7331
7459
|
- **State**: TanStack Query v6
|
package/guidance/AGENTS.md
CHANGED
|
@@ -13,6 +13,45 @@ acceptance checks in this project.
|
|
|
13
13
|
custom markup or raw colors.
|
|
14
14
|
5. Verify loading, empty, partial, error, permission, success, and mobile states.
|
|
15
15
|
|
|
16
|
+
## Type-safe AI development
|
|
17
|
+
|
|
18
|
+
- Read the actual provider API and resource models before generating a page.
|
|
19
|
+
Do not invent fields or duplicate server/schema types.
|
|
20
|
+
- Define module-level contracts with `defineResource(name, schemas)` and derive
|
|
21
|
+
models from TypeBox `Static<typeof Schema>`. Do not duplicate models or use
|
|
22
|
+
declaration merging as proof of runtime validation.
|
|
23
|
+
- Use `useList`, `useOne`, `useShow`, and `useMany` with a resource contract.
|
|
24
|
+
`useTable` also infers query rows and `clientData`; `useInfiniteList` and `useSelect`
|
|
25
|
+
use the same contract. String resources, implicit routes, and data generics are
|
|
26
|
+
rejected by the default entry point. Use getters for reactive query inputs.
|
|
27
|
+
- Treat network responses, storage contents, and AI tool arguments as `unknown`
|
|
28
|
+
until validated at their boundary. Resource contracts validate CRUD requests
|
|
29
|
+
and responses automatically, but they do not authorize a mutation.
|
|
30
|
+
- Provide the full closed record schema with a required string/number `id`.
|
|
31
|
+
Supply create and update schemas explicitly; missing schemas disable writes.
|
|
32
|
+
Extra fields, nested `any`, open dictionaries, and unsupported schema kinds
|
|
33
|
+
are rejected. Do not coerce values or silently remove unexpected fields.
|
|
34
|
+
- Define custom calls with `defineCommand` and use `useCustom` or
|
|
35
|
+
`useCustomMutation`. Inputs and outputs must have schemas; URLs and methods
|
|
36
|
+
cannot be overridden by individual calls.
|
|
37
|
+
- Bind `useCreate`, `useUpdate`, `useDelete`, and their `Many` variants to an
|
|
38
|
+
explicit contract to infer mutation inputs and results. A bound mutation
|
|
39
|
+
cannot target another resource. `useUpdate` and `useDelete` require an ID.
|
|
40
|
+
- Keep required delete variables in `useDeleteMany` calls; both native batch
|
|
41
|
+
providers and single-record fallbacks receive them.
|
|
42
|
+
- Bind forms to a contract and a fixed create/edit action. `defaultValues` and
|
|
43
|
+
`setFieldValue` follow the operation schema, including required fields.
|
|
44
|
+
Edit backfill excludes record-only fields.
|
|
45
|
+
- Do not import `@svadmin/core/unsafe` in new application pages. It is reserved
|
|
46
|
+
for reviewed metadata-driven framework boundaries, not compatibility fallback.
|
|
47
|
+
- Do not automatically retry an invalid write response: inspect the structured
|
|
48
|
+
`writeMayHaveSucceeded` diagnostic and read back the server state first.
|
|
49
|
+
- Never silence a mismatch with `any`, double assertions, `@ts-ignore`, or a
|
|
50
|
+
broader index signature. Fix the source contract or validate and narrow it.
|
|
51
|
+
- Run the generated project's `bun run check` and focused tests after each
|
|
52
|
+
change. Cover invalid fields and payloads as well as successful paths.
|
|
53
|
+
Passing runtime tests alone does not prove compile-time type safety.
|
|
54
|
+
|
|
16
55
|
## Feedback invariant
|
|
17
56
|
|
|
18
57
|
`one event -> one primary feedback surface`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/create",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Scaffolding tool for svadmin projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"@types/node": "^26.4.0",
|
|
28
28
|
"@types/prompts": "^2.4.9",
|
|
29
29
|
"typescript": "^7.0.2",
|
|
30
|
-
"@svadmin/core": "^0.
|
|
30
|
+
"@svadmin/core": "^0.53.0"
|
|
31
31
|
},
|
|
32
32
|
"keywords": [
|
|
33
33
|
"svadmin",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|
|
42
|
-
"url": "https://github.com/vibeunion/svadmin.git",
|
|
42
|
+
"url": "git+https://github.com/vibeunion/svadmin.git",
|
|
43
43
|
"directory": "packages/create-svadmin"
|
|
44
44
|
}
|
|
45
45
|
}
|
package/scaffold-manifest.json
CHANGED
|
@@ -10,20 +10,17 @@
|
|
|
10
10
|
"check": "svelte-check --tsconfig ./tsconfig.json"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@svadmin/core": "^0.
|
|
14
|
-
"@svadmin/ai-elements": "^0.
|
|
15
|
-
"@svadmin/ui": "^0.
|
|
13
|
+
"@svadmin/core": "^0.53.0",
|
|
14
|
+
"@svadmin/ai-elements": "^0.8.0",
|
|
15
|
+
"@svadmin/ui": "^0.73.0",
|
|
16
16
|
"@sinclair/typebox": "^0.34.52",
|
|
17
17
|
"@tanstack/svelte-query": "^6.1.48",
|
|
18
18
|
"@lucide/svelte": "^1.35.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
22
|
-
"@tailwindcss/vite": "^4.3.3",
|
|
23
22
|
"svelte": "^5.56.10",
|
|
24
23
|
"svelte-check": "^4.7.6",
|
|
25
|
-
"tailwindcss": "^4.3.3",
|
|
26
|
-
"tw-animate-css": "^1.4.0",
|
|
27
24
|
"typescript": "~6.0.3",
|
|
28
25
|
"vite": "^8.2.2"
|
|
29
26
|
},
|
|
@@ -33,16 +30,16 @@
|
|
|
33
30
|
"@refinedev/core": "^5.0.12"
|
|
34
31
|
},
|
|
35
32
|
"simple-rest": {
|
|
36
|
-
"@svadmin/simple-rest": "^0.
|
|
33
|
+
"@svadmin/simple-rest": "^0.10.7",
|
|
37
34
|
"@refinedev/simple-rest": "^6.0.1"
|
|
38
35
|
},
|
|
39
36
|
"supabase": {
|
|
40
|
-
"@svadmin/supabase": "^0.
|
|
37
|
+
"@svadmin/supabase": "^0.15.1",
|
|
41
38
|
"@supabase/supabase-js": "^2.112.4",
|
|
42
39
|
"@refinedev/supabase": "^6.0.2"
|
|
43
40
|
},
|
|
44
41
|
"graphql": {
|
|
45
|
-
"@svadmin/graphql": "^0.
|
|
42
|
+
"@svadmin/graphql": "^0.10.7",
|
|
46
43
|
"@refinedev/graphql": "^8.0.1",
|
|
47
44
|
"graphql-request": "^7.4.0",
|
|
48
45
|
"graphql": "^16.8.0"
|
package/template/src/app.css
CHANGED
|
@@ -1,8 +1,2 @@
|
|
|
1
|
-
@import "
|
|
2
|
-
@import "tw-animate-css";
|
|
3
|
-
|
|
4
|
-
/* Tailwind source tokens and component discovery are owned by SVAdmin packages. */
|
|
5
|
-
@import "@svadmin/ui/app.theme.css";
|
|
1
|
+
@import "@svadmin/ui/app.css";
|
|
6
2
|
@import "@svadmin/ai-elements/ai.css";
|
|
7
|
-
|
|
8
|
-
@custom-variant dark (&:is(.dark *));
|
|
@@ -1,79 +1,92 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useList } from '@svadmin/core';
|
|
3
|
-
import {
|
|
3
|
+
import { posts, users, comments } from '../resource-contracts';
|
|
4
|
+
import { Button, DataState, MetricBlock } from '@svadmin/ui';
|
|
4
5
|
|
|
5
|
-
const postsQuery = useList({ resource:
|
|
6
|
-
const usersQuery = useList({ resource:
|
|
7
|
-
const commentsQuery = useList({ resource:
|
|
6
|
+
const postsQuery = useList({ resource: posts, pagination: { current: 1, pageSize: 1 } });
|
|
7
|
+
const usersQuery = useList({ resource: users, pagination: { current: 1, pageSize: 1 } });
|
|
8
|
+
const commentsQuery = useList({ resource: comments, pagination: { current: 1, pageSize: 1 } });
|
|
8
9
|
|
|
9
10
|
const stats = $derived([
|
|
10
|
-
{ label: 'Total Posts',
|
|
11
|
-
{ label: 'Total Users',
|
|
12
|
-
{ label: 'Total Comments',
|
|
11
|
+
{ label: 'Total Posts', query: postsQuery },
|
|
12
|
+
{ label: 'Total Users', query: usersQuery },
|
|
13
|
+
{ label: 'Total Comments', query: commentsQuery },
|
|
13
14
|
]);
|
|
14
15
|
|
|
15
|
-
const
|
|
16
|
+
const hasMetricError = $derived(stats.some(stat => stat.query.isError));
|
|
16
17
|
|
|
17
|
-
const recentPosts = useList({ resource:
|
|
18
|
-
const recentUsers = useList({ resource:
|
|
18
|
+
const recentPosts = useList({ resource: posts, pagination: { current: 1, pageSize: 5 } });
|
|
19
|
+
const recentUsers = useList({ resource: users, pagination: { current: 1, pageSize: 5 } });
|
|
19
20
|
</script>
|
|
20
21
|
|
|
21
|
-
<div class="space-y-
|
|
22
|
-
<h1 class="text-
|
|
22
|
+
<div class="space-y-4">
|
|
23
|
+
<h1 class="text-xl font-semibold text-foreground">Dashboard</h1>
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
{#if hasMetricError}
|
|
26
|
+
<DataState state="error" title="Some totals are unavailable" retry={() => {
|
|
27
|
+
for (const stat of stats) if (stat.query.isError && !stat.query.isFetching) void stat.query.refetch();
|
|
28
|
+
}} />
|
|
29
|
+
{/if}
|
|
25
30
|
<div class="grid gap-4 sm:grid-cols-3">
|
|
26
|
-
{#each stats as stat
|
|
27
|
-
<
|
|
28
|
-
<div class="flex h-12 w-12 items-center justify-center rounded-xl {stat.color}">
|
|
29
|
-
<stat.Icon class="h-6 w-6" />
|
|
30
|
-
</div>
|
|
31
|
-
<div>
|
|
32
|
-
<p class="text-sm text-gray-500">{stat.label}</p>
|
|
33
|
-
{#if isLoading}
|
|
34
|
-
<Loader2 class="mt-1 h-5 w-5 animate-spin text-gray-300" />
|
|
35
|
-
{:else}
|
|
36
|
-
<p class="text-2xl font-bold text-gray-900">{stat.value}</p>
|
|
37
|
-
{/if}
|
|
38
|
-
</div>
|
|
39
|
-
</div>
|
|
31
|
+
{#each stats as stat (stat.label)}
|
|
32
|
+
<MetricBlock label={stat.label} value={stat.query.isError ? '—' : stat.query.data?.total ?? '—'} loading={stat.query.isLoading} />
|
|
40
33
|
{/each}
|
|
41
34
|
</div>
|
|
42
35
|
|
|
43
36
|
<!-- Recent Data -->
|
|
44
37
|
<div class="grid gap-6 lg:grid-cols-2">
|
|
45
|
-
<
|
|
46
|
-
<div class="flex items-center justify-between border-b
|
|
47
|
-
<h2 class="font-semibold text-
|
|
38
|
+
<section class="min-w-0">
|
|
39
|
+
<div class="flex items-center justify-between border-b py-3">
|
|
40
|
+
<h2 class="font-semibold text-foreground">Recent Posts</h2>
|
|
48
41
|
<a href="#/posts" class="text-sm text-primary hover:underline">View all</a>
|
|
49
42
|
</div>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
43
|
+
{#if recentPosts.isLoading}
|
|
44
|
+
<DataState state="loading" />
|
|
45
|
+
{:else if recentPosts.isError}
|
|
46
|
+
<DataState state="error" retry={() => { if (!recentPosts.isFetching) void recentPosts.refetch(); }} />
|
|
47
|
+
{:else}
|
|
48
|
+
<div class="divide-y">
|
|
49
|
+
{#each recentPosts.data?.data ?? [] as post, _i (_i)}
|
|
50
|
+
<div class="flex items-center justify-between py-3">
|
|
51
|
+
<div class="min-w-0">
|
|
52
|
+
<p class="break-words text-sm font-medium text-foreground">{post.title}</p>
|
|
53
|
+
<p class="text-xs text-muted-foreground">User #{post.userId}</p>
|
|
56
54
|
</div>
|
|
57
55
|
</div>
|
|
56
|
+
{:else}
|
|
57
|
+
<DataState state="empty">
|
|
58
|
+
{#snippet action()}<Button href="#/posts/create">Create post</Button>{/snippet}
|
|
59
|
+
</DataState>
|
|
58
60
|
{/each}
|
|
59
61
|
</div>
|
|
60
|
-
|
|
62
|
+
{/if}
|
|
63
|
+
</section>
|
|
61
64
|
|
|
62
|
-
<
|
|
63
|
-
<div class="flex items-center justify-between border-b
|
|
64
|
-
<h2 class="font-semibold text-
|
|
65
|
+
<section class="min-w-0">
|
|
66
|
+
<div class="flex items-center justify-between border-b py-3">
|
|
67
|
+
<h2 class="font-semibold text-foreground">Users</h2>
|
|
65
68
|
<a href="#/users" class="text-sm text-primary hover:underline">View all</a>
|
|
66
69
|
</div>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
{#if recentUsers.isLoading}
|
|
71
|
+
<DataState state="loading" />
|
|
72
|
+
{:else if recentUsers.isError}
|
|
73
|
+
<DataState state="error" retry={() => { if (!recentUsers.isFetching) void recentUsers.refetch(); }} />
|
|
74
|
+
{:else}
|
|
75
|
+
<div class="divide-y">
|
|
76
|
+
{#each recentUsers.data?.data ?? [] as user, _i (_i)}
|
|
77
|
+
<div class="flex items-center justify-between py-3">
|
|
78
|
+
<div class="min-w-0">
|
|
79
|
+
<p class="break-words text-sm font-medium text-foreground">{user.name}</p>
|
|
80
|
+
<p class="break-words text-xs text-muted-foreground">{user.email}</p>
|
|
73
81
|
</div>
|
|
74
82
|
</div>
|
|
83
|
+
{:else}
|
|
84
|
+
<DataState state="empty">
|
|
85
|
+
{#snippet action()}<Button href="#/users/create">Create user</Button>{/snippet}
|
|
86
|
+
</DataState>
|
|
75
87
|
{/each}
|
|
76
88
|
</div>
|
|
77
|
-
|
|
89
|
+
{/if}
|
|
90
|
+
</section>
|
|
78
91
|
</div>
|
|
79
92
|
</div>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { defineResource } from '@svadmin/core';
|
|
3
|
+
|
|
4
|
+
export const posts = defineResource('posts', {
|
|
5
|
+
record: Type.Object({
|
|
6
|
+
id: Type.Number(), userId: Type.Number(), title: Type.String(), body: Type.String(),
|
|
7
|
+
}),
|
|
8
|
+
});
|
|
9
|
+
export const users = defineResource('users', {
|
|
10
|
+
record: Type.Object({
|
|
11
|
+
id: Type.Number(), name: Type.String(), username: Type.String(), email: Type.String(),
|
|
12
|
+
phone: Type.String(), website: Type.String(),
|
|
13
|
+
address: Type.Object({
|
|
14
|
+
street: Type.String(), suite: Type.String(), city: Type.String(), zipcode: Type.String(),
|
|
15
|
+
geo: Type.Object({ lat: Type.String(), lng: Type.String() }),
|
|
16
|
+
}),
|
|
17
|
+
company: Type.Object({ name: Type.String(), catchPhrase: Type.String(), bs: Type.String() }),
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
export const comments = defineResource('comments', {
|
|
21
|
+
record: Type.Object({
|
|
22
|
+
id: Type.Number(), postId: Type.Number(), name: Type.String(), email: Type.String(), body: Type.String(),
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
export const todos = defineResource('todos', {
|
|
26
|
+
record: Type.Object({
|
|
27
|
+
id: Type.Number(), userId: Type.Number(), title: Type.String(), completed: Type.Boolean(),
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Example resource definitions — generic demo data (JSONPlaceholder-style)
|
|
2
2
|
|
|
3
3
|
import type { ResourceDefinition } from '@svadmin/core';
|
|
4
|
+
import { posts, users, comments, todos } from './resource-contracts';
|
|
4
5
|
|
|
5
6
|
export const resources: ResourceDefinition[] = [
|
|
6
7
|
{
|
|
7
8
|
name: 'posts',
|
|
9
|
+
contract: posts,
|
|
8
10
|
label: 'Posts',
|
|
9
11
|
icon: 'file-text',
|
|
10
12
|
fields: [
|
|
@@ -18,6 +20,7 @@ export const resources: ResourceDefinition[] = [
|
|
|
18
20
|
|
|
19
21
|
{
|
|
20
22
|
name: 'users',
|
|
23
|
+
contract: users,
|
|
21
24
|
label: 'Users',
|
|
22
25
|
icon: 'users',
|
|
23
26
|
canCreate: false,
|
|
@@ -34,6 +37,7 @@ export const resources: ResourceDefinition[] = [
|
|
|
34
37
|
|
|
35
38
|
{
|
|
36
39
|
name: 'comments',
|
|
40
|
+
contract: comments,
|
|
37
41
|
label: 'Comments',
|
|
38
42
|
icon: 'message-circle',
|
|
39
43
|
fields: [
|
|
@@ -48,6 +52,7 @@ export const resources: ResourceDefinition[] = [
|
|
|
48
52
|
|
|
49
53
|
{
|
|
50
54
|
name: 'todos',
|
|
55
|
+
contract: todos,
|
|
51
56
|
label: 'Todos',
|
|
52
57
|
icon: 'check-square',
|
|
53
58
|
fields: [
|
package/template/vite.config.ts
CHANGED