@olenbetong/appframe-vite 6.1.2 → 6.2.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 CHANGED
@@ -80,12 +80,122 @@ This package reads `package.json.appframe` to know what to proxy and how to buil
80
80
  - Resolve
81
81
  - Adds the alias `~/` to `/src/` so imports like `~/components/Button` resolve to `src/components/Button`.
82
82
 
83
- ## Exported helpers
83
+ ## CLI — `appframe-vite`
84
84
 
85
- Although most users only need the default plugin, the package also exports:
85
+ The package ships a CLI (`appframe-vite`) alongside the Vite plugin.
86
86
 
87
- - `addAppframeBuildConfig(config)`: applies the Appframe build defaults (paths, externals, visualizer).
88
- - `createDevMiddleware(server)`: serves transformed article HTML and injects cached i18n strings.
87
+ ### `generate-types`
88
+
89
+ Generates TypeScript type definitions from the article's data objects and procedures:
90
+
91
+ ```sh
92
+ appframe-vite generate-types
93
+ ```
94
+
95
+ ### `resources generate`
96
+
97
+ Reads `resources.yaml` and regenerates all data object / procedure files:
98
+
99
+ ```sh
100
+ appframe-vite resources generate
101
+ # custom config file path:
102
+ appframe-vite resources generate --config path/to/resources.yaml
103
+ ```
104
+
105
+ ### `resources add`
106
+
107
+ Interactive wizard — prompts for resource, ID, output path, permissions, fields, and more, then appends an entry to `resources.yaml` and generates the output file:
108
+
109
+ ```sh
110
+ appframe-vite resources add
111
+ ```
112
+
113
+ ### `resources edit [id]`
114
+
115
+ Interactive editor — pre-fills the wizard with the current values from `resources.yaml` and regenerates the file on save:
116
+
117
+ ```sh
118
+ appframe-vite resources edit dsAccountGroups
119
+ # or without an id (shows a searchable list):
120
+ appframe-vite resources edit
121
+ ```
122
+
123
+ ## `resources.yaml` config file
124
+
125
+ Place `resources.yaml` at the project root to declare all data objects and procedures for an app. The Vite dev server watches this file and auto-regenerates all output files when it changes.
126
+
127
+ **Example:**
128
+
129
+ ```yaml
130
+ dataObjects:
131
+ - id: dsAccountGroups
132
+ resource: atbv_Accounting_SubsidiaryLedgerGroups
133
+ global: true
134
+ types: true
135
+ maxRecords: -1
136
+ expose: true
137
+ fields:
138
+ - Domain
139
+ - SubsidiaryLedgerGroup
140
+ - PrimKey
141
+ - Description
142
+ output: src/data/dsAccountGroups.ts
143
+
144
+ - id: dsSubsidiaryLedger
145
+ resource: atbv_Accounting_SubsidiaryLedger
146
+ global: true
147
+ types: true
148
+ permissions: IUD
149
+ output: src/data/dsSubsidiaryLedger.ts
150
+
151
+ procedures:
152
+ - id: procCreateCustomer
153
+ resource: astp_Accounting_SubsidiaryLedger_Create
154
+ global: true
155
+ types: true
156
+ expose: true
157
+ output: src/data/procCreateCustomer.ts
158
+ ```
159
+
160
+ **Supported fields per entry:**
161
+
162
+ | Field | Type | Description |
163
+ |---|---|---|
164
+ | `id` | `string` | Variable name used in generated code (e.g. `dsAccountGroups`) |
165
+ | `resource` | `string` | Database object ID (e.g. `atbv_Accounting_SubsidiaryLedgerGroups`) |
166
+ | `output` | `string` | Output file path relative to project root |
167
+ | `global` | `boolean` | Use `af.data.generateApiDataObject` / `new af.ProcedureAPI` globals |
168
+ | `types` | `boolean` | Emit TypeScript type definitions |
169
+ | `permissions` | `string` | Permissions: I = insert, U = update, D = delete (e.g. `IUD`) |
170
+ | `maxRecords` | `number` | Max records to fetch (default `50`; `-1` for all) |
171
+ | `sortOrder` | `string \| string[]` | Sort order, e.g. `Created:Desc` or `[Created:Desc, Name]` |
172
+ | `master` | `string` | Master data object name (or `name:importPath`) |
173
+ | `linkFields` | `string \| string[]` | Fields linking child to master |
174
+ | `expose` | `boolean \| string` | Expose on `af.article.dataObjects` / `af.article.procedures` |
175
+ | `dynamic` | `boolean` | Enable dynamic loading |
176
+ | `unique` | `string` | Unique table name for update/delete |
177
+ | `overrides` | `string \| string[]` | Type overrides, e.g. `MyField:string[]` |
178
+ | `distinct` | `boolean` | Fetch distinct rows |
179
+ | `aggregates` | `string \| string[]` | Aggregate bindings, e.g. `Qty:SUM` |
180
+ | `groupBy` | `string \| string[]` | Group-by fields |
181
+ | `where` | `string` | Initial where clause |
182
+ | `fields` | `string \| string[]` | Fields to include (all if omitted) |
183
+
184
+ A top-level `server` key can override the hostname (defaults to `appframe.proxy.hostname` from `package.json`).
185
+
186
+ ## `@olenbetong/appframe-vite/resources` export
187
+
188
+ Shared code generation utilities for Node.js consumers:
189
+
190
+ ```ts
191
+ import {
192
+ fetchAndGenerate,
193
+ buildYamlConfig,
194
+ parseYamlConfig,
195
+ formatWithBiome,
196
+ getCustomImportPath,
197
+ type CLIOptions,
198
+ } from "@olenbetong/appframe-vite/resources";
199
+ ```
89
200
 
