@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 +34 -34
- package/README.md +266 -113
- package/bin/tabbr-module.js +97 -7
- package/package.json +34 -33
- package/src/auth.js +144 -144
- package/src/backend-build.js +123 -0
- package/src/build.js +123 -91
- package/src/config.js +75 -75
- package/src/fetch-types.js +49 -49
- package/src/local.js +62 -0
- package/src/publish.js +103 -29
- package/src/source.js +70 -0
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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`).
|
package/bin/tabbr-module.js
CHANGED
|
@@ -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>
|
|
56
|
-
--cwd <path>
|
|
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 (
|
|
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
|
});
|