@olenbetong/appframe-vite 6.8.1 → 6.10.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
@@ -68,12 +68,28 @@ This package reads `package.json.appframe` to know what to proxy and how to buil
68
68
  "routes": ["^/custom/.*"], // Optional: extra proxied routes
69
69
  },
70
70
  "build": {
71
- "externals": true, // Map React and ReactDOM to globals
71
+ "platform": true, // Import React and the af packages from the site's platform bundle
72
+ "externals": true, // Legacy: map React and ReactDOM to globals
72
73
  },
73
74
  },
74
75
  }
75
76
  ```
76
77
 
78
+ ### `build.platform`
79
+
80
+ Apps on the 2026 site templates (`ob.2026.application`) set `"platform": true`. The production
81
+ build then leaves `react`, `react/jsx-runtime`, `react/jsx-dev-runtime`, `react-dom`,
82
+ `react-dom/client`, `@olenbetong/appframe-core` and `@olenbetong/appframe-data` as bare imports,
83
+ which the template's import map resolves to `@olenbetong/synergi-platform` — one cached React
84
+ per page, shared with the site chrome. The `af.data` global redirect (`dataGlobal`) then only
85
+ applies to the dev server, which has no import map. The build also writes `dist/platform.json`
86
+ with the platform version the app was built alongside (the workspace's
87
+ `packages/synergi-platform`) and the versions it compiled against; `scripts/app-deploy.ts` pins
88
+ the article to that platform version (its `PlatformHead` and `PlatformStyles` blocks) and
89
+ refuses to deploy when the article is not on a 2026 template, that version is not served on the
90
+ target site, or its manifest carries other versions than the app (override with
91
+ `--allow-platform-mismatch`).
92
+
77
93
  ## What it does (at a glance)
78
94
 
79
95
  - Dev server
@@ -86,7 +102,7 @@ This package reads `package.json.appframe` to know what to proxy and how to buil
86
102
  - Writes filenames to Appframe paths, using the article ID, e.g.:
87
103
  - `file/article/script/<ARTICLE_ID>/main.[hash].min.js`
88
104
  - `file/article/style/<ARTICLE_ID>/style.[hash].min.css`
89
- - Optionally treats `react` and `react-dom` as externals and maps them to `React` and `ReactDOM` globals.
105
+ - With `build.platform`, leaves React and the af packages as bare imports for the template's import map and writes `dist/platform.json`; the legacy `build.externals` maps `react` and `react-dom` to `React` and `ReactDOM` globals instead.
90
106
  - Adds a Rollup visualizer report at `dist/stats.html`.
91
107
  - Resolve
92
108
  - Adds the alias `~/` to `/src/` so imports like `~/components/Button` resolve to `src/components/Button`.
@@ -240,27 +256,29 @@ procedures:
240
256
 
241
257
  **Supported fields per entry:**
242
258
 
243
- | Field | Type | Description |
244
- | ------------- | -------------------- | ------------------------------------------------------------------- |
245
- | `id` | `string` | Variable name used in generated code (e.g. `dsAccountGroups`) |
246
- | `resource` | `string` | Database object ID (e.g. `atbv_Accounting_SubsidiaryLedgerGroups`) |
247
- | `output` | `string` | Output file path relative to project root |
248
- | `global` | `boolean` | Use `af.data.generateApiDataObject` / `new af.ProcedureAPI` globals |
249
- | `types` | `boolean` | Emit TypeScript type definitions |
250
- | `permissions` | `string` | Permissions: I = insert, U = update, D = delete (e.g. `IUD`) |
251
- | `maxRecords` | `number` | Max records to fetch (default `50`; `-1` for all) |
252
- | `sortOrder` | `string \| string[]` | Sort order, e.g. `Created:Desc` or `[Created:Desc, Name]` |
253
- | `master` | `string` | Master data object name (or `name:importPath`) |
254
- | `linkFields` | `string \| string[]` | Fields linking child to master |
255
- | `expose` | `boolean \| string` | Expose on `af.article.dataObjects` / `af.article.procedures` |
256
- | `dynamic` | `boolean` | Enable dynamic loading |
257
- | `unique` | `string` | Unique table name for update/delete |
258
- | `overrides` | `string \| string[]` | Type overrides, e.g. `MyField:string[]` |
259
- | `distinct` | `boolean` | Fetch distinct rows |
260
- | `aggregates` | `string \| string[]` | Aggregate bindings, e.g. `Qty:SUM` |
261
- | `groupBy` | `string \| string[]` | Group-by fields |
262
- | `where` | `string` | Initial where clause |
263
- | `fields` | `string \| string[]` | Fields to include (all if omitted) |
259
+ | Field | Type | Description |
260
+ | ------------- | -------------------- | ------------------------------------------------------------------------------- |
261
+ | `id` | `string` | Variable name used in generated code (e.g. `dsAccountGroups`) |
262
+ | `resource` | `string` | Database object ID (e.g. `atbv_Accounting_SubsidiaryLedgerGroups`) |
263
+ | `output` | `string` | Output file path relative to project root |
264
+ | `global` | `boolean` | Use `af.data.generateApiDataObject` / `new af.ProcedureAPI` globals |
265
+ | `types` | `boolean` | Emit TypeScript type definitions |
266
+ | `permissions` | `string` | Permissions: I = insert, U = update, D = delete (e.g. `IUD`) |
267
+ | `maxRecords` | `number` | Max records to fetch (default `50`; `-1` for all) |
268
+ | `sortOrder` | `string \| string[]` | Sort order, e.g. `Created:Desc` or `[Created:Desc, Name]` |
269
+ | `master` | `string` | Master data object name (or `name:importPath`) |
270
+ | `linkFields` | `string \| string[]` | Fields linking child to master |
271
+ | `expose` | `boolean \| string` | Expose on `af.article.dataObjects` / `af.article.procedures` |
272
+ | `dynamic` | `boolean` | Enable dynamic loading |
273
+ | `unique` | `string` | Unique table name for update/delete |
274
+ | `overrides` | `string \| string[]` | Type overrides, e.g. `MyField:string[]` |
275
+ | `distinct` | `boolean` | Fetch distinct rows |
276
+ | `aggregates` | `string \| string[]` | Aggregate bindings, e.g. `Qty:SUM` |
277
+ | `groupBy` | `string \| string[]` | Group-by fields |
278
+ | `where` | `string` | Initial where clause |
279
+ | `fields` | `string \| string[]` | Fields to include (all if omitted) |
280
+ | `transaction` | `boolean` | Procedures only. `false` stops the framework wrapping the call in a transaction |
281
+ | `timeout` | `number` | Procedures only. Milliseconds before the call is abandoned (default `30000`) |
264
282
 
265
283
  A top-level `server` key can override the hostname (defaults to `appframe.proxy.hostname` from `package.json`).
266
284
 
@@ -1,4 +1,19 @@
1
1
  import type { Plugin } from "vite";
2
+ /**
3
+ * Names the framework puts on the `af.data` global. These are the exports shared by
4
+ * `bundle-entries/browser.esm.ts` and `bundle-entries/browser-compat.esm.ts` — which of
5
+ * the two a page loads depends on the user's UserMode, so only names present in *both*
6
+ * are safe to read off the global.
7
+ */
8
+ export declare const GLOBAL_EXPORTS: string[];
9
+ /**
10
+ * Helpers the `af.data` bundles do not expose, mapped to the module they live in. These
11
+ * are pure functions whose only dependencies on the rest of the package are types, so
12
+ * bundling them creates no second client, data object or event bus — the state that must
13
+ * stay single-instance still comes from the global. Move an entry up to GLOBAL_EXPORTS
14
+ * once the browser bundle entries export it and the new af.data has been deployed.
15
+ */
16
+ export declare const LOCAL_EXPORTS: Record<string, string[]>;
2
17
  /**
3
18
  * Redirects imports of `@olenbetong/appframe-data` to the `af.data` global the article
4
19
  * already loads.
@@ -7,7 +7,7 @@ const VIRTUAL_ID = "\0appframe-data-global";
7
7
  * the two a page loads depends on the user's UserMode, so only names present in *both*
8
8
  * are safe to read off the global.
9
9
  */
10
- const GLOBAL_EXPORTS = [
10
+ export const GLOBAL_EXPORTS = [
11
11
  "Client",
12
12
  "DataHandler",
13
13
  "DataObject",
@@ -32,7 +32,7 @@ const GLOBAL_EXPORTS = [
32
32
  * stay single-instance still comes from the global. Move an entry up to GLOBAL_EXPORTS
33
33
  * once the browser bundle entries export it and the new af.data has been deployed.
34
34
  */
35
- const LOCAL_EXPORTS = {
35
+ export const LOCAL_EXPORTS = {
36
36
  "formDataToObject.js": ["formDataToDataObjectRecord", "formDataToObject", "formDataToProcedureParameters"],
37
37
  "exportDataObject.js": ["exportOptionsFromDataObject", "getDataObjectReportRequest", "openDataObjectReport"],
38
38
  "DataHandler.js": ["isRequestError"],
package/lib/build.d.ts CHANGED
@@ -1,2 +1,41 @@
1
- import type { UserConfig } from "vite";
1
+ import { type Plugin, type UserConfig } from "vite";
2
+ /**
3
+ * The bare specifiers the 2026 site templates resolve through their import map to the
4
+ * `@olenbetong/synergi-platform` bundle: one React per page, and the `af` packages as
5
+ * shims over the globals the article loads. An app built with `appframe.build.platform`
6
+ * leaves these external and so must run on a template that renders the import map.
7
+ */
8
+ export declare const PLATFORM_EXTERNALS: string[];
9
+ /**
10
+ * The IIFE global each platform specifier is published under (`buildConfig.entries` in the
11
+ * platform package), for builds that cannot use the import map.
12
+ */
13
+ export declare const PLATFORM_GLOBALS: Record<string, string>;
14
+ export declare function usesPlatform(appframe: {
15
+ build?: {
16
+ platform?: boolean;
17
+ };
18
+ } | undefined): boolean;
19
+ /**
20
+ * The packages whose version the platform manifest records; an app must match the served
21
+ * platform on every one it has installed.
22
+ */
23
+ export declare const PLATFORM_UPSTREAM: string[];
24
+ export type PlatformManifest = {
25
+ /**
26
+ * The `@olenbetong/synergi-platform` version the app was built alongside - the one its
27
+ * article is pinned to on deploy. Missing when the build found no platform package.
28
+ */
29
+ platform?: string;
30
+ /** The package versions the app resolved at build time. */
31
+ upstream: Record<string, string>;
32
+ };
33
+ /**
34
+ * Writes `dist/platform.json` after a platform build: the platform version the app was built
35
+ * alongside and the versions of the platform packages it compiled against. The deploy pins the
36
+ * article to that platform version (its `PlatformHead` block) and compares the versions with the
37
+ * manifest that version serves on the target site, since an app compiled against one React and
38
+ * run on another is a runtime error, not a build error.
39
+ */
40
+ export declare function platformManifest(): Plugin;
2
41
  export declare function addAppframeBuildConfig(config: UserConfig): Promise<UserConfig>;
package/lib/build.js CHANGED
@@ -1,7 +1,130 @@
1
1
  import { existsSync } from "node:fs";
2
- import { resolve } from "node:path";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, resolve } from "node:path";
3
4
  import { visualizer } from "rollup-plugin-visualizer";
5
+ import { esmExternalRequirePlugin } from "vite";
4
6
  import { importJson } from "./importJson.js";
7
+ /**
8
+ * The bare specifiers the 2026 site templates resolve through their import map to the
9
+ * `@olenbetong/synergi-platform` bundle: one React per page, and the `af` packages as
10
+ * shims over the globals the article loads. An app built with `appframe.build.platform`
11
+ * leaves these external and so must run on a template that renders the import map.
12
+ */
13
+ export const PLATFORM_EXTERNALS = [
14
+ "react",
15
+ "react/jsx-runtime",
16
+ "react/jsx-dev-runtime",
17
+ "react-dom",
18
+ "react-dom/client",
19
+ "react-router",
20
+ "@digdir/designsystemet-web",
21
+ "@digdir/designsystemet-react",
22
+ "@olenbetong/appframe-core",
23
+ "@olenbetong/appframe-data",
24
+ "@olenbetong/appframe-react",
25
+ "@olenbetong/appframe-ds",
26
+ "@olenbetong/appframe-ds/grid",
27
+ "@olenbetong/appframe-ds/mdi",
28
+ "@olenbetong/appframe-ds/progress",
29
+ "@olenbetong/appframe-ds/report-downloader",
30
+ ];
31
+ /**
32
+ * The IIFE global each platform specifier is published under (`buildConfig.entries` in the
33
+ * platform package), for builds that cannot use the import map.
34
+ */
35
+ export const PLATFORM_GLOBALS = {
36
+ react: "React",
37
+ "react/jsx-runtime": "ReactJSXRuntime",
38
+ "react/jsx-dev-runtime": "ReactJSXDevRuntime",
39
+ "react-dom": "ReactDOM",
40
+ "react-dom/client": "ReactDOMClient",
41
+ "react-router": "ReactRouter",
42
+ "@digdir/designsystemet-web": "Designsystemet.web",
43
+ "@digdir/designsystemet-react": "Designsystemet.react",
44
+ "@olenbetong/appframe-core": "SynergiPlatform.appframeCore",
45
+ "@olenbetong/appframe-data": "SynergiPlatform.appframeData",
46
+ "@olenbetong/appframe-react": "SynergiPlatform.appframeReact",
47
+ "@olenbetong/appframe-ds": "SynergiPlatform.appframeDs",
48
+ "@olenbetong/appframe-ds/grid": "SynergiPlatform.appframeDsGrid",
49
+ "@olenbetong/appframe-ds/mdi": "SynergiPlatform.appframeDsMdi",
50
+ "@olenbetong/appframe-ds/progress": "SynergiPlatform.appframeDsProgress",
51
+ "@olenbetong/appframe-ds/report-downloader": "SynergiPlatform.appframeDsReportDownloader",
52
+ };
53
+ export function usesPlatform(appframe) {
54
+ return appframe?.build?.platform === true;
55
+ }
56
+ /**
57
+ * The packages whose version the platform manifest records; an app must match the served
58
+ * platform on every one it has installed.
59
+ */
60
+ export const PLATFORM_UPSTREAM = [
61
+ "react",
62
+ "react-dom",
63
+ "react-router",
64
+ "@digdir/designsystemet-react",
65
+ "@digdir/designsystemet-web",
66
+ "@digdir/designsystemet-css",
67
+ "@olenbetong/appframe-core",
68
+ "@olenbetong/appframe-data",
69
+ "@olenbetong/appframe-react",
70
+ "@olenbetong/appframe-ds",
71
+ ];
72
+ /**
73
+ * The platform package's version as the app sees it: its own copy under node_modules when it
74
+ * depends on `@olenbetong/synergi-platform`, otherwise the workspace's `packages/synergi-platform`
75
+ * found by walking up from the app - every app lives in the monorepo next to it.
76
+ */
77
+ async function platformVersion(cwd) {
78
+ let candidates = [resolve(cwd, "node_modules", "@olenbetong", "synergi-platform", "package.json")];
79
+ let dir = cwd;
80
+ while (true) {
81
+ candidates.push(resolve(dir, "packages", "synergi-platform", "package.json"));
82
+ let parent = dirname(dir);
83
+ if (parent === dir)
84
+ break;
85
+ dir = parent;
86
+ }
87
+ for (let file of candidates) {
88
+ try {
89
+ return JSON.parse(await readFile(file, "utf8")).version;
90
+ }
91
+ catch {
92
+ // Not here; try the next candidate.
93
+ }
94
+ }
95
+ return undefined;
96
+ }
97
+ /**
98
+ * Writes `dist/platform.json` after a platform build: the platform version the app was built
99
+ * alongside and the versions of the platform packages it compiled against. The deploy pins the
100
+ * article to that platform version (its `PlatformHead` block) and compares the versions with the
101
+ * manifest that version serves on the target site, since an app compiled against one React and
102
+ * run on another is a runtime error, not a build error.
103
+ */
104
+ export function platformManifest() {
105
+ return {
106
+ name: "appframe:platform-manifest",
107
+ apply: "build",
108
+ async writeBundle() {
109
+ let manifest = { platform: await platformVersion(process.cwd()), upstream: {} };
110
+ if (manifest.platform === undefined) {
111
+ delete manifest.platform;
112
+ this.warn("No @olenbetong/synergi-platform package found; dist/platform.json records no platform version and the app cannot be deployed to a platform article.");
113
+ }
114
+ for (let name of PLATFORM_UPSTREAM) {
115
+ let file = resolve(process.cwd(), "node_modules", name, "package.json");
116
+ try {
117
+ manifest.upstream[name] = JSON.parse(await readFile(file, "utf8")).version;
118
+ }
119
+ catch {
120
+ // Not every app depends on every platform package (an app without a grid has no
121
+ // appframe-ds, say); only what the app resolved is recorded and checked.
122
+ }
123
+ }
124
+ await writeFile(resolve(process.cwd(), "dist", "platform.json"), `${JSON.stringify(manifest, null, "\t")}\n`);
125
+ },
126
+ };
127
+ }
5
128
  export async function addAppframeBuildConfig(config) {
6
129
  let appPkg = await importJson("./package.json", true);
7
130
  let { appframe } = appPkg;
@@ -22,9 +145,21 @@ export async function addAppframeBuildConfig(config) {
22
145
  config.appType = "custom";
23
146
  config.build.rolldownOptions = {
24
147
  input: { main: entry },
25
- // Cast visualizer plugin to any to avoid Rollup type version clashes
26
- plugins: [visualizer({ filename: "./dist/stats.html", gzipSize: true })],
27
- external: appframe.build.externals !== false ? ["react", "react-dom", "react-dom/client"] : [],
148
+ plugins: [
149
+ // Cast visualizer plugin to any to avoid Rollup type version clashes
150
+ visualizer({ filename: "./dist/stats.html", gzipSize: true }),
151
+ // The platform set stays external through this plugin rather than the top-level
152
+ // `external`: CommonJS dependencies in the app (MUI X, exceljs) `require("react")`, and
153
+ // only the plugin turns those requires into imports the import map can resolve. A
154
+ // plain external leaves a `require` shim that throws in the browser. The plugin must
155
+ // own the list, so the externals are not repeated below.
156
+ ...(usesPlatform(appframe) ? [esmExternalRequirePlugin({ external: PLATFORM_EXTERNALS })] : []),
157
+ ],
158
+ external: usesPlatform(appframe)
159
+ ? []
160
+ : appframe.build.externals !== false
161
+ ? ["react", "react-dom", "react-dom/client"]
162
+ : [],
28
163
  treeshake: {
29
164
  /*
30
165
  * `@digdir/designsystemet-react` does not declare `sideEffects: false`, so the
@@ -39,7 +174,9 @@ export async function addAppframeBuildConfig(config) {
39
174
  moduleSideEffects: (id) => !/[\\/]node_modules[\\/].*designsystemet-react[\\/]/.test(id.replace(/\?.*$/, "")),
40
175
  },
41
176
  output: {
42
- globals: appframe.build.externals !== false
177
+ // Platform builds keep the bare specifiers (ESM, resolved by the import map); the old
178
+ // externals mode maps them to window globals instead.
179
+ globals: !usesPlatform(appframe) && appframe.build.externals !== false
43
180
  ? {
44
181
  react: "React",
45
182
  "react-dom": "ReactDOM",
package/lib/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Plugin } from "vite";
2
2
  import { appframeDataGlobal } from "./appframeDataGlobal.js";
3
- import { addAppframeBuildConfig } from "./build.js";
3
+ import { addAppframeBuildConfig, PLATFORM_EXTERNALS, PLATFORM_GLOBALS, PLATFORM_UPSTREAM, platformManifest, usesPlatform } from "./build.js";
4
4
  import { createDevMiddleware } from "./devServer.js";
5
5
  import { publishSafeOutput } from "./publishSafeOutput.js";
6
6
  export interface AppframePluginOptions {
@@ -32,6 +32,7 @@ export interface AppframePluginOptions {
32
32
  dataGlobal?: boolean;
33
33
  }
34
34
  export default function appframe(options?: AppframePluginOptions): Plugin[];
35
- export { addAppframeBuildConfig, appframeDataGlobal, createDevMiddleware, publishSafeOutput };
35
+ export { addAppframeBuildConfig, appframeDataGlobal, createDevMiddleware, PLATFORM_EXTERNALS, PLATFORM_GLOBALS, PLATFORM_UPSTREAM, platformManifest, publishSafeOutput, usesPlatform, };
36
+ export type { PlatformManifest } from "./build.js";
36
37
  export { DEVTOOLS_API_VERSION, type DevtoolsDiscovery, type DevtoolsServerInfo } from "./devtoolsServer.js";
37
38
  export type { JobOptions, ReleaseEvent, ReleaseScriptEvent, ReleaseSnapshot, ReleaseStep } from "./releaseServer.js";
package/lib/index.js CHANGED
@@ -1,8 +1,9 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import { resolve } from "node:path";
2
3
  import bodyParser from "body-parser";
3
4
  import { watch } from "chokidar";
4
5
  import { appframeDataGlobal } from "./appframeDataGlobal.js";
5
- import { addAppframeBuildConfig } from "./build.js";
6
+ import { addAppframeBuildConfig, PLATFORM_EXTERNALS, PLATFORM_GLOBALS, PLATFORM_UPSTREAM, platformManifest, usesPlatform, } from "./build.js";
6
7
  import { generateFromConfig } from "./cli-resources-generate.js";
7
8
  import { createDevMiddleware, getLoginInfo, getProxyRoutes } from "./devServer.js";
8
9
  import { createDevtoolsMiddleware } from "./devtoolsServer.js";
@@ -21,19 +22,27 @@ let lastHostname;
21
22
  let watcher;
22
23
  let resourcesWatcher = null;
23
24
  let resourcesDebounce = null;
24
- try {
25
- let appPkgUrl = `file://${process.cwd()}/package.json`;
26
- watcher = watch(appPkgUrl).on("all", async () => {
27
- if (server) {
28
- let { hostname } = await getLoginInfo();
29
- if (lastHostname !== hostname) {
30
- server.restart(false);
25
+ /**
26
+ * Restarts the dev server when the app's package.json changes its hostname. Started with the
27
+ * dev server, never at import: a watcher keeps node alive, and the deploy and release scripts
28
+ * import this package for its exports without ever running a server.
29
+ */
30
+ function watchAppPackage() {
31
+ watcher?.close();
32
+ try {
33
+ let appPkgUrl = `file://${process.cwd()}/package.json`;
34
+ watcher = watch(appPkgUrl).on("all", async () => {
35
+ if (server) {
36
+ let { hostname } = await getLoginInfo();
37
+ if (lastHostname !== hostname) {
38
+ server.restart(false);
39
+ }
31
40
  }
32
- }
33
- });
34
- }
35
- catch (error) {
36
- console.log(createLogMessage(`failed to watch package.json: ${error.message}`, { type: "warn" }));
41
+ });
42
+ }
43
+ catch (error) {
44
+ console.log(createLogMessage(`failed to watch package.json: ${error.message}`, { type: "warn" }));
45
+ }
37
46
  }
38
47
  const jsonParser = bodyParser.json();
39
48
  export default function appframe(options = {}) {
@@ -120,10 +129,10 @@ export default function appframe(options = {}) {
120
129
  },
121
130
  };
122
131
  if (command === "build") {
123
- watcher.close();
124
132
  await addAppframeBuildConfig(config);
125
133
  }
126
134
  else {
135
+ watchAppPackage();
127
136
  let proxy = config.server?.proxy ?? {};
128
137
  let routes = getProxyRoutes();
129
138
  let { hostname, username, password } = await getLoginInfo();
@@ -240,7 +249,30 @@ export default function appframe(options = {}) {
240
249
  };
241
250
  },
242
251
  };
