@olenbetong/appframe-vite 6.9.0 → 6.11.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
@@ -1,297 +1,300 @@
1
- # @olenbetong/appframe-vite
2
-
3
- Tools to develop and build Vite applications that run inside Appframe articles.
4
-
5
- - Provides a Vite plugin you add in `vite.config.*`.
6
- - Proxies API and static routes to an Appframe server during `pnpm start`.
7
- - Logs in automatically and refreshes the session on an interval.
8
- - Serves real article HTML from Appframe in dev, but swaps production assets for your local entry.
9
- - Caches i18n strings locally to speed up development loads.
10
- - Shapes production builds to Appframe paths and filenames, with optional React externals.
11
-
12
- ## Installation
13
-
14
- ```sh
15
- pnpm add -D @olenbetong/appframe-vite
16
- ```
17
-
18
- ## Usage (Vite)
19
-
20
- ```ts
21
- // vite.config.mjs or vite.config.ts
22
- import appframe from "@olenbetong/appframe-vite";
23
- import { defineConfig } from "vite";
24
-
25
- export default defineConfig({
26
- plugins: [appframe()],
27
- });
28
- ```
29
-
30
- Then run your app:
31
-
32
- ```sh
33
- pnpm --filter <app> start
34
- ```
35
-
36
- Make sure your Appframe credentials are available as environment variables:
37
-
38
- - `APPFRAME_LOGIN`: username
39
- - `APPFRAME_PWD`: password
40
-
41
- On first run, the plugin logs in, configures Vite’s proxy for Appframe routes, and serves the article HTML with your local entry injected.
42
-
43
- ### MUI X license key
44
-
45
- The plugin injects the MUI X Premium license key into the bundle as the global `__MUI_X_LICENSE_KEY__`, for both `serve` and `build`. `@olenbetong/appframe-ds/grid` reads it and registers the license itself, so an app using the Designsystemet `AfGrid` does not need its own `LicenseInfo.setLicenseKey()` call.
46
-
47
- The key is resolved from, in order:
48
-
49
- 1. the `MUI_X_LICENSE_KEY` environment variable (CI, a local `.env`, or the shell),
50
- 2. the `MUI_X_LICENSE_KEY` constant declared in the app's `src/shared/licenses.ts`.
51
-
52
- The fallback keeps existing apps working with no changes; setting the environment variable is the forward path for getting the key out of the repository.
53
-
54
- ## Project configuration (package.json)
55
-
56
- This package reads `package.json.appframe` to know what to proxy and how to build.
57
-
58
- ```jsonc
59
- {
60
- "appframe": {
61
- "article": {
62
- "id": "my-article", // Required
63
- "altNames": ["my-article-dev"], // Optional: alternate root paths
64
- },
65
- "deploy": { "hostname": "dev.obet.no" }, // Default server if proxy not set
66
- "proxy": {
67
- "hostname": "dev.obet.no", // Optional override for dev proxy
68
- "routes": ["^/custom/.*"], // Optional: extra proxied routes
69
- },
70
- "build": {
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
73
- },
74
- },
75
- }
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 versions the app compiled against; `scripts/app-deploy.ts` refuses to deploy when the
87
- target article renders no import map or its platform carries other versions (override with
88
- `--allow-platform-mismatch`).
89
-
90
- ## What it does (at a glance)
91
-
92
- - Dev server
93
- - Proxies Appframe routes (e.g. `/api`, `/file`, `/lib`, etc.) to the configured hostname.
94
- - Performs an initial login and refreshes the session every 5 minutes to prevent 403 errors.
95
- - Serves the real article HTML from Appframe, but removes production assets and injects your local entry (`/src/index.*`).
96
- - Caches localized strings under `node_modules/.appframe/localizeCache.json` and injects them into the page to avoid many small HTTP 1.1 requests.
97
- - Build
98
- - Enables `manifest` and `sourcemap`.
99
- - Writes filenames to Appframe paths, using the article ID, e.g.:
100
- - `file/article/script/<ARTICLE_ID>/main.[hash].min.js`
101
- - `file/article/style/<ARTICLE_ID>/style.[hash].min.css`
102
- - 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.
103
- - Adds a Rollup visualizer report at `dist/stats.html`.
104
- - Resolve
105
- - Adds the alias `~/` to `/src/` so imports like `~/components/Button` resolve to `src/components/Button`.
106
- - DevTools API (see below)
107
-
108
- ## DevTools
109
-
110
- In dev mode the plugin exposes a small JSON API at `/__appframe_devtools__/api` for the **Appframe DevTools
111
- browser extension** (`packages/appframe-devtools` in the SynergiWeb repo). The extension adds an "Appframe"
112
- panel to Chrome/Edge DevTools with a live view of the app's data objects, an editor for `resources.yaml`
113
- and a browser for the data API catalog. Nothing is injected into the page the toolbar that older
114
- versions added to every dev-served page is gone.
115
-
116
- | Endpoint | Purpose |
117
- | --------------------------- | --------------------------------------------------------------------------------------------------------------------- |
118
- | `GET /api/config` | Discovery: `{ appframe: true, apiVersion, hostname, articleId, articleHostname, appName, appVersion, pluginVersion }` |
119
- | `GET /api/resources` | Parsed `resources.yaml` |
120
- | `POST /api/resources` | Add an entry (`type: "dataObject" \| "procedure"` plus the entry fields) |
121
- | `PUT /api/resources/:id` | Replace an entry |
122
- | `DELETE /api/resources/:id` | Remove an entry and its generated output file |
123
- | `POST /api/catalog` | Register a `dbObjectId` as a data API resource on dev, stage and prod |
124
- | `GET /api/release` | Current session (`session: null` when none) and the capabilities derived from the app's scripts |
125
- | `POST /api/release` | Start a job: `{ kind: "release", dry?, force? }` or `{ kind: "deploy", target, copyDatasources? }`; 409 while running |
126
- | `DELETE /api/release` | Cancel the running job |
127
- | `POST /api/release/input` | Answer one of the script's questions (`{ name, value }`: the release type and notes, or the apply confirmation) |
128
- | `GET /api/release/events` | Server-sent events: the session snapshot, then every step, log line and question as it happens |
129
-
130
- The release job runs whatever `scripts.release` is in the app's package.json, from the app directory, with
131
- `--ui` appended. The deploy job runs `pnpm run build`, optionally `pnpm run cd` (or `copy-datasources`) and
132
- `pnpm run deploy` with `APPFRAME_DEPLOY_HOSTNAME` set to the target, which `app-deploy.ts` and
133
- `app-copy-datasources.ts` honour over the hostnames in package.json. The script is expected to follow the protocol in `scripts/lib/releaseUi.ts` (JSON event lines
134
- prefixed with `@@appframe-devtools ` on stdout, answers as JSON lines on stdin).
135
-
136
- Disable it with `appframe({ devtools: false })` when there is no article context (Storybook does this).
137
- The extension page has host permissions for `localhost`, so the API needs no CORS headers.
138
-
139
- ## CLI `appframe-vite`
140
-
141
- The package ships a CLI (`appframe-vite`) alongside the Vite plugin.
142
-
143
- ### `generate-types`
144
-
145
- Generates TypeScript type definitions from the article's data objects and procedures:
146
-
147
- ```sh
148
- appframe-vite generate-types
149
- ```
150
-
151
- ### `resources generate`
152
-
153
- Reads `resources.yaml` and regenerates all data object / procedure files:
154
-
155
- ```sh
156
- appframe-vite resources generate
157
- # custom config file path:
158
- appframe-vite resources generate --config path/to/resources.yaml
159
- ```
160
-
161
- ### `resources add`
162
-
163
- Interactive wizard — prompts for resource, ID, output path, permissions, fields, and more, then appends an entry to `resources.yaml` and generates the output file:
164
-
165
- ```sh
166
- appframe-vite resources add
167
- ```
168
-
169
- ### `resources edit [id]`
170
-
171
- Interactive editor — pre-fills the wizard with the current values from `resources.yaml` and regenerates the file on save:
172
-
173
- ```sh
174
- appframe-vite resources edit dsAccountGroups
175
- # or without an id (shows a searchable list):
176
- appframe-vite resources edit
177
- ```
178
-
179
- ### `resources migrate [id]`
180
-
181
- Migrates data objects and procedures that are configured on the article in the appdesigner over to `resources.yaml`. The article's data sources and their fields are read from the CMS, converted to `resources.yaml` entries, and the output files are generated.
182
-
183
- Each migrated view/procedure is also registered as a Data API resource if needed (the same thing `af resources add` does), because `af.data.generateApiDataObject` and `af.ProcedureAPI` go through `/api/data` instead of the article's data sources.
184
-
185
- ```sh
186
- # migrate everything on the article
187
- appframe-vite resources migrate
188
- # migrate a single data object or procedure
189
- appframe-vite resources migrate dsVouchers
190
- # preview without changing anything
191
- appframe-vite resources migrate --dry-run
192
- ```
193
-
194
- Options:
195
-
196
- | Option | Description |
197
- | ------------------------ | -------------------------------------------------------------- |
198
- | `-c, --config <path>` | Path to the resources config file (default: `resources.yaml`) |
199
- | `-o, --output-dir <dir>` | Directory for the generated files (default: `src/data`) |
200
- | `--dry-run` | Show what would be migrated without writing anything |
201
- | `--no-generate` | Only update `resources.yaml`, do not generate the output files |
202
-
203
- Existing `resources.yaml` entries with the same id are updated in place. Field aliases have no `resources.yaml` equivalent and are reported as warnings.
204
-
205
- After migrating, import the generated files from your app, remove the data objects and procedures from the article in the appdesigner, and run `pnpm generate-types`.
206
-
207
- ## `resources.yaml` config file
208
-
209
- 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.
210
-
211
- **Example:**
212
-
213
- ```yaml
214
- dataObjects:
215
- - id: dsAccountGroups
216
- resource: atbv_Accounting_SubsidiaryLedgerGroups
217
- global: true
218
- types: true
219
- maxRecords: -1
220
- expose: true
221
- fields:
222
- - Domain
223
- - SubsidiaryLedgerGroup
224
- - PrimKey
225
- - Description
226
- output: src/data/dsAccountGroups.ts
227
-
228
- - id: dsSubsidiaryLedger
229
- resource: atbv_Accounting_SubsidiaryLedger
230
- global: true
231
- types: true
232
- permissions: IUD
233
- output: src/data/dsSubsidiaryLedger.ts
234
-
235
- - id: dsProjectLocations
236
- resource: aviw_Accounting_ProjectsLocations
237
- global: true
238
- types: true
239
- master: dsProjects
240
- linkFields:
241
- - Domain
242
- - ProjectID
243
- output: src/data/dsProjectLocations.ts
244
-
245
- procedures:
246
- - id: procCreateCustomer
247
- resource: astp_Accounting_SubsidiaryLedger_Create
248
- global: true
249
- types: true
250
- expose: true
251
- output: src/data/procCreateCustomer.ts
252
- ```
253
-
254
- **Supported fields per entry:**
255
-
256
- | Field | Type | Description |
257
- | ------------- | -------------------- | ------------------------------------------------------------------------------- |
258
- | `id` | `string` | Variable name used in generated code (e.g. `dsAccountGroups`) |
259
- | `resource` | `string` | Database object ID (e.g. `atbv_Accounting_SubsidiaryLedgerGroups`) |
260
- | `output` | `string` | Output file path relative to project root |
261
- | `global` | `boolean` | Use `af.data.generateApiDataObject` / `new af.ProcedureAPI` globals |
262
- | `types` | `boolean` | Emit TypeScript type definitions |
263
- | `permissions` | `string` | Permissions: I = insert, U = update, D = delete (e.g. `IUD`) |
264
- | `maxRecords` | `number` | Max records to fetch (default `50`; `-1` for all) |
265
- | `sortOrder` | `string \| string[]` | Sort order, e.g. `Created:Desc` or `[Created:Desc, Name]` |
266
- | `master` | `string` | Master data object name (or `name:importPath`) |
267
- | `linkFields` | `string \| string[]` | Fields linking child to master |
268
- | `expose` | `boolean \| string` | Expose on `af.article.dataObjects` / `af.article.procedures` |
269
- | `dynamic` | `boolean` | Enable dynamic loading |
270
- | `unique` | `string` | Unique table name for update/delete |
271
- | `overrides` | `string \| string[]` | Type overrides, e.g. `MyField:string[]` |
272
- | `distinct` | `boolean` | Fetch distinct rows |
273
- | `aggregates` | `string \| string[]` | Aggregate bindings, e.g. `Qty:SUM` |
274
- | `groupBy` | `string \| string[]` | Group-by fields |
275
- | `where` | `string` | Initial where clause |
276
- | `fields` | `string \| string[]` | Fields to include (all if omitted) |
277
- | `transaction` | `boolean` | Procedures only. `false` stops the framework wrapping the call in a transaction |
278
- | `timeout` | `number` | Procedures only. Milliseconds before the call is abandoned (default `30000`) |
279
-
280
- A top-level `server` key can override the hostname (defaults to `appframe.proxy.hostname` from `package.json`).
281
-
282
- **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.
283
-
284
- ## `@olenbetong/appframe-vite/resources` export
285
-
286
- Shared code generation utilities for Node.js consumers:
287
-
288
- ```ts
289
- import {
290
- fetchAndGenerate,
291
- buildYamlConfig,
292
- parseYamlConfig,
293
- writeGeneratedFile,
294
- getCustomImportPath,
295
- type CLIOptions,
296
- } from "@olenbetong/appframe-vite/resources";
297
- ```
1
+ # @olenbetong/appframe-vite
2
+
3
+ Tools to develop and build Vite applications that run inside Appframe articles.
4
+
5
+ - Provides a Vite plugin you add in `vite.config.*`.
6
+ - Proxies API and static routes to an Appframe server during `pnpm start`.
7
+ - Logs in automatically and refreshes the session on an interval.
8
+ - Serves real article HTML from Appframe in dev, but swaps production assets for your local entry.
9
+ - Caches i18n strings locally to speed up development loads.
10
+ - Shapes production builds to Appframe paths and filenames, with optional React externals.
11
+
12
+ ## Installation
13
+
14
+ ```sh
15
+ pnpm add -D @olenbetong/appframe-vite
16
+ ```
17
+
18
+ ## Usage (Vite)
19
+
20
+ ```ts
21
+ // vite.config.mjs or vite.config.ts
22
+ import appframe from "@olenbetong/appframe-vite";
23
+ import { defineConfig } from "vite";
24
+
25
+ export default defineConfig({
26
+ plugins: [appframe()],
27
+ });
28
+ ```
29
+
30
+ Then run your app:
31
+
32
+ ```sh
33
+ pnpm --filter <app> start
34
+ ```
35
+
36
+ Make sure your Appframe credentials are available as environment variables:
37
+
38
+ - `APPFRAME_LOGIN`: username
39
+ - `APPFRAME_PWD`: password
40
+
41
+ On first run, the plugin logs in, configures Vite’s proxy for Appframe routes, and serves the article HTML with your local entry injected.
42
+
43
+ ### MUI X license key
44
+
45
+ The plugin injects the MUI X Premium license key into the bundle as the global `__MUI_X_LICENSE_KEY__`, for both `serve` and `build`. `@olenbetong/appframe-ds/grid` reads it and registers the license itself, so an app using the Designsystemet `AfGrid` does not need its own `LicenseInfo.setLicenseKey()` call.
46
+
47
+ The key is resolved from, in order:
48
+
49
+ 1. the `MUI_X_LICENSE_KEY` environment variable (CI, a local `.env`, or the shell),
50
+ 2. the `MUI_X_LICENSE_KEY` constant declared in the app's `src/shared/licenses.ts`.
51
+
52
+ The fallback keeps existing apps working with no changes; setting the environment variable is the forward path for getting the key out of the repository.
53
+
54
+ ## Project configuration (package.json)
55
+
56
+ This package reads `package.json.appframe` to know what to proxy and how to build.
57
+
58
+ ```jsonc
59
+ {
60
+ "appframe": {
61
+ "article": {
62
+ "id": "my-article", // Required
63
+ "altNames": ["my-article-dev"], // Optional: alternate root paths
64
+ },
65
+ "deploy": { "hostname": "dev.obet.no" }, // Default server if proxy not set
66
+ "proxy": {
67
+ "hostname": "dev.obet.no", // Optional override for dev proxy
68
+ "routes": ["^/custom/.*"], // Optional: extra proxied routes
69
+ },
70
+ "build": {
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
73
+ },
74
+ },
75
+ }
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
+
93
+ ## What it does (at a glance)
94
+
95
+ - Dev server
96
+ - Proxies Appframe routes (e.g. `/api`, `/file`, `/lib`, etc.) to the configured hostname.
97
+ - Performs an initial login and refreshes the session every 5 minutes to prevent 403 errors.
98
+ - Serves the real article HTML from Appframe, but removes production assets and injects your local entry (`/src/index.*`).
99
+ - Caches localized strings under `node_modules/.appframe/localizeCache.json` and injects them into the page to avoid many small HTTP 1.1 requests.
100
+ - Build
101
+ - Enables `manifest` and `sourcemap`.
102
+ - Writes filenames to Appframe paths, using the article ID, e.g.:
103
+ - `file/article/script/<ARTICLE_ID>/main.[hash].min.js`
104
+ - `file/article/style/<ARTICLE_ID>/style.[hash].min.css`
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.
106
+ - Adds a Rollup visualizer report at `dist/stats.html`.
107
+ - Resolve
108
+ - Adds the alias `~/` to `/src/` so imports like `~/components/Button` resolve to `src/components/Button`.
109
+ - DevTools API (see below)
110
+
111
+ ## DevTools
112
+
113
+ In dev mode the plugin exposes a small JSON API at `/__appframe_devtools__/api` for the **Appframe DevTools
114
+ browser extension** (`packages/appframe-devtools` in the SynergiWeb repo). The extension adds an "Appframe"
115
+ panel to Chrome/Edge DevTools with a live view of the app's data objects, an editor for `resources.yaml`
116
+ and a browser for the data API catalog. Nothing is injected into the page — the toolbar that older
117
+ versions added to every dev-served page is gone.
118
+
119
+ | Endpoint | Purpose |
120
+ | --------------------------- | --------------------------------------------------------------------------------------------------------------------- |
121
+ | `GET /api/config` | Discovery: `{ appframe: true, apiVersion, hostname, articleId, articleHostname, appName, appVersion, pluginVersion }` |
122
+ | `GET /api/resources` | Parsed `resources.yaml` |
123
+ | `POST /api/resources` | Add an entry (`type: "dataObject" \| "procedure"` plus the entry fields) |
124
+ | `PUT /api/resources/:id` | Replace an entry |
125
+ | `DELETE /api/resources/:id` | Remove an entry and its generated output file |
126
+ | `POST /api/catalog` | Register a `dbObjectId` as a data API resource on dev, stage and prod |
127
+ | `GET /api/release` | Current session (`session: null` when none) and the capabilities derived from the app's scripts |
128
+ | `POST /api/release` | Start a job: `{ kind: "release", dry?, force? }` or `{ kind: "deploy", target, copyDatasources? }`; 409 while running |
129
+ | `DELETE /api/release` | Cancel the running job |
130
+ | `POST /api/release/input` | Answer one of the script's questions (`{ name, value }`: the release type and notes, or the apply confirmation) |
131
+ | `GET /api/release/events` | Server-sent events: the session snapshot, then every step, log line and question as it happens |
132
+
133
+ The release job runs whatever `scripts.release` is in the app's package.json, from the app directory, with
134
+ `--ui` appended. The deploy job runs `pnpm run build`, optionally `pnpm run cd` (or `copy-datasources`) and
135
+ `pnpm run deploy` with `APPFRAME_DEPLOY_HOSTNAME` set to the target, which `app-deploy.ts` and
136
+ `app-copy-datasources.ts` honour over the hostnames in package.json. The script is expected to follow the protocol in `scripts/lib/releaseUi.ts` (JSON event lines
137
+ prefixed with `@@appframe-devtools ` on stdout, answers as JSON lines on stdin).
138
+
139
+ Disable it with `appframe({ devtools: false })` when there is no article context (Storybook does this).
140
+ The extension page has host permissions for `localhost`, so the API needs no CORS headers.
141
+
142
+ ## CLI — `appframe-vite`
143
+
144
+ The package ships a CLI (`appframe-vite`) alongside the Vite plugin.
145
+
146
+ ### `generate-types`
147
+
148
+ Generates TypeScript type definitions from the article's data objects and procedures:
149
+
150
+ ```sh
151
+ appframe-vite generate-types
152
+ ```
153
+
154
+ ### `resources generate`
155
+
156
+ Reads `resources.yaml` and regenerates all data object / procedure files:
157
+
158
+ ```sh
159
+ appframe-vite resources generate
160
+ # custom config file path:
161
+ appframe-vite resources generate --config path/to/resources.yaml
162
+ ```
163
+
164
+ ### `resources add`
165
+
166
+ Interactive wizard — prompts for resource, ID, output path, permissions, fields, and more, then appends an entry to `resources.yaml` and generates the output file:
167
+
168
+ ```sh
169
+ appframe-vite resources add
170
+ ```
171
+
172
+ ### `resources edit [id]`
173
+
174
+ Interactive editor — pre-fills the wizard with the current values from `resources.yaml` and regenerates the file on save:
175
+
176
+ ```sh
177
+ appframe-vite resources edit dsAccountGroups
178
+ # or without an id (shows a searchable list):
179
+ appframe-vite resources edit
180
+ ```
181
+
182
+ ### `resources migrate [id]`
183
+
184
+ Migrates data objects and procedures that are configured on the article in the appdesigner over to `resources.yaml`. The article's data sources and their fields are read from the CMS, converted to `resources.yaml` entries, and the output files are generated.
185
+
186
+ Each migrated view/procedure is also registered as a Data API resource if needed (the same thing `af resources add` does), because `af.data.generateApiDataObject` and `af.ProcedureAPI` go through `/api/data` instead of the article's data sources.
187
+
188
+ ```sh
189
+ # migrate everything on the article
190
+ appframe-vite resources migrate
191
+ # migrate a single data object or procedure
192
+ appframe-vite resources migrate dsVouchers
193
+ # preview without changing anything
194
+ appframe-vite resources migrate --dry-run
195
+ ```
196
+
197
+ Options:
198
+
199
+ | Option | Description |
200
+ | ------------------------ | -------------------------------------------------------------- |
201
+ | `-c, --config <path>` | Path to the resources config file (default: `resources.yaml`) |
202
+ | `-o, --output-dir <dir>` | Directory for the generated files (default: `src/data`) |
203
+ | `--dry-run` | Show what would be migrated without writing anything |
204
+ | `--no-generate` | Only update `resources.yaml`, do not generate the output files |
205
+
206
+ Existing `resources.yaml` entries with the same id are updated in place. Field aliases have no `resources.yaml` equivalent and are reported as warnings.
207
+
208
+ After migrating, import the generated files from your app, remove the data objects and procedures from the article in the appdesigner, and run `pnpm generate-types`.
209
+
210
+ ## `resources.yaml` config file
211
+
212
+ 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.
213
+
214
+ **Example:**
215
+
216
+ ```yaml
217
+ dataObjects:
218
+ - id: dsAccountGroups
219
+ resource: atbv_Accounting_SubsidiaryLedgerGroups
220
+ global: true
221
+ types: true
222
+ maxRecords: -1
223
+ expose: true
224
+ fields:
225
+ - Domain
226
+ - SubsidiaryLedgerGroup
227
+ - PrimKey
228
+ - Description
229
+ output: src/data/dsAccountGroups.ts
230
+
231
+ - id: dsSubsidiaryLedger
232
+ resource: atbv_Accounting_SubsidiaryLedger
233
+ global: true
234
+ types: true
235
+ permissions: IUD
236
+ output: src/data/dsSubsidiaryLedger.ts
237
+
238
+ - id: dsProjectLocations
239
+ resource: aviw_Accounting_ProjectsLocations
240
+ global: true
241
+ types: true
242
+ master: dsProjects
243
+ linkFields:
244
+ - Domain
245
+ - ProjectID
246
+ output: src/data/dsProjectLocations.ts
247
+
248
+ procedures:
249
+ - id: procCreateCustomer
250
+ resource: astp_Accounting_SubsidiaryLedger_Create
251
+ global: true
252
+ types: true
253
+ expose: true
254
+ output: src/data/procCreateCustomer.ts
255
+ ```
256
+
257
+ **Supported fields per entry:**
258
+
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`) |
282
+
283
+ A top-level `server` key can override the hostname (defaults to `appframe.proxy.hostname` from `package.json`).
284
+
285
+ **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
+ ## `@olenbetong/appframe-vite/resources` export
288
+
289
+ Shared code generation utilities for Node.js consumers:
290
+
291
+ ```ts
292
+ import {
293
+ fetchAndGenerate,
294
+ buildYamlConfig,
295
+ parseYamlConfig,
296
+ writeGeneratedFile,
297
+ getCustomImportPath,
298
+ type CLIOptions,
299
+ } from "@olenbetong/appframe-vite/resources";
300
+ ```
package/lib/build.d.ts CHANGED
@@ -22,14 +22,20 @@ export declare function usesPlatform(appframe: {
22
22
  */
23
23
  export declare const PLATFORM_UPSTREAM: string[];
24
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;
25
30
  /** The package versions the app resolved at build time. */
26
31
  upstream: Record<string, string>;
27
32
  };
28
33
  /**
29
- * Writes `dist/platform.json` after a platform build: the versions of the platform packages the
30
- * app was compiled against. The deploy script compares it with the manifest of the platform the
31
- * target site serves, since an app compiled against one React and run on another is a runtime
32
- * error, not a build error.
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.
33
39
  */
34
40
  export declare function platformManifest(): Plugin;
35
41
  export declare function addAppframeBuildConfig(config: UserConfig): Promise<UserConfig>;
package/lib/build.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { readFile, writeFile } from "node:fs/promises";
3
- import { resolve } from "node:path";
3
+ import { dirname, resolve } from "node:path";
4
4
  import { visualizer } from "rollup-plugin-visualizer";
5
5
  import { esmExternalRequirePlugin } from "vite";
6
6
  import { importJson } from "./importJson.js";
@@ -27,6 +27,9 @@ export const PLATFORM_EXTERNALS = [
27
27
  "@olenbetong/appframe-ds/mdi",
28
28
  "@olenbetong/appframe-ds/progress",
29
29
  "@olenbetong/appframe-ds/report-downloader",
30
+ "pdfjs-dist",
31
+ "react-pdf",
32
+ "pdf-lib",
30
33
  ];
31
34
  /**
32
35
  * The IIFE global each platform specifier is published under (`buildConfig.entries` in the
@@ -49,6 +52,9 @@ export const PLATFORM_GLOBALS = {
49
52
  "@olenbetong/appframe-ds/mdi": "SynergiPlatform.appframeDsMdi",
50
53
  "@olenbetong/appframe-ds/progress": "SynergiPlatform.appframeDsProgress",
51
54
  "@olenbetong/appframe-ds/report-downloader": "SynergiPlatform.appframeDsReportDownloader",
55
+ "pdfjs-dist": "SynergiPlatform.pdfjs",
56
+ "react-pdf": "SynergiPlatform.reactPdf",
57
+ "pdf-lib": "SynergiPlatform.pdfLib",
52
58
  };
53
59
  export function usesPlatform(appframe) {
54
60
  return appframe?.build?.platform === true;
@@ -68,19 +74,52 @@ export const PLATFORM_UPSTREAM = [
68
74
  "@olenbetong/appframe-data",
69
75
  "@olenbetong/appframe-react",
70
76
  "@olenbetong/appframe-ds",
77
+ "pdfjs-dist",
78
+ "react-pdf",
79
+ "pdf-lib",
71
80
  ];
72
81
  /**
73
- * Writes `dist/platform.json` after a platform build: the versions of the platform packages the
74
- * app was compiled against. The deploy script compares it with the manifest of the platform the
75
- * target site serves, since an app compiled against one React and run on another is a runtime
76
- * error, not a build error.
82
+ * The platform package's version as the app sees it: its own copy under node_modules when it
83
+ * depends on `@olenbetong/synergi-platform`, otherwise the workspace's `packages/synergi-platform`
84
+ * found by walking up from the app - every app lives in the monorepo next to it.
85
+ */
86
+ async function platformVersion(cwd) {
87
+ let candidates = [resolve(cwd, "node_modules", "@olenbetong", "synergi-platform", "package.json")];
88
+ let dir = cwd;
89
+ while (true) {
90
+ candidates.push(resolve(dir, "packages", "synergi-platform", "package.json"));
91
+ let parent = dirname(dir);
92
+ if (parent === dir)
93
+ break;
94
+ dir = parent;
95
+ }
96
+ for (let file of candidates) {
97
+ try {
98
+ return JSON.parse(await readFile(file, "utf8")).version;
99
+ }
100
+ catch {
101
+ // Not here; try the next candidate.
102
+ }
103
+ }
104
+ return undefined;
105
+ }
106
+ /**
107
+ * Writes `dist/platform.json` after a platform build: the platform version the app was built
108
+ * alongside and the versions of the platform packages it compiled against. The deploy pins the
109
+ * article to that platform version (its `PlatformHead` block) and compares the versions with the
110
+ * manifest that version serves on the target site, since an app compiled against one React and
111
+ * run on another is a runtime error, not a build error.
77
112
  */
78
113
  export function platformManifest() {
79
114
  return {
80
115
  name: "appframe:platform-manifest",
81
116
  apply: "build",
82
117
  async writeBundle() {
83
- let manifest = { upstream: {} };
118
+ let manifest = { platform: await platformVersion(process.cwd()), upstream: {} };
119
+ if (manifest.platform === undefined) {
120
+ delete manifest.platform;
121
+ 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.");
122
+ }
84
123
  for (let name of PLATFORM_UPSTREAM) {
85
124
  let file = resolve(process.cwd(), "node_modules", name, "package.json");
86
125
  try {
package/lib/index.js CHANGED
@@ -22,19 +22,27 @@ let lastHostname;
22
22
  let watcher;
23
23
  let resourcesWatcher = null;
24
24
  let resourcesDebounce = null;
25
- try {
26
- let appPkgUrl = `file://${process.cwd()}/package.json`;
27
- watcher = watch(appPkgUrl).on("all", async () => {
28
- if (server) {
29
- let { hostname } = await getLoginInfo();
30
- if (lastHostname !== hostname) {
31
- 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
+ }
32
40
  }
33
- }
34
- });
35
- }
36
- catch (error) {
37
- 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
+ }
38
46
  }
