bare-stow 0.0.0 → 0.1.1

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,219 @@
1
+ const path = require('path')
2
+ const { fileURLToPath } = require('url')
3
+ const pack = require('bare-pack')
4
+ const id = require('bare-bundle-id')
5
+ const strip = require('bare-type-stripper')
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
+ const resolve = require('./lib/resolve')
11
+
12
+ module.exports = async function* stow(entry, target, out, opts = {}) {
13
+ if (!target) throw new Error("'target' is required")
14
+ if (!out) throw new Error("'out' is required")
15
+
16
+ let { client, server, base, hosts, resolveTarget, resolveRPC, ...packOpts } = opts
17
+
18
+ const t = harness(target, resolveTarget)
19
+
20
+ const targetName = typeof target === 'string' ? target : t.name
21
+
22
+ entry = new URL(entry)
23
+ out = new URL(out)
24
+ base = base ? new URL(base) : new URL('./', entry)
25
+
26
+ if (hosts) {
27
+ for (const host of hosts) {
28
+ if (!t.hosts.includes(host)) {
29
+ throw new Error(`Host '${host}' is not supported by target '${targetName}'`)
30
+ }
31
+ }
32
+ } else {
33
+ hosts = t.hosts
34
+ }
35
+
36
+ const shimURL = shim.url(base)
37
+
38
+ // The server only contributes runtime wiring to the shim; the shim is not a
39
+ // typed, host-facing module so it has no declaration artifact.
40
+ const serverSetup = server
41
+ ? fragment(
42
+ rpc(server, resolveRPC).generate({
43
+ ipc: 'ipc',
44
+ rpc: 'rpc',
45
+ module: 'esm',
46
+ role: 'server'
47
+ }),
48
+ null
49
+ )
50
+ : null
51
+
52
+ const shimSource = shim(entry, shimURL, { server: serverSetup })
53
+
54
+ const readModule = wrapReadModule(fs.readModule, shimURL, shimSource)
55
+
56
+ const bundleURL = siblingURL(out, t.extension)
57
+ const bundleSpecifier = relativeSpecifier(out, bundleURL)
58
+
59
+ const offloaded = []
60
+
61
+ let writeFile
62
+
63
+ if (isOffloadEnabled(t.offload)) writeFile = collectOffloaded(base, out, offloaded)
64
+
65
+ const bundle = await pack(
66
+ shimURL,
67
+ {
68
+ ...packOpts,
69
+ resolve,
70
+ aliases: {
71
+ '.ts': '.js',
72
+ '.mts': '.mjs',
73
+ '.cts': '.cjs'
74
+ },
75
+ base,
76
+ hosts,
77
+ linked: t.linked,
78
+ offload: t.offload
79
+ },
80
+ readModule,
81
+ fs.listPrefix,
82
+ writeFile
83
+ )
84
+
85
+ bundle.id = id(bundle).toString('hex')
86
+
87
+ // The client contributes both runtime wiring (spliced into the harness) and a
88
+ // type expression (spliced into the harness declaration), so it is resolved
89
+ // into both channels before handing it to the target.
90
+ let clientSetup = null
91
+
92
+ if (client) {
93
+ const artifacts = rpc(client, resolveRPC).generate({
94
+ ipc: 'ipc',
95
+ rpc: 'rpc',
96
+ module: t.module,
97
+ role: 'client'
98
+ })
99
+
100
+ clientSetup = {
101
+ source: fragment(artifacts, null),
102
+ type: fragment(artifacts, '.d.ts')
103
+ }
104
+ }
105
+
106
+ const artifacts = t.generate({
107
+ bundleSpecifier,
108
+ ipc: 'ipc',
109
+ rpc: 'rpc',
110
+ client: clientSetup
111
+ })
112
+
113
+ // The first artifact is the harness written to `out`; any further artifacts
114
+ // are siblings named after `out` with the artifact's own extension.
115
+ for (const artifact of artifacts) {
116
+ const url = artifact.extension ? siblingURL(out, artifact.extension) : out
117
+
118
+ await fs.writeFile(url, artifact.source)
119
+ yield { url }
120
+ }
121
+
122
+ await fs.writeFile(bundleURL, encodeBundle(bundle, t.format, t.encoding ?? 'utf8'))
123
+ yield { url: bundleURL }
124
+
125
+ for (const artifact of offloaded) {
126
+ await fs.writeFile(artifact.url, artifact.source)
127
+ yield { url: artifact.url }
128
+ }
129
+ }
130
+
131
+ function isOffloadEnabled(offload) {
132
+ if (offload === true) return true
133
+ if (offload && (offload.addons || offload.assets)) return true
134
+
135
+ return false
136
+ }
137
+
138
+ function wrapReadModule(readModule, shimURL, shimSource) {
139
+ return async function (url) {
140
+ if (url.href === shimURL.href) return shimSource
141
+
142
+ const source = await readModule(url)
143
+
144
+ if (source === null) return null
145
+
146
+ if (/\.(c|m)?ts$/.test(url.pathname)) return strip(source)
147
+
148
+ return source
149
+ }
150
+ }
151
+
152
+ function collectOffloaded(base, out, sink) {
153
+ const dir = new URL('./', out)
154
+
155
+ return function writeFile(url, source) {
156
+ let relative
157
+
158
+ const nm = url.pathname.indexOf('/node_modules/')
159
+
160
+ if (nm >= 0) {
161
+ relative = url.pathname.slice(nm + 1)
162
+ } else if (url.pathname.startsWith(base.pathname)) {
163
+ relative = url.pathname.slice(base.pathname.length)
164
+ } else {
165
+ relative = url.pathname.replace(/^\//, '')
166
+ }
167
+
168
+ sink.push({ url: new URL(relative, dir), source })
169
+
170
+ return null
171
+ }
172
+ }
173
+
174
+ function siblingURL(out, extension) {
175
+ const dir = new URL('./', out)
176
+ const base = path.basename(out.pathname, path.extname(out.pathname))
177
+
178
+ return new URL(base + extension, dir)
179
+ }
180
+
181
+ function fragment(artifacts, extension) {
182
+ for (const artifact of artifacts) {
183
+ if ((artifact.extension ?? null) === extension) return artifact.source
184
+ }
185
+
186
+ return null
187
+ }
188
+
189
+ function relativeSpecifier(from, to) {
190
+ if (from.protocol === 'file:' && to.protocol === 'file:') {
191
+ const fromDir = path.dirname(fileURLToPath(from))
192
+ const toPath = fileURLToPath(to)
193
+
194
+ let relative = path.relative(fromDir, toPath).split(path.sep).join('/')
195
+
196
+ if (!relative.startsWith('.')) relative = './' + relative
197
+
198
+ return relative
199
+ }
200
+
201
+ return to.href
202
+ }
203
+
204
+ function encodeBundle(bundle, format, encoding) {
205
+ const data = bundle.toBuffer()
206
+
207
+ switch (format) {
208
+ case 'bundle':
209
+ return data
210
+ case 'bundle.cjs':
211
+ return `module.exports = ${JSON.stringify(data.toString(encoding))}\n`
212
+ case 'bundle.mjs':
213
+ return `export default ${JSON.stringify(data.toString(encoding))}\n`
214
+ case 'bundle.json':
215
+ return JSON.stringify(data.toString(encoding)) + '\n'
216
+ default:
217
+ throw new Error(`Unknown format '${format}'`)
218
+ }
219
+ }
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
+ }
package/lib/resolve.js ADDED
@@ -0,0 +1,40 @@
1
+ const lex = require('bare-module-lexer')
2
+ const { resolve } = require('bare-module-traverse')
3
+
4
+ module.exports = function bare(entry, parentURL, opts = {}) {
5
+ const { linked = false, hosts } = opts
6
+
7
+ let extensions
8
+ let conditions = hosts.map((host) => ['bare', 'node', ...host.split('-')])
9
+
10
+ if (entry.type & lex.constants.ADDON) {
11
+ extensions = linked ? [] : ['.bare', '.node']
12
+ conditions = conditions.map((conditions) => ['addon', ...conditions])
13
+
14
+ return resolve.addon(entry.specifier || '.', parentURL, {
15
+ ...opts,
16
+ extensions,
17
+ conditions,
18
+ hosts,
19
+ linked
20
+ })
21
+ }
22
+
23
+ if (entry.type & lex.constants.ASSET) {
24
+ conditions = conditions.map((conditions) => ['asset', ...conditions])
25
+ } else {
26
+ extensions = ['.js', '.cjs', '.mjs', '.ts', '.cts', '.mts', '.json', '.bare', '.node']
27
+
28
+ if (entry.type & lex.constants.REQUIRE) {
29
+ conditions = conditions.map((conditions) => ['require', ...conditions])
30
+ } else if (entry.type & lex.constants.IMPORT) {
31
+ conditions = conditions.map((conditions) => ['import', ...conditions])
32
+ }
33
+ }
34
+
35
+ return resolve.module(entry.specifier, parentURL, {
36
+ ...opts,
37
+ extensions,
38
+ conditions
39
+ })
40
+ }
@@ -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
+ }