@tabbr/module-cli 0.1.0 → 0.3.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/LICENSE CHANGED
@@ -1,34 +1,34 @@
1
- Tabbr Module CLI License
2
-
3
- Copyright (c) 2026 SystemWorks BV. All rights reserved.
4
-
5
- 1. Grant. Subject to the terms below, SystemWorks BV ("Licensor") grants you a
6
- free, non-exclusive, non-transferable, revocable license to use, copy and
7
- modify the Tabbr Module CLI (the "Software") solely to develop, test and
8
- run modules that are deployed on and used with the Tabbr platform operated
9
- by Licensor.
10
-
11
- 2. Restrictions. You may not:
12
- a. use the Software, or any part or derivative of it, for any purpose other
13
- than the one in section 1, including with any platform, product or
14
- service other than Tabbr;
15
- b. distribute, sublicense, sell or publish the Software or derivatives of it,
16
- except as part of a module distributed solely for use on the Tabbr
17
- platform;
18
- c. remove or alter this license or any copyright notice.
19
-
20
- 3. Ownership. The Software is licensed, not sold. Licensor retains all rights,
21
- title and interest in the Software. Modules you write remain yours, but this
22
- license does not grant any right to the Tabbr platform itself, which is
23
- governed by its own terms of use.
24
-
25
- 4. Termination. This license ends automatically if you breach it, or when your
26
- access to the Tabbr platform ends. On termination you must stop using the
27
- Software.
28
-
29
- 5. No warranty. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
30
- TO THE EXTENT PERMITTED BY LAW, LICENSOR IS NOT LIABLE FOR ANY DAMAGES
31
- ARISING FROM THE USE OF THE SOFTWARE.
32
-
33
- 6. Governing law. This license is governed by Belgian law. The courts of Ghent
34
- (Gent), Belgium have exclusive jurisdiction.
1
+ Tabbr Module CLI License
2
+
3
+ Copyright (c) 2026 SystemWorks BV. All rights reserved.
4
+
5
+ 1. Grant. Subject to the terms below, SystemWorks BV ("Licensor") grants you a
6
+ free, non-exclusive, non-transferable, revocable license to use, copy and
7
+ modify the Tabbr Module CLI (the "Software") solely to develop, test and
8
+ run modules that are deployed on and used with the Tabbr platform operated
9
+ by Licensor.
10
+
11
+ 2. Restrictions. You may not:
12
+ a. use the Software, or any part or derivative of it, for any purpose other
13
+ than the one in section 1, including with any platform, product or
14
+ service other than Tabbr;
15
+ b. distribute, sublicense, sell or publish the Software or derivatives of it,
16
+ except as part of a module distributed solely for use on the Tabbr
17
+ platform;
18
+ c. remove or alter this license or any copyright notice.
19
+
20
+ 3. Ownership. The Software is licensed, not sold. Licensor retains all rights,
21
+ title and interest in the Software. Modules you write remain yours, but this
22
+ license does not grant any right to the Tabbr platform itself, which is
23
+ governed by its own terms of use.
24
+
25
+ 4. Termination. This license ends automatically if you breach it, or when your
26
+ access to the Tabbr platform ends. On termination you must stop using the
27
+ Software.
28
+
29
+ 5. No warranty. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
30
+ TO THE EXTENT PERMITTED BY LAW, LICENSOR IS NOT LIABLE FOR ANY DAMAGES
31
+ ARISING FROM THE USE OF THE SOFTWARE.
32
+
33
+ 6. Governing law. This license is governed by Belgian law. The courts of Ghent
34
+ (Gent), Belgium have exclusive jurisdiction.
package/README.md CHANGED
@@ -1,113 +1,266 @@
1
- # @tabbr/module-cli
2
-
3
- Builds a Native Federation club-module workspace and publishes it to a tabbr club. Also fetches
4
- a club's own generated TypeScript typings into your module workspace.
5
-
6
- The quickest start is the starter project,
7
- [systemworks-bv/tabbr-module-starter](https://github.com/systemworks-bv/tabbr-module-starter),
8
- which has this CLI and npm scripts for it set up already.
9
-
10
- ```bash
11
- npm install -D @tabbr/module-cli
12
- ```
13
-
14
- ## Configuration: tabbr.config.json
15
-
16
- Every option can be passed on the command line, but normally lives in `tabbr.config.json` in the
17
- workspace root (option names in camelCase; command-line options win). `init` writes it for you:
18
-
19
- ```bash
20
- npx tabbr-module init # asks for the club code, module id and title
21
- ```
22
-
23
- ```json
24
- {
25
- "club": "ABC",
26
- "apiHost": "https://api.tabbr.be",
27
- "moduleId": "club.abc.my-module",
28
- "title": "My module",
29
- "exposedModule": "./Module",
30
- "exportName": "Module"
31
- }
32
- ```
33
-
34
- ## Usage: publish
35
-
36
- ```
37
- npx tabbr-module publish [--club ABC] [--module-id club.abc.hello] \
38
- [--exposed-module ./Module] [--export-name Module] \
39
- [--icon waving_hand] [--title "Hello"] [--cwd .] [--api-host https://api.tabbr.be]
40
- ```
41
-
42
- You need the ADMIN role in the club you publish to.
43
-
44
- `--cwd` defaults to the current directory; run the command from your module workspace's root
45
- (where its `angular.json`/`federation.config.js` live), or pass `--cwd` to point at it.
46
-
47
- ### What it does
48
-
49
- 1. Runs `ng build` in the target workspace.
50
- 2. Locates the build output (via `angular.json`'s `build-original` target) and verifies
51
- `remoteEntry.json` exists at its root.
52
- 3. Zips the output.
53
- 4. Logs you in via a browser redirect to Keycloak (PKCE, no password ever touches this CLI) and
54
- caches the resulting token at `~/.tabbr/credentials.json` for reuse on later runs.
55
- 5. Uploads the zip to `POST /api/custommodule` with the club in the `X-club` header, which hosts
56
- the files and registers the module against that club. You need the ADMIN role in that club.
57
-
58
- ## Usage: fetch-types
59
-
60
- ```
61
- npx tabbr-module fetch-types [--club ABC] \
62
- [--out src/types/tabbr/entities.d.ts] [--cwd .] [--api-host https://api.tabbr.be]
63
- ```
64
-
65
- **Default (no `--entity`)**: fetches `GET /api/club/{club}/entities/types` - every entity this
66
- club has (custom fields included), as plain TypeScript data interfaces in ONE combined file
67
- (`src/types/tabbr/entities.d.ts` by default). No script-sandbox globals, so safe to have all of
68
- them in scope together - meant for typing your own component code (e.g. an `httpResource`'s
69
- response), not the script editor. This is the easy way to get everything at once.
70
-
71
- ```
72
- npx tabbr-module fetch-types [--club ABC] --entity flight \
73
- [--kind HOOK|CALCULATION|DEFAULT_CALCULATION|RELEVANT_CONDITION] \
74
- [--out src/types/tabbr/flight.hook.d.ts] [--cwd .] [--api-host https://api.tabbr.be]
75
- ```
76
-
77
- **With `--entity`**: fetches `GET /api/{entity}/meta/script?kind={kind}` instead - the same
78
- generated ambient `.d.ts` tabbr-ui's own script editor (Monaco) feeds itself for that entity's
79
- hook/calculation scripts. It types the script sandbox, not your REST responses, and declares
80
- script-only globals (`current`, `onCreate`, ...) that don't exist in a browser context - see the
81
- comment header the command writes atop the output file for the full explanation, and for why
82
- files fetched for different entities/kinds should generally not sit in the same TypeScript
83
- project as each other (or as the combined `entities.d.ts` above).
84
-
85
- Fetched files reflect one specific club's live, possibly-custom field configuration (field names,
86
- custom entities, ...) and are regenerable on demand - add `src/types/tabbr/` (or wherever `--out`
87
- points) to your workspace's `.gitignore` rather than committing them.
88
-
89
- ## Prerequisite: Keycloak redirect URI
90
-
91
- Login opens your browser to Keycloak and catches the redirect on a temporary local HTTP server
92
- (an ephemeral `http://localhost:<port>/callback`). This requires `http://localhost:*` (or your
93
- chosen fixed port) to be added to the `tabbr` Keycloak client's **Valid Redirect URIs** in the
94
- Keycloak admin console - a one-time setup step, not something this CLI can configure itself.
95
-
96
- ## Non-goals (v1)
97
-
98
- No `login`/`logout`/`whoami` subcommands, no multi-club profiles - `publish` covers the
99
- end-to-end flow on its own.
100
-
101
- ## Releasing
102
-
103
- Commit your CLI changes first, then from this package:
104
-
105
- ```bash
106
- npm run release -- patch --dry-run # preview
107
- npm run release -- patch # or minor / major / an explicit 0.3.0
108
- ```
109
-
110
- Same flow as the SDK (see `tabbr-module-sdk/README.md`): the script bumps the version, commits,
111
- tags `cli-v<version>` and pushes; the tag triggers `.github/workflows/publish-module-cli.yml`,
112
- which publishes via npm Trusted Publishing (owner `systemworks-bv`, repository `gliderdesk`,
113
- workflow `publish-module-cli.yml`).
1
+ # @tabbr/module-cli
2
+
3
+ Builds a Native Federation club-module workspace and publishes it to a tabbr club. Also fetches
4
+ a club's own generated TypeScript typings into your module workspace.
5
+
6
+ The quickest start is the starter project,
7
+ [systemworks-bv/tabbr-module-starter](https://github.com/systemworks-bv/tabbr-module-starter),
8
+ which has this CLI and npm scripts for it set up already.
9
+
10
+ ```bash
11
+ npm install -D @tabbr/module-cli
12
+ ```
13
+
14
+ ## Configuration: tabbr.config.json
15
+
16
+ Every option can be passed on the command line, but normally lives in `tabbr.config.json` in the
17
+ workspace root (option names in camelCase; command-line options win). `init` writes it for you:
18
+
19
+ ```bash
20
+ npx tabbr-module init # asks for the club code, module id and a fallback title
21
+ ```
22
+
23
+ ```json
24
+ {
25
+ "club": "ABC",
26
+ "apiHost": "https://api.tabbr.be",
27
+ "moduleId": "club.abc.my-module",
28
+ "title": "My module",
29
+ "exposedModule": "./Module",
30
+ "exportName": "Module"
31
+ }
32
+ ```
33
+
34
+ ### The title and description of your module
35
+
36
+ The name Tabbr shows for your module (in the settings list and its page header) is translated like the one of its
37
+ own modules: put `module.title` and `module.description` in your translation bundles (nested under your module id,
38
+ e.g. `'module.title': 'My module'`) and list the bundles on your module class so the shell can read them:
39
+
40
+ ```ts
41
+ export class Module extends SimpleModuleBase {
42
+ static readonly i18nBundles = I18N_BUNDLES; // the same map you give provideModuleI18n()
43
+ // ...
44
+ }
45
+ ```
46
+
47
+ `title` in `tabbr.config.json` is then only the **fallback**: what is shown while there is no translation (a module
48
+ without bundles, a language it lacks). `description` falls back to the module id.
49
+
50
+ ## Backend modules
51
+
52
+ A module can also have a **backend** part: TypeScript that runs server-side (not in the browser),
53
+ sandboxed, triggered on a cron schedule or on an entity's create/update - see
54
+ `BackendModuleRuntimeService` in `tabbr-api-server`. Add a `"backend"` block to
55
+ `tabbr.config.json`:
56
+
57
+ ```json
58
+ {
59
+ "backend": {
60
+ "entry": "backend/src/index.ts",
61
+ "allowedHosts": ["admin.gliding.app"],
62
+ "localAllowedHosts": ["localhost"],
63
+ "secretFieldNames": ["apiKey"],
64
+ "triggers": [
65
+ { "type": "CRON", "name": "pollFlights", "cronExpression": "*/5 * * * *", "handler": "pollFlights" },
66
+ { "type": "ENTITY_HOOK", "name": "onFlightSaved", "entity": "flight", "event": "onUpdate", "handler": "onFlightSaved" }
67
+ ]
68
+ }
69
+ }
70
+ ```
71
+
72
+ `entry` (default `backend/src/index.ts`) is a normal TypeScript file exporting plain functions:
73
+
74
+ ```ts
75
+ export function pollFlights() {
76
+ const result = http.fetchJson('https://admin.gliding.app/club/abc/api/flights.json', {
77
+ headers: { 'X-API-KEY': tabbr.settings.get('apiKey') },
78
+ });
79
+ // ...
80
+ tabbr.settings.set('lastPullAt', new Date().toISOString());
81
+ }
82
+ ```
83
+
84
+ - `http.fetchJson(url, options?)` - network access, restricted to `allowedHosts`. `localAllowedHosts` is added to that list
85
+ only by a `publish --local` (e.g. `localhost` for a mock of the external service you develop against) - never by a normal publish.
86
+ - `tabbr.settings.get(key)` / `.set(key, value)` - this module's own per-club settings, including
87
+ its own secrets (never sent back to any settings GET response once saved - `secretFieldNames`).
88
+ - `tabbr.access` - the club's roles (`roles()`), and - with `"manageRoles": true` in the `backend` block - the roles
89
+ this module gives persons: `setManagedRoles(personId, roleCodes)` makes them exactly these (e.g. groups giving its
90
+ members a role), `managedRoles(personId)`, `managedAssignments()`. A module only ever touches the roles it gives
91
+ itself (roles assigned by hand stay), can't give a system role such as ADMIN, and the roles it gave go when it is
92
+ uninstalled or blocked. The club sees "gives roles" when it installs the module.
93
+ - `triggers[].type: "CRON"` runs on the given cron expression (server-local time), with no
94
+ triggering entity. `"ENTITY_HOOK"` runs on the named `entity`'s `event` (`onCreate`/`onUpdate`/
95
+ `onDelete`), with that entity bound under its own name (e.g. `flight`) in the handler's scope - before the save.
96
+ `onCreated` / `onUpdated` run after it, once committed, in the background: for a handler that reads the database
97
+ (e.g. "is this person in that list now"). A module's own writes don't trigger its handlers.
98
+ - Needs `esbuild` installed in the workspace (`npm install -D esbuild`) - only used at publish
99
+ time, bundled into one dependency-free `bundle.js` the server runs in a GraalVM sandbox, so
100
+ regular npm packages can be imported in your TS but nothing Node-specific (no `fs`, no native
101
+ addons) works at runtime.
102
+
103
+ A "resync now" button (or similar) can fire a `CRON` trigger manually:
104
+ `POST /api/custommodule/{moduleId}/trigger/{triggerName}` (ADMIN).
105
+
106
+ ## Entities
107
+
108
+ A module can declare its own **entities** - records Tabbr stores and serves like its own (lists, filters, access
109
+ rules, the club's extra fields, hooks, audit). Add an `"entities"` array to `tabbr.config.json`:
110
+
111
+ ```json
112
+ {
113
+ "entities": [
114
+ {
115
+ "name": "group",
116
+ "label": "Groep",
117
+ "display": ["name"],
118
+ "fields": [
119
+ { "key": "name", "label": "Naam", "type": "tabbr.text", "includeInSearch": true },
120
+ { "key": "parent", "label": "Bovenliggende groep", "type": "tabbr.entity", "target": "group" }
121
+ ]
122
+ }
123
+ ]
124
+ }
125
+ ```
126
+
127
+ `publish` checks the declarations (names, keys, types) before uploading and sends them in the backend manifest - also
128
+ for a module that has no backend code (then only the entities are published). The frontend addresses records by field
129
+ key with the SDK's `injectModuleEntity()`, the backend with `tabbr.entities.<name>`. See `docs/module-entities.md` in the
130
+ Tabbr repository for the field types, access rules and what the club gets; `tabbr-groups-module/` is a complete example.
131
+
132
+ ## Usage: publish
133
+
134
+ ```
135
+ npx tabbr-module publish [--club ABC] [--module-id club.abc.hello] \
136
+ [--exposed-module ./Module] [--export-name Module] \
137
+ [--icon waving_hand] [--title "Hello"] # --title: fallback text, see above [--cwd .] [--api-host https://api.tabbr.be]
138
+ ```
139
+
140
+ You need the ADMIN role in the club you publish to.
141
+
142
+ `--cwd` defaults to the current directory; run the command from your module workspace's root
143
+ (where its `angular.json`/`federation.config.js` live), or pass `--cwd` to point at it.
144
+
145
+ ### What it does
146
+
147
+ Builds and publishes whichever parts are present - frontend, backend, or both - in one zip, one
148
+ upload:
149
+
150
+ 1. If `angular.json` exists: runs `ng build`, locates the output (via `angular.json`'s
151
+ `build-original` target) and verifies `remoteEntry.json` exists at its root.
152
+ 2. If `"backend"` is set in `tabbr.config.json`: bundles the backend entry with `esbuild` into a
153
+ single `bundle.js`, and writes `manifest.json` alongside it from the `backend` config.
154
+ 3. Zips it all together - frontend files flat at the zip root (as before), backend files under a
155
+ `backend/` prefix.
156
+ 4. Logs you in via a browser redirect to Keycloak (PKCE, no password ever touches this CLI) and
157
+ caches the resulting token at `~/.tabbr/credentials.json` for reuse on later runs.
158
+ 5. Uploads the zip to `POST /api/custommodule` with the club in the `X-club` header, which hosts
159
+ the frontend files publicly, stores the backend files privately, and registers the module
160
+ against that club. You need the ADMIN role in that club.
161
+
162
+ ### Publishing to a local environment
163
+
164
+ `npx tabbr-module publish --local` publishes into your own local tabbr (the environment behind
165
+ `<club>.local.tabbr.be`) instead of production: it uploads to `https://api.local.tabbr.be` (set
166
+ `"localApiHost"` in `tabbr.config.json` to use another URL) and trusts your mkcert root CA, which Node
167
+ doesn't do on its own (`NODE_EXTRA_CA_CERTS` is set automatically; if mkcert's root CA can't be found,
168
+ set it yourself - `mkcert -CAROOT` shows where it lives). `--club` picks the club in that environment;
169
+ login still goes through the real Keycloak, which local backends validate too.
170
+
171
+ ### Publishing to the module store
172
+
173
+ `npx tabbr-module publish --store [--changelog "..."]` submits a new **version of a store
174
+ package** instead of publishing it into one club (see `docs/module-store.md` in the Tabbr repo):
175
+ the workspace's **source** - the files git tracks (plus new ones git doesn't ignore), never
176
+ `node_modules` or build output - with its `package-lock.json`. The store builds it with the
177
+ platform's toolchain (`tabbr-module-builder`: its own `@tabbr/module-sdk` and `-cli`, your locked
178
+ dependencies, your `test` and `typecheck` scripts, no network during the build); then it goes into
179
+ review; once approved, every club can install it under *Instellingen › Modules › Store*. Unlink
180
+ yalc first (`npm run unlink-local`): the store installs from the lockfile. `--prebuilt` uploads a
181
+ local build instead (trusted publishers only). For now only platform admins (`SUPER_ADMIN`) can
182
+ submit.
183
+
184
+ `npx tabbr-module build --out module.zip` builds the module into a zip without publishing it -
185
+ what the store's builder runs.
186
+
187
+ It needs, in `tabbr.config.json`:
188
+
189
+ ```jsonc
190
+ {
191
+ "moduleId": "groups",
192
+ "package": "tabbr/groups", // <publisher>/<name>
193
+ "version": "1.2.0", // higher than every earlier version of the package
194
+ "engines": { "tabbr": "^1.0" }, // the module API levels it runs on (MODULE_API_LEVEL in the SDK)
195
+ "dependencies": { }, // optional: { "acme/payments": "^1", "sys.messaging": "*" }
196
+ "optionalDependencies": { "sys.messaging": "*" },
197
+ "permissions": { // shown to the club before it installs or updates
198
+ "entities": { "read": ["person"], "write": [] },
199
+ "personalData": ["contact"],
200
+ "messaging": true
201
+ },
202
+ "summary": "One line for the store",
203
+ "visibility": "PUBLIC" // or UNLISTED
204
+ }
205
+ ```
206
+
207
+ The server refuses a version that can never work: a version not higher than the last, `engines`
208
+ that don't include the server's module API level, a changed `moduleId`, or - within the same major
209
+ version - entity declarations that remove or change something (a new major may).
210
+
211
+ ## Usage: fetch-types
212
+
213
+ ```
214
+ npx tabbr-module fetch-types [--club ABC] \
215
+ [--out src/types/tabbr/entities.d.ts] [--cwd .] [--api-host https://api.tabbr.be]
216
+ ```
217
+
218
+ **Default (no `--entity`)**: fetches `GET /api/club/{club}/entities/types` - every entity this
219
+ club has (custom fields included), as plain TypeScript data interfaces in ONE combined file
220
+ (`src/types/tabbr/entities.d.ts` by default). No script-sandbox globals, so safe to have all of
221
+ them in scope together - meant for typing your own component code (e.g. an `httpResource`'s
222
+ response), not the script editor. This is the easy way to get everything at once.
223
+
224
+ ```
225
+ npx tabbr-module fetch-types [--club ABC] --entity flight \
226
+ [--kind HOOK|CALCULATION|DEFAULT_CALCULATION|RELEVANT_CONDITION] \
227
+ [--out src/types/tabbr/flight.hook.d.ts] [--cwd .] [--api-host https://api.tabbr.be]
228
+ ```
229
+
230
+ **With `--entity`**: fetches `GET /api/{entity}/meta/script?kind={kind}` instead - the same
231
+ generated ambient `.d.ts` tabbr-ui's own script editor (Monaco) feeds itself for that entity's
232
+ hook/calculation scripts. It types the script sandbox, not your REST responses, and declares
233
+ script-only globals (`current`, `onCreate`, ...) that don't exist in a browser context - see the
234
+ comment header the command writes atop the output file for the full explanation, and for why
235
+ files fetched for different entities/kinds should generally not sit in the same TypeScript
236
+ project as each other (or as the combined `entities.d.ts` above).
237
+
238
+ Fetched files reflect one specific club's live, possibly-custom field configuration (field names,
239
+ custom entities, ...) and are regenerable on demand - add `src/types/tabbr/` (or wherever `--out`
240
+ points) to your workspace's `.gitignore` rather than committing them.
241
+
242
+ ## Prerequisite: Keycloak redirect URI
243
+
244
+ Login opens your browser to Keycloak and catches the redirect on a temporary local HTTP server
245
+ (an ephemeral `http://localhost:<port>/callback`). This requires `http://localhost:*` (or your
246
+ chosen fixed port) to be added to the `tabbr` Keycloak client's **Valid Redirect URIs** in the
247
+ Keycloak admin console - a one-time setup step, not something this CLI can configure itself.
248
+
249
+ ## Non-goals (v1)
250
+
251
+ No `login`/`logout`/`whoami` subcommands, no multi-club profiles - `publish` covers the
252
+ end-to-end flow on its own.
253
+
254
+ ## Releasing
255
+
256
+ Commit your CLI changes first, then from this package:
257
+
258
+ ```bash
259
+ npm run release -- patch --dry-run # preview
260
+ npm run release -- patch # or minor / major / an explicit 0.3.0
261
+ ```
262
+
263
+ Same flow as the SDK (see `tabbr-module-sdk/README.md`): the script bumps the version, commits,
264
+ tags `cli-v<version>` and pushes; the tag triggers `.github/workflows/publish-module-cli.yml`,
265
+ which publishes via npm Trusted Publishing (owner `systemworks-bv`, repository `gliderdesk`,
266
+ workflow `publish-module-cli.yml`).
@@ -5,9 +5,11 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const { getAccessToken } = require('../src/auth');
7
7
  const { buildAndZip } = require('../src/build');