243
- return [...(dataGlobal ? [appframeDataGlobal()] : []), plugin, publishSafeOutput()];
252
+ let plugins = [plugin, publishSafeOutput()];
253
+ let platform = usesPlatform(readAppframeConfig());
254
+ if (platform) {
255
+ plugins.push(platformManifest());
256
+ }
257
+ if (dataGlobal) {
258
+ // An app built against the platform leaves `@olenbetong/appframe-data` as a bare
259
+ // specifier for the import map, so the global redirect only applies to the dev server
260
+ // (which has no import map) and to builds that bundle their own runtime.
261
+ plugins.unshift({
262
+ ...appframeDataGlobal(),
263
+ apply: (_config, env) => env.command === "serve" || !platform,
264
+ });
265
+ }
266
+ return plugins;
267
+ }
268
+ /** The `appframe` section of the app's package.json, read synchronously at plugin creation. */
269
+ function readAppframeConfig() {
270
+ try {
271
+ return JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf8")).appframe;
272
+ }
273
+ catch {
274
+ return undefined;
275
+ }
244
276
  }
245
- export { addAppframeBuildConfig, appframeDataGlobal, createDevMiddleware, publishSafeOutput };
277
+ export { addAppframeBuildConfig, appframeDataGlobal, createDevMiddleware, PLATFORM_EXTERNALS, PLATFORM_GLOBALS, PLATFORM_UPSTREAM, platformManifest, publishSafeOutput, usesPlatform, };
246
278
  export { DEVTOOLS_API_VERSION } from "./devtoolsServer.js";
