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/index.js CHANGED
@@ -1,89 +1,111 @@
1
1
  const path = require('path')
2
- const { fileURLToPath } = require('url')
3
2
  const pack = require('bare-pack')
4
3
  const id = require('bare-bundle-id')
5
- const strip = require('bare-type-stripper')
6
- const { resolve } = require('bare-module-traverse')
7
- const lex = require('bare-module-lexer')
8
4
  const fs = require('./lib/fs')
5
+ const assets = require('./lib/assets')
9
6
  const shim = require('./lib/shim')
10
- const harness = require('./lib/target')
11
- const rpc = require('./lib/rpc')
7
+ const graph = require('./lib/graph')
8
+ const rpcs = require('./lib/rpc')
9
+ const { select } = require('./lib/provider')
12
10
 
13
- module.exports = async function* stow(entry, target, out, opts = {}) {
11
+ module.exports = exports = async function* stow(entry, target, out, opts = {}) {
14
12
  if (!target) throw new Error("'target' is required")
15
13
  if (!out) throw new Error("'out' is required")
16
14
 
17
- let { client, server, base, hosts, resolveTarget, resolveRPC, ...packOpts } = opts
15
+ let {
16
+ language,
17
+ languageOptions,
18
+ targetOptions,
19
+ client,
20
+ server,
21
+ base,
22
+ hosts,
23
+ resolveTarget,
24
+ resolveRPC,
25
+ resolveLanguage,
26
+ readModule = fs.readModule,
27
+ listPrefix = fs.listPrefix,
28
+ resolveModule = fs.resolveModule,
29
+ ...packOpts
30
+ } = opts
31
+
32
+ entry = await resolveModule(new URL(entry))
33
+ out = new URL(out)
18
34
 
19
- const t = harness(target, resolveTarget)
35
+ if (!out.pathname.endsWith('/')) out.pathname += '/'
20
36
 
21
- const targetName = typeof target === 'string' ? target : t.name
37
+ const resolved = graph.providers(target, {
38
+ language,
39
+ languageOptions,
40
+ targetOptions,
41
+ hosts,
42
+ resolveTarget,
43
+ resolveLanguage
44
+ })
22
45
 
23
- entry = await fs.resolveModule(new URL(entry))
24
- out = new URL(out)
46
+ const { harness, options } = resolved
25
47
 
26
- // The target may pin a module system; otherwise it follows the output path.
27
- const module = t.module || (await resolveModule(out))
48
+ target = resolved.target
49
+ language = resolved.language
50
+ hosts = resolved.hosts
28
51
 
29
- if (hosts) {
30
- for (const host of hosts) {
31
- if (!t.hosts.includes(host)) {
32
- throw new Error(`Host '${host}' is not supported by target '${targetName}'`)
33
- }
52
+ if (client) client = select('RPC library', rpcs(client, resolveRPC), language.name)
53
+ if (server) server = rpcs(server, resolveRPC)
54
+
55
+ const layout = language.layout(out, options)
56
+
57
+ const bundleURL = new URL('index' + target.extension, layout.artifacts ?? layout.harness)
58
+
59
+ // Linked bundles leave their assets out, as files that the runtime loads from
60
+ // next to the bundle or from the application.
61
+ const resources = target.linked && target.linking === 'app' ? assets.app : null
62
+ const name = resources ?? 'index'
63
+
64
+ if (client) {
65
+ client = {
66
+ url: language.sibling(layout.harness, 'client'),
67
+ ...client.generate({ role: 'client', options })
34
68
  }
35
- } else {
36
- hosts = t.hosts
37
69
  }
38
70
 
39
- const shimURL = shim.url(new URL('./', entry))
40
-
41
- // The server only contributes runtime wiring to the shim; the shim is not a
42
- // typed, host-facing module so it has no declaration artifact.
43
- const serverSetup = server
44
- ? fragment(
45
- rpc(server, resolveRPC).generate({
46
- ipc: 'ipc',
47
- rpc: 'rpc',
48
- module: 'esm',
49
- role: 'server'
50
- }),
51
- null
52
- )
53
- : null
71
+ const files = [
72
+ {
73
+ url: layout.harness,
74
+ ...harness.generate({
75
+ bundle: language.reference(layout.harness, bundleURL),
76
+ client: client && language.reference(layout.harness, client.url),
77
+ resources,
78
+ options
79
+ })
80
+ }
81
+ ]
54
82
 
55
- const shimSource = shim(entry, shimURL, { server: serverSetup })
83
+ if (client) files.push(client)
56
84
 
57
- const readModule = wrapReadModule(fs.readModule, shimURL, shimSource)
85
+ const main = shim(entry, target.isolation, server)
58
86
 
59
- const bundleURL = siblingURL(out, t.extension)
60
- const bundleSpecifier = relativeSpecifier(out, bundleURL)
87
+ const hooks = {
88
+ readModule: graph.readModule(main.modules, readModule),
89
+ listPrefix,
90
+ resolveModule
91
+ }
61
92
 
62
93
  const offloaded = []
63
94
 
64
95
  let writeFile
65
96
 
66
- if (isOffloadEnabled(t.offload)) writeFile = collectOffloaded(offloaded)
97
+ if (isOffloadEnabled(target.offload)) writeFile = collectOffloaded(offloaded)
98
+
99
+ const traversal = graph.traversal(packOpts, hosts)
67
100
 
68
101
  // The base of the bundle depends on where its modules live, so the graph is
69
102
  // packed unbased and unmounted once that is known.
70
103
  let bundle = await pack(
71
- shimURL,
72
- {
73
- ...packOpts,
74
- resolve: resolve.bare,
75
- aliases: {
76
- '.ts': '.js',
77
- '.mts': '.mjs',
78
- '.cts': '.cjs'
79
- },
80
- hosts,
81
- linked: t.linked,
82
- offload: t.offload
83
- },
84
- readModule,
85
- fs.listPrefix,
86
- fs.resolveModule,
104
+ main.url,
105
+ { ...traversal, linked: target.linked, offload: target.offload },
106
+ hooks.readModule,
107
+ hooks.listPrefix,
108
+ hooks.resolveModule,
87
109
  writeFile
88
110
  )
89
111
 
@@ -91,50 +113,72 @@ module.exports = async function* stow(entry, target, out, opts = {}) {
91
113
 
92
114
  bundle = rerootOffloaded(bundle.unmount(base), base, bundleURL, offloaded)
93
115
 
116
+ let unpacked = []
117
+
118
+ if (target.linked) {
119
+ const result = await assets.unpack(bundle, base, name, hooks.readModule)
120
+
121
+ bundle = result.bundle
122
+
123
+ // Assets left to the application are written by `link()` instead.
124
+ if (resources === null) unpacked = result.assets
125
+ }
126
+
94
127
  bundle.id = id(bundle).toString('hex')
95
128
 
96
- // The client contributes both runtime wiring (spliced into the harness) and a
97
- // type expression (spliced into the harness declaration), so it is resolved
98
- // into both channels before handing it to the target.
99
- let clientSetup = null
129
+ // The manifest lists the offloaded files only once it's known there are any.
130
+ const artifacts = offloaded.length > 0 ? [bundleURL, offloadURL(bundleURL)] : [bundleURL]
100
131
 
101
- if (client) {
102
- const artifacts = rpc(client, resolveRPC).generate({
103
- ipc: 'ipc',
104
- rpc: 'rpc',
105
- role: 'client',
106
- module
107
- })
132
+ if (unpacked.length > 0) {
133
+ artifacts.push(new URL(name + '/', bundleURL), new URL(name + '.assets', bundleURL))
134
+ }
135
+
136
+ if (layout.listing) artifacts.push(listingURL(bundleURL))
108
137
 
109
- clientSetup = {
110
- source: fragment(artifacts, null),
111
- type: fragment(artifacts, '.d.ts')
138
+ const addons = []
139
+
140
+ // Targets that leave linking to the application use `link()` instead.
141
+ if (target.linked && target.linking === 'package') {
142
+ if (!layout.addons) {
143
+ throw new Error(`Language '${language.name}' does not support linked addons`)
144
+ }
145
+
146
+ for await (const url of graph.link([main.url], layout.addons, traversal, hooks)) {
147
+ addons.push(url)
148
+ yield { url }
112
149
  }
113
150
  }
114
151
 
115
- const artifacts = t.generate({
116
- bundleSpecifier,
117
- ipc: 'ipc',
118
- rpc: 'rpc',
119
- module,
120
- client: clientSetup
152
+ files.push({
153
+ url: layout.manifest,
154
+ source: language.manifest({
155
+ name: layout.name,
156
+ dependencies: Object.assign({}, ...files.map((file) => file.dependencies)),
157
+ options,
158
+ hosts,
159
+ artifacts: artifacts.map((url) => language.reference(layout.manifest, url)),
160
+ addons: addons.map((url) => language.reference(layout.manifest, url))
161
+ })
121
162
  })
122
163
 
123
- for (const artifact of artifacts) {
124
- if (!artifact.extension) await assertImportsResolve(artifact.source, out)
125
- }
164
+ for (const file of files) {
165
+ await fs.writeFile(file.url, file.source)
166
+ yield { url: file.url }
126
167
 
127
- // The first artifact is the harness written to `out`; any further artifacts
128
- // are siblings named after `out` with the artifact's own extension.
129
- for (const artifact of artifacts) {
130
- const url = artifact.extension ? siblingURL(out, artifact.extension) : out
168
+ if (file.types) {
169
+ const typesURL = language.types(file.url)
131
170
 
132
- await fs.writeFile(url, artifact.source)
133
- yield { url }
171
+ await fs.writeFile(typesURL, file.types)
172
+ yield { url: typesURL }
173
+ }
134
174
  }
135
175
 
136
- await fs.writeFile(bundleURL, encodeBundle(bundle, t.format, t.encoding ?? 'utf8'))
137
- yield { url: bundleURL }
176
+ await fs.writeFile(bundleURL, encodeBundle(bundle, target.format, target.encoding ?? 'utf8'))
177
+ yield { url: bundleURL, base }
178
+
179
+ if (unpacked.length > 0) {
180
+ for await (const url of assets.write(new URL('./', bundleURL), name, unpacked)) yield { url }
181
+ }
138
182
 
139
183
  // Offloaded files are collected in the order their reads complete, so they
140
184
  // are sorted to keep the output stable.
@@ -144,10 +188,29 @@ module.exports = async function* stow(entry, target, out, opts = {}) {
144
188
  await fs.writeFile(artifact.url, artifact.source)
145
189
  yield { url: artifact.url }
146
190
  }
191
+
192
+ if (layout.listing) {
193
+ const url = listingURL(bundleURL)
194
+ const dir = path.posix.dirname(bundleURL.pathname)
195
+
196
+ const listing = offloaded.map(
197
+ (artifact) => '/' + decodeURIComponent(path.posix.relative(dir, artifact.url.pathname)) + '\n'
198
+ )
199
+
200
+ await fs.writeFile(url, listing.join(''))
201
+ yield { url }
202
+ }
147
203
  }
148
204
 
149
- function resolveBase(base, graph) {
150
- const modules = [...graph.keys()].map((href) => new URL(href))
205
+ // Languages that package the bundle where the runtime can't read it, such as
206
+ // in a jar, list the files beside it, one `/<path>` per line, for the harness to
207
+ // extract along with the bundle.
208
+ function listingURL(bundleURL) {
209
+ return new URL(path.basename(bundleURL.pathname) + '.files', bundleURL)
210
+ }
211
+
212
+ function resolveBase(base, bundle) {
213
+ const modules = [...bundle.keys()].map((href) => new URL(href))
151
214
 
152
215
  if (base) {
153
216
  base = new URL(base)
@@ -191,20 +254,6 @@ function isOffloadEnabled(offload) {
191
254
  return false
192
255
  }
193
256
 
194
- function wrapReadModule(readModule, shimURL, shimSource) {
195
- return async function (url) {
196
- if (url.href === shimURL.href) return shimSource
197
-
198
- const source = await readModule(url)
199
-
200
- if (source === null) return null
201
-
202
- if (/\.(c|m)?ts$/.test(url.pathname)) return strip(source)
203
-
204
- return source
205
- }
206
- }
207
-
208
257
  function collectOffloaded(sink) {
209
258
  return function writeFile(url, source) {
210
259
  sink.push({ url, source })
@@ -217,11 +266,15 @@ function collectOffloaded(sink) {
217
266
  // next to the harness, as a mirrored `node_modules` beside the harness would
218
267
  // shadow the packages it imports. Their resolutions are rewritten to point into
219
268
  // that directory, along with the directories that hold them.
269
+ function offloadURL(bundleURL) {
270
+ return new URL(path.basename(bundleURL.pathname) + '.d/', bundleURL)
271
+ }
272
+
220
273
  function rerootOffloaded(bundle, base, bundleURL, offloaded) {
221
274
  if (offloaded.length === 0) return bundle
222
275
 
223
- const name = path.basename(bundleURL.pathname) + '.d'
224
- const dir = new URL(name + '/', bundleURL)
276
+ const dir = offloadURL(bundleURL)
277
+ const name = path.basename(dir.pathname)
225
278
  const rewrites = new Map()
226
279
 
227
280
  for (const file of offloaded) {
@@ -288,107 +341,6 @@ function isTerminator(input) {
288
341
  return input === '' || input.endsWith('/') || input.endsWith(':')
289
342
  }
290
343
 
291
- async function assertImportsResolve(source, out) {
292
- for (const entry of lex(source).imports) {
293
- const { specifier } = entry
294
-
295
- if (specifier.startsWith('.') || specifier.startsWith('/')) continue
296
-
297
- if (await resolves(entry, out)) continue
298
-
299
- throw new Error(
300
- `Harness '${fileURLToPath(out)}' imports '${specifier}', which the package containing it does not depend on`
301
- )
302
- }
303
- }
304
-
305
- async function resolves(entry, parentURL) {
306
- const resolver = resolve.bare(entry, parentURL)
307
-
308
- let next = resolver.next()
309
-
310
- while (next.done !== true) {
311
- const value = next.value
312
-
313
- if (value.package) {
314
- const source = await fs.readModule(value.package)
315
-
316
- next = resolver.next(source === null ? null : JSON.parse(source))
317
- } else {
318
- const exists = await fs.isFile(value.resolution)
319
-
320
- if (exists) return true
321
-
322
- next = resolver.next(false)
323
- }
324
- }
325
-
326
- return false
327
- }
328
-
329
- async function resolveModule(out) {
330
- switch (path.extname(out.pathname)) {
331
- case '.mjs':
332
- return 'esm'
333
- case '.cjs':
334
- return 'cjs'
335
- }
336
-
337
- // For an ambiguous extension (e.g. '.js') the module system follows the
338
- // `type` of the closest enclosing `package.json`, defaulting to CommonJS.
339
- return (await packageType(new URL('./', out))) === 'module' ? 'esm' : 'cjs'
340
- }
341
-
342
- async function packageType(dir) {
343
- while (true) {
344
- let source = null
345
-
346
- try {
347
- source = await fs.readFile(new URL('package.json', dir))
348
- } catch {
349
- // No `package.json` in this directory; keep searching upwards.
350
- }
351
-
352
- if (source !== null) return JSON.parse(source).type ?? null
353
-
354
- const parent = new URL('../', dir)
355
-
356
- if (parent.pathname === dir.pathname) return null
357
-
358
- dir = parent
359
- }
360
- }
361
-
362
- function siblingURL(out, extension) {
363
- const dir = new URL('./', out)
364
- const base = path.basename(out.pathname, path.extname(out.pathname))
365
-
366
- return new URL(base + extension, dir)
367
- }
368
-
369
- function fragment(artifacts, extension) {
370
- for (const artifact of artifacts) {
371
- if ((artifact.extension ?? null) === extension) return artifact.source
372
- }
373
-
374
- return null
375
- }
376
-
377
- function relativeSpecifier(from, to) {
378
- if (from.protocol === 'file:' && to.protocol === 'file:') {
379
- const fromDir = path.dirname(fileURLToPath(from))
380
- const toPath = fileURLToPath(to)
381
-
382
- let relative = path.relative(fromDir, toPath).split(path.sep).join('/')
383
-
384
- if (!relative.startsWith('.')) relative = './' + relative
385
-
386
- return relative
387
- }
388
-
389
- return to.href
390
- }
391
-
392
344
  function encodeBundle(bundle, format, encoding) {
393
345
  const data = bundle.toBuffer()
394
346
 
@@ -405,3 +357,7 @@ function encodeBundle(bundle, format, encoding) {
405
357
  throw new Error(`Unknown format '${format}'`)
406
358
  }
407
359
  }
360
+
361
+ exports.link = require('./lib/link')
362
+
363
+ exports.resources = assets.app
package/lib/assets.js ADDED
@@ -0,0 +1,54 @@
1
+ const unpack = require('bare-unpack')
2
+ const fs = require('./fs')
3
+
4
+ // The assets of bundles linked by the application share one directory, named
5
+ // after the bundle that Bare Kit starts them as.
6
+ exports.app = 'bare-stow'
7
+
8
+ // Assets are laid out the way Bare Kit expects: the assets of `<name>.bundle`
9
+ // live in `<name>/`, listed in `<name>.assets` for platforms that extract them.
10
+ exports.unpack = async function (bundle, base, name, readModule) {
11
+ const assets = []
12
+
13
+ bundle = await unpack(bundle, { files: false, assets: true }, async (key) => {
14
+ const relative = await exports.path(new URL('.' + key, base), readModule)
15
+
16
+ assets.push({ path: relative, source: bundle.read(key) })
17
+
18
+ return '/../' + name + '/' + relative
19
+ })
20
+
21
+ return { bundle, assets }
22
+ }
23
+
24
+ // Assets of dependencies keep their path from `node_modules`, and others their
25
+ // path within their package, so that every bundle places an asset alike.
26
+ exports.path = async function (url, readModule) {
27
+ const nm = url.pathname.indexOf('/node_modules/')
28
+
29
+ if (nm >= 0) return url.pathname.slice(nm + 1)
30
+
31
+ for (let dir = new URL('./', url); ; dir = new URL('../', dir)) {
32
+ if ((await readModule(new URL('package.json', dir))) !== null) {
33
+ return url.pathname.slice(dir.pathname.length)
34
+ }
35
+
36
+ if (dir.pathname === '/') throw new Error(`Asset '${url.href}' is not part of a package`)
37
+ }
38
+ }
39
+
40
+ exports.write = async function* (dir, name, assets) {
41
+ assets = [...assets].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
42
+
43
+ for (const asset of assets) {
44
+ const url = new URL(name + '/' + asset.path, dir)
45
+
46
+ await fs.writeFile(url, asset.source)
47
+ yield url
48
+ }
49
+
50
+ const listing = new URL(name + '.assets', dir)
51
+
52
+ await fs.writeFile(listing, assets.map((asset) => '/' + asset.path + '\n').join(''))
53
+ yield listing
54
+ }
package/lib/fs.js CHANGED
@@ -43,7 +43,7 @@ exports.readModule = async function readModule(url) {
43
43
  }
44
44
  }
45
45
 
46
- function isFile(url) {
46
+ exports.isFile = function isFile(url) {
47
47
  return new Promise((resolve) => {
48
48
  fs.stat(fileURLToPath(url), (err, stat) => {
49
49
  resolve(err === null && stat.isFile())
@@ -60,7 +60,7 @@ function isDir(url) {
60
60
  }
61
61
 
62
62
  exports.listPrefix = async function* listPrefix(url) {
63
- if (await isFile(url)) return yield url
63
+ if (await exports.isFile(url)) return yield url
64
64
 
65
65
  if (url.pathname[url.pathname.length - 1] !== '/') {
66
66
  url.pathname += '/'
@@ -77,6 +77,4 @@ exports.listPrefix = async function* listPrefix(url) {
77
77
  }
78
78
  }
79
79
 
80
- exports.isFile = isFile
81
-
82
80
  exports.resolveModule = require('bare-pack/fs').resolveModule
package/lib/graph.js ADDED
@@ -0,0 +1,94 @@
1
+ const { fileURLToPath, pathToFileURL } = require('url')
2
+ const bareLink = require('bare-link')
3
+ const strip = require('bare-type-stripper')
4
+ const { resolve } = require('bare-module-traverse')
5
+ const targets = require('./target')
6
+ const languages = require('./language')
7
+ const { select } = require('./provider')
8
+
9
+ exports.providers = function providers(target, opts = {}) {
10
+ let {
11
+ language = 'javascript',
12
+ languageOptions,
13
+ targetOptions,
14
+ hosts,
15
+ resolveTarget,
16
+ resolveLanguage
17
+ } = opts
18
+
19
+ target = targets(target, resolveTarget)
20
+ language = languages(language, resolveLanguage)
21
+
22
+ const harness = select('Target', target, language.name)
23
+
24
+ // A target may pin options that it can't do without, overriding those given.
25
+ const options = { ...language.options(languageOptions), ...harness.options }
26
+
27
+ targetOptions = targets.options(target, targetOptions)
28
+
29
+ target = targets.resolve(target, { language: language.name, options, targetOptions })
30
+
31
+ hosts = resolveHosts(hosts, target.hosts, target.name)
32
+
33
+ return { target, language, harness, options, targetOptions, hosts }
34
+ }
35
+
36
+ function resolveHosts(hosts, supported, name) {
37
+ if (!hosts) return supported
38
+
39
+ for (const host of hosts) {
40
+ if (!supported.includes(host)) {
41
+ throw new Error(`Host '${host}' is not supported by target '${name}'`)
42
+ }
43
+ }
44
+
45
+ return hosts
46
+ }
47
+
48
+ // The graph is traversed the same way when packing it as when linking its
49
+ // addons.
50
+ exports.traversal = function traversal(opts, hosts) {
51
+ return {
52
+ ...opts,
53
+ resolve: resolve.bare,
54
+ aliases: {
55
+ '.ts': '.js',
56
+ '.mts': '.mjs',
57
+ '.cts': '.cjs',
58
+ ...opts.aliases
59
+ },
60
+ hosts
61
+ }
62
+ }
63
+
64
+ exports.readModule = function readModule(modules, read) {
65
+ return async function (url) {
66
+ if (modules.has(url.href)) return modules.get(url.href)
67
+
68
+ const source = await read(url)
69
+
70
+ if (source === null) return null
71
+
72
+ if (/\.(c|m)?ts$/.test(url.pathname)) return strip(source)
73
+
74
+ return source
75
+ }
76
+ }
77
+
78
+ // The layout must not depend on the hosts, as Swift packages only accept
79
+ // XCFrameworks. Resources linked outside of the directory are intermediate.
80
+ exports.link = async function* link(entries, dir, traversal, hooks) {
81
+ for await (const resource of bareLink(
82
+ entries.map((url) => fileURLToPath(url)),
83
+ {
84
+ ...traversal,
85
+ out: fileURLToPath(dir),
86
+ alwaysMultiHost: true,
87
+ ...hooks
88
+ }
89
+ )) {
90
+ const url = pathToFileURL(resource)
91
+
92
+ if (url.href.startsWith(dir.href)) yield url
93
+ }
94
+ }