39
47
  const jsonParser = bodyParser.json();
40
48
  export default function appframe(options = {}) {
@@ -121,10 +129,10 @@ export default function appframe(options = {}) {
121
129
  },
122
130
  };
123
131
  if (command === "build") {
124
- watcher.close();
125
132
  await addAppframeBuildConfig(config);
126
133
  }
127
134
  else {
135
+ watchAppPackage();
128
136
  let proxy = config.server?.proxy ?? {};
129
137
  let routes = getProxyRoutes();
130
138
  let { hostname, username, password } = await getLoginInfo();
@@ -302,10 +302,10 @@ export function getProcedureDefinition(name, procDefinition, options) {
302
302
  output.push(`export type ${paramTypeName} = null | undefined | Record<string, unknown>;\n`);
303
303
  }
304
304
  }
305
- output.push(`export const ${procName} = new ${options.global ? "af." : ""}ProcedureAPI${options.types ? `<${paramTypeName}, ${options.typesJsonReturnType ?? "{ Table?: unknown[] }"}>` : ""}({
306
- procedureId: "${name}",
307
- parameters: ${JSON.stringify(parameters, null, 2)},
308
- timeout: ${options.timeout ?? 30000}${options.transaction === false ? ",\n transaction: false" : ""}
305
+ output.push(`export const ${procName} = new ${options.global ? "af." : ""}ProcedureAPI${options.types ? `<${paramTypeName}, ${options.typesJsonReturnType ?? "{ Table?: unknown[] }"}>` : ""}({
306
+ procedureId: "${name}",
307
+ parameters: ${JSON.stringify(parameters, null, 2)},
308
+ timeout: ${options.timeout ?? 30000}${options.transaction === false ? ",\n transaction: false" : ""}
309
309
  });`);
310
310
  if (options.expose) {
311
311
  output.push("");
@@ -389,7 +389,7 @@ export function getDataObjectDefinition(name, viewDefinition, options) {
389
389
  }
390
390
  fieldTypes += ` ${field.name}: ${type};\n`;
391
391
  }
392
- types = `export type ${typeName} = {
392
+ types = `export type ${typeName} = {
393
393
  ${fieldTypes}}`;
394
394
  }
395
395
  if (options.global) {
@@ -473,9 +473,9 @@ ${fieldTypes}}`;
473
473
  parametersOption.push(`distinctRows: true`);
474
474
  }
475
475
  dsOptions.push(`parameters: {\n\t\t${parametersOption.join(",\n\t\t")}\n\t}`);
476
- output += `export const ${options.id} = ${api}({
477
- ${dsOptions.join(",\n\t")}
478
- });
476
+ output += `export const ${options.id} = ${api}({
477
+ ${dsOptions.join(",\n\t")}
478
+ });
479
479
  `;
480
480
  if (options.expose) {
481
481
  let id = typeof options.expose === "string" ? options.expose : options.id;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olenbetong/appframe-vite",
3
- "version": "6.9.0",
3
+ "version": "6.11.0",
4
4
  "description": "Tools to use and deploy Vite applications to Appframe",
5
5
  "main": "./lib/index.js",
6
6
  "type": "module",
@@ -37,14 +37,14 @@
37
37
  "fuzzy": "^0.1.3",
38
38
  "inquirer": "^14.2.2",
39
39
  "inquirer-autocomplete-standalone": "^0.8.1",
40
- "jsdom": "30.0.1",
40
+ "jsdom": "30.1.0",
41
41
  "rollup-plugin-visualizer": "^7.1.1",
42
42
  "yaml": "^2.9.1",
43
43
  "@olenbetong/appframe-data": "1.7.2"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/jsdom": "^30.0.0",
47
- "@types/node": "26.5.1",
47
+ "@types/node": "26.6.1",
48
48
  "typescript": "7.0.2",
49
49
  "vite": "8.3.0"
50
50
  },