@olenbetong/appframe-vite 6.14.0 → 6.16.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
@@ -279,11 +279,51 @@ procedures:
279
279
  | `fields` | `string \| string[]` | Fields to include (all if omitted) |
280
280
  | `transaction` | `boolean` | Procedures only. `false` stops the framework wrapping the call in a transaction |
281
281
  | `timeout` | `number` | Procedures only. Milliseconds before the call is abandoned (default `30000`) |
282
+ | `returnType` | `object[] \| string` | Procedures only. The result type; see below |
282
283
 
283
284
  A top-level `server` key can override the hostname (defaults to `appframe.proxy.hostname` from `package.json`).
284
285
 
285
286
  **Master/child relationships:** set both `master` and `linkFields` on the child object. The generated data object will include `masterDataObject` and `linkFields` in the `generateApiDataObject(...)` options.
286
287
 
288
+ **Procedure result types:** the server only describes a procedure's parameters, so its result is
289
+ typed `{ Table?: unknown[] }` unless `returnType` says otherwise. Never type the result by editing
290
+ the generated file — every regeneration (a `resources.yaml` change seen by the dev server, a
291
+ `git pull --rebase` that touches it, `resources generate`) writes the file from scratch. With
292
+ `returnType` (and `types: true`) the generated file exports the type under the procedure's name,
293
+ next to the parameters type — `procIndexDNA` gets `IndexDNAParams` and `IndexDNAResult` — so
294
+ components import it from the generated file.
295
+
296
+ `returnType` is either a list of result sets, in the order the procedure returns them (`Table`,
297
+ `Table1`, …; `table` overrides the key), with each field's TypeScript type:
298
+
299
+ ```yaml
300
+ procedures:
301
+ - id: procIndexDNA
302
+ resource: astp_Operations_IndexDNA
303
+ global: true
304
+ types: true
305
+ returnType:
306
+ - fields:
307
+ - name: IndexName
308
+ type: string
309
+ - name: CurrentFF
310
+ type: number
311
+ - fields:
312
+ - name: PageSort
313
+ type: number
314
+ output: src/features/IndexDetails/procIndexDNA.ts
315
+ ```
316
+
317
+ or a TypeScript type written out as a string, for anything the list cannot express. It may refer
318
+ to types in the app's `custom.d.ts` through the `Custom.` prefix, which adds the import:
319
+
320
+ ```yaml
321
+ returnType: "Custom.IndexDNAResult"
322
+ ```
323
+
324
+ `procedureReturnTypes` in `types.json`, keyed by the procedure's database name, does the same
325
+ and is used when the entry has no `returnType`.
326
+
287
327
  ## `@olenbetong/appframe-vite/resources` export
288
328
 
