@tabbr/module-cli 0.4.1 → 0.6.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
@@ -31,6 +31,30 @@ npx tabbr-module init # asks for the club code, module id and a fallback t
31
31
  }
32
32
  ```
33
33
 
34
+ ### Where you develop against: tabbr.config.local.json
35
+
36
+ `tabbr.config.json` describes the module and is committed. The club and API host *you* work against are yours, not
37
+ the module's: put them in `tabbr.config.local.json` - add it to `.gitignore` - and leave `tabbr.config.json` as it is
38
+ (e.g. on production, for the store). `init --local` writes it:
39
+
40
+ ```bash
41
+ npx tabbr-module init --local # asks for the club code and the API host (default: your local backend,
42
+ # https://api.local.tabbr.be; https://api.tabbr.be for a club on production)
43
+ ```
44
+
45
+ ```json
46
+ { "club": "ABC", "apiHost": "https://api.local.tabbr.be" }
47
+ ```
48
+
49
+ It may set `club`, `apiHost`, `localApiHost` and `shellUrl` (other keys are ignored, with a warning) and overrides
50
+ `tabbr.config.json` for them in every command - except `publish --store`, which goes where `tabbr.config.json` says
51
+ unless you pass `--local` or `--api-host`. Command-line options still win, and the CLI says which file it used.
52
+ The one next to `tabbr.config.json` counts, else the one a folder up - one file for every module in that folder, as
53
+ the Tabbr repository's `modules/` has. The module's dev preview reads the same one: its `src/app/app.config.ts` imports
54
+ `tabbr.config.local.json`, which the `paths` in `tsconfig.local-config.json` resolve to that file, else to
55
+ `tabbr.config.json` itself (copy both from one of the Tabbr repository's modules). It's never part of a store
56
+ submission's source.
57
+
34
58
  ### The title and description of your module
35
59
 
36
60
  The name Tabbr shows for your module (in the settings list and its page header) is translated like the one of its
@@ -63,7 +87,8 @@ sandboxed, triggered on a cron schedule or on an entity's create/update - see
63
87
  "secretFieldNames": ["apiKey"],
64
88
  "triggers": [
65
89
  { "type": "CRON", "name": "pollFlights", "cronExpression": "*/5 * * * *", "handler": "pollFlights" },
66
- { "type": "ENTITY_HOOK", "name": "onFlightSaved", "entity": "flight", "event": "onUpdate", "handler": "onFlightSaved" }
90
+ { "type": "ENTITY_HOOK", "name": "onFlightSaved", "entity": "flight", "event": "onUpdate", "handler": "onFlightSaved" },
91
+ { "type": "OGN_EVENT", "name": "onOgn", "handler": "onOgnEvent" }
67
92
  ]
68
93
  }
69
94
  }
@@ -95,6 +120,21 @@ export function pollFlights() {
95
120
  `onDelete`), with that entity bound under its own name (e.g. `flight`) in the handler's scope - before the save.
96
121
  `onCreated` / `onUpdated` run after it, once committed, in the background: for a handler that reads the database
97
122
  (e.g. "is this person in that list now"). A module's own writes don't trigger its handlers.
123
+ - `"OGN_EVENT"` (module API 1.10, `"engines": { "tabbr": "^1.10" }`) runs for every start and landing OGN reports that
124
+ the club gets: on one of its airfields, or by one of the airplanes it follows wherever they fly - the club sets both
125
+ in Instellingen › Vliegtuigen › OGN. The handler gets an `OgnEvent` (`@tabbr/module-sdk/backend`):
126
+ `{ type: 'TAKEOFF' | 'LANDING', fieldCode, address, time, atClubField, followedAirplane, airplaneId }`. One handler
127
+ runs at a time, in the order the events came; keep it quick (OGN can report many in a busy hour), and make getting
128
+ the same event twice harmless. The club sees "krijgt de starts en landingen van OGN" when
129
+ it installs the module.
130
+
131
+ ```ts
132
+ export function onOgnEvent(event: OgnEvent) {
133
+ if (event.type === 'LANDING' && event.airplaneId) {
134
+ // e.g. close the flight of that airplane that is still in the air
135
+ }
136
+ }
137
+ ```
98
138
  - Needs `esbuild` installed in the workspace (`npm install -D esbuild`) - only used at publish
99
139
  time, bundled into one dependency-free `bundle.js` the server runs in a GraalVM sandbox, so
100
140
  regular npm packages can be imported in your TS but nothing Node-specific (no `fs`, no native
@@ -127,7 +167,7 @@ rules, the club's extra fields, hooks, audit). Add an `"entities"` array to `tab
127
167
  `publish` checks the declarations (names, keys, types) before uploading and sends them in the backend manifest - also