90
- See `docs/reference/appframe-vite/` for a deeper dive into internals and configuration.
91
201
 
@@ -0,0 +1 @@
1
+ export declare function addResource(): Promise<void>;
@@ -0,0 +1,184 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, relative, resolve } from "node:path";
3
+ import { Client, generateApiDataHandler } from "@olenbetong/appframe-data";
4
+ import { config } from "dotenv";
5
+ import fuzzy from "fuzzy";
6
+ import inquirer from "inquirer";
7
+ import autocomplete from "inquirer-autocomplete-standalone";
8
+ import { getLoginInfo } from "./devServer.js";
9
+ import { fetchAndGenerate, fetchResourceDefinition, formatWithBiome } from "./resourceGenerate.js";
10
+ import { entryToCLIOptions, readResourcesConfig, writeResourcesConfig } from "./resourcesConfig.js";
11
+ config({ quiet: true });
12
+ async function getResources(client) {
13
+ let ds = generateApiDataHandler({
14
+ client,
15
+ resource: "API_Resources",
16
+ fields: [
17
+ { name: "DBObjectID", type: "string", nullable: false },
18
+ { name: "Name", type: "string", nullable: true },
19
+ { name: "ObjectType", type: "string", nullable: true },
20
+ ],
21
+ });
22
+ return ds.retrieve({ maxRecords: -1 });
23
+ }
24
+ export async function addResource() {
25
+ let { hostname, username, password } = await getLoginInfo();
26
+ let client = new Client(hostname);
27
+ await client.login(username, password);
28
+ // Step 1: pick a resource
29
+ let resources = await getResources(client);
30
+ let resourceChoices = resources.map((r) => ({
31
+ name: r.Name && r.Name !== r.DBObjectID ? `${r.DBObjectID} / ${r.Name}` : r.DBObjectID,
32
+ value: r.DBObjectID,
33
+ objectType: r.ObjectType,
34
+ }));
35
+ let chosenResource = await autocomplete({
36
+ message: "Select a resource",
37
+ source: async (input) => {
38
+ return fuzzy
39
+ .filter(input ?? "", resourceChoices, { extract: (el) => el.name })
40
+ .map((el) => ({ name: el.original.name, value: el.original.value }));
41
+ },
42
+ });
43
+ let chosenMeta = resourceChoices.find((r) => r.value === chosenResource);
44
+ let isView = chosenMeta?.objectType === "V";
45
+ // Fetch definition for field picker
46
+ let definition = await fetchResourceDefinition(client, chosenResource);
47
+ let allFields = definition.Parameters?.filter((p) => !["CUT", "CDL"].includes(p.Name)).map((p) => isView ? p.Name : p.ParamName) ?? [];
48
+ // Step 2: choose ID
49
+ let defaultId = isView
50
+ ? `ds${chosenResource.replace(/^a[a-z]+_[A-Za-z]+_/, "")}`
51
+ : `proc${chosenResource.replace(/^astp_[A-Za-z]+_/, "")}`;
52
+ let { id } = await inquirer.prompt([
53
+ {
54
+ type: "input",
55
+ name: "id",
56
+ message: "Data object / procedure ID",
57
+ default: defaultId,
58
+ },
59
+ ]);
60
+ // Step 3: output path
61
+ let defaultOutput = `src/data/${id}.ts`;
62
+ let { output } = await inquirer.prompt([
63
+ {
64
+ type: "input",
65
+ name: "output",
66
+ message: "Output file path (relative to project root)",
67
+ default: defaultOutput,
68
+ },
69
+ ]);
70
+ // Step 4: global vs module imports
71
+ let { useGlobal } = await inquirer.prompt([
72
+ {
73
+ type: "confirm",
74
+ name: "useGlobal",
75
+ message: "Use global af.data / af.ProcedureAPI (instead of ES imports)?",
76
+ default: true,
77
+ },
78
+ ]);
79
+ // Step 5: TypeScript types
80
+ let { withTypes } = await inquirer.prompt([
81
+ {
82
+ type: "confirm",
83
+ name: "withTypes",
84
+ message: "Generate TypeScript type definitions?",
85
+ default: true,
86
+ },
87
+ ]);
88
+ // Step 6: expose
89
+ let { exposeIt } = await inquirer.prompt([
90
+ {
91
+ type: "confirm",
92
+ name: "exposeIt",
93
+ message: `Expose on af.article.${isView ? "dataObjects" : "procedures"}?`,
94
+ default: false,
95
+ },
96
+ ]);
97
+ // View-specific options
98
+ let permissions;
99
+ let maxRecords = "50";
100
+ let selectedFields = [];
101
+ let sortOrder;
102
+ if (isView) {
103
+ // Permissions
104
+ let { permInput } = await inquirer.prompt([
105
+ {
106
+ type: "checkbox",
107
+ name: "permInput",
108
+ message: "Permissions",
109
+ choices: [
110
+ { name: "Insert (I)", value: "I" },
111
+ { name: "Update (U)", value: "U" },
112
+ { name: "Delete (D)", value: "D" },
113
+ ],
114
+ },
115
+ ]);
116
+ permissions = permInput.length > 0 ? permInput.join("") : undefined;
117
+ // Max records
118
+ let { maxRec } = await inquirer.prompt([
119
+ {
120
+ type: "input",
121
+ name: "maxRec",
122
+ message: "Max records (-1 for all)",
123
+ default: "50",
124
+ validate: (v) => (!Number.isNaN(Number(v)) ? true : "Must be a number"),
125
+ },
126
+ ]);
127
+ maxRecords = maxRec;
128
+ // Field picker
129
+ if (allFields.length > 0) {
130
+ let { fields } = await inquirer.prompt([
131
+ {
132
+ type: "checkbox",
133
+ name: "fields",
134
+ message: "Select fields to include (leave all unchecked to include all)",
135
+ choices: allFields,
136
+ },
137
+ ]);
138
+ selectedFields = fields;
139
+ }
140
+ // Sort order
141
+ let { sortInput } = await inquirer.prompt([
142
+ {
143
+ type: "input",
144
+ name: "sortInput",
145
+ message: "Sort order (e.g. Created:Desc,Name — leave blank for none)",
146
+ default: "",
147
+ },
148
+ ]);
149
+ sortOrder = sortInput || undefined;
150
+ }
151
+ // Build entry
152
+ let relativeOutput = relative(process.cwd(), resolve(process.cwd(), output)).replace(/\\/g, "/");
153
+ let entry = {
154
+ id,
155
+ resource: chosenResource,
156
+ output: relativeOutput,
157
+ global: useGlobal || undefined,
158
+ types: withTypes || undefined,
159
+ expose: exposeIt || undefined,
160
+ permissions: permissions,
161
+ maxRecords: maxRecords !== "50" ? Number(maxRecords) : undefined,
162
+ sortOrder: sortOrder ? sortOrder.split(",") : undefined,
163
+ fields: selectedFields.length > 0 ? selectedFields : undefined,
164
+ };
165
+ // Write to resources.yaml
166
+ let config2 = await readResourcesConfig();
167
+ if (isView) {
168
+ config2.dataObjects = [...(config2.dataObjects ?? []), entry];
169
+ }
170
+ else {
171
+ config2.procedures = [...(config2.procedures ?? []), entry];
172
+ }
173
+ await writeResourcesConfig(config2);
174
+ console.log(`Added '${id}' to resources.yaml`);
175
+ // Generate the file
176
+ let options = entryToCLIOptions(entry, hostname);
177
+ options.output = resolve(process.cwd(), output);
178
+ let content = await fetchAndGenerate(chosenResource, options, client);
179
+ let outputPath = resolve(process.cwd(), output);
180
+ await mkdir(dirname(outputPath), { recursive: true });
181
+ await writeFile(outputPath, content, "utf-8");
182
+ await formatWithBiome(outputPath);
183
+ console.log(`Generated ${output}`);
184
+ }
@@ -0,0 +1 @@
1
+ export declare function editResource(id?: string): Promise<void>;
@@ -0,0 +1,177 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, relative, resolve } from "node:path";
3
+ import { Client } from "@olenbetong/appframe-data";
4
+ import { config } from "dotenv";
5
+ import fuzzy from "fuzzy";
6
+ import inquirer from "inquirer";
7
+ import autocomplete from "inquirer-autocomplete-standalone";
8
+ import { getLoginInfo } from "./devServer.js";
9
+ import { fetchAndGenerate, fetchResourceDefinition, formatWithBiome } from "./resourceGenerate.js";
10
+ import { entryToCLIOptions, readResourcesConfig, writeResourcesConfig } from "./resourcesConfig.js";
11
+ config({ quiet: true });
12
+ export async function editResource(id) {
13
+ let resourcesConfig = await readResourcesConfig();
14
+ let allEntries = [
15
+ ...(resourcesConfig.dataObjects ?? []).map((e) => ({ isView: true, entry: e })),
16
+ ...(resourcesConfig.procedures ?? []).map((e) => ({ isView: false, entry: e })),
17
+ ];
18
+ if (allEntries.length === 0) {
19
+ console.error("No resources found in resources.yaml. Run 'appframe-vite resources add' to add one.");
20
+ process.exit(1);
21
+ }
22
+ // Select entry to edit
23
+ let selected;
24
+ if (id) {
25
+ let found = allEntries.find((e) => e.entry.id === id);
26
+ if (!found) {
27
+ console.error(`No resource with id '${id}' found in resources.yaml.`);
28
+ process.exit(1);
29
+ }
30
+ selected = found;
31
+ }
32
+ else {
33
+ let entryChoices = allEntries.map((e) => ({
34
+ name: `${e.isView ? "[view]" : "[proc]"} ${e.entry.id} (${e.entry.resource})`,
35
+ value: e.entry.id,
36
+ }));
37
+ let chosenId = await autocomplete({
38
+ message: "Select a resource to edit",
39
+ source: async (input) => {
40
+ return fuzzy
41
+ .filter(input ?? "", entryChoices, { extract: (el) => el.name })
42
+ .map((el) => ({ name: el.original.name, value: el.original.value }));
43
+ },
44
+ });
45
+ selected = allEntries.find((e) => e.entry.id === chosenId);
46
+ }
47
+ let { entry, isView } = selected;
48
+ let { hostname, username, password } = await getLoginInfo();
49
+ let client = new Client(hostname);
50
+ await client.login(username, password);
51
+ // Fetch definition for field picker
52
+ let definition = await fetchResourceDefinition(client, entry.resource);
53
+ let allFields = definition.Parameters?.filter((p) => !["CUT", "CDL"].includes(p.Name)).map((p) => isView ? p.Name : p.ParamName) ?? [];
54
+ let currentFields = Array.isArray(entry.fields) ? entry.fields : (entry.fields?.split(",").filter(Boolean) ?? []);
55
+ // Re-run wizard pre-filled with current values
56
+ let { id: newId } = await inquirer.prompt([
57
+ {
58
+ type: "input",
59
+ name: "id",
60
+ message: "Data object / procedure ID",
61
+ default: entry.id,
62
+ },
63
+ ]);
64
+ let { output } = await inquirer.prompt([
65
+ {
66
+ type: "input",
67
+ name: "output",
68
+ message: "Output file path (relative to project root)",
69
+ default: entry.output,
70
+ },
71
+ ]);
72
+ let { useGlobal } = await inquirer.prompt([
73
+ {
74
+ type: "confirm",
75
+ name: "useGlobal",
76
+ message: "Use global af.data / af.ProcedureAPI?",
77
+ default: entry.global ?? false,
78
+ },
79
+ ]);
80
+ let { withTypes } = await inquirer.prompt([
81
+ {
82
+ type: "confirm",
83
+ name: "withTypes",
84
+ message: "Generate TypeScript type definitions?",
85
+ default: entry.types ?? false,
86
+ },
87
+ ]);
88
+ let { exposeIt } = await inquirer.prompt([
89
+ {
90
+ type: "confirm",
91
+ name: "exposeIt",
92
+ message: `Expose on af.article.${isView ? "dataObjects" : "procedures"}?`,
93
+ default: !!entry.expose,
94
+ },
95
+ ]);
96
+ let permissions = entry.permissions;
97
+ let maxRecords = String(entry.maxRecords ?? 50);
98
+ let selectedFields = currentFields;
99
+ let sortOrder = Array.isArray(entry.sortOrder) ? entry.sortOrder.join(",") : entry.sortOrder;
100
+ if (isView) {
101
+ let currentPerms = entry.permissions ?? "";
102
+ let { permInput } = await inquirer.prompt([
103
+ {
104
+ type: "checkbox",
105
+ name: "permInput",
106
+ message: "Permissions",
107
+ choices: [
108
+ { name: "Insert (I)", value: "I", checked: currentPerms.includes("I") },
109
+ { name: "Update (U)", value: "U", checked: currentPerms.includes("U") },
110
+ { name: "Delete (D)", value: "D", checked: currentPerms.includes("D") },
111
+ ],
112
+ },
113
+ ]);
114
+ permissions = permInput.length > 0 ? permInput.join("") : undefined;
115
+ let { maxRec } = await inquirer.prompt([
116
+ {
117
+ type: "input",
118
+ name: "maxRec",
119
+ message: "Max records (-1 for all)",
120
+ default: String(entry.maxRecords ?? 50),
121
+ validate: (v) => (!Number.isNaN(Number(v)) ? true : "Must be a number"),
122
+ },
123
+ ]);
124
+ maxRecords = maxRec;
125
+ if (allFields.length > 0) {
126
+ let { fields } = await inquirer.prompt([
127
+ {
128
+ type: "checkbox",
129
+ name: "fields",
130
+ message: "Select fields to include (leave all unchecked to include all)",
131
+ choices: allFields.map((f) => ({ name: f, checked: currentFields.includes(f) })),
132
+ },
133
+ ]);
134
+ selectedFields = fields;
135
+ }
136
+ let { sortInput } = await inquirer.prompt([
137
+ {
138
+ type: "input",
139
+ name: "sortInput",
140
+ message: "Sort order (e.g. Created:Desc,Name — leave blank for none)",
141
+ default: sortOrder ?? "",
142
+ },
143
+ ]);
144
+ sortOrder = sortInput || undefined;
145
+ }
146
+ let relativeOutput = relative(process.cwd(), resolve(process.cwd(), output)).replace(/\\/g, "/");
147
+ let updatedEntry = {
148
+ id: newId,
149
+ resource: entry.resource,
150
+ output: relativeOutput,
151
+ global: useGlobal || undefined,
152
+ types: withTypes || undefined,
153
+ expose: exposeIt || undefined,
154
+ permissions,
155
+ maxRecords: maxRecords !== "50" ? Number(maxRecords) : undefined,
156
+ sortOrder: sortOrder ? sortOrder.split(",") : undefined,
157
+ fields: selectedFields.length > 0 ? selectedFields : undefined,
158
+ };
159
+ // Update resources.yaml
160
+ if (isView) {
161
+ resourcesConfig.dataObjects = (resourcesConfig.dataObjects ?? []).map((e) => e.id === entry.id ? updatedEntry : e);
162
+ }
163
+ else {
164
+ resourcesConfig.procedures = (resourcesConfig.procedures ?? []).map((e) => (e.id === entry.id ? updatedEntry : e));
165
+ }
166
+ await writeResourcesConfig(resourcesConfig);
167
+ console.log(`Updated '${newId}' in resources.yaml`);
168
+ // Regenerate
169
+ let options = entryToCLIOptions(updatedEntry, hostname);
170
+ options.output = resolve(process.cwd(), output);
171
+ let content = await fetchAndGenerate(entry.resource, options, client);
172
+ let outputPath = resolve(process.cwd(), output);
173
+ await mkdir(dirname(outputPath), { recursive: true });
174
+ await writeFile(outputPath, content, "utf-8");
175
+ await formatWithBiome(outputPath);
176
+ console.log(`Regenerated ${output}`);
177
+ }
@@ -0,0 +1 @@
1
+ export declare function generateFromConfig(configPath?: string): Promise<void>;
@@ -0,0 +1,44 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import { Client } from "@olenbetong/appframe-data";
4
+ import { config } from "dotenv";
5
+ import { getLoginInfo } from "./devServer.js";
6
+ import { fetchAndGenerate, formatWithBiome } from "./resourceGenerate.js";
7
+ import { entryToCLIOptions, readResourcesConfig } from "./resourcesConfig.js";
8
+ config({ quiet: true });
9
+ export async function generateFromConfig(configPath) {
10
+ let { hostname, username, password } = await getLoginInfo();
11
+ let resourcesConfig = await readResourcesConfig(configPath);
12
+ let allEntries = [
13
+ ...(resourcesConfig.dataObjects ?? []).map((e) => ({ type: "dataObject", entry: e })),
14
+ ...(resourcesConfig.procedures ?? []).map((e) => ({ type: "procedure", entry: e })),
15
+ ];
16
+ if (allEntries.length === 0) {
17
+ console.log("No resources found in resources.yaml");
18
+ return;
19
+ }
20
+ let client = new Client(hostname);
21
+ await client.login(username, password);
22
+ let errors = [];
23
+ for (let { entry } of allEntries) {
24
+ let options = entryToCLIOptions(entry, resourcesConfig.server ?? hostname);
25
+ options.output = resolve(process.cwd(), entry.output);
26
+ try {
27
+ let content = await fetchAndGenerate(entry.resource, options, client);
28
+ let outputPath = resolve(process.cwd(), entry.output);
29
+ await mkdir(dirname(outputPath), { recursive: true });
30
+ await writeFile(outputPath, content, "utf-8");
31
+ await formatWithBiome(outputPath);
32
+ console.log(` ✓ ${entry.id} → ${entry.output}`);
33
+ }
34
+ catch (error) {
35
+ let message = error?.message ?? String(error);
36
+ console.error(` ✗ ${entry.id}: ${message}`);
37
+ errors.push({ id: entry.id, error: message });
38
+ }
39
+ }
40
+ if (errors.length > 0) {
41
+ console.error(`\n${errors.length} resource(s) failed to generate.`);
42
+ process.exit(1);
43
+ }
44
+ }
package/lib/cli.js CHANGED
@@ -1,29 +1,53 @@
1
1
  #!/usr/bin/env node