289
329
  Shared code generation utilities for Node.js consumers:
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Resolves the app's entry module. Apps have no `index.html`: the dev server fetches the
3
+ * article's HTML from Synergi and injects a `<script type="module">` pointing at this file,
4
+ * and the production build uses it as the single rolldown input.
5
+ *
6
+ * `relative` is a POSIX path from the root, usable as a glob pattern (`optimizeDeps.entries`
7
+ * takes globs, and an absolute Windows path would be read as one with escapes in it).
8
+ * The last candidate is returned even when it does not exist, so a missing entry fails
9
+ * with a resolve error that names the file instead of a vaguer one later on.
10
+ */
11
+ export declare function getAppEntry(root?: string): {
12
+ absolute: string;
13
+ relative: string;
14
+ url: string;
15
+ };
@@ -0,0 +1,19 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ /** The entry file candidates, in the order they are tried, relative to the app root. */
4
+ const ENTRY_CANDIDATES = ["src/index.tsx", "src/index.ts", "src/index.js"];
5
+ /**
6
+ * Resolves the app's entry module. Apps have no `index.html`: the dev server fetches the
7
+ * article's HTML from Synergi and injects a `<script type="module">` pointing at this file,
8
+ * and the production build uses it as the single rolldown input.
9
+ *
10
+ * `relative` is a POSIX path from the root, usable as a glob pattern (`optimizeDeps.entries`
11
+ * takes globs, and an absolute Windows path would be read as one with escapes in it).
12
+ * The last candidate is returned even when it does not exist, so a missing entry fails
13
+ * with a resolve error that names the file instead of a vaguer one later on.
14
+ */
15
+ export function getAppEntry(root = process.cwd()) {
16
+ let found = ENTRY_CANDIDATES.find((candidate) => existsSync(resolve(root, candidate)));
17
+ let relative = found ?? ENTRY_CANDIDATES[ENTRY_CANDIDATES.length - 1];
18
+ return { absolute: resolve(root, relative), relative, url: `/${relative}` };
19
+ }
@@ -19,6 +19,8 @@ export const GLOBAL_EXPORTS = [
19
19
  "Procedure",
20
20
  "ProcedureAPI",
21
21
  "SortOrder",
22
+ "UploadManager",
23
+ "UploadTask",
22
24
  "generateApiDataHandler",
23
25
  "generateApiDataObject",
24
26
  "getDefaultClient",
package/lib/build.js CHANGED
@@ -1,8 +1,8 @@
1
- import { existsSync } from "node:fs";
2
1
  import { readdir, readFile, writeFile } from "node:fs/promises";
3
2
  import { dirname, resolve } from "node:path";
4
3
  import { visualizer } from "rollup-plugin-visualizer";
5
4
  import { esmExternalRequirePlugin } from "vite";
5
+ import { getAppEntry } from "./appEntry.js";
6
6
  import { importJson } from "./importJson.js";
7
7
  /**
8
8
  * The bare specifiers the 2026 site templates resolve through their import map to the
@@ -38,6 +38,10 @@ export const PLATFORM_EXTERNALS = [
38
38
  "highcharts-react-official",
39
39
  "mqtt",
40
40
  "temporal-polyfill",
41
+ // The code editor. Its stylesheet and language workers are served next to the entry, not
42
+ // linked by the template: only the pages that open an editor load them (synergi-react
43
+ // `CodeEditor` resolves them from the specifier).
44
+ "monaco-editor",
41
45
  ];
42
46
  /**
43
47
  * The IIFE global each platform specifier is published under (`buildConfig.entries` in the
@@ -68,6 +72,7 @@ export const PLATFORM_GLOBALS = {
68
72
  "highcharts-react-official": "SynergiPlatform.highchartsReact",
69
73
  mqtt: "SynergiPlatform.mqtt",
70
74
  "temporal-polyfill": "SynergiPlatform.temporal",
75
+ "monaco-editor": "SynergiPlatform.monaco",
71
76
  };
72
77
  export function usesPlatform(appframe) {
73
78
  return appframe?.build?.platform === true;
@@ -94,6 +99,7 @@ export const PLATFORM_UPSTREAM = [
94
99
  "highcharts-react-official",
95
100
  "mqtt",
96
101
  "temporal-polyfill",
102
+ "monaco-editor",
97
103
  ];
98
104
  /**
99
105
  * Whether the app renders `ToolbarDrawerToggle` (synergi-react) anywhere under `src/`: a plain
@@ -196,16 +202,9 @@ export async function addAppframeBuildConfig(config) {
196
202
  sourcemap: true,
197
203
  };
198
204
  config.plugins = config.plugins ?? [];
199
- let entry = resolve(process.cwd(), "./src/index.tsx");
200
- if (!existsSync(resolve(entry))) {
201
- entry = resolve(process.cwd(), "./src/index.ts");
202
- }
203
- if (!existsSync(resolve(entry))) {
204
- entry = resolve(process.cwd(), "./src/index.js");
205
- }
206
205
  config.appType = "custom";
207
206
  config.build.rolldownOptions = {
208
- input: { main: entry },
207
+ input: { main: getAppEntry().absolute },
209
208
  plugins: [
210
209
  // Cast visualizer plugin to any to avoid Rollup type version clashes
211
210
  visualizer({ filename: "./dist/stats.html", gzipSize: true }),
@@ -144,7 +144,10 @@ export async function editResource(id) {
144
144
  sortOrder = sortInput || undefined;
145
145
  }
146
146
  let relativeOutput = relative(process.cwd(), resolve(process.cwd(), output)).replace(/\\/g, "/");
147
+ // Start from the existing entry so settings the wizard does not ask about (returnType,
148
+ // master, linkFields, timeout, transaction, ...) are kept
147
149
  let updatedEntry = {
150
+ ...entry,
148
151
  id: newId,
149
152
  resource: entry.resource,
150
153
  output: relativeOutput,
@@ -182,6 +182,10 @@ function dataObjectToEntry(datasource, fields, outputDir, warnings) {
182
182
  if (sortFields.length > 0) {
183
183
  entry.sortOrder = sortFields.map((f) => {
184
184
  let direction = (f.AscDesc ?? "asc").toLowerCase();
185
+ // Keep the null placement of AscNullsLast / DescNullsFirst
186
+ if (direction === "ascnullslast" || direction === "descnullsfirst") {
187
+ return { field: f.FieldName, direction };
188
+ }
185
189
  return direction.startsWith("desc") ? { field: f.FieldName, direction: "desc" } : { field: f.FieldName };
186
190
  });
187
191
  }
package/lib/devServer.js CHANGED
@@ -5,6 +5,7 @@ import { AuthenticationError, authenticate, loginHint, resolveCredentials } from
5
5
  import { Client } from "@olenbetong/appframe-data";
6
6
  import dotenv from "dotenv";
7
7
  import { JSDOM } from "jsdom";
8
+ import { getAppEntry } from "./appEntry.js";
8
9
  import { importJson } from "./importJson.js";
9
10
  import { getStringCache } from "./localization.js";
10
11
  import { diagnoseServerResponse } from "./utils.js";
@@ -68,16 +69,7 @@ export function getProxyRoutes() {
68
69
  * @returns
69
70
  */
70
71
  async function transformArticleHtml(html) {
71
- let entry;
72
- if (existsSync(resolve(process.cwd(), "./src/index.tsx"))) {
73
- entry = "/src/index.tsx";
74
- }
75
- else if (existsSync(resolve(process.cwd(), "./src/index.ts"))) {
76
- entry = "/src/index.ts";
77
- }
78
- else {
79
- entry = "/src/index.js";
80
- }
72
+ let entry = getAppEntry().url;
81
73
  let article = appframe.article?.id;
82
74
  let { hostname } = await getAppHostname();
83
75
  let dom = new JSDOM(html);
package/lib/index.js CHANGED
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import bodyParser from "body-parser";
4
4
  import { watch } from "chokidar";
5
+ import { getAppEntry } from "./appEntry.js";
5
6
  import { appframeDataGlobal } from "./appframeDataGlobal.js";
6
7
  import { addAppframeBuildConfig, PLATFORM_EXTERNALS, PLATFORM_GLOBALS, PLATFORM_UPSTREAM, platformManifest, usesPlatform, } from "./build.js";
7
8
  import { generateFromConfig } from "./cli-resources-generate.js";
@@ -184,6 +185,19 @@ export default function appframe(options = {}) {
184
185
  ...config.server,
185
186
  proxy,
186
187
  };
188
+ // Apps have no index.html, so the dependency scanner Vite runs at startup has
189
+ // nothing to crawl (it globs "**/*.html") and skips pre-bundling altogether. Every
190
+ // dependency is then discovered when a module first imports it: the ones behind
191
+ // the initial page load are batched, but a lazy route that pulls in a new one
192
+ // re-runs the optimizer and forces a full reload, which lands back on the page
193
+ // you navigated from. Crawling from the entry module finds the lazy imports up
194
+ // front, so the deps are ready before the browser asks for them.
195
+ if (!config.optimizeDeps?.entries) {
196
+ config.optimizeDeps = {
197
+ ...config.optimizeDeps,
198
+ entries: [getAppEntry().relative],
199
+ };
200
+ }
187
201
  }
188
202
  return config;
189
203
  },
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { getProcedureDefinition } from "./resourceGenerate.js";
3
+ import { entryToCLIOptions } from "./resourcesConfig.js";
4
+ const definition = {
5
+ Parameters: [{ ParamName: "pIndexID", TypeName: "int", has_default_value: false, is_nullable: false }],
6
+ };
7
+ function generate(returnType) {
8
+ let options = entryToCLIOptions({
9
+ id: "procIndexDNA",
10
+ resource: "astp_Operations_IndexDNA",
11
+ output: "src/procIndexDNA.ts",
12
+ global: true,
13
+ types: true,
14
+ returnType,
15
+ }, "example.com");
16
+ return getProcedureDefinition("astp_Operations_IndexDNA", definition, options);
17
+ }
18
+ describe("procedure return types", () => {
19
+ it("keeps the untyped default without a returnType", () => {
20
+ let output = generate(undefined);
21
+ expect(output).not.toContain("IndexDNAResult");
22
+ expect(output).toContain("ProcedureAPI<IndexDNAParams, { Table?: unknown[] }>");
23
+ });
24
+ it("names a structured returnType after the procedure", () => {
25
+ let output = generate([
26
+ { fields: [{ name: "IndexName", type: "string" }] },
27
+ { fields: [{ name: "PageSort", type: "number" }] },
28
+ ]);
29
+ expect(output).toContain("export type IndexDNAResult = { Table: { IndexName: string }[]; Table1: { PageSort: number }[] };");
30
+ expect(output).toContain("ProcedureAPI<IndexDNAParams, IndexDNAResult>");
31
+ });
32
+ it("uses a string returnType as written", () => {
33
+ let output = generate("Custom.IndexDNAResult");
34
+ expect(output).toContain("export type IndexDNAResult = Custom.IndexDNAResult;");
35
+ expect(output).toContain('import type * as Custom from "');
36
+ });
37
+ it("does not emit a result type without types", () => {
38
+ let options = entryToCLIOptions({
39
+ id: "procIndexDNA",
40
+ resource: "astp_Operations_IndexDNA",
41
+ output: "src/procIndexDNA.ts",
42
+ global: true,
43
+ returnType: "{ Table: [] }",
44
+ }, "example.com");
45
+ let output = getProcedureDefinition("astp_Operations_IndexDNA", definition, options);
46
+ expect(output).not.toContain("IndexDNAResult");
47
+ });
48
+ });
@@ -169,6 +169,13 @@ export function parseYamlConfig(fileContent) {
169
169
  // ---------------------------------------------------------------------------
170
170
  // Code generation helpers
171
171
  // ---------------------------------------------------------------------------
172
+ /**
173
+ * A field name as an object key in generated code, quoted when it is not a valid identifier,
174
+ * such as a view column named `EU-Neste`.
175
+ */
176
+ function propertyKey(name) {
177
+ return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
178
+ }
172
179
  /**
173
180
  * Convert Appframe (SQL) data types to typescript/field definition types
174
181
  */
@@ -302,7 +309,14 @@ export function getProcedureDefinition(name, procDefinition, options) {
302
309
  output.push(`export type ${paramTypeName} = null | undefined | Record<string, unknown>;\n`);
303
310
  }
304
311
  }