128
168
  for a module that has no backend code (then only the entities are published). The frontend addresses records by field
129
169
  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.
170
+ Tabbr repository for the field types, access rules and what the club gets; `modules/tabbr-groups-module/` is a complete example.
131
171
 
132
172
  ## Contracts
133
173
 
@@ -195,7 +235,7 @@ upload:
195
235
 
196
236
  `npx tabbr-module publish --local` publishes into your own local tabbr (the environment behind
197
237
  `<club>.local.tabbr.be`) instead of production: it uploads to `https://api.local.tabbr.be` (set
198
- `"localApiHost"` in `tabbr.config.json` to use another URL) and trusts your mkcert root CA, which Node
238
+ `"localApiHost"` in `tabbr.config.local.json` to use another URL) and trusts your mkcert root CA, which Node
199
239
  doesn't do on its own (`NODE_EXTRA_CA_CERTS` is set automatically; if mkcert's root CA can't be found,
200
240
  set it yourself - `mkcert -CAROOT` shows where it lives). `--club` picks the club in that environment;
201
241
  login still goes through the real Keycloak, which local backends validate too.
@@ -9,7 +9,9 @@ const { publish, publishToStore, submitSourceToStore, storeMeta } = require('../
9
9
  const { packSource } = require('../src/source');
10
10
  const { fetchScriptTypes, fetchAllEntityTypes, fetchContractTypes, VALID_KINDS } = require('../src/fetch-types');
11
11
  const { usesOf } = require('../src/backend-build');
12
- const { CONFIG_FILE, DEFAULT_API_HOST, readConfig, requireClub, runInit } = require('../src/config');
12
+ const {
13
+ CONFIG_FILE, LOCAL_CONFIG_FILE, LOCAL_CONFIG_KEYS, DEFAULT_API_HOST, readConfig, readLocalConfig, requireClub, runInit,
14
+ } = require('../src/config');
13
15
  const { DEFAULT_LOCAL_API_HOST, trustLocalCertificateOrRespawn } = require('../src/local');
14
16
  const DEFAULT_KIND = 'HOOK'; // matches the server's own @RequestParam default for /meta/script
15
17
 
@@ -46,10 +48,22 @@ function printUsage() {
46
48
  console.log(`Every option below can also be set in ${CONFIG_FILE} in the workspace root (option name
47
49
  in camelCase, e.g. "club", "moduleId", "apiHost"); command-line options win.
48
50
 
51
+ Where YOU develop against goes in ${LOCAL_CONFIG_FILE} instead: not committed (add it to
52
+ .gitignore), next to ${CONFIG_FILE} or a folder up (then for every module in that folder; the
53
+ one in the workspace wins). Its ${LOCAL_CONFIG_KEYS.join(', ')} override those in ${CONFIG_FILE} -
54
+ here and in the module's dev preview; not the API host of "publish --store". E.g. { "club": "ABC",
55
+ "apiHost": "https://api.local.tabbr.be" } works against your local backend while ${CONFIG_FILE}
56
+ keeps production; "init --local" writes it.
57
+
49
58
  Usage: tabbr-module init [--club <CODE>] [--module-id <id>] [--title <text>] [--api-host <url>]
50
59
 
51
60
  Asks for the club code (and module id / title) and writes ${CONFIG_FILE}.
52
61
 
62
+ Usage: tabbr-module init --local [--club <CODE>] [--api-host <url>]
63
+
64
+ Asks for the club code and the API host (by default your own local backend, ${DEFAULT_LOCAL_API_HOST})
65
+ and writes only ${LOCAL_CONFIG_FILE}. ${CONFIG_FILE} is left as it is.
66
+
53
67
  Usage: tabbr-module publish [options]
54
68
 
55
69
  Builds and publishes whichever parts of the workspace are present, in ONE upload: a frontend (an
@@ -71,7 +85,12 @@ Backend part (all in ${CONFIG_FILE} under "backend", not as CLI flags):
71
85
  secretFieldNames Names of your module's settings keys that hold secrets (masked from
72
86
  every settings GET response once saved)
73
87
  triggers [{type: "CRON", name, cronExpression, handler} |
74
- {type: "ENTITY_HOOK", name, entity, event, handler}]
88
+ {type: "ENTITY_HOOK", name, entity, event, handler, serializeOn?} |
89
+ {type: "PUBLIC", name, handler, params} (module API 1.5:
90
+ GET /api/<club>/public/module/<moduleId>/data/<name>, no login) |
91
+ {type: "OGN_EVENT", name, handler} (module API 1.10: the club's OGN starts/landings)]
92
+ publicActions [{name, handler, rateLimit?: "10/h", verifiedEmail?: true}] - functions anyone may
93
+ call without logging in: POST /api/<club>/public/custommodule/<id>/action/<name>
75
94
 
76
95
  Optional:
77
96
  --icon <name> Menu icon name
@@ -316,13 +335,31 @@ async function main() {
316
335
  await runInit(flags.cwd, flags);
317
336
  return;
318
337
  }
319
- const args = { apiHost: DEFAULT_API_HOST, ...readConfig(flags.cwd), ...flags };
338
+ const local = readLocalConfig(flags.cwd);
339
+ const localValues = { ...(local ? local.values : {}) };
340
+ // A store submission goes where tabbr.config.json says (production), not to a local file's API host: --local or
341
+ // --api-host for another one.
342
+ const storeHostKept = command === 'publish' && flags.store && localValues.apiHost !== undefined;
343
+ if (storeHostKept) delete localValues.apiHost;
344
+ const args = { apiHost: DEFAULT_API_HOST, ...readConfig(flags.cwd), ...localValues, ...flags };
320
345
  if (args.local && !flags.apiHost) {
321
346
  // --local targets your own local backend (https://api.local.tabbr.be, the same one
322
- // <club>.local.tabbr.be talks to); "localApiHost" in tabbr.config.json overrides it.
347
+ // <club>.local.tabbr.be talks to); "localApiHost" in tabbr.config.local.json or tabbr.config.json overrides it.
323
348
  args.apiHost = args.localApiHost || DEFAULT_LOCAL_API_HOST;
324
349
  }
325
350
  trustLocalCertificateOrRespawn(args.apiHost);
351
+ // after a respawn for the local certificate: said once
352
+ if (local) {
353
+ const where = path.relative(flags.cwd, local.file);
354
+ const set = Object.entries(localValues).map(([key, value]) => `${key} ${value}`).join(', ');
355
+ console.log(`Using ${where}${set ? `: ${set}` : ''}`);
356
+ if (storeHostKept) {
357
+ console.log(` not its apiHost: the module store at ${args.apiHost}${args.local || flags.apiHost ? '' : ' (--local for your local backend\'s)'}`);
358
+ }
359
+ if (local.ignored.length) {
360
+ console.warn(` ignored there: ${local.ignored.join(', ')} - only ${LOCAL_CONFIG_KEYS.join(', ')} can be set in ${LOCAL_CONFIG_FILE}`);
361
+ }
362
+ }
326
363
  if (command === 'publish') {
327
364
  await runPublish(args);
328
365
  } else if (command === 'build') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tabbr/module-cli",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "Set up, build and publish a Native Federation club module to a tabbr club.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "SystemWorks BV",
@@ -46,6 +46,41 @@ function entitiesOf(config) {
46
46
  return entities;
47
47
  }
48
48
 
49
+ /**
50
+ * The audiences the module declares (`"audiences"` in tabbr.config.json, module API 1.8): `{ key, name, description,
51
+ * criteria }` - made among the club's audiences when the module is installed. Checked just enough to fail here: keys,
52
+ * names, and every `{audience:<key>}` in the entities' rules declared. Tabbr checks them again (criteria about the
53
+ * logged-in user, audiences in a circle).
54
+ */
55
+ function audiencesOf(config, entities) {
56
+ const audiences = config.audiences || [];
57
+ if (!Array.isArray(audiences)) {
58
+ throw new Error('"audiences" in tabbr.config.json must be an array: [{ "key": "uploaders", "name": "Fotografen", "criteria": [...] }].');
59
+ }
60
+ const keys = new Set();
61
+ for (const audience of audiences) {
62
+ if (!audience || !/^[a-z][a-zA-Z0-9]{0,39}$/.test(audience.key || '')) {
63
+ throw new Error(`Audience key "${audience && audience.key}" is not valid: camelCase letters and digits, starting lowercase.`);
64
+ }
65
+ if (keys.has(audience.key)) {
66
+ throw new Error(`Audience "${audience.key}" is declared twice.`);
67
+ }
68
+ keys.add(audience.key);
69
+ if (typeof audience.name !== 'string' || !audience.name.trim() || audience.name.length > 80) {
70
+ throw new Error(`Audience "${audience.key}" needs a name (at most 80 characters).`);
71
+ }
72
+ if (audience.criteria != null && !Array.isArray(audience.criteria)) {
73
+ throw new Error(`Audience "${audience.key}": "criteria" must be an array of lines ({ "andFilterValues": [...] }).`);
74
+ }
75
+ }
76
+ for (const match of JSON.stringify(entities).matchAll(/\{audience:([^}]*)\}/g)) {
77
+ if (!keys.has(match[1])) {
78
+ throw new Error(`A rule refers to {audience:${match[1]}}, which "audiences" in tabbr.config.json doesn't declare.`);
79
+ }
80
+ }
81
+ return audiences;
82
+ }
83
+
49
84
  const CONTRACT_NAME = /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/;