2
+ import { Command } from "commander";
2
3
  import { config } from "dotenv";
3
4
  import { runGenerateTypes } from "./generateTypes.js";
4
5
  import { importJson } from "./importJson.js";
5
6
  import { createLogMessage } from "./utils.js";
7
+ config({ path: `${process.cwd()}/.env`, quiet: true });
8
+ const pkg = await importJson("../package.json");
9
+ // ---------------------------------------------------------------------------
10
+ // generate-types
11
+ // ---------------------------------------------------------------------------
6
12
  async function generateTypes() {
7
- config({ path: `${process.cwd()}/.env`, quiet: true });
8
- const pkg = await importJson("./package.json", true);
9
- const hostname = pkg.appframe.proxy?.hostname ?? "dev.obet.no";
13
+ const appPkg = await importJson("./package.json", true);
14
+ const hostname = appPkg.appframe.proxy?.hostname ?? "dev.obet.no";
10
15
  const { APPFRAME_LOGIN: username = "", APPFRAME_PWD: password = "" } = process.env;
11
16
  const logger = {
12
17
  info: (msg) => console.log(msg),
13
18
  error: (msg) => console.error(msg),
14
19
  };
15
20
  logger.info(createLogMessage("Generating types…", { source: hostname }));
16
- await runGenerateTypes(hostname, username, password, pkg.appframe, logger);
17
- }
18
- const [, , command] = process.argv;
19
- switch (command) {
20
- case "generate-types":
21
- await generateTypes();
22
- break;
23
- default:
24
- console.error(createLogMessage(`Unknown command: ${command ?? "(none)"}`, { type: "error" }));
25
- console.error("Usage: appframe-vite <command>");
26
- console.error("Commands:");
27
- console.error(" generate-types Generate TypeScript type definitions");
28
- process.exit(1);
21
+ await runGenerateTypes(hostname, username, password, appPkg.appframe, logger);
29
22
  }