305
- output.push(`export const ${procName} = new ${options.global ? "af." : ""}ProcedureAPI${options.types ? `<${paramTypeName}, ${options.typesJsonReturnType ?? "{ Table?: unknown[] }"}>` : ""}({
312
+ // A configured return type gets a name of its own, so components can import it and it
313
+ // survives every regeneration instead of being written into the output by hand.
314
+ let resultType = "{ Table?: unknown[] }";
315
+ if (options.types && options.typesJsonReturnType) {
316
+ resultType = `${paramTypeName.replace(/Params$/, "")}Result`;
317
+ output.push(`export type ${resultType} = ${options.typesJsonReturnType};\n`);
318
+ }
319
+ output.push(`export const ${procName} = new ${options.global ? "af." : ""}ProcedureAPI${options.types ? `<${paramTypeName}, ${resultType}>` : ""}({
306
320
  procedureId: "${name}",
307
321
  parameters: ${JSON.stringify(parameters, null, 2)},
308
322
  timeout: ${options.timeout ?? 30000}${options.transaction === false ? ",\n transaction: false" : ""}
@@ -387,7 +401,7 @@ export function getDataObjectDefinition(name, viewDefinition, options) {
387
401
  type += " | null";
388
402
  }
389
403
  }
390
- fieldTypes += ` ${field.name}: ${type};\n`;
404
+ fieldTypes += ` ${propertyKey(field.name)}: ${type};\n`;
391
405
  }
392
406
  types = `export type ${typeName} = {
393
407
  ${fieldTypes}}`;
@@ -458,7 +472,7 @@ ${fieldTypes}}`;
458
472
  order = "DescNullsFirst";
459
473
  break;
460
474
  }
461
- return `{ ${field}: ${sortPrefix}${order} }`;
475
+ return `{ ${propertyKey(field)}: ${sortPrefix}${order} }`;
462
476
  })
463
477
  .join(", ");
464
478
  parametersOption.push(`sortOrder: [${sortOrder}]`);
@@ -77,8 +77,12 @@ export type ResourceEntry = {
77
77
  where?: string;
78
78
  /** Fields to include: comma-separated or array of field names */
79
79
  fields?: string | string[];
80
- /** Structured return type definition for procedures (generates inline TS type) */
81
- returnType?: ReturnTypeTable[];
80
+ /**
81
+ * Procedures only. The result type, emitted as `export type <Name>Result` next to the
82
+ * parameters type. Either a list of result sets with their fields, or a TypeScript type
83
+ * written out as a string (which may reference `Custom.*` types from `custom.d.ts`).
84
+ */
85
+ returnType?: ReturnTypeTable[] | string;
82
86
  };
83
87
  export type ResourcesConfig = {
84
88
  /** Optional server hostname override (defaults to `appframe.proxy.hostname` from package.json) */
@@ -113,7 +113,11 @@ export function entryToCLIOptions(entry, hostname) {
113
113
  where: entry.where,
114
114
  fields: arrayOrStringToString(entry.fields) ?? false,
115
115
  output: resolve(process.cwd(), entry.output ?? `src/data/${entry.id}.ts`),
116
- typesJsonReturnType: entry.returnType ? returnTypeToString(entry.returnType) : undefined,
116
+ typesJsonReturnType: typeof entry.returnType === "string"
117
+ ? entry.returnType
118
+ : entry.returnType
119
+ ? returnTypeToString(entry.returnType)
120
+ : undefined,
117
121
  };
118
122
  }
119
123
  /** Convert a `CLIOptions` back to a `ResourceEntry` for saving to YAML. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olenbetong/appframe-vite",
3
- "version": "6.14.0",
3
+ "version": "6.16.0",
4
4
  "description": "Tools to use and deploy Vite applications to Appframe",
5
5
  "main": "./lib/index.js",
6
6
  "type": "module",
@@ -29,25 +29,25 @@
29
29
  "author": "Bjørnar Vister Hansen <bvh@olenbetong.no>",
30
30
  "license": "MIT",
31
31
  "dependencies": {
32
- "@olenbetong/appframe-auth": "0.1.0",
33
- "@olenbetong/appframe-data": "1.8.2",
32
+ "@olenbetong/appframe-auth": "^0.1.0",
33
+ "@olenbetong/appframe-data": "^1.8.3",
34
34
  "body-parser": "^2.3.0",
35
35
  "chalk": "6.0.1",
36
36
  "chokidar": "^5.0.0",
37
37
  "commander": "15.0.0",
38
- "dotenv": "^18.0.4",
38
+ "dotenv": "^18.0.8",
39
39
  "fuzzy": "^0.1.3",
40
- "inquirer": "^14.2.2",
40
+ "inquirer": "^14.2.4",
41
41
  "inquirer-autocomplete-standalone": "^0.8.1",
42
- "jsdom": "30.1.1",
42
+ "jsdom": "30.1.3",
43
43
  "rollup-plugin-visualizer": "^7.1.1",
44
44
  "yaml": "^2.9.1"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/jsdom": "^30.0.0",
48
- "@types/node": "26.6.3",
48
+ "@types/node": "26.6.5",
49
49
  "typescript": "7.0.2",
50
- "vite": "8.3.1"
50
+ "vite": "8.3.5"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "vite": ">=8.1.5"