@tabbr/module-cli 0.3.0 → 0.4.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 +26 -0
- package/bin/tabbr-module.js +31 -1
- package/package.json +1 -1
- package/src/backend-build.js +30 -5
- package/src/fetch-types.js +16 -1
package/README.md
CHANGED
|
@@ -129,6 +129,23 @@ for a module that has no backend code (then only the entities are published). Th
|
|
|
129
129
|
key with the SDK's `injectModuleEntity()`, the backend with `tabbr.entities.<name>`. See `docs/module-entities.md` in the
|
|
130
130
|
Tabbr repository for the field types, access rules and what the club gets; `tabbr-groups-module/` is a complete example.
|
|
131
131
|
|
|
132
|
+
## Contracts
|
|
133
|
+
|
|
134
|
+
What a module uses of Tabbr (or, later, of another module) is a **contract** - never its code. Declare the ones you use
|
|
135
|
+
with a version range, and the module API level that has contracts:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"engines": { "tabbr": "^1.1" },
|
|
140
|
+
"uses": { "sys.access": "^1" }
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`publish` puts `uses` in the manifest (also for a module without backend code); Tabbr only installs the module where
|
|
145
|
+
the club has those contracts, and its server-side part gets them as `tabbr.uses['sys.access']`. The frontend gets a
|
|
146
|
+
contract with the SDK's `MODULE_CONTRACTS` (`await inject(MODULE_CONTRACTS).get('sys.access')`). Their types come from
|
|
147
|
+
`fetch-types --contracts` (below). See `docs/module-contracts.md` in the Tabbr repository.
|
|
148
|
+
|
|
132
149
|
## Usage: publish
|
|
133
150
|
|
|
134
151
|
```
|
|
@@ -239,6 +256,15 @@ Fetched files reflect one specific club's live, possibly-custom field configurat
|
|
|
239
256
|
custom entities, ...) and are regenerable on demand - add `src/types/tabbr/` (or wherever `--out`
|
|
240
257
|
points) to your workspace's `.gitignore` rather than committing them.
|
|
241
258
|
|
|
259
|
+
```
|
|
260
|
+
npx tabbr-module fetch-types --contracts [--cwd .] [--api-host https://api.tabbr.be]
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**With `--contracts`**: fetches `GET /api/contracts/{name}/types` for every contract in `uses` into
|
|
264
|
+
`contracts/<name>.d.ts`. Unlike the entity types these don't depend on a club - **commit them**: the store builds your
|
|
265
|
+
module with them. Add `contracts/*.d.ts` to your tsconfig(s); it types `MODULE_CONTRACTS.get(name)` and
|
|
266
|
+
`tabbr.uses[name]`, and its own types can be imported from `@tabbr/contracts/<name>`.
|
|
267
|
+
|
|
242
268
|
## Prerequisite: Keycloak redirect URI
|
|
243
269
|
|
|
244
270
|
Login opens your browser to Keycloak and catches the redirect on a temporary local HTTP server
|
package/bin/tabbr-module.js
CHANGED
|
@@ -7,7 +7,8 @@ const { getAccessToken } = require('../src/auth');
|
|
|
7
7
|
const { buildAndZip } = require('../src/build');
|
|
8
8
|
const { publish, publishToStore, submitSourceToStore, storeMeta } = require('../src/publish');
|
|
9
9
|
const { packSource } = require('../src/source');
|
|
10
|
-
const { fetchScriptTypes, fetchAllEntityTypes, VALID_KINDS } = require('../src/fetch-types');
|
|
10
|
+
const { fetchScriptTypes, fetchAllEntityTypes, fetchContractTypes, VALID_KINDS } = require('../src/fetch-types');
|
|
11
|
+
const { usesOf } = require('../src/backend-build');
|
|
11
12
|
const { CONFIG_FILE, DEFAULT_API_HOST, readConfig, requireClub, runInit } = require('../src/config');
|
|
12
13
|
const { DEFAULT_LOCAL_API_HOST, trustLocalCertificateOrRespawn } = require('../src/local');
|
|
13
14
|
const DEFAULT_KIND = 'HOOK'; // matches the server's own @RequestParam default for /meta/script
|
|
@@ -30,6 +31,7 @@ function parseArgs(argv) {
|
|
|
30
31
|
case '--entity': args.entity = next(); break;
|
|
31
32
|
case '--kind': args.kind = next(); break;
|
|
32
33
|
case '--out': args.out = next(); break;
|
|
34
|
+
case '--contracts': args.contracts = true; break;
|
|
33
35
|
case '--store': args.store = true; break;
|
|
34
36
|
case '--prebuilt': args.prebuilt = true; break;
|
|
35
37
|
case '--changelog': args.changelog = next(); break;
|
|
@@ -102,6 +104,13 @@ Usage: tabbr-module build --out <module.zip> [--cwd <path>]
|
|
|
102
104
|
Builds the module (frontend and/or backend part, as publish does) into a zip without
|
|
103
105
|
publishing it - what the store's builder runs.
|
|
104
106
|
|
|
107
|
+
Usage: tabbr-module fetch-types --contracts [--cwd <path>] [--api-host <url>]
|
|
108
|
+
|
|
109
|
+
Fetches the contracts the module uses ("uses" in ${CONFIG_FILE}, e.g. { "sys.access": "^1" }) into
|
|
110
|
+
contracts/<name>.d.ts - commit them: the store builds your module with them. Include contracts/ in your
|
|
111
|
+
tsconfig(s); then MODULE_CONTRACTS.get('<name>') (frontend) and tabbr.uses['<name>'] (backend) are typed,
|
|
112
|
+
and the contract's own types can be imported from '@tabbr/contracts/<name>'. No --club needed.
|
|
113
|
+
|
|
105
114
|
Usage: tabbr-module fetch-types [--club <CODE>] [--entity <name>] [options]
|
|
106
115
|
|
|
107
116
|
With --entity: fetches the ambient .d.ts for that ONE entity's script sandbox - the same typings
|
|
@@ -251,7 +260,28 @@ ${dts}
|
|
|
251
260
|
console.log(`Wrote ${outPath}`);
|
|
252
261
|
}
|
|
253
262
|
|
|
263
|
+
/** One file per contract the module uses: contracts/<name>.d.ts. */
|
|
264
|
+
async function runFetchContractTypes(args) {
|
|
265
|
+
const names = Object.keys(usesOf(args));
|
|
266
|
+
if (names.length === 0) {
|
|
267
|
+
throw new Error(`No contracts to fetch: add "uses" to ${CONFIG_FILE}, e.g. "uses": { "sys.access": "^1" }.`);
|
|
268
|
+
}
|
|
269
|
+
const accessToken = await getAccessToken();
|
|
270
|
+
const dir = path.join(args.cwd, 'contracts');
|
|
271
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
272
|
+
for (const name of names) {
|
|
273
|
+
const dts = await fetchContractTypes({ apiHost: args.apiHost, name, accessToken });
|
|
274
|
+
const outPath = path.join(dir, `${name}.d.ts`);
|
|
275
|
+
fs.writeFileSync(outPath, dts);
|
|
276
|
+
console.log(`Wrote ${outPath} (${args.uses[name]})`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
254
280
|
async function runFetchTypes(args) {
|
|
281
|
+
if (args.contracts) {
|
|
282
|
+
await runFetchContractTypes(args);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
255
285
|
args.club = requireClub(args.club);
|
|
256
286
|
if (args.kind !== DEFAULT_KIND && !args.entity) {
|
|
257
287
|
console.error('--kind only applies to a single --entity fetch (the combined file has no script sandbox, so no kind).\n');
|
package/package.json
CHANGED
package/src/backend-build.js
CHANGED
|
@@ -46,6 +46,27 @@ function entitiesOf(config) {
|
|
|
46
46
|
return entities;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* The contracts the module uses (`"uses"` in tabbr.config.json, e.g. `{ "sys.access": "^1" }`): their names and version
|
|
51
|
+
* ranges, checked just enough to fail here - Tabbr checks them again (a contract it doesn't have, a range it can't
|
|
52
|
+
* satisfy) when the module is published or installed.
|
|
53
|
+
*/
|
|
54
|
+
function usesOf(config) {
|
|
55
|
+
const uses = config.uses || {};
|
|
56
|
+
if (typeof uses !== 'object' || Array.isArray(uses)) {
|
|
57
|
+
throw new Error('"uses" in tabbr.config.json must be an object: { "<contract>": "<version range>" }, e.g. { "sys.access": "^1" }.');
|
|
58
|
+
}
|
|
59
|
+
for (const [name, range] of Object.entries(uses)) {
|
|
60
|
+
if (!/^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/.test(name)) {
|
|
61
|
+
throw new Error(`"uses": "${name}" is not a contract name (e.g. "sys.access").`);
|
|
62
|
+
}
|
|
63
|
+
if (typeof range !== 'string' || !range.trim()) {
|
|
64
|
+
throw new Error(`"uses": "${name}" needs a version range, e.g. "^1".`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return uses;
|
|
68
|
+
}
|
|
69
|
+
|
|
49
70
|
/**
|
|
50
71
|
* Bundles a module workspace's backend part (if configured) into a single, dependency-free
|
|
51
72
|
* `bundle.js`, using esbuild so module authors can write normal TypeScript (types, imports,
|
|
@@ -54,16 +75,18 @@ function entitiesOf(config) {
|
|
|
54
75
|
* `BackendModuleRuntimeService`).
|
|
55
76
|
*
|
|
56
77
|
* Also writes the manifest the server reads (`manifest.json`): the backend config plus the entities the module declares
|
|
57
|
-
* - which a module can do without any backend code, so then only the manifest is produced
|
|
78
|
+
* and the contracts it uses - which a module can do without any backend code, so then only the manifest is produced
|
|
79
|
+
* (`bundleJs` null).
|
|
58
80
|
*
|
|
59
|
-
* Returns `null` when the workspace has
|
|
60
|
-
* it entirely for a frontend-only module.
|
|
81
|
+
* Returns `null` when the workspace has no backend part, no entities and uses no contracts, so `build.js` can skip
|
|
82
|
+
* it entirely for such a frontend-only module.
|
|
61
83
|
*/
|
|
62
84
|
async function buildBackend(cwd, config) {
|
|
63
85
|
const backendConfig = config.backend;
|
|
64
86
|
const entities = entitiesOf(config);
|
|
87
|
+
const uses = usesOf(config);
|
|
65
88
|
const bundleJs = backendConfig ? await bundle(cwd, backendConfig) : null;
|
|
66
|
-
if (bundleJs == null && entities.length === 0) {
|
|
89
|
+
if (bundleJs == null && entities.length === 0 && Object.keys(uses).length === 0) {
|
|
67
90
|
return null;
|
|
68
91
|
}
|
|
69
92
|
const settings = backendConfig || {};
|
|
@@ -75,6 +98,8 @@ async function buildBackend(cwd, config) {
|
|
|
75
98
|
entities,
|
|
76
99
|
// the module gives persons roles (tabbr.access.setManagedRoles) - shown to the club when it installs the module
|
|
77
100
|
manageRoles: bundleJs != null && settings.manageRoles === true,
|
|
101
|
+
// the contracts it uses - for its frontend part as well, so the manifest is written for a frontend-only module too
|
|
102
|
+
uses,
|
|
78
103
|
};
|
|
79
104
|
|
|
80
105
|
return { bundleJs, manifestJson: JSON.stringify(manifest, null, 2) };
|
|
@@ -120,4 +145,4 @@ async function bundle(cwd, backendConfig) {
|
|
|
120
145
|
return result.outputFiles[0].text;
|
|
121
146
|
}
|
|
122
147
|
|
|
123
|
-
module.exports = { buildBackend, entitiesOf, DEFAULT_ENTRY };
|
|
148
|
+
module.exports = { buildBackend, entitiesOf, usesOf, DEFAULT_ENTRY };
|
package/src/fetch-types.js
CHANGED
|
@@ -46,4 +46,19 @@ async function fetchAllEntityTypes({ apiHost, club, accessToken }) {
|
|
|
46
46
|
return response.text();
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
/**
|
|
50
|
+
* A contract a module uses (`"uses"` in tabbr.config.json), as Tabbr serves it: its types as the module
|
|
51
|
+
* `@tabbr/contracts/<name>`, plus its entries in `TabbrUiContracts` (what `MODULE_CONTRACTS.get(name)` returns) and
|
|
52
|
+
* `TabbrContracts` (`tabbr.uses[name]` server-side). See docs/module-contracts.md (tabbr repo).
|
|
53
|
+
*/
|
|
54
|
+
async function fetchContractTypes({ apiHost, name, accessToken }) {
|
|
55
|
+
const url = `${apiHost}/api/contracts/${encodeURIComponent(name)}/types`;
|
|
56
|
+
console.log(`Fetching the contract ${name} from ${url} ...`);
|
|
57
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${accessToken}` } });
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
throw new Error(`Fetching the contract ${name} failed: ${response.status} ${await response.text()}`);
|
|
60
|
+
}
|
|
61
|
+
return response.text();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { fetchScriptTypes, fetchAllEntityTypes, fetchContractTypes, VALID_KINDS };
|