23
+ // ---------------------------------------------------------------------------
24
+ // Program
25
+ // ---------------------------------------------------------------------------
26
+ const program = new Command();
27
+ program.name("appframe-vite").version(pkg.version).description("Appframe Vite tooling CLI");
28
+ program.command("generate-types").description("Generate TypeScript type definitions").action(generateTypes);
29
+ // resources sub-command
30
+ const resources = program.command("resources").description("Manage data objects and procedures via resources.yaml");
31
+ resources
32
+ .command("generate")
33
+ .description("Generate all output files from resources.yaml")
34
+ .option("-c, --config <path>", "Path to resources config file (default: resources.yaml)")
35
+ .action(async (opts) => {
36
+ let { generateFromConfig } = await import("./cli-resources-generate.js");
37
+ await generateFromConfig(opts.config);
38
+ });
39
+ resources
40
+ .command("add")
41
+ .description("Interactively add a data object or procedure to resources.yaml")
42
+ .action(async () => {
43
+ let { addResource } = await import("./cli-resources-add.js");
44
+ await addResource();
45
+ });
46
+ resources
47
+ .command("edit [id]")
48
+ .description("Interactively edit an existing resource in resources.yaml")
49
+ .action(async (id) => {
50
+ let { editResource } = await import("./cli-resources-edit.js");
51
+ await editResource(id);
52
+ });
53
+ await program.parseAsync(process.argv);
package/lib/index.js CHANGED
@@ -1,16 +1,21 @@
1
+ import { resolve } from "node:path";
1
2
  import bodyParser from "body-parser";