8
- const { publish } = require('../src/publish');
8
+ const { publish, publishToStore, submitSourceToStore, storeMeta } = require('../src/publish');
9
+ const { packSource } = require('../src/source');
9
10
  const { fetchScriptTypes, fetchAllEntityTypes, VALID_KINDS } = require('../src/fetch-types');
10
11
  const { CONFIG_FILE, DEFAULT_API_HOST, readConfig, requireClub, runInit } = require('../src/config');
12
+ const { DEFAULT_LOCAL_API_HOST, trustLocalCertificateOrRespawn } = require('../src/local');
11
13
  const DEFAULT_KIND = 'HOOK'; // matches the server's own @RequestParam default for /meta/script
12
14
 
13
15
  function parseArgs(argv) {
@@ -24,9 +26,13 @@ function parseArgs(argv) {
24
26
  case '--title': args.title = next(); break;
25
27
  case '--cwd': args.cwd = path.resolve(next()); break;
26
28
  case '--api-host': args.apiHost = next(); break;
29
+ case '--local': args.local = true; break;
27
30
  case '--entity': args.entity = next(); break;
28
31
  case '--kind': args.kind = next(); break;
29
32
  case '--out': args.out = next(); break;
33
+ case '--store': args.store = true; break;
34
+ case '--prebuilt': args.prebuilt = true; break;
35
+ case '--changelog': args.changelog = next(); break;
30
36
  default:
31
37
  throw new Error(`Unknown option: ${arg}`);
32
38
  }
@@ -44,21 +50,58 @@ Asks for the club code (and module id / title) and writes ${CONFIG_FILE}.
44
50
 
45
51
  Usage: tabbr-module publish [options]
46
52
 
53
+ Builds and publishes whichever parts of the workspace are present, in ONE upload: a frontend (an
54
+ Angular project with a Native Federation "exposes" entry) and/or a backend (a "backend" block in
55
+ ${CONFIG_FILE} pointing at TypeScript run server-side - see README.md). At least one must be
56
+ present.
57
+
47
58
  Required (command line or ${CONFIG_FILE}):
48
59
  --club <CODE> Club code to publish to, e.g. ABC; you need the ADMIN role there
49
60
  --module-id <id> Module id, unique within the club, e.g. club.abc.hello
61
+
62
+ Required for a module with a frontend part:
50
63
  --exposed-module <./Foo> Key in your federation.config.js "exposes" map
51
64
  --export-name <Foo> Named (or default) export implementing PlatformModuleImplementation
52
65
 
66
+ Backend part (all in ${CONFIG_FILE} under "backend", not as CLI flags):
67
+ entry TS entry point, default "backend/src/index.ts"
68
+ allowedHosts External hosts your module's http.fetchJson() may reach
69
+ secretFieldNames Names of your module's settings keys that hold secrets (masked from
70
+ every settings GET response once saved)
71
+ triggers [{type: "CRON", name, cronExpression, handler} |
72
+ {type: "ENTITY_HOOK", name, entity, event, handler}]
73
+
53
74
  Optional:
54
75
  --icon <name> Menu icon name
55
- --title <text> Display title
56
- --cwd <path> Angular workspace to build (default: current directory)
76
+ --title <text> Fallback title (module.title in your i18n bundle is what is shown)
77
+ --cwd <path> Workspace to build (default: current directory)
57
78
  --api-host <url> Tabbr API host (default: ${DEFAULT_API_HOST})
79
+ --local Publish to your own local backend (https://api.local.tabbr.be, i.e. the
80
+ environment behind <club>.local.tabbr.be) instead of production;
81
+ "localApiHost" in ${CONFIG_FILE} overrides the URL. Trusts your mkcert
82
+ root CA automatically.
58
83
 
59
84
  Requires the tabbr Keycloak client's Valid Redirect URIs to include http://localhost:*
60
85
  (a Keycloak admin-console setting - see README.md).
61
86
 
87
+ Usage: tabbr-module publish --store [--changelog <text>] [--prebuilt] [options]
88
+
89
+ Submits a new version of a package to the module store (docs/module-store.md) instead of
90
+ publishing it into one club: the workspace's SOURCE (git-tracked files, with package-lock.json).
91
+ The store builds it with the platform's toolchain, then it waits for review; once approved,
92
+ clubs install it from Instellingen > Modules. --prebuilt uploads a local build instead (only
93
+ for trusted publishers). No --club needed. Needs, in ${CONFIG_FILE}:
94
+ package "<publisher>/<name>", e.g. "tabbr/groups"
95
+ version "1.4.0" - higher than every earlier version of the package
96
+ engines { "tabbr": "^1.0" } - the module API levels it runs on
97
+ and optionally dependencies, optionalDependencies, permissions, summary, visibility (PUBLIC or
98
+ UNLISTED) - see the groups module's ${CONFIG_FILE} for an example.
99
+
100
+ Usage: tabbr-module build --out <module.zip> [--cwd <path>]
101
+
102
+ Builds the module (frontend and/or backend part, as publish does) into a zip without
103
+ publishing it - what the store's builder runs.
104
+
62
105
  Usage: tabbr-module fetch-types [--club <CODE>] [--entity <name>] [options]
