@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/src/build.js
CHANGED
|
@@ -1,91 +1,123 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { execFileSync } = require('child_process');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const archiver = require('archiver');
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*
|
|
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
|
-
archive
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const archiver = require('archiver');
|
|
7
|
+
const { buildBackend } = require('./backend-build');
|
|
8
|
+
|
|
9
|
+
/** Reads angular.json and resolves the browser output directory for the workspace's one project. */
|
|
10
|
+
function resolveOutputDir(cwd) {
|
|
11
|
+
const angularJsonPath = path.join(cwd, 'angular.json');
|
|
12
|
+
if (!fs.existsSync(angularJsonPath)) {
|
|
13
|
+
throw new Error(`No angular.json found in ${cwd} - run this from an Angular workspace root (or pass --cwd).`);
|
|
14
|
+
}
|
|
15
|
+
const angularJson = JSON.parse(fs.readFileSync(angularJsonPath, 'utf-8'));
|
|
16
|
+
const projectName = Object.keys(angularJson.projects)[0];
|
|
17
|
+
const project = angularJson.projects[projectName];
|
|
18
|
+
// native-federation's build target wraps the real Angular application builder under
|
|
19
|
+
// "build-original" (see tabbr-ui/angular.json and tabbr-custom-module-poc/angular.json) -
|
|
20
|
+
// that's where the actual outputPath lives.
|
|
21
|
+
const buildTarget = project.architect['build-original'] || project.architect['build'];
|
|
22
|
+
const outputPath = buildTarget.options.outputPath;
|
|
23
|
+
const base = typeof outputPath === 'string' ? outputPath : outputPath.base;
|
|
24
|
+
// Angular's application builder always nests the actual browser bundle under a "browser/"
|
|
25
|
+
// subfolder of outputPath - confirmed empirically against this exact builder this session.
|
|
26
|
+
return path.join(cwd, base, 'browser');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Runs `ng build` in `cwd` by invoking the workspace's own locally-installed Angular CLI entry
|
|
31
|
+
* point directly with the current Node binary.
|
|
32
|
+
*
|
|
33
|
+
* Deliberately NOT `execFileSync('npx', ['ng', 'build'], { shell: true })`, which was the previous
|
|
34
|
+
* implementation and had three problems at once:
|
|
35
|
+
* - Node's DEP0190 deprecation warning (passing an args array together with `shell: true`).
|
|
36
|
+
* - Passing args through a shell at all means they're re-parsed by the shell, so anything less
|
|
37
|
+
* trusted than a hardcoded literal would need manual escaping.
|
|
38
|
+
* - On Windows `npx` is `npx.cmd`, and since the CVE-2024-27980 fix Node refuses to spawn
|
|
39
|
+
* `.cmd`/`.bat` WITHOUT `shell: true` - so dropping the shell alone wasn't an option there.
|
|
40
|
+
* Going straight to `node node_modules/@angular/cli/bin/ng.js` sidesteps all three: no shell, no
|
|
41
|
+
* args re-parsing, no platform-specific launcher script, and it's faster (skips npx resolution).
|
|
42
|
+
*
|
|
43
|
+
* Explicitly `--configuration production`, not a bare `ng build`: a module workspace's
|
|
44
|
+
* angular.json typically sets `defaultConfiguration: "development"` on the native-federation
|
|
45
|
+
* build target (so a plain `ng build`/`ng serve` during local development doesn't need the flag
|
|
46
|
+
* spelled out every time) - which a bare `ng build` here would silently inherit, publishing an
|
|
47
|
+
* unoptimized dev build (unminified, `dev: true`, native-federation's build-notifications
|
|
48
|
+
* live-reload hook baked in) to the club instead of the production one. The live-reload hook in
|
|
49
|
+
* particular then 404s at runtime (it points at a dev server that doesn't exist once statically
|
|
50
|
+
* published) and the unshared-per-pass token identities a dev build produces differently from
|
|
51
|
+
* prod have caused real `NG0201: No provider found for ...` failures in published modules.
|
|
52
|
+
*/
|
|
53
|
+
function runNgBuild(cwd) {
|
|
54
|
+
const ngJs = path.join(cwd, 'node_modules', '@angular', 'cli', 'bin', 'ng.js');
|
|
55
|
+
if (!fs.existsSync(ngJs)) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`No local Angular CLI found at ${ngJs} - run "npm install" in ${cwd} first.`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
console.log(`Building in ${cwd} ...`);
|
|
61
|
+
execFileSync(process.execPath, [ngJs, 'build', '--configuration', 'production'], { cwd, stdio: 'inherit' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Zips frontendDir's contents flat at the zip root (remoteEntry.json at the root, as before) plus,
|
|
66
|
+
* when present, the backend bundle/manifest under a `backend/` prefix - one zip, one upload, no
|
|
67
|
+
* separate publish step per side (see `CustomModuleService#publish` on the server, which splits
|
|
68
|
+
* them back apart by that same prefix).
|
|
69
|
+
*/
|
|
70
|
+
function zipModule(frontendDir, backend) {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
73
|
+
const chunks = [];
|
|
74
|
+
archive.on('data', chunk => chunks.push(chunk));
|
|
75
|
+
archive.on('end', () => resolve(Buffer.concat(chunks)));
|
|
76
|
+
archive.on('error', reject);
|
|
77
|
+
if (frontendDir) {
|
|
78
|
+
archive.directory(frontendDir, false);
|
|
79
|
+
}
|
|
80
|
+
if (backend) {
|
|
81
|
+
if (backend.bundleJs != null) {
|
|
82
|
+
archive.append(backend.bundleJs, { name: 'backend/bundle.js' });
|
|
83
|
+
}
|
|
84
|
+
archive.append(backend.manifestJson, { name: 'backend/manifest.json' });
|
|
85
|
+
}
|
|
86
|
+
archive.finalize();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Builds whichever parts of the workspace at `cwd` are present - a frontend (an Angular project,
|
|
92
|
+
* i.e. angular.json exists) and/or a backend (`config.backend` set, see `backend-build.js`) - and
|
|
93
|
+
* returns a single zip Buffer with both. At least one part must be present.
|
|
94
|
+
*/
|
|
95
|
+
async function buildAndZip(cwd, config) {
|
|
96
|
+
const hasFrontend = fs.existsSync(path.join(cwd, 'angular.json'));
|
|
97
|
+
let frontendDir = null;
|
|
98
|
+
if (hasFrontend) {
|
|
99
|
+
runNgBuild(cwd);
|
|
100
|
+
frontendDir = resolveOutputDir(cwd);
|
|
101
|
+
const remoteEntryPath = path.join(frontendDir, 'remoteEntry.json');
|
|
102
|
+
if (!fs.existsSync(remoteEntryPath)) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Build output at ${frontendDir} has no remoteEntry.json. ` +
|
|
105
|
+
`Is this workspace's federation.config.js missing an "exposes" entry?`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const backend = await buildBackend(cwd, config || {});
|
|
111
|
+
|
|
112
|
+
if (!hasFrontend && !backend) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Nothing to build in ${cwd}: no angular.json (frontend) and no backend part configured ` +
|
|
115
|
+
`("backend" in tabbr.config.json, entry file missing).`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log('Zipping module ...');
|
|
120
|
+
return zipModule(frontendDir, backend);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { buildAndZip, resolveOutputDir };
|
package/src/config.js
CHANGED
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const readline = require('readline/promises');
|
|
6
|
-
|
|
7
|
-
const CONFIG_FILE = 'tabbr.config.json';
|
|
8
|
-
const DEFAULT_API_HOST = 'https://api.tabbr.be';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Reads the workspace's tabbr.config.json - the one place a module project records which club it
|
|
12
|
-
* targets, its module id and so on. Both this CLI and the module's own dev-preview harness read it,
|
|
13
|
-
* so they can't drift apart. Returns {} when there is no such file.
|
|
14
|
-
*/
|
|
15
|
-
function readConfig(cwd) {
|
|
16
|
-
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
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Uppercase club code, or throws with instructions when none is configured. */
|
|
26
|
-
function requireClub(club) {
|
|
27
|
-
if (!club) {
|
|
28
|
-
throw new Error(`No club configured. Run "npx tabbr-module init" (or set "club" in ${CONFIG_FILE}).`);
|
|
29
|
-
}
|
|
30
|
-
return club.toUpperCase();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function slug(text) {
|
|
34
|
-
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
35
|
-
}
|
|
36
|
-
|
|
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);
|
|
44
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
45
|
-
const ask = async (question, fallback) => {
|
|
46
|
-
if (!process.stdin.isTTY) return fallback;
|
|
47
|
-
const answer = (await rl.question(fallback ? `${question} [${fallback}]: ` : `${question}: `)).trim();
|
|
48
|
-
return answer || fallback;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
const club = (overrides.club || await ask('Club code (the part before .tabbr.be, e.g. abc)', current.club))?.toUpperCase();
|
|
53
|
-
if (!club) throw new Error('A club code is required.');
|
|
54
|
-
const name = slug(path.basename(cwd)) || 'my-module';
|
|
55
|
-
const moduleId = overrides.moduleId
|
|
56
|
-
|| await ask('Module id (unique within the club)', current.moduleId || `club.${club.toLowerCase()}.${name}`);
|
|
57
|
-
const title = overrides.title || await ask('
|
|
58
|
-
|
|
59
|
-
const config = {
|
|
60
|
-
...current,
|
|
61
|
-
club,
|
|
62
|
-
apiHost: overrides.apiHost || current.apiHost || DEFAULT_API_HOST,
|
|
63
|
-
moduleId,
|
|
64
|
-
title,
|
|
65
|
-
exposedModule: current.exposedModule || './Module',
|
|
66
|
-
exportName: current.exportName || 'Module',
|
|
67
|
-
};
|
|
68
|
-
fs.writeFileSync(file, JSON.stringify(config, null, 2) + '\n');
|
|
69
|
-
console.log(`\nWrote ${file}. Next: "npm start" to preview, "npm run publish-module" to publish.`);
|
|
70
|
-
} finally {
|
|
71
|
-
rl.close();
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
module.exports = { CONFIG_FILE, DEFAULT_API_HOST, readConfig, requireClub, runInit };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline/promises');
|
|
6
|
+
|
|
7
|
+
const CONFIG_FILE = 'tabbr.config.json';
|
|
8
|
+
const DEFAULT_API_HOST = 'https://api.tabbr.be';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Reads the workspace's tabbr.config.json - the one place a module project records which club it
|
|
12
|
+
* targets, its module id and so on. Both this CLI and the module's own dev-preview harness read it,
|
|
13
|
+
* so they can't drift apart. Returns {} when there is no such file.
|
|
14
|
+
*/
|
|
15
|
+
function readConfig(cwd) {
|
|
16
|
+
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
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Uppercase club code, or throws with instructions when none is configured. */
|
|
26
|
+
function requireClub(club) {
|
|
27
|
+
if (!club) {
|
|
28
|
+
throw new Error(`No club configured. Run "npx tabbr-module init" (or set "club" in ${CONFIG_FILE}).`);
|
|
29
|
+
}
|
|
30
|
+
return club.toUpperCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function slug(text) {
|
|
34
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
35
|
+
}
|
|
36
|
+
|
|
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);
|
|
44
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
45
|
+
const ask = async (question, fallback) => {
|
|
46
|
+
if (!process.stdin.isTTY) return fallback;
|
|
47
|
+
const answer = (await rl.question(fallback ? `${question} [${fallback}]: ` : `${question}: `)).trim();
|
|
48
|
+
return answer || fallback;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const club = (overrides.club || await ask('Club code (the part before .tabbr.be, e.g. abc)', current.club))?.toUpperCase();
|
|
53
|
+
if (!club) throw new Error('A club code is required.');
|
|
54
|
+
const name = slug(path.basename(cwd)) || 'my-module';
|
|
55
|
+
const moduleId = overrides.moduleId
|
|
56
|
+
|| await ask('Module id (unique within the club)', current.moduleId || `club.${club.toLowerCase()}.${name}`);
|
|
57
|
+
const title = overrides.title || await ask('Fallback title (the translated one is module.title in your i18n bundle)', current.title || 'My module');
|
|
58
|
+
|
|
59
|
+
const config = {
|
|
60
|
+
...current,
|
|
61
|
+
club,
|
|
62
|
+
apiHost: overrides.apiHost || current.apiHost || DEFAULT_API_HOST,
|
|
63
|
+
moduleId,
|
|
64
|
+
title,
|
|
65
|
+
exposedModule: current.exposedModule || './Module',
|
|
66
|
+
exportName: current.exportName || 'Module',
|
|
67
|
+
};
|
|
68
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2) + '\n');
|
|
69
|
+
console.log(`\nWrote ${file}. Next: "npm start" to preview, "npm run publish-module" to publish.`);
|
|
70
|
+
} finally {
|
|
71
|
+
rl.close();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { CONFIG_FILE, DEFAULT_API_HOST, readConfig, requireClub, runInit };
|
package/src/fetch-types.js
CHANGED
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Mirrors ScriptTypeGenerator.ScriptKind (tabbr-api-server) - keep in sync if that enum changes.
|
|
4
|
-
const VALID_KINDS = ['HOOK', 'CALCULATION', 'DEFAULT_CALCULATION', 'RELEVANT_CONDITION'];
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Fetches the ambient .d.ts tabbr-ui's own script editor (Monaco, for club-configured hook/
|
|
8
|
-
* calculation/default-value/relevancy-condition scripts) feeds itself, for a given club/entity/
|
|
9
|
-
* kind - see ScriptTypeGenerator (tabbr-api-server) and ScriptEditorMonacoService (tabbr-ui).
|
|
10
|
-
*/
|
|
11
|
-
async function fetchScriptTypes({ apiHost, club, entity, kind, accessToken }) {
|
|
12
|
-
if (!VALID_KINDS.includes(kind)) {
|
|
13
|
-
throw new Error(`Invalid --kind "${kind}". Must be one of: ${VALID_KINDS.join(', ')}`);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const url = `${apiHost}/api/${entity}/meta/script?kind=${kind}`;
|
|
17
|
-
console.log(`Fetching script types for entity "${entity}" (kind ${kind}) from ${url} ...`);
|
|
18
|
-
const response = await fetch(url, {
|
|
19
|
-
// X-club is required: CurrentClubFilter (tabbr-api-server) resolves the "current club" for
|
|
20
|
-
// this request from this header, not from any path variable - same pattern as publish.js.
|
|
21
|
-
headers: { Authorization: `Bearer ${accessToken}`, 'X-club': club.toUpperCase() },
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
if (!response.ok) {
|
|
25
|
-
throw new Error(`Fetching script types failed: ${response.status} ${await response.text()}`);
|
|
26
|
-
}
|
|
27
|
-
return response.text();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Every entity this club has, as plain TypeScript data interfaces in ONE combined file - no
|
|
32
|
-
* script-sandbox globals, so (unlike fetchScriptTypes) safe to fetch for every entity at once and
|
|
33
|
-
* write to a single .d.ts. See ScriptTypeGenerator#generateAllEntityTypes (tabbr-api-server) for
|
|
34
|
-
* why. For general club-data typing in module component code, not the script editor.
|
|
35
|
-
*/
|
|
36
|
-
async function fetchAllEntityTypes({ apiHost, club, accessToken }) {
|
|
37
|
-
const url = `${apiHost}/api/club/${club.toUpperCase()}/entities/types`;
|
|
38
|
-
console.log(`Fetching all entity types for club ${club.toUpperCase()} from ${url} ...`);
|
|
39
|
-
const response = await fetch(url, {
|
|
40
|
-
headers: { Authorization: `Bearer ${accessToken}`, 'X-club': club.toUpperCase() },
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
if (!response.ok) {
|
|
44
|
-
throw new Error(`Fetching entity types failed: ${response.status} ${await response.text()}`);
|
|
45
|
-
}
|
|
46
|
-
return response.text();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
module.exports = { fetchScriptTypes, fetchAllEntityTypes, VALID_KINDS };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Mirrors ScriptTypeGenerator.ScriptKind (tabbr-api-server) - keep in sync if that enum changes.
|
|
4
|
+
const VALID_KINDS = ['HOOK', 'CALCULATION', 'DEFAULT_CALCULATION', 'RELEVANT_CONDITION'];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Fetches the ambient .d.ts tabbr-ui's own script editor (Monaco, for club-configured hook/
|
|
8
|
+
* calculation/default-value/relevancy-condition scripts) feeds itself, for a given club/entity/
|
|
9
|
+
* kind - see ScriptTypeGenerator (tabbr-api-server) and ScriptEditorMonacoService (tabbr-ui).
|
|
10
|
+
*/
|
|
11
|
+
async function fetchScriptTypes({ apiHost, club, entity, kind, accessToken }) {
|
|
12
|
+
if (!VALID_KINDS.includes(kind)) {
|
|
13
|
+
throw new Error(`Invalid --kind "${kind}". Must be one of: ${VALID_KINDS.join(', ')}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const url = `${apiHost}/api/${entity}/meta/script?kind=${kind}`;
|
|
17
|
+
console.log(`Fetching script types for entity "${entity}" (kind ${kind}) from ${url} ...`);
|
|
18
|
+
const response = await fetch(url, {
|
|
19
|
+
// X-club is required: CurrentClubFilter (tabbr-api-server) resolves the "current club" for
|
|
20
|
+
// this request from this header, not from any path variable - same pattern as publish.js.
|
|
21
|
+
headers: { Authorization: `Bearer ${accessToken}`, 'X-club': club.toUpperCase() },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
throw new Error(`Fetching script types failed: ${response.status} ${await response.text()}`);
|
|
26
|
+
}
|
|
27
|
+
return response.text();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Every entity this club has, as plain TypeScript data interfaces in ONE combined file - no
|
|
32
|
+
* script-sandbox globals, so (unlike fetchScriptTypes) safe to fetch for every entity at once and
|
|
33
|
+
* write to a single .d.ts. See ScriptTypeGenerator#generateAllEntityTypes (tabbr-api-server) for
|
|
34
|
+
* why. For general club-data typing in module component code, not the script editor.
|
|
35
|
+
*/
|
|
36
|
+
async function fetchAllEntityTypes({ apiHost, club, accessToken }) {
|
|
37
|
+
const url = `${apiHost}/api/club/${club.toUpperCase()}/entities/types`;
|
|
38
|
+
console.log(`Fetching all entity types for club ${club.toUpperCase()} from ${url} ...`);
|
|
39
|
+
const response = await fetch(url, {
|
|
40
|
+
headers: { Authorization: `Bearer ${accessToken}`, 'X-club': club.toUpperCase() },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
throw new Error(`Fetching entity types failed: ${response.status} ${await response.text()}`);
|
|
45
|
+
}
|
|
46
|
+
return response.text();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { fetchScriptTypes, fetchAllEntityTypes, VALID_KINDS };
|
package/src/local.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
/** The developer's own backend, behind the local reverse proxy (see tabbr-ui's api-host.ts). */
|
|
9
|
+
const DEFAULT_LOCAL_API_HOST = 'https://api.local.tabbr.be';
|
|
10
|
+
|
|
11
|
+
const LOCAL_SUFFIX = '.local.tabbr.be';
|
|
12
|
+
const RESPAWNED_MARKER = 'TABBR_CLI_LOCAL_TRUST_APPLIED';
|
|
13
|
+
|
|
14
|
+
function isLocalApiHost(apiHost) {
|
|
15
|
+
try {
|
|
16
|
+
return new URL(apiHost).hostname.endsWith(LOCAL_SUFFIX);
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Where `mkcert -install` keeps its root CA, which signs the *.local.tabbr.be certificate. */
|
|
23
|
+
function mkcertRootCa() {
|
|
24
|
+
const candidates = [];
|
|
25
|
+
if (process.env.CAROOT) candidates.push(path.join(process.env.CAROOT, 'rootCA.pem'));
|
|
26
|
+
if (process.platform === 'win32') {
|
|
27
|
+
candidates.push(path.join(process.env.LOCALAPPDATA || '', 'mkcert', 'rootCA.pem'));
|
|
28
|
+
} else if (process.platform === 'darwin') {
|
|
29
|
+
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'mkcert', 'rootCA.pem'));
|
|
30
|
+
} else {
|
|
31
|
+
candidates.push(path.join(os.homedir(), '.local', 'share', 'mkcert', 'rootCA.pem'));
|
|
32
|
+
}
|
|
33
|
+
return candidates.find((candidate) => fs.existsSync(candidate));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Node ignores the OS trust store by default, so a local dev environment signed by mkcert's private
|
|
38
|
+
* root CA fails with a certificate error even though the browser trusts it. NODE_EXTRA_CA_CERTS is
|
|
39
|
+
* only read at process start, so when the target is a *.local.tabbr.be host this re-runs the CLI
|
|
40
|
+
* once as a child with it set (and exits with the child's status). No-op for any other host, when
|
|
41
|
+
* the variable is already set, or when mkcert's root CA can't be found.
|
|
42
|
+
*/
|
|
43
|
+
function trustLocalCertificateOrRespawn(apiHost) {
|
|
44
|
+
if (!isLocalApiHost(apiHost) || process.env.NODE_EXTRA_CA_CERTS || process.env[RESPAWNED_MARKER]) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const rootCa = mkcertRootCa();
|
|
48
|
+
if (!rootCa) {
|
|
49
|
+
console.warn(
|
|
50
|
+
`Publishing to ${apiHost} needs your mkcert root CA to be trusted, but it was not found. If the upload fails ` +
|
|
51
|
+
'with a certificate error, set NODE_EXTRA_CA_CERTS to the path of mkcert\'s rootCA.pem (see "mkcert -CAROOT").'
|
|
52
|
+
);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const result = spawnSync(process.execPath, process.argv.slice(1), {
|
|
56
|
+
stdio: 'inherit',
|
|
57
|
+
env: { ...process.env, NODE_EXTRA_CA_CERTS: rootCa, [RESPAWNED_MARKER]: '1' },
|
|
58
|
+
});
|
|
59
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { DEFAULT_LOCAL_API_HOST, isLocalApiHost, trustLocalCertificateOrRespawn };
|