@@ -9,6 +9,8 @@ export declare function writeGeneratedFile(filePath: string, content: string): P
9
9
  export type CLIOptions = {
10
10
  /** Procedures only. `false` stops the framework wrapping the call in a transaction. */
11
11
  transaction?: boolean;
12
+ /** Procedures only. Milliseconds before the call is abandoned; the generated default is 30000. */
13
+ timeout?: number;
12
14
  server: string;
13
15
  types?: boolean;
14
16
  global: boolean;
@@ -305,7 +305,7 @@ export function getProcedureDefinition(name, procDefinition, options) {
305
305
  output.push(`export const ${procName} = new ${options.global ? "af." : ""}ProcedureAPI${options.types ? `<${paramTypeName}, ${options.typesJsonReturnType ?? "{ Table?: unknown[] }"}>` : ""}({
306
306
  procedureId: "${name}",
307
307
  parameters: ${JSON.stringify(parameters, null, 2)},
308
- timeout: 30000${options.transaction === false ? ",\n transaction: false" : ""}
308
+ timeout: ${options.timeout ?? 30000}${options.transaction === false ? ",\n transaction: false" : ""}
309
309
  });`);
310
310
  if (options.expose) {
311
311
  output.push("");
@@ -37,6 +37,12 @@ export type ResourceEntry = {
37
37
  * remote write a distributed one, which fails without MSDTC configured between the servers.
38
38
  */
39
39
  transaction?: boolean;
40
+ /**
41
+ * Procedures only. Milliseconds the call may take before it is abandoned, client and server
42
+ * alike (default `30000`). For a procedure that reads across a linked server or scans a large
43
+ * table, where a preview alone can take a minute.
44
+ */
45
+ timeout?: number;
40
46
  /** Permissions to set: I = insert, U = update, D = delete (e.g. `IUD`) */
41
47
  permissions?: string;
42
48
  /** Maximum records to fetch (default `50`; use `-1` for all) */
@@ -98,6 +98,7 @@ export function entryToCLIOptions(entry, hostname) {
98
98
  dynamic: entry.dynamic ?? false,
99
99
  types: entry.types,
100
100
  transaction: entry.transaction,
101
+ timeout: entry.timeout,
101
102
  maxRecords: entry.maxRecords !== undefined ? String(entry.maxRecords) : "50",
102
103
  sortOrder: sortOrderToString(entry.sortOrder),
103
104
  permissions: entry.permissions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olenbetong/appframe-vite",
3
- "version": "6.8.1",
3
+ "version": "6.10.0",
4
4
  "description": "Tools to use and deploy Vite applications to Appframe",
5
5
  "main": "./lib/index.js",
6
6
  "type": "module",
@@ -39,14 +39,14 @@
39
39
  "inquirer-autocomplete-standalone": "^0.8.1",
40
40
  "jsdom": "30.0.1",
41
41
  "rollup-plugin-visualizer": "^7.1.1",
42
- "yaml": "^2.9.0",
43
- "@olenbetong/appframe-data": "1.7.1"
42
+ "yaml": "^2.9.1",
43
+ "@olenbetong/appframe-data": "1.7.2"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/jsdom": "^30.0.0",
47
47
  "@types/node": "26.5.1",
48
48
  "typescript": "7.0.2",
49
- "vite": "8.2.2"
49
+ "vite": "8.3.0"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "vite": ">=8.1.5"