63
106
 
64
107
  With --entity: fetches the ambient .d.ts for that ONE entity's script sandbox - the same typings
@@ -92,14 +135,19 @@ function requireFlags(args, requiredFlags) {
92
135
  }
93
136
 
94
137
  async function runPublish(args) {
138
+ if (args.store) {
139
+ await runPublishToStore(args);
140
+ return;
141
+ }
95
142
  args.club = requireClub(args.club);
96
143
  requireFlags(args, {
97
144
  moduleId: '--module-id',
98
- exposedModule: '--exposed-module',
99
- exportName: '--export-name',
100
145
  });
146
+ // exposedModule/exportName are only required for a module with a frontend part (an Angular
147
+ // project at --cwd); buildAndZip/the server reject a module with neither a frontend nor a
148
+ // backend part, so an all-blank publish still fails, just with a clearer error.
101
149
 
102
- const zipBuffer = await buildAndZip(args.cwd);
150
+ const zipBuffer = await buildAndZip(args.cwd, args);
103
151
  const accessToken = await getAccessToken();
104
152
  const result = await publish({
105
153
  apiHost: args.apiHost,
@@ -117,6 +165,33 @@ async function runPublish(args) {
117
165
  console.log(JSON.stringify(result, null, 2));
118
166
  }
119
167
 
168
+ async function runPublishToStore(args) {
169
+ requireFlags(args, { moduleId: '--module-id' });
170
+ const meta = storeMeta(args);
171
+ let version;
172
+ if (args.prebuilt) {
173
+ const zipBuffer = await buildAndZip(args.cwd, args);
174
+ const accessToken = await getAccessToken();
175
+ version = await publishToStore({ apiHost: args.apiHost, packageName: args.package, meta, zipBuffer, accessToken });
176
+ } else {
177
+ const { zip, files } = await packSource(args.cwd);
178
+ console.log(`Source: ${files.length} files, ${Math.round(zip.length / 1024)} KB`);
179
+ const accessToken = await getAccessToken();
180
+ version = await submitSourceToStore({ apiHost: args.apiHost, packageName: args.package, meta, sourceZip: zip, accessToken });
181
+ }
182
+ console.log(`Submitted ${version.packageName} ${version.version} - ${version.state}.` +
183
+ (version.state === 'BUILDING' ? ' The store builds it; then it is reviewed, and installable once approved.' : ' It can be installed once it is approved.'));
184
+ }
185
+
186
+ async function runBuild(args) {
187
+ requireFlags(args, { out: '--out' });
188
+ const zipBuffer = await buildAndZip(args.cwd, args);
189
+ const out = path.resolve(args.cwd, args.out);
190
+ fs.mkdirSync(path.dirname(out), { recursive: true });
191
+ fs.writeFileSync(out, zipBuffer);
192
+ console.log(`Wrote ${out} (${Math.round(zipBuffer.length / 1024)} KB)`);
193
+ }
194
+
120
195
  async function fetchAndWriteOne({ apiHost, club, entity, kind, cwd, out, accessToken }) {
121
196
  const dts = await fetchScriptTypes({ apiHost, club, entity, kind, accessToken });
122
197
 
@@ -197,7 +272,7 @@ async function runFetchTypes(args) {
197
272
  async function main() {
198
273
  const [command, ...rest] = process.argv.slice(2);
199
274
 
200
- if (command !== 'init' && command !== 'publish' && command !== 'fetch-types') {
275
+ if (!['init', 'publish', 'build', 'fetch-types'].includes(command)) {
201
276
  printUsage();
202
277
  process.exit(command ? 1 : 0);
203
278
  }
@@ -208,8 +283,16 @@ async function main() {
208
283
  return;
209
284
  }
210
285
  const args = { apiHost: DEFAULT_API_HOST, ...readConfig(flags.cwd), ...flags };
286
+ if (args.local && !flags.apiHost) {
287
+ // --local targets your own local backend (https://api.local.tabbr.be, the same one
288
+ // <club>.local.tabbr.be talks to); "localApiHost" in tabbr.config.json overrides it.
289
+ args.apiHost = args.localApiHost || DEFAULT_LOCAL_API_HOST;
290
+ }
291
+ trustLocalCertificateOrRespawn(args.apiHost);
211
292
  if (command === 'publish') {
212
293
  await runPublish(args);
294
+ } else if (command === 'build') {
295
+ await runBuild(args);
213
296
  } else {
214
297
  await runFetchTypes(args);
215
298
  }
@@ -217,5 +300,12 @@ async function main() {
217
300
 
218
301
  main().catch(err => {
219
302
  console.error(err.message || err);
303
+ const code = String(err && err.cause && err.cause.code || '');
304
+ if (/CERT|SELF_SIGNED|UNABLE_TO_(GET|VERIFY)/.test(code)) {
305
+ console.error(
306
+ '\nThe server\'s TLS certificate is not trusted by Node. For a local environment (*.local.tabbr.be) run ' +
307
+ '"mkcert -install" and set NODE_EXTRA_CA_CERTS to the path of mkcert\'s rootCA.pem ("mkcert -CAROOT").'
308
+ );
309
+ }
220
310
  process.exit(1);
221
311
  });