bare-stow 0.1.9 → 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
@@ -1,8 +1,6 @@
1
1
  # bare-stow
2
2
 
3
- Module bundling and harness generation for Bare. Built on top of `bare-pack` (<https://github.com/holepunchto/bare-pack>), `bare-stow` produces a bundle for a target runtime together with a small harness that boots it from the host environment.
4
-
5
- A [CLI](#cli) is also included and provides support for stowing a module graph from the command line.
3
+ Module bundling and harness generation for Bare. `bare-stow` packs a module graph into a bundle with <https://github.com/holepunchto/bare-pack> and generates a harness package that boots the bundle from a host application written in JavaScript, Kotlin, Python, or Swift.
6
4
 
7
5
  ```
8
6
  npm i [-g] bare-stow
@@ -10,7 +8,7 @@ npm i [-g] bare-stow
10
8
 
11
9
  ## Usage
12
10
 
13
- Given an entry module `core.js` that exports a `start` function receiving the IPC stream and an optional `ready` callback. The function may optionally return (or resolve to) a function that runs before the bundle exits:
11
+ The entry module exports a `start()` function that receives the IPC stream and an optional `ready` callback. It may return a function to run before the bundle exits:
14
12
 
15
13
  ```js
16
14
  module.exports = async function start(ipc, ready) {
@@ -18,8 +16,7 @@ module.exports = async function start(ipc, ready) {
18
16
  // Handle messages from the host.
19
17
  })
20
18
 
21
- // Optionally call `ready()` to signal readiness early. Otherwise it is
22
- // signaled automatically when `start()` resolves.
19
+ // Signal readiness early. Otherwise, it's signaled when `start()` resolves.
23
20
  ready()
24
21
 
25
22
  return async function stop() {
@@ -28,155 +25,137 @@ module.exports = async function start(ipc, ready) {
28
25
  }
29
26
  ```
30
27
 
31
- The `ipc` argument is a duplex byte stream. Anything written to it is delivered to the host, and `'data'` events fire when the host writes back.
28
+ If a `server` is requested, `start()` instead receives `{ rpc, ipc }`, with `rpc` bound to the IPC stream. Its handlers are registered in `start()`, before the bundle signals ready:
29
+
30
+ ```js
31
+ module.exports = function start({ rpc }) {
32
+ rpc.respond(1, (req, data) => data)
33
+ }
34
+ ```
32
35
 
33
- Stow it for the `bare-sidecar` target:
36
+ Stow it for a target:
34
37
 
35
38
  ```js
36
39
  const stow = require('bare-stow')
37
40
 
38
- const entry = new URL('file:///app/core.js')
39
- const out = new URL('file:///app/out/index.js')
41
+ const entry = 'file:///app/core.js'
42
+ const out = 'file:///app/core-harness/'
40
43
 
41
44
  for await (const artifact of stow(entry, 'bare-sidecar', out)) {
42
45
  console.log(artifact.url.href)
43
46
  }
44
47
  ```
45
48
 
46
- Or equivalently from the command line:
49
+ Or from the command line:
47
50
 
48
51
  ```console
49
- $ bare-stow --target bare-sidecar --out ./out/index.js ./core.js
52
+ $ bare-stow pack --target bare-sidecar --out ./core-harness ./core.js
50
53
  ```
51
54
 
52
- This writes the harness to `out` and the bundle alongside it. The harness can then be required from the host and booted with `start()`. The harness awaits the worker's `ready` signal before resolving, so by the time `start()` returns the bundle is up:
55
+ This writes a package to `out` with the harness, the bundle, and a manifest declaring the dependencies of the harness. `stow()` yields each file once written, and the bundle along with its `base`, the directory that its modules are named relative to. It's the `base` option if given, and otherwise the closest directory containing every module. Add the package as a dependency of the host and boot the bundle with `start()`, which resolves once the bundle is ready:
53
56
 
54
57
  ```js
55
- const harness = require('/app/out/index.js')
58
+ import * as harness from 'core-harness'
56
59
 
57
60
  const { ipc } = await harness.start()
58
61
 
59
62
  ipc.write(Buffer.from('hello from the host'))
60
63
 
61
- ipc.on('exit', (code) => {
62
- console.log('bundle exited with', code)
63
- })
64
-
65
- // Request a graceful shutdown.
66
- ipc.destroy()
64
+ // Request a graceful shutdown, which resolves with the exit code.
65
+ const code = await ipc.terminate()
67
66
  ```
68
67
 
69
- The harness imports its runtime by name, `bare-stow/host` along with `bare-sidecar` or `bare-worker` depending on the target, so the package that the harness is written into must depend on both. Relying on another package to bring them in only works while a package manager happens to hoist them, and not at all under an isolated layout such as that of pnpm. `stow()` resolves every import of the harness from `out` before writing anything and throws if one of them can't be found.
68
+ The bundle imports `bare-stow-runtime`, and the RPC library if a `server` is requested, so the package containing the entry must depend on them.
70
69
 
71
- For the `bare-sidecar` target, `start()` accepts the options of `bare-sidecar`. The sidecar inherits the standard streams of the host unless `stdio` says otherwise.
70
+ `start()` accepts `timeout`, how long to wait in milliseconds for the bundle to signal ready, and `grace`, how long a terminating bundle gets to run `stop()` and exit before it's killed. `timeout` defaults to waiting indefinitely and `grace` to 5 seconds. On Windows, a sidecar is killed along with its host, so `stop()` only runs when the host terminates it.
72
71
 
73
- `start()` also accepts `timeout`, the number of milliseconds to wait for the worker to signal ready before rejecting and terminating it, and `grace`, the number of milliseconds a terminating worker gets to run `stop()` and exit before it is killed. `timeout` defaults to waiting indefinitely and `grace` to 5 seconds. The worker holds itself to the same `grace` whenever it shuts down, including when it receives `SIGTERM` or the host goes away without terminating it. On Windows, a sidecar is killed along with its host instead, so `stop()` only runs when the host terminates it.
72
+ `stow()` and `link()` read the module graph from disk by default. To read it from elsewhere, such as modules that aren't written to disk, pass `readModule`, `listPrefix`, and `resolveModule`. Generated files are still written to disk. A `readModule` that changes the type of a module also passes `aliases`, as for <https://github.com/holepunchto/bare-pack>, such as `{ '.mjs': '.js' }` to have ES modules converted to CommonJS loaded as such. They're merged over the aliases that `bare-stow` uses for TypeScript.
74
73
 
75
- ## Protocol
76
-
77
- The harness multiplexes a control channel and a user data channel over the underlying binary duplex stream. The `ipc` returned from `start()` is itself a duplex stream carrying the user data; control frames ride alongside it on the same handle. RPC libraries (such as `bare-rpc`) bind to `ipc` just like a raw stream.
78
-
79
- Control frames signal lifecycle events:
74
+ ## Targets
80
75
 
81
- | Direction | Type | Payload |
82
- | :------------- | :---------- | :------------------- |
83
- | Worker -> Host | `ready` | - |
84
- | Worker -> Host | `exit` | `{ code }` |
85
- | Worker -> Host | `error` | `{ message, stack }` |
86
- | Host -> Worker | `terminate` | - |
76
+ - `bare-sidecar` runs the bundle in a separate process using <https://github.com/holepunchto/bare-sidecar>, with the `javascript`, `python`, and `swift` languages on desktop. In JavaScript and Python, `start()` also accepts the options of `bare-sidecar`.
77
+ - `bare-worker` runs the bundle in a thread of a Bare host using <https://github.com/holepunchto/bare-worker>, with the `javascript` language.
78
+ - `bare-kit` runs the bundle in a worklet using <https://github.com/holepunchto/bare-kit>, with the `swift` language on macOS and iOS and the `kotlin` language on Android and the JVM. `start()` also accepts the options of the worklet and returns it alongside `ipc`, and on Android takes the `Context` first. In Kotlin, the worklet is closed once it exits, so it needs no closing of its own, and in Swift it's released with its last reference. Native addons are linked with <https://github.com/holepunchto/bare-link> and declared in the manifest, and assets are written next to the bundle. With the target option `linking` set to `'app'`, both are left to `link()` instead. On the JVM, addons and assets are written next to the bundle as files, which `start()` extracts along with the bundle to a temporary directory, so `linking` must be `'package'`.
87
79
 
88
- An incoming `error` frame destroys the local `ipc` with the carried error, surfacing it as a normal stream `error` event. Other control frames emit a regular event named after the type.
80
+ ## Linking
89
81
 
90
- The `ipc` handle exposes the lifecycle as events and methods on the same duplex:
82
+ A target may leave addons and assets to the application, so that bundles sharing them don't each bring a copy. For `bare-kit`, stow each bundle with the target option `linking` set to `'app'`:
91
83
 
92
- - `ipc.ready`: A promise that resolves once the worker has signaled ready. Rejects if the worker errors before ready.
93
- - `ipc.on('exit', code)`: Emitted when the worker signals exit. `code` carries the exit code.
94
- - `ipc.on('error', err)`: Emitted when the worker reports a fault, when it exits without saying why, such as through `Bare.exit()`, a signal, or a crash, or when the underlying transport errors. Either way the `ipc` is destroyed. For a worker that exited without saying why, `err.exitCode` and `err.signalCode` carry the reason.
95
- - `ipc.on('close')`: Emitted when the underlying transport closes.
96
- - `ipc.destroy()`: Standard stream destroy. Sends a `terminate` control frame to the worker and waits for the underlying transport to close.
97
- - `ipc.terminate()`: Like `destroy()` but resolves with the worker's exit code.
84
+ ```console
85
+ $ bare-stow pack --target bare-kit --target-option linking=app --language swift --out ./ios/Core ./core.js
86
+ ```
98
87
 
99
- The framing layer is also exported directly for embedders that bring their own transport:
88
+ `link()` then links the addons and assets of all bundles once, into a package the application depends on:
100
89
 
101
90
  ```js
102
- const protocol = require('bare-stow/protocol')
103
-
104
- const ipc = protocol.attach(stream) // Any duplex byte stream
105
- ipc.write(Buffer.from('payload')) // User data
106
- ipc.send('ready') // Control frame
107
- ipc.on('exit', () => {
108
- /* ... */
109
- })
91
+ const entries = [new URL('file:///app/core.js'), new URL('file:///app/push.js')]
92
+ const out = new URL('file:///app/ios/BareKitAddons/')
93
+
94
+ for await (const artifact of stow.link(entries, 'bare-kit', out, { language: 'swift' })) {
95
+ console.log(artifact.url.href)
96
+ }
110
97
  ```
111
98
 
112
- The host helper layers the lifecycle promise and `terminate` method on top of the protocol:
99
+ The language lays out the package and the target adds what loads the addons. For `bare-kit`, that's `bare-kit-swift` or `bare-kit-kotlin`. The assets go in a folder named by `stow.resources`, listed in a file of the same name with the `.assets` extension, which a Swift application copies into its bundle. `link()` takes the same options as `stow()`, except `client` and `base`.
113
100
 
114
- ```js
115
- const { wrap } = require('bare-stow/host')
101
+ ## Languages
102
+
103
+ The harness is generated in one of these languages, selected with `language` and configured with `languageOptions`:
104
+
105
+ - `javascript` writes an npm package. The `module` option is either `'esm'`, the default, or `'cjs'`.
106
+ - `kotlin` writes a Gradle module. The `package` option names the package of the generated code and is required. The `platform` option is either `'android'`, the default, for an Android library module, or `'jvm'` for a Kotlin/JVM library module on Linux, macOS, and Windows. The dependencies are published to JitPack (<https://jitpack.io>), so add it to the repositories of the application.
107
+ - `python` writes a Python package.
108
+ - `swift` writes a Swift package.
109
+
110
+ For example, with an RPC client and server:
116
111
 
117
- const ipc = wrap(stream) // Any duplex byte stream
118
- await ipc.ready
112
+ ```console
113
+ $ bare-stow pack --target bare-sidecar --language python --client bare-rpc --server bare-rpc --out ./my-core ./core.js
119
114
  ```
120
115
 
121
- ## Targets and RPC providers
116
+ ```python
117
+ import asyncio
122
118
 
123
- The harness and RPC code generators are decoupled from the bundler: `target`, `client`, and `server` each accept a provider object directly, so generators can live outside `bare-stow`. Passing a name instead resolves it through `resolveTarget` / `resolveRPC`, which default to the built-in registries.
119
+ import my_core
124
120
 
125
- Both kinds of provider return an ordered array of artifacts. An artifact is `{ extension?, source }`:
126
121
 
127
- ```js
128
- artifacts = [
129
- { source }, // The primary output.
130
- { extension, source } // A companion, named after the primary plus this extension.
131
- ]
132
- ```
122
+ async def main():
123
+ ipc, rpc = await my_core.start(
124
+ handlers=lambda router: router.on_request(2, lambda req: b"hello")
125
+ )
133
126
 
134
- A target provider describes the runtime profile it owns and generates the harness that boots the bundle from the host:
127
+ await rpc.request(1, b"ping")
128
+ await ipc.terminate()
135
129
 
136
- ```js
137
- target = {
138
- name, // The target's name, used in diagnostics.
139
- linked, // Whether assets and addons are linked into the bundle.
140
- offload, // Whether (and which) assets and addons are offloaded to a `<bundle>.d` directory next to the bundle.
141
- format, // The bundle encoding: 'bundle', 'bundle.cjs', 'bundle.mjs', or 'bundle.json'.
142
- encoding, // The string encoding for encoded formats, or null for raw bytes.
143
- extension, // The bundle file extension, e.g. '.bundle'.
144
- module, // Optional. Pins the harness module system to 'esm' or 'cjs'.
145
- hosts, // The host triples the target supports.
146
- generate({ bundleSpecifier, ipc, rpc, module, client }) {
147
- // Return an array of harness artifacts.
148
- }
149
- }
130
+
131
+ asyncio.run(main())
150
132
  ```
151
133
 
152
- For a target, each artifact is a file. The first has no `extension` and is written to `out`; each remaining artifact is written next to `out` with its own `extension` substituted for the harness extension (e.g. `{ extension: '.d.ts' }` becomes `index.d.ts`). The built-in targets emit two artifacts: The harness and its TypeScript declaration.
134
+ With an RPC client, `start()` takes the handlers for the requests and events of the bundle, so that they are registered before the bundle can send any.
153
135
 
154
- `generate()` receives `bundleSpecifier`, the relative specifier from the harness to the bundle artifact; `ipc`, the identifier the harness should bind the IPC stream to; `rpc`, the identifier the RPC client setup binds its instance to; `module`, the harness module system (`'esm'` or `'cjs'`) so the harness can emit the matching module and import forms; and `client`, the resolved RPC client (or `null` when no client is requested). When present, `client` is `{ source, type }`: `source` is the runtime setup to splice into the harness (referencing `ipc` and binding `rpc`), and `type` is the TypeScript type of the bound instance to splice into the declaration. The target owns `linked`, `offload`, `format`, `encoding`, and `extension`; they are not overridable through `opts`.
136
+ ## Protocol
155
137
 
156
- The harness `module` system is derived from the output path: `out` ending in `.mjs` is ESM and `.cjs` is CommonJS, while an ambiguous `.js` (or other) extension follows the `type` of the closest enclosing `package.json` (`"module"` is ESM, otherwise CommonJS). A target may set its own `module` to pin the harness to one system regardless of the output path.
138
+ The harness and the bundle multiplex a control channel and the user data over a single duplex stream. The `ipc` returned from `start()` is a duplex stream of the user data, so RPC libraries such as `bare-rpc` bind to it like any other stream. Embedders bringing their own transport can use the protocol directly:
157
139
 
158
- An RPC provider generates the wiring that binds an RPC instance to the IPC stream, used on both the client (harness) and server (shim) side:
140
+ - <https://github.com/holepunchto/bare-stow-protocol> implements the framing.
141
+ - <https://github.com/holepunchto/bare-stow-host> implements the host side and documents `ipc`, with <https://github.com/holepunchto/bare-stow-host-kotlin>, <https://github.com/holepunchto/bare-stow-host-python>, and <https://github.com/holepunchto/bare-stow-host-swift> for the other languages.
142
+ - <https://github.com/holepunchto/bare-stow-runtime> implements the bundle side.
159
143
 
160
- ```js
161
- rpc = {
162
- name, // The library's name, used in diagnostics.
163
- generate({ ipc, rpc, module, role }) {
164
- // Return an array of RPC artifacts.
165
- }
166
- }
167
- ```
144
+ ## Providers
168
145
 
169
- For an RPC provider, each artifact is a fragment contributed to the harness file with the matching extension. The artifact with no `extension` is the runtime setup (reading `ipc`, binding `rpc`); the `{ extension: '.d.ts' }` artifact is the TypeScript type of the bound instance. `generate()` receives `ipc`, the identifier of the IPC stream in scope; `rpc`, the identifier to bind the RPC instance to; `module`, the surrounding module system (`'esm'` or `'cjs'`) so the runtime fragment can emit the matching import form; and `role`, either `'client'` or `'server'`. The bundler owns the `ipc` and `rpc` binding names and threads them to both the target and the RPC provider so neither has to assume them. It renders the client in the harness `module` and the server as `'esm'` (the shim is always an ES module), splicing the runtime fragment and, for the client, the type fragment into the target's artifacts. The shim is not a typed, host-facing module, so the server's type fragment is unused.
146
+ Languages, targets, and RPC libraries are providers that may live outside `bare-stow`. `stow()` and `link()` accept a provider object in place of a name, and names other than the built-in ones are resolved through `resolveLanguage`, `resolveTarget`, and `resolveRPC`. For example, <https://github.com/holepunchto/bare-stow-target-react-native> is a target for React Native. Targets and RPC libraries generate code for each language they list under `languages`, and providers without an entry for the language of the harness are rejected. Targets may also take options, given as `targetOptions`. The server side of an RPC library is always generated as JavaScript, as the bundle runs in Bare. See `index.d.ts` for the interfaces.
170
147
 
171
148
  ## CLI
172
149
 
173
- #### `bare-stow [flags] <entry>`
150
+ #### `bare-stow pack [flags] <entry>`
174
151
 
175
- Stow the module graph rooted at `<entry>`, writing the harness and bundle to the path given by `--out`.
152
+ Stow the module graph rooted at `<entry>` into the directory given by `--out`.
176
153
 
177
154
  ```console
178
- --version|-v
179
155
  --target <name>
156
+ --target-option <key=value>
157
+ --language <name>
158
+ --language-option <key=value>
180
159
  --client <name>
181
160
  --server <name>
182
161
  --base <path>
@@ -188,25 +167,27 @@ Stow the module graph rooted at `<entry>`, writing the harness and bundle to the
188
167
  --help|-h
189
168
  ```
190
169
 
191
- ##### `--target <name>`
192
-
193
- The target runtime. Required. The built-in `bare-sidecar` and `bare-worker` are resolved directly; any other `<name>` is loaded from its `bare-stow-target-<name>` package, or from `<name>` itself as a full module specifier, resolved from the working directory.
194
-
195
- ##### `--client <name>` and `--server <name>`
196
-
197
- The RPC library to wire into the harness (`--client`) or the bundle entry shim (`--server`). The built-in `bare-rpc` is resolved directly; any other `<name>` is loaded from its `bare-stow-rpc-<name>` package, or from `<name>` itself as a full module specifier, resolved from the working directory.
198
-
199
- ##### `--builtins <path>` and `--imports <path>`
170
+ #### `bare-stow link [flags] <entry...>`
200
171
 
201
- Paths to JavaScript or JSON files exporting a list of builtin module names and a map of global import overrides respectively. Forwarded to `bare-pack`.
172
+ Link the addons and assets of the module graphs rooted at each `<entry>` into the package given by `--out`.
202
173
 
203
- ##### `--defer <specifier>`
204
-
205
- A module specifier whose resolution should be deferred. May be passed multiple times.
174
+ ```console
175
+ --target <name>
176
+ --target-option <key=value>
177
+ --language <name>
178
+ --language-option <key=value>
179
+ --server <name>
180
+ --out|-o <path>
181
+ --builtins <path>
182
+ --imports <path>
183
+ --defer <specifier>
184
+ --host <name>
185
+ --help|-h
186
+ ```
206
187
 
207
- ##### `--host <name>`
188
+ `--target`, `--language`, `--client`, and `--server` take a built-in name, the name of a `bare-stow-target-<name>`, `bare-stow-language-<name>`, or `bare-stow-rpc-<name>` package, or a module specifier, resolved from the working directory. `--target-option`, `--language-option`, `--defer`, and `--host` may be passed multiple times. `--host` must name hosts the target supports, and defaults to all of them.
208
189
 
209
- A host triple to build for. Must be a subset of the host triples supported by the target. May be passed multiple times. Defaults to all host triples supported by the target.
190
+ `--builtins` and `--imports` are paths to JavaScript or JSON files exporting a list of builtin modules and a map of import overrides, which are forwarded to `bare-pack`.
210
191
 
211
192
  ## License
212
193
 
package/bin.js CHANGED
@@ -1,79 +1,116 @@
1
1
  #!/usr/bin/env node
2
2
  const path = require('path')
3
3
  const { pathToFileURL } = require('url')
4
- const { command, flag, arg, summary } = require('paparam')
4
+ const { command, flag, arg, rest, summary } = require('paparam')
5
5
  const pkg = require('./package')
6
6
  const stow = require('.')
7
7
 
8
- const cmd = command(
9
- pkg.name,
10
- summary(pkg.description),
8
+ const flags = {
9
+ target: () =>
10
+ flag(
11
+ '--target <name>',
12
+ 'The target runtime (bare-kit, bare-sidecar, bare-worker, or a bare-stow-target-<name> package)'
13
+ ),
14
+ targetOption: () => flag('--target-option <key=value>', 'An option for the target').multiple(),
15
+ language: () =>
16
+ flag(
17
+ '--language <name>',
18
+ 'The language of the harness (javascript, kotlin, python, swift, or a bare-stow-language-<name> package; defaults to javascript)'
19
+ ),
20
+ languageOption: () =>
21
+ flag(
22
+ '--language-option <key=value>',
23
+ 'An option for the language, such as module=cjs for javascript'
24
+ ).multiple(),
25
+ server: () =>
26
+ flag(
27
+ '--server <name>',
28
+ 'The RPC server to include (bare-rpc, or a bare-stow-rpc-<name> package)'
29
+ ),
30
+ builtins: () => flag('--builtins <path>', 'A list of builtin modules'),
31
+ imports: () => flag('--imports <path>', 'A map of global import overrides'),
32
+ defer: () => flag('--defer <specifier>', 'A module specifier to defer resolution of').multiple(),
33
+ host: () =>
34
+ flag('--host <name>', 'A host triple to include (must be a subset of the target)').multiple()
35
+ }
36
+
37
+ const link = command(
38
+ 'link',
39
+ summary('Link the addons and assets of the module graphs of an application'),
40
+ rest('<entry...>', 'The entry points of the module graphs'),
41
+ flags.target(),
42
+ flags.targetOption(),
43
+ flags.language(),
44
+ flags.languageOption(),
45
+ flags.server(),
46
+ flag('--out|-o <path>', 'The directory to write the package of linked addons and assets to'),
47
+ flags.builtins(),
48
+ flags.imports(),
49
+ flags.defer(),
50
+ flags.host(),
51
+ async (cmd) => {
52
+ const entries = cmd.rest || []
53
+ const { target, language, server, out, defer, host: hosts } = cmd.flags
54
+
55
+ if (!target) throw new Error('--target is required')
56
+ if (!out) throw new Error('--out is required')
57
+ if (entries.length === 0) throw new Error('<entry...> is required')
58
+
59
+ for await (const _ of stow.link(
60
+ entries.map((entry) => pathToFileURL(entry)),
61
+ target,
62
+ pathToFileURL(out),
63
+ {
64
+ ...common(cmd.flags),
65
+ language,
66
+ server,
67
+ hosts,
68
+ defer
69
+ }
70
+ )) {
71
+ //
72
+ }
73
+ }
74
+ )
75
+
76
+ const pack = command(
77
+ 'pack',
78
+ summary('Stow a module graph as a bundle and the harness that boots it'),
11
79
  arg('<entry>', 'The entry point of the module graph'),
12
- flag('--version|-v', 'Print the current version'),
13
- flag(
14
- '--target <name>',
15
- 'The target runtime (bare-sidecar, or a bare-stow-target-<name> package)'
16
- ),
80
+ flags.target(),
81
+ flags.targetOption(),
82
+ flags.language(),
83
+ flags.languageOption(),
17
84
  flag(
18
85
  '--client <name>',
19
86
  'The RPC client to include (bare-rpc, or a bare-stow-rpc-<name> package)'
20
87
  ),
21
- flag(
22
- '--server <name>',
23
- 'The RPC server to include (bare-rpc, or a bare-stow-rpc-<name> package)'
24
- ),
88
+ flags.server(),
25
89
  flag(
26
90
  '--base <path>',
27
91
  'The base path of the bundle (defaults to the closest directory containing every bundled module)'
28
92
  ),
29
- flag('--out|-o <path>', 'The output path of the harness'),
30
- flag('--builtins <path>', 'A list of builtin modules'),
31
- flag('--imports <path>', 'A map of global import overrides'),
32
- flag('--defer <specifier>', 'A module specifier to defer resolution of').multiple(),
33
- flag('--host <name>', 'A host triple to include (must be a subset of the target)').multiple(),
93
+ flag('--out|-o <path>', 'The directory to write the harness package to'),
94
+ flags.builtins(),
95
+ flags.imports(),
96
+ flags.defer(),
97
+ flags.host(),
34
98
  async (cmd) => {
35
99
  const { entry } = cmd.args
36
- let {
37
- version,
38
- target,
39
- client,
40
- server,
41
- base,
42
- out,
43
- builtins,
44
- imports,
45
- defer,
46
- host: hosts
47
- } = cmd.flags
48
-
49
- if (version) return console.log(`v${pkg.version}`)
100
+ let { target, language, client, server, base, out, defer, host: hosts } = cmd.flags
50
101
 
51
102
  if (!target) throw new Error('--target is required')
52
103
  if (!out) throw new Error('--out is required')
53
104
 
54
- if (builtins) {
55
- builtins = require(path.resolve(builtins))
56
-
57
- if ('default' in builtins) builtins = builtins.default
58
- }
59
-
60
- if (imports) {
61
- imports = require(path.resolve(imports))
62
-
63
- if ('default' in imports) imports = imports.default
64
- }
65
-
66
105
  if (base) base = pathToFileURL(path.resolve(base) + '/')
67
106
 
68
107
  for await (const _ of stow(pathToFileURL(entry), target, pathToFileURL(out), {
108
+ ...common(cmd.flags),
109
+ language,
69
110
  client,
70
111
  server,
71
112
  base,
72
113
  hosts,
73
- resolveTarget,
74
- resolveRPC,
75
- builtins,
76
- imports,
77
114
  defer
78
115
  })) {
79
116
  //
@@ -81,19 +118,59 @@ const cmd = command(
81
118
  }
82
119
  )
83
120
 
121
+ // The options shared by pack and link.
122
+ function common(flags) {
123
+ return {
124
+ languageOptions: keyValues('--language-option', flags.languageOption),
125
+ targetOptions: keyValues('--target-option', flags.targetOption),
126
+ builtins: load(flags.builtins),
127
+ imports: load(flags.imports),
128
+ resolveTarget,
129
+ resolveRPC,
130
+ resolveLanguage
131
+ }
132
+ }
133
+
134
+ function keyValues(name, options = []) {
135
+ const result = {}
136
+
137
+ for (const option of options) {
138
+ const i = option.indexOf('=')
139
+
140
+ if (i === -1) throw new Error(`${name} must be <key>=<value>, got '${option}'`)
141
+
142
+ result[option.slice(0, i)] = option.slice(i + 1)
143
+ }
144
+
145
+ return result
146
+ }
147
+
148
+ function load(file) {
149
+ if (!file) return undefined
150
+
151
+ const value = require(path.resolve(file))
152
+
153
+ return 'default' in value ? value.default : value
154
+ }
155
+
84
156
  // Built-in names are resolved by bare-stow itself; any other name is loaded
85
- // from its `bare-stow-target-<name>` / `bare-stow-rpc-<name>` package, or as a
86
- // full module specifier, resolved from the working directory.
157
+ // from its `bare-stow-target-<name>`, `bare-stow-rpc-<name>`, or
158
+ // `bare-stow-language-<name>` package, or as a full module specifier, resolved
159
+ // from the working directory.
87
160
 
88
161
  function resolveTarget(name) {
89
- return load('bare-stow-target', name)
162
+ return resolveProvider('bare-stow-target', name)
90
163
  }
91
164
 
92
165
  function resolveRPC(name) {
93
- return load('bare-stow-rpc', name)
166
+ return resolveProvider('bare-stow-rpc', name)
94
167
  }
95
168
 
96
- function load(prefix, name) {
169
+ function resolveLanguage(name) {
170
+ return resolveProvider('bare-stow-language', name)
171
+ }
172
+
173
+ function resolveProvider(prefix, name) {
97
174
  for (const specifier of [`${prefix}-${name}`, name]) {
98
175
  try {
99
176
  return require(require.resolve(specifier, { paths: [process.cwd()] }))
@@ -105,4 +182,16 @@ function load(prefix, name) {
105
182
  throw new Error(`Cannot resolve '${name}'`)
106
183
  }
107
184
 
185
+ const cmd = command(
186
+ pkg.name,
187
+ summary(pkg.description),
188
+ flag('--version|-v', 'Print the current version'),
189
+ pack,
190
+ link,
191
+ ({ flags }) => {
192
+ if (flags.version) console.log(`v${pkg.version}`)
193
+ else console.log(cmd.help())
194
+ }
195
+ )
196
+
108
197
  cmd.parse()