2
3
  import { watch } from "chokidar";
3
4
  import { addAppframeBuildConfig } from "./build.js";
5
+ import { generateFromConfig } from "./cli-resources-generate.js";
4
6
  import { createDevMiddleware, getLoginInfo, getProxyRoutes } from "./devServer.js";
5
7
  import { runGenerateTypes } from "./generateTypes.js";
6
8
  import { localizeMiddleware } from "./localization.js";
7
9
  import { checkSession, getLastSession, login } from "./proxy.js";
10
+ import { RESOURCES_CONFIG_FILE } from "./resourcesConfig.js";
8
11
  import { createLogMessage, getServerName } from "./utils.js";
9
12
  let command = "build";
10
13
  let interval;
11
14
  let server;
12
15
  let lastHostname;
13
16
  let watcher;
17
+ let resourcesWatcher = null;
18
+ let resourcesDebounce = null;
14
19
  try {
15
20
  let appPkgUrl = `file://${process.cwd()}/package.json`;
16
21
  watcher = watch(appPkgUrl).on("all", async () => {
@@ -139,6 +144,28 @@ export default function appframe() {
139
144
  server = _server;
140
145
  // Run type generation in the background — doesn't block the dev server from starting.
141
146
  runGenerateTypes(hostname, username, password, appframe, _server.config.logger);
147
+ // Watch resources.yaml and regenerate on change
148
+ let resourcesConfigPath = resolve(process.cwd(), RESOURCES_CONFIG_FILE);
149
+ if (resourcesWatcher) {
150
+ await resourcesWatcher.close();
151
+ }
152
+ resourcesWatcher = watch(resourcesConfigPath, { ignoreInitial: true }).on("all", () => {
153
+ if (resourcesDebounce)
154
+ clearTimeout(resourcesDebounce);
155
+ resourcesDebounce = setTimeout(async () => {
156
+ _server.config.logger.info(createLogMessage("resources.yaml changed — regenerating…", { source: hostname }));
157
+ try {
158
+ await generateFromConfig();
159
+ _server.config.logger.info(createLogMessage("resources regenerated", { source: hostname }));
160
+ }
161
+ catch (error) {
162
+ _server.config.logger.error(createLogMessage(`resources regeneration failed: ${error?.message ?? error}`, {
163
+ source: hostname,
164
+ type: "error",
165
+ }));
166
+ }
167
+ }, 300);
168
+ });
142
169
  _server.middlewares.use(`/api/user/localize/new/${appframe.article.id}`, jsonParser);
143
170
  _server.middlewares.use(`/api/user/localize/new/${appframe.article.id}`, localizeMiddleware);
144
171
  _server.middlewares.use("/data/Logger/LogError", jsonParser);