50
85
 
51
86
  /**
@@ -118,18 +153,19 @@ function providesOf(cwd, config) {
118
153
  * and the contracts it uses and provides - which a module can do without any backend code, so then only the manifest
119
154
  * is produced (`bundleJs` null). `files`: more files for the build's `backend/` (the TypeScript of what it provides).
120
155
  *
121
- * Returns `null` when the workspace has no backend part, no entities and uses or provides no contracts, so `build.js`
156
+ * Returns `null` when the workspace has no backend part, no entities, no audiences and uses or provides no contracts, so `build.js`
122
157
  * can skip it entirely for such a frontend-only module.
123
158
  */
124
159
  async function buildBackend(cwd, config) {
125
160
  const backendConfig = config.backend;
126
161
  const entities = entitiesOf(config);
162
+ const audiences = audiencesOf(config, entities);
127
163
  const uses = usesOf(config);
128
164
  const optionalUses = usesOf(config, 'optionalUses');
129
165
  const provides = providesOf(cwd, config);
130
166
  const bundleJs = backendConfig ? await bundle(cwd, backendConfig) : null;
131
- if (bundleJs == null && entities.length === 0 && Object.keys(uses).length === 0 && Object.keys(optionalUses).length === 0
132
- && Object.keys(provides.versions).length === 0) {
167
+ if (bundleJs == null && entities.length === 0 && audiences.length === 0 && Object.keys(uses).length === 0
168
+ && Object.keys(optionalUses).length === 0 && Object.keys(provides.versions).length === 0) {
133
169
  return null;
134
170
  }
135
171
  const settings = backendConfig || {};
@@ -138,19 +174,34 @@ async function buildBackend(cwd, config) {
138
174
  allowedHosts: [...(settings.allowedHosts || []), ...(config.local ? settings.localAllowedHosts || [] : [])],
139
175
  secretFieldNames: settings.secretFieldNames || [],
140
176
  triggers: bundleJs == null ? [] : settings.triggers || [],
177
+ // functions anyone may call without logging in (e.g. submitting a public form) - shown to the club when it installs
178
+ publicActions: bundleJs == null ? [] : settings.publicActions || [],
141
179
  entities,
142
180
  // the module gives persons roles (tabbr.access.setManagedRoles) - shown to the club when it installs the module
143
181
  manageRoles: bundleJs != null && settings.manageRoles === true,
182
+ // the built-in entities (besides its own) it may change - permissions.entities.write; the server refuses any other write
183
+ writeEntities: bundleJs != null ? writeEntitiesOf(config) : [],
144
184
  // the contracts it uses - for its frontend part as well, so the manifest is written for a frontend-only module too
145
185
  uses,
146
186
  optionalUses,
147
187
  // the contracts it provides, with their version; their TypeScript goes along as backend/contracts/<name>.d.ts
148
188
  provides: provides.versions,
189
+ // its own audiences (module API 1.8), made among the club's when it is installed
190
+ audiences,
149
191
  };
150
192
 
151
193
  return { bundleJs, manifestJson: JSON.stringify(manifest, null, 2), files: provides.files };
152
194
  }
153
195
 
196
+ /** `permissions.entities.write`: names of entities the module may change besides its own (e.g. "person"). */
197
+ function writeEntitiesOf(config) {
198
+ const write = config.permissions?.entities?.write ?? [];
199
+ if (!Array.isArray(write) || write.some((name) => typeof name !== 'string' || !/^[a-z][a-zA-Z0-9]{1,39}$/.test(name))) {
200
+ throw new Error('"permissions.entities.write" must be a list of entity names, e.g. ["person"].');
201
+ }
202
+ return [...new Set(write)];
203
+ }
204
+
154
205
  /** The backend entry bundled into one script, or null when there is no entry file (and none was configured explicitly). */
155
206
  async function bundle(cwd, backendConfig) {
156
207
  const entry = backendConfig.entry || DEFAULT_ENTRY;
@@ -191,4 +242,4 @@ async function bundle(cwd, backendConfig) {
191
242
  return result.outputFiles[0].text;
192
243
  }
193
244
 
194
- module.exports = { buildBackend, entitiesOf, usesOf, providesOf, DEFAULT_ENTRY };
245
+ module.exports = { buildBackend, entitiesOf, audiencesOf, usesOf, providesOf, DEFAULT_ENTRY };
package/src/config.js CHANGED
@@ -3,10 +3,23 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const readline = require('readline/promises');
6
+ const { execFileSync } = require('child_process');
7
+ const { DEFAULT_LOCAL_API_HOST } = require('./local');
6
8
 
7
9
  const CONFIG_FILE = 'tabbr.config.json';
10
+ const LOCAL_CONFIG_FILE = 'tabbr.config.local.json';
11
+ /** What a tabbr.config.local.json may set: where this checkout talks to - never what the module is. */
12
+ const LOCAL_CONFIG_KEYS = ['club', 'apiHost', 'localApiHost', 'shellUrl'];
8
13
  const DEFAULT_API_HOST = 'https://api.tabbr.be';
9
14
 
15
+ function readJson(file) {
16
+ try {
17
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
18
+ } catch (err) {
19
+ throw new Error(`${file} is not valid JSON: ${err.message}`);
20
+ }
21
+ }
22
+
10
23
  /**
11
24
  * Reads the workspace's tabbr.config.json - the one place a module project records which club it
12
25
  * targets, its module id and so on. Both this CLI and the module's own dev-preview harness read it,
@@ -14,18 +27,31 @@ const DEFAULT_API_HOST = 'https://api.tabbr.be';
14
27
  */
15
28
  function readConfig(cwd) {
16
29
  const file = path.join(cwd, CONFIG_FILE);
17
- if (!fs.existsSync(file)) return {};
18
- try {
19
- return JSON.parse(fs.readFileSync(file, 'utf-8'));
20
- } catch (err) {
21
- throw new Error(`${file} is not valid JSON: ${err.message}`);
22
- }
30
+ return fs.existsSync(file) ? readJson(file) : {};
31
+ }
32
+
33
+ /**
34
+ * The developer's own tabbr.config.local.json (not committed): the club and API host this checkout
35
+ * talks to, over those in tabbr.config.json - so a module whose tabbr.config.json targets
36
+ * production can be developed against a local backend without editing it. The one in the
37
+ * workspace, else the one a folder up (which then covers every module in that folder); the module's
38
+ * dev preview finds the same one (tsconfig.local-config.json). Only LOCAL_CONFIG_KEYS count, the
39
+ * other keys are returned as `ignored`. Returns null when there is none.
40
+ */
41
+ function readLocalConfig(cwd) {
42
+ const file = [path.join(cwd, LOCAL_CONFIG_FILE), path.join(cwd, '..', LOCAL_CONFIG_FILE)].find(f => fs.existsSync(f));
43
+ if (!file) return null;
44
+ const all = readJson(file);
45
+ const values = Object.fromEntries(LOCAL_CONFIG_KEYS.filter(key => all[key] !== undefined).map(key => [key, all[key]]));
46
+ const ignored = Object.keys(all).filter(key => !LOCAL_CONFIG_KEYS.includes(key));
47
+ return { file, values, ignored };
23
48
  }
24
49
 
25
50
  /** Uppercase club code, or throws with instructions when none is configured. */
26
51
  function requireClub(club) {
27
52
  if (!club) {
28
- throw new Error(`No club configured. Run "npx tabbr-module init" (or set "club" in ${CONFIG_FILE}).`);
53
+ throw new Error(`No club configured. Set "club" in ${LOCAL_CONFIG_FILE} (yours, not committed) or ${CONFIG_FILE}, ` +
54
+ 'or run "npx tabbr-module init".');
29
55
  }
30
56
  return club.toUpperCase();
31
57
  }
@@ -34,19 +60,39 @@ function slug(text) {
34
60
  return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
35
61
  }
36
62
 
37
- /**
38
- * Asks for the club code (and offers defaults for the rest) and writes tabbr.config.json, keeping
39
- * any values already in it as the defaults.
40
- */
41
- async function runInit(cwd, overrides) {
42
- const file = path.join(cwd, CONFIG_FILE);
43
- const current = readConfig(cwd);
63
+ /** False only when the workspace is in a git repository that would track `file`. */
64
+ function gitIgnores(cwd, file) {
65
+ try {
66
+ execFileSync('git', ['check-ignore', '-q', file], { cwd, stdio: 'ignore' });
67
+ return true;
68
+ } catch (err) {
69
+ return err.status !== 1; // 1: not ignored; anything else: no git, or not a repository
70
+ }
71
+ }
72
+
73
+ function asker() {
44
74
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
45
75
  const ask = async (question, fallback) => {
46
76
  if (!process.stdin.isTTY) return fallback;
47
77
  const answer = (await rl.question(fallback ? `${question} [${fallback}]: ` : `${question}: `)).trim();
48
78
  return answer || fallback;
49
79
  };
80
+ return { ask, close: () => rl.close() };
81
+ }
82
+
83
+ /**
84
+ * Asks for the club code (and offers defaults for the rest) and writes tabbr.config.json, keeping
85
+ * any values already in it as the defaults. With --local: only where you develop against, in
86
+ * tabbr.config.local.json - see runInitLocal.
87
+ */
88
+ async function runInit(cwd, overrides) {
89
+ if (overrides.local) {
90
+ await runInitLocal(cwd, overrides);
91
+ return;
92
+ }
93
+ const file = path.join(cwd, CONFIG_FILE);
94
+ const current = readConfig(cwd);
95
+ const { ask, close } = asker();
50
96
 
51
97
  try {
52
98
  const club = (overrides.club || await ask('Club code (the part before .tabbr.be, e.g. abc)', current.club))?.toUpperCase();
@@ -68,8 +114,35 @@ async function runInit(cwd, overrides) {
68
114
  fs.writeFileSync(file, JSON.stringify(config, null, 2) + '\n');
69
115
  console.log(`\nWrote ${file}. Next: "npm start" to preview, "npm run publish-module" to publish.`);
70
116
  } finally {
71
- rl.close();
117
+ close();
118
+ }
119
+ }
120
+
121
+ /**
122
+ * `init --local`: asks for the club code and the API host - by default your own local backend
123
+ * (https://api.local.tabbr.be) - and writes them in the workspace's tabbr.config.local.json, and
124
+ * nothing else: tabbr.config.json, the module's manifest, stays as it is (e.g. on production, for
125
+ * the store).
126
+ */
127
+ async function runInitLocal(cwd, overrides) {
128
+ const file = path.join(cwd, LOCAL_CONFIG_FILE);
129
+ const current = fs.existsSync(file) ? readJson(file) : {};
130
+ const { ask, close } = asker();
131
+ try {
132
+ const club = (overrides.club || await ask('Club code (e.g. abc)', current.club || readConfig(cwd).club))?.toUpperCase();
133
+ if (!club) throw new Error('A club code is required.');
134
+ const apiHost = overrides.apiHost
135
+ || await ask(`API host (${DEFAULT_API_HOST} for a club on production)`, current.apiHost || DEFAULT_LOCAL_API_HOST);
136
+ const local = { ...current, club, apiHost };
137
+ fs.writeFileSync(file, JSON.stringify(local, null, 2) + '\n');
138
+ console.log(`\nWrote ${file}: club ${club}, apiHost ${apiHost}. Next: "npm start" to preview, ` +
139
+ '"npm run publish-module" to publish there.');
140
+ if (!gitIgnores(cwd, file)) console.warn(`It's yours, not the module's: add ${LOCAL_CONFIG_FILE} to .gitignore.`);
141
+ } finally {
142
+ close();
72
143
  }
73
144
  }
74
145
 
75
- module.exports = { CONFIG_FILE, DEFAULT_API_HOST, readConfig, requireClub, runInit };
146
+ module.exports = {
147
+ CONFIG_FILE, LOCAL_CONFIG_FILE, LOCAL_CONFIG_KEYS, DEFAULT_API_HOST, readConfig, readLocalConfig, requireClub, runInit,
148
+ };
package/src/source.js CHANGED
@@ -5,8 +5,12 @@ const path = require('path');
5
5
  const { execFileSync } = require('child_process');
6
6
  const archiver = require('archiver');
7
7
 
8
- /** Never part of a module's source: installed packages, build output, caches, editor state. */
9
- const SKIP = new Set(['node_modules', 'dist', '.angular', 'out-tsc', '.git', '.yalc', '.idea', '.vscode', 'coverage']);
8
+ /**
9
+ * Never part of a module's source: installed packages, build output, caches, editor state - and the developer's own
10
+ * tabbr.config.local.json (where they develop against), also when it isn't in .gitignore.
11
+ */
12
+ const SKIP = new Set(['node_modules', 'dist', '.angular', 'out-tsc', '.git', '.yalc', '.idea', '.vscode', 'coverage',
13
+ 'tabbr.config.local.json']);
10
14
 
11
15
  /**
12
16
  * The workspace's source files, as the module store builds them: the files git tracks (plus new ones git doesn't ignore)