@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/publish.js
CHANGED
|
@@ -1,29 +1,103 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/** Uploads the zipped module build to the tabbr backend and returns the parsed JSON response. */
|
|
4
|
-
async function publish({ apiHost, club, moduleId, exposedModule, exportName, icon, title, zipBuffer, accessToken }) {
|
|
5
|
-
const form = new FormData();
|
|
6
|
-
form.append('file', new Blob([zipBuffer], { type: 'application/zip' }), 'module.zip');
|
|
7
|
-
form.append('moduleId', moduleId);
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** Uploads the zipped module build to the tabbr backend and returns the parsed JSON response. */
|
|
4
|
+
async function publish({ apiHost, club, moduleId, exposedModule, exportName, icon, title, zipBuffer, accessToken }) {
|
|
5
|
+
const form = new FormData();
|
|
6
|
+
form.append('file', new Blob([zipBuffer], { type: 'application/zip' }), 'module.zip');
|
|
7
|
+
form.append('moduleId', moduleId);
|
|
8
|
+
// Only present for a module with a frontend part - a backend-only module has neither, and the
|
|
9
|
+
// server's /api/custommodule accepts both as optional for exactly that reason.
|
|
10
|
+
if (exposedModule) form.append('exposedModule', exposedModule);
|
|
11
|
+
if (exportName) form.append('exportName', exportName);
|
|
12
|
+
if (icon) form.append('icon', icon);
|
|
13
|
+
if (title) form.append('title', title);
|
|
14
|
+
|
|
15
|
+
const url = `${apiHost}/api/custommodule`;
|
|
16
|
+
console.log(`Publishing ${moduleId} to club ${club.toUpperCase()} at ${url} ...`);
|
|
17
|
+
const response = await fetch(url, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
// X-club selects the club: the server publishes into the club CurrentClubFilter
|
|
20
|
+
// (tabbr-api-server) resolves from this header, the same club its ADMIN check runs against.
|
|
21
|
+
headers: { Authorization: `Bearer ${accessToken}`, 'X-club': club.toUpperCase() },
|
|
22
|
+
body: form,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(`Publish failed: ${response.status} ${await response.text()}`);
|
|
27
|
+
}
|
|
28
|
+
return response.json();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Submits the zipped build as a new version of a store package (docs/module-store.md): POST
|
|
33
|
+
* /api/store/packages/<publisher>/<name>/versions with the build and the version's metadata. The
|
|
34
|
+
* version then waits for review; clubs install it from the store once it is approved.
|
|
35
|
+
*/
|
|
36
|
+
async function publishToStore({ apiHost, packageName, meta, zipBuffer, accessToken }) {
|
|
37
|
+
const form = new FormData();
|
|
38
|
+
form.append('file', new Blob([zipBuffer], { type: 'application/zip' }), 'module.zip');
|
|
39
|
+
form.append('meta', new Blob([JSON.stringify(meta)], { type: 'application/json' }));
|
|
40
|
+
|
|
41
|
+
const url = `${apiHost}/api/store/packages/${packageName}/versions`;
|
|
42
|
+
console.log(`Submitting ${packageName} ${meta.version} to the module store at ${url} ...`);
|
|
43
|
+
const response = await fetch(url, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
46
|
+
body: form,
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(`Submit failed: ${response.status} ${await response.text()}`);
|
|
50
|
+
}
|
|
51
|
+
return response.json();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The version's metadata for the store, from tabbr.config.json (and the command line). */
|
|
55
|
+
function storeMeta(args) {
|
|
56
|
+
const missing = [];
|
|
57
|
+
if (!args.package) missing.push('"package" (e.g. "acme/groups")');
|
|
58
|
+
if (!args.version) missing.push('"version" (e.g. "1.0.0")');
|
|
59
|
+
if (!args.engines || !args.engines.tabbr) missing.push('"engines": { "tabbr": "^1.0" } (the module API levels it runs on)');
|
|
60
|
+
if (missing.length) {
|
|
61
|
+
throw new Error(`Missing in tabbr.config.json for --store: ${missing.join(', ')}`);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
version: args.version,
|
|
65
|
+
moduleId: args.moduleId,
|
|
66
|
+
exposedModule: args.exposedModule,
|
|
67
|
+
exportName: args.exportName,
|
|
68
|
+
title: args.title,
|
|
69
|
+
icon: args.icon,
|
|
70
|
+
summary: args.summary,
|
|
71
|
+
changelog: args.changelog,
|
|
72
|
+
visibility: args.visibility,
|
|
73
|
+
engines: args.engines,
|
|
74
|
+
dependencies: args.dependencies || {},
|
|
75
|
+
optionalDependencies: args.optionalDependencies || {},
|
|
76
|
+
permissions: args.permissions || {},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Submits the workspace's source as a new version of a store package: POST
|
|
82
|
+
* /api/store/packages/<publisher>/<name>/sources. The store builds it (tabbr-module-builder) and
|
|
83
|
+
* only then puts it in review.
|
|
84
|
+
*/
|
|
85
|
+
async function submitSourceToStore({ apiHost, packageName, meta, sourceZip, accessToken }) {
|
|
86
|
+
const form = new FormData();
|
|
87
|
+
form.append('source', new Blob([sourceZip], { type: 'application/zip' }), 'source.zip');
|
|
88
|
+
form.append('meta', new Blob([JSON.stringify(meta)], { type: 'application/json' }));
|
|
89
|
+
|
|
90
|
+
const url = `${apiHost}/api/store/packages/${packageName}/sources`;
|
|
91
|
+
console.log(`Submitting the source of ${packageName} ${meta.version} to the module store at ${url} ...`);
|
|
92
|
+
const response = await fetch(url, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
95
|
+
body: form,
|
|
96
|
+
});
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
throw new Error(`Submit failed: ${response.status} ${await response.text()}`);
|
|
99
|
+
}
|
|
100
|
+
return response.json();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { publish, publishToStore, submitSourceToStore, storeMeta };
|
package/src/source.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
6
|
+
const archiver = require('archiver');
|
|
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']);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The workspace's source files, as the module store builds them: the files git tracks (plus new ones git doesn't ignore)
|
|
13
|
+
* when the workspace is in a git repository, otherwise every file outside the SKIP folders. Paths relative to `cwd`.
|
|
14
|
+
*/
|
|
15
|
+
function sourceFiles(cwd) {
|
|
16
|
+
let listed = null;
|
|
17
|
+
try {
|
|
18
|
+
const out = execFileSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', '.'],
|
|
19
|
+
{ cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
20
|
+
listed = out.split('\0').filter(Boolean);
|
|
21
|
+
} catch {
|
|
22
|
+
// not a git repository (or no git): walk the folder instead
|
|
23
|
+
}
|
|
24
|
+
const files = listed ?? walk(cwd, '');
|
|
25
|
+
return files
|
|
26
|
+
.filter(file => !file.split('/').some(part => SKIP.has(part)))
|
|
27
|
+
.filter(file => fs.existsSync(path.join(cwd, file)) && fs.statSync(path.join(cwd, file)).isFile())
|
|
28
|
+
.sort();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function walk(root, rel) {
|
|
32
|
+
const result = [];
|
|
33
|
+
for (const entry of fs.readdirSync(path.join(root, rel), { withFileTypes: true })) {
|
|
34
|
+
if (SKIP.has(entry.name)) continue;
|
|
35
|
+
const child = rel ? `${rel}/${entry.name}` : entry.name;
|
|
36
|
+
if (entry.isDirectory()) result.push(...walk(root, child));
|
|
37
|
+
else if (entry.isFile()) result.push(child);
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A zip of the workspace's source (see sourceFiles), checked to have what the store needs to build it. */
|
|
43
|
+
function packSource(cwd) {
|
|
44
|
+
const files = sourceFiles(cwd);
|
|
45
|
+
for (const required of ['package.json', 'package-lock.json']) {
|
|
46
|
+
if (!files.includes(required)) {
|
|
47
|
+
throw new Error(`${required} is missing in ${cwd}` +
|
|
48
|
+
(required === 'package-lock.json' ? ' - the store installs exactly the locked dependencies; run "npm install" and commit it' : ''));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// yalc-linked packages exist only on this machine: the store uses the platform's own SDK and CLI instead
|
|
52
|
+
const lock = fs.readFileSync(path.join(cwd, 'package-lock.json'), 'utf8');
|
|
53
|
+
if (lock.includes('file:.yalc/')) {
|
|
54
|
+
throw new Error('package-lock.json links @tabbr packages with yalc ("npm run link-local") - run "npm run unlink-local" ' +
|
|
55
|
+
'first: the store builds against the published packages and replaces @tabbr/module-sdk and -cli with the platform\'s own.');
|
|
56
|
+
}
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
59
|
+
const chunks = [];
|
|
60
|
+
archive.on('data', chunk => chunks.push(chunk));
|
|
61
|
+
archive.on('end', () => resolve({ zip: Buffer.concat(chunks), files }));
|
|
62
|
+
archive.on('error', reject);
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
archive.file(path.join(cwd, file), { name: file });
|
|
65
|
+
}
|
|
66
|
+
archive.finalize();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { packSource, sourceFiles };
|