@tabbr/module-cli 0.2.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/README.md CHANGED
@@ -85,9 +85,16 @@ export function pollFlights() {
85
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
86
  - `tabbr.settings.get(key)` / `.set(key, value)` - this module's own per-club settings, including
87
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.
88
93
  - `triggers[].type: "CRON"` runs on the given cron expression (server-local time), with no
89
94
  triggering entity. `"ENTITY_HOOK"` runs on the named `entity`'s `event` (`onCreate`/`onUpdate`/
90
- `onDelete`), with that entity bound under its own name (e.g. `flight`) in the handler's scope.
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.
91
98
  - Needs `esbuild` installed in the workspace (`npm install -D esbuild`) - only used at publish
92
99
  time, bundled into one dependency-free `bundle.js` the server runs in a GraalVM sandbox, so
93
100
  regular npm packages can be imported in your TS but nothing Node-specific (no `fs`, no native
@@ -96,6 +103,32 @@ export function pollFlights() {
96
103
  A "resync now" button (or similar) can fire a `CRON` trigger manually:
97
104
  `POST /api/custommodule/{moduleId}/trigger/{triggerName}` (ADMIN).
98
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
+
99
132
  ## Usage: publish
100
133
 
101
134
  ```
@@ -135,6 +168,46 @@ doesn't do on its own (`NODE_EXTRA_CA_CERTS` is set automatically; if mkcert's r
135
168
  set it yourself - `mkcert -CAROOT` shows where it lives). `--club` picks the club in that environment;
136
169
  login still goes through the real Keycloak, which local backends validate too.
137
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
+
138
211
  ## Usage: fetch-types
139
212
 
140
213
  ```
@@ -5,7 +5,8 @@ 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');
11
12
  const { DEFAULT_LOCAL_API_HOST, trustLocalCertificateOrRespawn } = require('../src/local');
@@ -29,6 +30,9 @@ function parseArgs(argv) {
29
30
  case '--entity': args.entity = next(); break;
30
31
  case '--kind': args.kind = next(); break;
31
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;
32
36
  default:
33
37
  throw new Error(`Unknown option: ${arg}`);
34
38
  }
@@ -80,6 +84,24 @@ Optional:
80
84
  Requires the tabbr Keycloak client's Valid Redirect URIs to include http://localhost:*
81
85
  (a Keycloak admin-console setting - see README.md).
82
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
+
83
105
  Usage: tabbr-module fetch-types [--club <CODE>] [--entity <name>] [options]
84
106
 
85
107
  With --entity: fetches the ambient .d.ts for that ONE entity's script sandbox - the same typings
@@ -113,6 +135,10 @@ function requireFlags(args, requiredFlags) {
113
135
  }
114
136
 
115
137
  async function runPublish(args) {
138
+ if (args.store) {
139
+ await runPublishToStore(args);
140
+ return;
141
+ }
116
142
  args.club = requireClub(args.club);
117
143
  requireFlags(args, {
118
144
  moduleId: '--module-id',
@@ -139,6 +165,33 @@ async function runPublish(args) {
139
165
  console.log(JSON.stringify(result, null, 2));
140
166
  }
141
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
+
142
195
  async function fetchAndWriteOne({ apiHost, club, entity, kind, cwd, out, accessToken }) {
143
196
  const dts = await fetchScriptTypes({ apiHost, club, entity, kind, accessToken });
144
197
 
@@ -219,7 +272,7 @@ async function runFetchTypes(args) {
219
272
  async function main() {
220
273
  const [command, ...rest] = process.argv.slice(2);
221
274
 
222
- if (command !== 'init' && command !== 'publish' && command !== 'fetch-types') {
275
+ if (!['init', 'publish', 'build', 'fetch-types'].includes(command)) {
223
276
  printUsage();
224
277
  process.exit(command ? 1 : 0);
225
278
  }
@@ -238,6 +291,8 @@ async function main() {
238
291
  trustLocalCertificateOrRespawn(args.apiHost);
239
292
  if (command === 'publish') {
240
293
  await runPublish(args);
294
+ } else if (command === 'build') {
295
+ await runBuild(args);
241
296
  } else {
242
297
  await runFetchTypes(args);
243
298
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tabbr/module-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",
@@ -9,6 +9,43 @@ const DEFAULT_ENTRY = 'backend/src/index.ts';
9
9
  * in the module's TS ends up reachable as the global `__module.foo` once bundled this way. */
10
10
  const GLOBAL_NAME = '__module';
11
11
 
12
+ /**
13
+ * The entities the module declares (`"entities"` in tabbr.config.json), checked just enough to fail here rather than at
14
+ * upload with a vaguer message: Tabbr validates them again against the club (names taken, entity fields pointing at
15
+ * entities the club has, ...). See `ModuleEntityDefinition` in tabbr-api-server for the full shape.
16
+ */
17
+ function entitiesOf(config) {
18
+ const entities = config.entities || [];
19
+ if (!Array.isArray(entities)) {
20
+ throw new Error('"entities" in tabbr.config.json must be an array.');
21
+ }
22
+ for (const entity of entities) {
23
+ if (!entity || !/^[a-z][a-zA-Z0-9]{1,39}$/.test(entity.name || '')) {
24
+ throw new Error(`Entity name "${entity && entity.name}" is not valid: camelCase letters and digits, starting lowercase.`);
25
+ }
26
+ if (!Array.isArray(entity.fields) || entity.fields.length === 0) {
27
+ throw new Error(`Entity "${entity.name}" declares no fields.`);
28
+ }
29
+ const keys = new Set();
30
+ for (const field of entity.fields) {
31
+ if (!/^[a-z][a-zA-Z0-9]{0,39}$/.test(field.key || '')) {
32
+ throw new Error(`Entity "${entity.name}": field key "${field.key}" is not valid.`);
33
+ }
34
+ if (keys.has(field.key)) {
35
+ throw new Error(`Entity "${entity.name}": field "${field.key}" is declared twice.`);
36
+ }
37
+ keys.add(field.key);
38
+ if (!field.type && !field.persistentType) {
39
+ throw new Error(`Entity "${entity.name}": field "${field.key}" has no type (e.g. "tabbr.text").`);
40
+ }
41
+ if (field.type === 'tabbr.entity' && !field.target) {
42
+ throw new Error(`Entity "${entity.name}": entity field "${field.key}" has no "target".`);
43
+ }
44
+ }
45
+ }
46
+ return entities;
47
+ }
48
+
12
49
  /**
13
50
  * Bundles a module workspace's backend part (if configured) into a single, dependency-free
14
51
  * `bundle.js`, using esbuild so module authors can write normal TypeScript (types, imports,
@@ -16,14 +53,35 @@ const GLOBAL_NAME = '__module';
16
53
  * server evaluates it in a sandboxed GraalVM context with no module resolution of its own (see
17
54
  * `BackendModuleRuntimeService`).
18
55
  *
19
- * Returns `null` when the workspace has no backend part configured, so `build.js` can skip it
20
- * entirely for a frontend-only module.
56
+ * 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 (`bundleJs` null).
58
+ *
59
+ * Returns `null` when the workspace has neither a backend part nor entities, so `build.js` can skip
60
+ * it entirely for a frontend-only module.
21
61
  */
22
62
  async function buildBackend(cwd, config) {
23
63
  const backendConfig = config.backend;
24
- if (!backendConfig) {
64
+ const entities = entitiesOf(config);
65
+ const bundleJs = backendConfig ? await bundle(cwd, backendConfig) : null;
66
+ if (bundleJs == null && entities.length === 0) {
25
67
  return null;
26
68
  }
69
+ const settings = backendConfig || {};
70
+ const manifest = {
71
+ // localAllowedHosts (e.g. a mock on localhost) only ever go into a --local publish, never into one for a real environment.
72
+ allowedHosts: [...(settings.allowedHosts || []), ...(config.local ? settings.localAllowedHosts || [] : [])],
73
+ secretFieldNames: settings.secretFieldNames || [],
74
+ triggers: bundleJs == null ? [] : settings.triggers || [],
75
+ entities,
76
+ // the module gives persons roles (tabbr.access.setManagedRoles) - shown to the club when it installs the module
77
+ manageRoles: bundleJs != null && settings.manageRoles === true,
78
+ };
79
+
80
+ return { bundleJs, manifestJson: JSON.stringify(manifest, null, 2) };
81
+ }
82
+
83
+ /** The backend entry bundled into one script, or null when there is no entry file (and none was configured explicitly). */
84
+ async function bundle(cwd, backendConfig) {
27
85
  const entry = backendConfig.entry || DEFAULT_ENTRY;
28
86
  const entryPath = path.join(cwd, entry);
29
87
  if (!fs.existsSync(entryPath)) {
@@ -59,16 +117,7 @@ async function buildBackend(cwd, config) {
59
117
  if (!result.outputFiles || result.outputFiles.length === 0) {
60
118
  throw new Error(`esbuild produced no output for ${entryPath}`);
61
119
  }
62
- const bundleJs = result.outputFiles[0].text;
63
-
64
- const manifest = {
65
- // localAllowedHosts (e.g. a mock on localhost) only ever go into a --local publish, never into one for a real environment.
66
- allowedHosts: [...(backendConfig.allowedHosts || []), ...(config.local ? backendConfig.localAllowedHosts || [] : [])],
67
- secretFieldNames: backendConfig.secretFieldNames || [],
68
- triggers: backendConfig.triggers || [],
69
- };
70
-
71
- return { bundleJs, manifestJson: JSON.stringify(manifest, null, 2) };
120
+ return result.outputFiles[0].text;
72
121
  }
73
122
 
74
- module.exports = { buildBackend, DEFAULT_ENTRY };
123
+ module.exports = { buildBackend, entitiesOf, DEFAULT_ENTRY };
package/src/build.js CHANGED
@@ -78,7 +78,9 @@ function zipModule(frontendDir, backend) {
78
78
  archive.directory(frontendDir, false);
79
79
  }
80
80
  if (backend) {
81
- archive.append(backend.bundleJs, { name: 'backend/bundle.js' });
81
+ if (backend.bundleJs != null) {
82
+ archive.append(backend.bundleJs, { name: 'backend/bundle.js' });
83
+ }
82
84
  archive.append(backend.manifestJson, { name: 'backend/manifest.json' });
83
85
  }
84
86
  archive.finalize();
package/src/publish.js CHANGED
@@ -28,4 +28,76 @@ async function publish({ apiHost, club, moduleId, exposedModule, exportName, ico
28
28
  return response.json();
29
29
  }
30
30
 
31
- module.exports = { publish };
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 };