bare-stow 0.0.0 → 0.1.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/index.js ADDED
@@ -0,0 +1,207 @@
1
+ const path = require('path')
2
+ const { fileURLToPath } = require('url')
3
+ const pack = require('bare-pack')
4
+ const { resolve } = require('bare-module-traverse')
5
+ const id = require('bare-bundle-id')
6
+ const fs = require('./lib/fs')
7
+ const shim = require('./lib/shim')
8
+ const harness = require('./lib/target')
9
+ const rpc = require('./lib/rpc')
10
+
11
+ module.exports = async function* stow(entry, target, out, opts = {}) {
12
+ if (!target) throw new Error("'target' is required")
13
+ if (!out) throw new Error("'out' is required")
14
+
15
+ let { client, server, base, hosts, resolveTarget, resolveRPC, ...packOpts } = opts
16
+
17
+ const t = harness(target, resolveTarget)
18
+
19
+ const targetName = typeof target === 'string' ? target : t.name
20
+
21
+ entry = new URL(entry)
22
+ out = new URL(out)
23
+ base = base ? new URL(base) : new URL('./', entry)
24
+
25
+ if (hosts) {
26
+ for (const host of hosts) {
27
+ if (!t.hosts.includes(host)) {
28
+ throw new Error(`Host '${host}' is not supported by target '${targetName}'`)
29
+ }
30
+ }
31
+ } else {
32
+ hosts = t.hosts
33
+ }
34
+
35
+ const shimURL = shim.url(base)
36
+
37
+ // The server only contributes runtime wiring to the shim; the shim is not a
38
+ // typed, host-facing module so it has no declaration artifact.
39
+ const serverSetup = server
40
+ ? fragment(
41
+ rpc(server, resolveRPC).generate({
42
+ ipc: 'ipc',
43
+ rpc: 'rpc',
44
+ module: 'esm',
45
+ role: 'server'
46
+ }),
47
+ null
48
+ )
49
+ : null
50
+
51
+ const shimSource = shim(entry, shimURL, { server: serverSetup })
52
+
53
+ const readModule = wrapReadModule(fs.readModule, shimURL, shimSource)
54
+
55
+ const bundleURL = siblingURL(out, t.extension)
56
+ const bundleSpecifier = relativeSpecifier(out, bundleURL)
57
+
58
+ const offloaded = []
59
+
60
+ let writeFile
61
+
62
+ if (isOffloadEnabled(t.offload)) writeFile = collectOffloaded(base, out, offloaded)
63
+
64
+ const bundle = await pack(
65
+ shimURL,
66
+ {
67
+ resolve: resolve.bare,
68
+ ...packOpts,
69
+ hosts,
70
+ linked: t.linked,
71
+ offload: t.offload,
72
+ base: base
73
+ },
74
+ readModule,
75
+ fs.listPrefix,
76
+ writeFile
77
+ )
78
+
79
+ bundle.id = id(bundle).toString('hex')
80
+
81
+ // The client contributes both runtime wiring (spliced into the harness) and a
82
+ // type expression (spliced into the harness declaration), so it is resolved
83
+ // into both channels before handing it to the target.
84
+ let clientSetup = null
85
+
86
+ if (client) {
87
+ const artifacts = rpc(client, resolveRPC).generate({
88
+ ipc: 'ipc',
89
+ rpc: 'rpc',
90
+ module: t.module,
91
+ role: 'client'
92
+ })
93
+
94
+ clientSetup = {
95
+ source: fragment(artifacts, null),
96
+ type: fragment(artifacts, '.d.ts')
97
+ }
98
+ }
99
+
100
+ const artifacts = t.generate({
101
+ bundleSpecifier,
102
+ ipc: 'ipc',
103
+ rpc: 'rpc',
104
+ client: clientSetup
105
+ })
106
+
107
+ // The first artifact is the harness written to `out`; any further artifacts
108
+ // are siblings named after `out` with the artifact's own extension.
109
+ for (const artifact of artifacts) {
110
+ const url = artifact.extension ? siblingURL(out, artifact.extension) : out
111
+
112
+ await fs.writeFile(url, artifact.source)
113
+ yield { url }
114
+ }
115
+
116
+ await fs.writeFile(bundleURL, encodeBundle(bundle, t.format, t.encoding ?? 'utf8'))
117
+ yield { url: bundleURL }
118
+
119
+ for (const artifact of offloaded) {
120
+ await fs.writeFile(artifact.url, artifact.source)
121
+ yield { url: artifact.url }
122
+ }
123
+ }
124
+
125
+ function isOffloadEnabled(offload) {
126
+ if (offload === true) return true
127
+ if (offload && (offload.addons || offload.assets)) return true
128
+
129
+ return false
130
+ }
131
+
132
+ function wrapReadModule(readModule, shimURL, shimSource) {
133
+ return function (url) {
134
+ if (url.href === shimURL.href) return shimSource
135
+
136
+ return readModule(url)
137
+ }
138
+ }
139
+
140
+ function collectOffloaded(base, out, sink) {
141
+ const dir = new URL('./', out)
142
+
143
+ return function writeFile(url, source) {
144
+ let relative
145
+
146
+ const nm = url.pathname.indexOf('/node_modules/')
147
+
148
+ if (nm >= 0) {
149
+ relative = url.pathname.slice(nm + 1)
150
+ } else if (url.pathname.startsWith(base.pathname)) {
151
+ relative = url.pathname.slice(base.pathname.length)
152
+ } else {
153
+ relative = url.pathname.replace(/^\//, '')
154
+ }
155
+
156
+ sink.push({ url: new URL(relative, dir), source })
157
+
158
+ return null
159
+ }
160
+ }
161
+
162
+ function siblingURL(out, extension) {
163
+ const dir = new URL('./', out)
164
+ const base = path.basename(out.pathname, path.extname(out.pathname))
165
+
166
+ return new URL(base + extension, dir)
167
+ }
168
+
169
+ function fragment(artifacts, extension) {
170
+ for (const artifact of artifacts) {
171
+ if ((artifact.extension ?? null) === extension) return artifact.source
172
+ }
173
+
174
+ return null
175
+ }
176
+
177
+ function relativeSpecifier(from, to) {
178
+ if (from.protocol === 'file:' && to.protocol === 'file:') {
179
+ const fromDir = path.dirname(fileURLToPath(from))
180
+ const toPath = fileURLToPath(to)
181
+
182
+ let relative = path.relative(fromDir, toPath).split(path.sep).join('/')
183
+
184
+ if (!relative.startsWith('.')) relative = './' + relative
185
+
186
+ return relative
187
+ }
188
+
189
+ return to.href
190
+ }
191
+
192
+ function encodeBundle(bundle, format, encoding) {
193
+ const data = bundle.toBuffer()
194
+
195
+ switch (format) {
196
+ case 'bundle':
197
+ return data
198
+ case 'bundle.cjs':
199
+ return `module.exports = ${JSON.stringify(data.toString(encoding))}\n`
200
+ case 'bundle.mjs':
201
+ return `export default ${JSON.stringify(data.toString(encoding))}\n`
202
+ case 'bundle.json':
203
+ return JSON.stringify(data.toString(encoding)) + '\n'
204
+ default:
205
+ throw new Error(`Unknown format '${format}'`)
206
+ }
207
+ }
package/lib/fs.js ADDED
@@ -0,0 +1,78 @@
1
+ const { fileURLToPath } = require('url')
2
+ const fs = require('fs')
3
+
4
+ exports.writeFile = async function writeFile(url, data) {
5
+ await exports.makeDir(new URL('.', url))
6
+
7
+ return new Promise((resolve, reject) => {
8
+ fs.writeFile(fileURLToPath(url), data, (err) => {
9
+ err ? reject(err) : resolve()
10
+ })
11
+ })
12
+ }
13
+
14
+ exports.readFile = function readFile(url) {
15
+ return new Promise((resolve, reject) => {
16
+ fs.readFile(fileURLToPath(url), (err, data) => {
17
+ err ? reject(err) : resolve(data)
18
+ })
19
+ })
20
+ }
21
+
22
+ exports.makeDir = function makeDir(url) {
23
+ return new Promise((resolve, reject) => {
24
+ fs.mkdir(fileURLToPath(url), { recursive: true }, (err) => {
25
+ err ? reject(err) : resolve()
26
+ })
27
+ })
28
+ }
29
+
30
+ exports.openDir = function openDir(url) {
31
+ return new Promise((resolve, reject) => {
32
+ fs.opendir(fileURLToPath(url), (err, dir) => {
33
+ err ? reject(err) : resolve(dir)
34
+ })
35
+ })
36
+ }
37
+
38
+ exports.readModule = async function readModule(url) {
39
+ try {
40
+ return await exports.readFile(url)
41
+ } catch {
42
+ return null
43
+ }
44
+ }
45
+
46
+ function isFile(url) {
47
+ return new Promise((resolve) => {
48
+ fs.stat(fileURLToPath(url), (err, stat) => {
49
+ resolve(err === null && stat.isFile())
50
+ })
51
+ })
52
+ }
53
+
54
+ function isDir(url) {
55
+ return new Promise((resolve) => {
56
+ fs.stat(fileURLToPath(url), (err, stat) => {
57
+ resolve(err === null && stat.isDirectory())
58
+ })
59
+ })
60
+ }
61
+
62
+ exports.listPrefix = async function* listPrefix(url) {
63
+ if (await isFile(url)) return yield url
64
+
65
+ if (url.pathname[url.pathname.length - 1] !== '/') {
66
+ url.pathname += '/'
67
+ }
68
+
69
+ if (await isDir(url)) {
70
+ for await (const entry of await exports.openDir(url)) {
71
+ if (entry.isDirectory()) {
72
+ yield* listPrefix(new URL(entry.name, url))
73
+ } else {
74
+ yield new URL(entry.name, url)
75
+ }
76
+ }
77
+ }
78
+ }
package/lib/host.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { Duplex } from 'bare-stream'
2
+ import { Protocol } from './protocol'
3
+
4
+ interface IPC extends Protocol {
5
+ readonly ready: Promise<void>
6
+ terminate(): Promise<number | undefined>
7
+ }
8
+
9
+ declare class IPC {
10
+ constructor(stream: Duplex)
11
+ }
12
+
13
+ declare function wrap(stream: Duplex): IPC
14
+
15
+ declare namespace wrap {
16
+ export { IPC }
17
+ }
18
+
19
+ export = wrap
package/lib/host.js ADDED
@@ -0,0 +1,62 @@
1
+ const { Protocol } = require('./protocol')
2
+
3
+ class IPC extends Protocol {
4
+ constructor(stream) {
5
+ super(stream)
6
+
7
+ this._ready = new Promise((resolve, reject) => {
8
+ const onready = () => {
9
+ this.off('ready', onready).off('error', onerror)
10
+ resolve()
11
+ }
12
+
13
+ const onerror = (err) => {
14
+ this.off('ready', onready).off('error', onerror)
15
+ reject(err)
16
+ }
17
+
18
+ this.on('ready', onready).on('error', onerror)
19
+ })
20
+
21
+ this._ready.catch(() => {})
22
+ }
23
+
24
+ get ready() {
25
+ return this._ready
26
+ }
27
+
28
+ terminate() {
29
+ return new Promise((resolve) => {
30
+ const cleanup = () => {
31
+ this.off('exit', onexit)
32
+ this._stream.off('close', onclose)
33
+ }
34
+
35
+ const onexit = (code) => {
36
+ cleanup()
37
+ resolve(code)
38
+ }
39
+
40
+ const onclose = () => {
41
+ cleanup()
42
+ resolve(undefined)
43
+ }
44
+
45
+ this.once('exit', onexit).send('terminate')
46
+ this._stream.once('close', onclose)
47
+ })
48
+ }
49
+
50
+ _destroy(err, cb) {
51
+ if (this._stream.destroyed) return cb(null)
52
+
53
+ this.send('terminate')
54
+ this._stream.once('close', () => cb(null))
55
+ }
56
+ }
57
+
58
+ exports.IPC = IPC
59
+
60
+ exports.wrap = function wrap(stream) {
61
+ return new IPC(stream)
62
+ }
@@ -0,0 +1,16 @@
1
+ import { Duplex } from 'bare-stream'
2
+
3
+ declare const CONTROL: number
4
+ declare const USER: number
5
+
6
+ interface Protocol extends Duplex {
7
+ send(type: string, payload?: object): Promise<void>
8
+ }
9
+
10
+ declare class Protocol {
11
+ constructor(stream: Duplex)
12
+ }
13
+
14
+ declare function attach(stream: Duplex): Protocol
15
+
16
+ export { type Protocol, attach, CONTROL, USER }
@@ -0,0 +1,129 @@
1
+ const { Duplex, Writable } = require('bare-stream')
2
+
3
+ const CONTROL = 0x00
4
+ const USER = 0x01
5
+
6
+ exports.CONTROL = CONTROL
7
+ exports.USER = USER
8
+
9
+ const HEADER_LENGTH = 5
10
+
11
+ class Protocol extends Duplex {
12
+ constructor(stream) {
13
+ super()
14
+
15
+ this._stream = stream
16
+ this._buffer = []
17
+ this._buffered = 0
18
+ this._frame = -1
19
+
20
+ stream
21
+ .on('data', (chunk) => this._ondata(chunk))
22
+ .on('end', () => this.push(null))
23
+ .on('close', () => this.destroy())
24
+ .on('error', (err) => this.destroy(err))
25
+ }
26
+
27
+ send(type, payload) {
28
+ const frame = encodeFrame(CONTROL, Buffer.from(JSON.stringify({ type, ...payload })))
29
+
30
+ this._stream.write(frame)
31
+
32
+ return Writable.drained(this._stream)
33
+ }
34
+
35
+ _write(data, encoding, cb) {
36
+ const frame = encodeFrame(USER, data)
37
+
38
+ const ok = this._stream.write(frame)
39
+
40
+ if (ok) return cb(null)
41
+
42
+ this._stream.once('drain', () => cb(null))
43
+ }
44
+
45
+ _destroy(err, cb) {
46
+ this._stream.destroy()
47
+ cb(null)
48
+ }
49
+
50
+ _ondata(chunk) {
51
+ this._buffer.push(chunk)
52
+ this._buffered += chunk.byteLength
53
+
54
+ if (this._frame === -1) this._onbeforeframe()
55
+ else this._onafterframe()
56
+ }
57
+
58
+ _onbeforeframe() {
59
+ if (this._buffered < 4) return
60
+
61
+ const buffer = this._buffer.length === 1 ? this._buffer[0] : Buffer.concat(this._buffer)
62
+
63
+ this._buffer = [buffer]
64
+ this._frame = 4 + buffer.readUInt32LE(0)
65
+
66
+ this._onafterframe()
67
+ }
68
+
69
+ _onafterframe() {
70
+ if (this._buffered < this._frame) return
71
+
72
+ const buffer = this._buffer.length === 1 ? this._buffer[0] : Buffer.concat(this._buffer)
73
+ const frame = this._frame
74
+
75
+ this._buffered -= frame
76
+ this._buffer = this._buffered > 0 ? [buffer.subarray(frame)] : []
77
+ this._frame = -1
78
+
79
+ this._onframe(buffer.subarray(0, frame))
80
+ this._onbeforeframe()
81
+ }
82
+
83
+ _onframe(frame) {
84
+ const tag = frame.readUInt8(4)
85
+ const payload = frame.subarray(HEADER_LENGTH)
86
+
87
+ if (tag === USER) {
88
+ this.push(Buffer.from(payload))
89
+ } else if (tag === CONTROL) {
90
+ let message
91
+
92
+ try {
93
+ message = JSON.parse(payload.toString())
94
+ } catch {
95
+ return
96
+ }
97
+
98
+ if (message.type === 'error') {
99
+ const err = new Error(message.message)
100
+
101
+ if (message.stack) err.stack = message.stack
102
+
103
+ this.destroy(err)
104
+ } else if (message.type === 'exit') {
105
+ this.emit('exit', message.code)
106
+ } else {
107
+ this.emit(message.type, message)
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ exports.Protocol = Protocol
114
+
115
+ exports.attach = function attach(stream) {
116
+ return new Protocol(stream)
117
+ }
118
+
119
+ function encodeFrame(tag, data) {
120
+ const length = data.length + 1
121
+ const frame = Buffer.alloc(HEADER_LENGTH + data.length)
122
+
123
+ frame.writeUInt32LE(length, 0)
124
+ frame.writeUInt8(tag, 4)
125
+
126
+ data.copy(frame, HEADER_LENGTH)
127
+
128
+ return frame
129
+ }
@@ -0,0 +1,24 @@
1
+ exports.name = 'bare-rpc'
2
+
3
+ exports.generate = function generate(opts) {
4
+ const { ipc, rpc } = opts
5
+
6
+ const esm = opts.module === 'esm'
7
+
8
+ return [
9
+ {
10
+ source: `\
11
+ ${esm ? "const { default: RPC } = await import('bare-rpc')" : "const RPC = require('bare-rpc')"}
12
+
13
+ const router = new RPC.CommandRouter()
14
+ const ${rpc} = new RPC(${ipc}, router)
15
+
16
+ ${rpc}.respond = router.respond.bind(router)
17
+ `
18
+ },
19
+ {
20
+ extension: '.d.ts',
21
+ source: "import('bare-rpc')"
22
+ }
23
+ ]
24
+ }
package/lib/rpc.js ADDED
@@ -0,0 +1,23 @@
1
+ const registry = {
2
+ 'bare-rpc': () => require('./rpc/bare-rpc')
3
+ }
4
+
5
+ module.exports = function rpc(name, resolve) {
6
+ if (typeof name !== 'string') return validate(name)
7
+
8
+ const load = registry[name]
9
+
10
+ if (load) return load()
11
+
12
+ if (resolve) return validate(resolve(name))
13
+
14
+ throw new Error(`Unknown RPC library '${name}'`)
15
+ }
16
+
17
+ function validate(rpc) {
18
+ if (!rpc || typeof rpc.generate !== 'function') {
19
+ throw new Error('RPC library must provide a generate() function')
20
+ }
21
+
22
+ return rpc
23
+ }
package/lib/shim.js ADDED
@@ -0,0 +1,86 @@
1
+ const path = require('path')
2
+ const { fileURLToPath } = require('url')
3
+
4
+ module.exports = exports = function shim(entry, shimURL, opts = {}) {
5
+ // `server` is a pre-rendered RPC server setup fragment (binding `rpc`), or
6
+ // null. The bundler resolves the RPC library and renders it; the shim only
7
+ // decides where the fragment goes.
8
+ const { server = null } = opts
9
+
10
+ const specifier = relativeSpecifier(shimURL, entry)
11
+
12
+ const setup = server
13
+
14
+ const arg = setup ? '{ rpc, ipc }' : 'ipc'
15
+
16
+ return `\
17
+ import protocol from 'bare-stow/protocol'
18
+
19
+ const ipc = protocol.attach(Bare.IPC)
20
+ ${setup ? '\n' + setup : ''}
21
+ let stop = null
22
+ let readied = false
23
+ let exited = false
24
+
25
+ function ready() {
26
+ if (readied) return
27
+ readied = true
28
+
29
+ ipc.send('ready')
30
+ }
31
+
32
+ async function shutdown(code, err = null) {
33
+ if (exited) return
34
+ exited = true
35
+
36
+ try {
37
+ if (stop) await stop()
38
+ } catch (e) {
39
+ err = err || e
40
+ }
41
+
42
+ if (err) ipc.send('error', { message: err.message, stack: err.stack })
43
+
44
+ await ipc.send('exit', { code })
45
+
46
+ Bare.exit(code)
47
+ }
48
+
49
+ ipc.on('terminate', () => shutdown(0))
50
+
51
+ Bare
52
+ .on('beforeExit', (exitCode) => shutdown(exitCode))
53
+ .on('uncaughtException', (err) => shutdown(1, err))
54
+ .on('unhandledRejection', (err) => shutdown(1, err))
55
+
56
+ try {
57
+ const entry = await import(${JSON.stringify(specifier)})
58
+ const start = entry.default
59
+
60
+ stop = await start(${arg}, ready) || null
61
+
62
+ ready()
63
+ } catch (err) {
64
+ shutdown(1, err)
65
+ }
66
+ `
67
+ }
68
+
69
+ exports.url = function url(base) {
70
+ return new URL('__main__.mjs', base)
71
+ }
72
+
73
+ function relativeSpecifier(from, to) {
74
+ if (from.protocol === 'file:' && to.protocol === 'file:') {
75
+ const fromPath = path.dirname(fileURLToPath(from))
76
+ const toPath = fileURLToPath(to)
77
+
78
+ let relative = path.relative(fromPath, toPath).split(path.sep).join('/')
79
+
80
+ if (!relative.startsWith('.')) relative = './' + relative
81
+
82
+ return relative
83
+ }
84
+
85
+ return to.href
86
+ }
@@ -0,0 +1,63 @@
1
+ exports.name = 'bare-sidecar'
2
+ exports.linked = false
3
+ exports.offload = true
4
+ exports.format = 'bundle'
5
+ exports.encoding = null
6
+ exports.extension = '.bundle'
7
+ exports.module = 'cjs'
8
+ exports.hosts = [
9
+ 'darwin-arm64',
10
+ 'darwin-x64',
11
+ 'linux-arm64',
12
+ 'linux-x64',
13
+ 'win32-arm64',
14
+ 'win32-x64'
15
+ ]
16
+
17
+ exports.generate = function generate(opts) {
18
+ const { bundleSpecifier, ipc, rpc, client = null } = opts
19
+
20
+ const returned = client ? `{ ${ipc}, ${rpc} }` : `{ ${ipc} }`
21
+
22
+ const handle = client
23
+ ? `{ ${ipc}: import('bare-stow/host').IPC; ${rpc}: ${client.type} }`
24
+ : `{ ${ipc}: import('bare-stow/host').IPC }`
25
+
26
+ return [
27
+ {
28
+ source: `\
29
+ const path = require('path')
30
+ const Sidecar = require('bare-sidecar')
31
+ const stow = require('bare-stow/host')
32
+
33
+ const bundle = path.join(__dirname, ${JSON.stringify(bundleSpecifier)})
34
+
35
+ module.exports = {
36
+ async start(opts = {}) {
37
+ const sidecar = new Sidecar(bundle, opts)
38
+
39
+ const ${ipc} = stow.wrap(sidecar)
40
+ ${client ? '\n' + indent(client.source, 4) : ''}
41
+ await ${ipc}.ready
42
+
43
+ return ${returned}
44
+ }
45
+ }
46
+ `
47
+ },
48
+ {
49
+ extension: '.d.ts',
50
+ source: `\
51
+ export function start(opts?: import('bare-sidecar').SidecarOptions): Promise<${handle}>
52
+ `
53
+ }
54
+ ]
55
+ }
56
+
57
+ function indent(source, spaces) {
58
+ const pad = ' '.repeat(spaces)
59
+ return source
60
+ .split('\n')
61
+ .map((line) => (line.length === 0 ? line : pad + line))
62
+ .join('\n')
63
+ }