bare-stow 0.1.8 → 0.2.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,86 +1,101 @@
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
18
-
19
- const t = harness(target, resolveTarget)
20
-
21
- const targetName = typeof target === 'string' ? target : t.name
15
+ let {
16
+ language,
17
+ languageOptions,
18
+ targetOptions,
19
+ client,
20
+ server,
21
+ base,
22
+ hosts,
23
+ resolveTarget,
24
+ resolveRPC,
25
+ resolveLanguage,
26
+ ...packOpts
27
+ } = opts
22
28
 
23
29
  entry = await fs.resolveModule(new URL(entry))
24
30
  out = new URL(out)
25
31
 
26
- // The target may pin a module system; otherwise it follows the output path.
27
- const module = t.module || (await resolveModule(out))
32
+ if (!out.pathname.endsWith('/')) out.pathname += '/'
28
33
 
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
- }
34
+ const resolved = graph.providers(target, {
35
+ language,
36
+ languageOptions,
37
+ targetOptions,
38
+ hosts,
39
+ resolveTarget,
40
+ resolveLanguage
41
+ })
42
+
43
+ const { harness, options } = resolved
44
+
45
+ target = resolved.target
46
+ language = resolved.language
47
+ hosts = resolved.hosts
48
+
49
+ if (client) client = select('RPC library', rpcs(client, resolveRPC), language.name)
50
+ if (server) server = rpcs(server, resolveRPC)
51
+
52
+ const layout = language.layout(out, options)
53
+
54
+ const bundleURL = new URL('index' + target.extension, layout.artifacts ?? layout.harness)
55
+
56
+ // Linked bundles leave their assets out, as files that the runtime loads from
57
+ // next to the bundle or from the application.
58
+ const resources = target.linked && target.linking === 'app' ? assets.app : null
59
+ const name = resources ?? 'index'
60
+
61
+ if (client) {
62
+ client = {
63
+ url: language.sibling(layout.harness, 'client'),
64
+ ...client.generate({ role: 'client', options })
34
65
  }
35
- } else {
36
- hosts = t.hosts
37
66
  }
38
67
 
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
68
+ const files = [
69
+ {
70
+ url: layout.harness,
71
+ ...harness.generate({
72
+ bundle: language.reference(layout.harness, bundleURL),
73
+ client: client && language.reference(layout.harness, client.url),
74
+ resources,
75
+ options
76
+ })
77
+ }
78
+ ]
54
79
 
55
- const shimSource = shim(entry, shimURL, { server: serverSetup })
80
+ if (client) files.push(client)
56
81
 
57
- const readModule = wrapReadModule(fs.readModule, shimURL, shimSource)
82
+ const main = shim(entry, target.isolation, server)
58
83
 
59
- const bundleURL = siblingURL(out, t.extension)
60
- const bundleSpecifier = relativeSpecifier(out, bundleURL)
84
+ const readModule = graph.readModule(main.modules)
61
85
 
62
86
  const offloaded = []
63
87
 
64
88
  let writeFile
65
89
 
66
- if (isOffloadEnabled(t.offload)) writeFile = collectOffloaded(offloaded)
90
+ if (isOffloadEnabled(target.offload)) writeFile = collectOffloaded(offloaded)
91
+
92
+ const traversal = graph.traversal(packOpts, hosts)
67
93
 
68
94
  // The base of the bundle depends on where its modules live, so the graph is
69
95
  // packed unbased and unmounted once that is known.
70
96
  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
- },
97
+ main.url,
98
+ { ...traversal, linked: target.linked, offload: target.offload },
84
99
  readModule,
85
100
  fs.listPrefix,
86
101
  fs.resolveModule,
@@ -91,50 +106,70 @@ module.exports = async function* stow(entry, target, out, opts = {}) {
91
106
 
92
107
  bundle = rerootOffloaded(bundle.unmount(base), base, bundleURL, offloaded)
93
108
 
109
+ let unpacked = []
110
+
111
+ if (target.linked) {
112
+ const result = await assets.unpack(bundle, base, name)
113
+
114
+ bundle = result.bundle
115
+
116
+ // Assets left to the application are written by `link()` instead.
117
+ if (resources === null) unpacked = result.assets
118
+ }
119
+
94
120
  bundle.id = id(bundle).toString('hex')
95
121
 
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
122
+ // The manifest lists the offloaded files only once it's known there are any.
123
+ const artifacts = offloaded.length > 0 ? [bundleURL, offloadURL(bundleURL)] : [bundleURL]
100
124
 
101
- if (client) {
102
- const artifacts = rpc(client, resolveRPC).generate({
103
- ipc: 'ipc',
104
- rpc: 'rpc',
105
- role: 'client',
106
- module
107
- })
125
+ if (unpacked.length > 0) {
126
+ artifacts.push(new URL(name + '/', bundleURL), new URL(name + '.assets', bundleURL))
127
+ }
128
+
129
+ const addons = []
130
+
131
+ // Targets that leave linking to the application use `link()` instead.
132
+ if (target.linked && target.linking === 'package') {
133
+ if (!layout.addons) {
134
+ throw new Error(`Language '${language.name}' does not support linked addons`)
135
+ }
108
136
 
109
- clientSetup = {
110
- source: fragment(artifacts, null),
111
- type: fragment(artifacts, '.d.ts')
137
+ for await (const url of graph.link([main.url], layout.addons, traversal, readModule)) {
138
+ addons.push(url)
139
+ yield { url }
112
140
  }
113
141
  }
114
142
 
115
- const artifacts = t.generate({
116
- bundleSpecifier,
117
- ipc: 'ipc',
118
- rpc: 'rpc',
119
- module,
120
- client: clientSetup
143
+ files.push({
144
+ url: layout.manifest,
145
+ source: language.manifest({
146
+ name: layout.name,
147
+ dependencies: Object.assign({}, ...files.map((file) => file.dependencies)),
148
+ options,
149
+ hosts,
150
+ artifacts: artifacts.map((url) => language.reference(layout.manifest, url)),
151
+ addons: addons.map((url) => language.reference(layout.manifest, url))
152
+ })
121
153
  })
122
154
 
123
- for (const artifact of artifacts) {
124
- if (!artifact.extension) await assertImportsResolve(artifact.source, out)
125
- }
155
+ for (const file of files) {
156
+ await fs.writeFile(file.url, file.source)
157
+ yield { url: file.url }
126
158
 
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
159
+ if (file.types) {
160
+ const typesURL = language.types(file.url)
131
161
 
132
- await fs.writeFile(url, artifact.source)
133
- yield { url }
162
+ await fs.writeFile(typesURL, file.types)
163
+ yield { url: typesURL }
164
+ }
134
165
  }
135
166
 
136
- await fs.writeFile(bundleURL, encodeBundle(bundle, t.format, t.encoding ?? 'utf8'))
137
- yield { url: bundleURL }
167
+ await fs.writeFile(bundleURL, encodeBundle(bundle, target.format, target.encoding ?? 'utf8'))
168
+ yield { url: bundleURL, base }
169
+
170
+ if (unpacked.length > 0) {
171
+ for await (const url of assets.write(new URL('./', bundleURL), name, unpacked)) yield { url }
172
+ }
138
173
 
139
174
  // Offloaded files are collected in the order their reads complete, so they
140
175
  // are sorted to keep the output stable.
@@ -146,8 +181,8 @@ module.exports = async function* stow(entry, target, out, opts = {}) {
146
181
  }
147
182
  }
148
183
 
149
- function resolveBase(base, graph) {
150
- const modules = [...graph.keys()].map((href) => new URL(href))
184
+ function resolveBase(base, bundle) {
185
+ const modules = [...bundle.keys()].map((href) => new URL(href))
151
186
 
152
187
  if (base) {
153
188
  base = new URL(base)
@@ -191,20 +226,6 @@ function isOffloadEnabled(offload) {
191
226
  return false
192
227
  }
193
228
 
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
229
  function collectOffloaded(sink) {
209
230
  return function writeFile(url, source) {
210
231
  sink.push({ url, source })
@@ -217,11 +238,15 @@ function collectOffloaded(sink) {
217
238
  // next to the harness, as a mirrored `node_modules` beside the harness would
218
239
  // shadow the packages it imports. Their resolutions are rewritten to point into
219
240
  // that directory, along with the directories that hold them.
241
+ function offloadURL(bundleURL) {
242
+ return new URL(path.basename(bundleURL.pathname) + '.d/', bundleURL)
243
+ }
244
+
220
245
  function rerootOffloaded(bundle, base, bundleURL, offloaded) {
221
246
  if (offloaded.length === 0) return bundle
222
247
 
223
- const name = path.basename(bundleURL.pathname) + '.d'
224
- const dir = new URL(name + '/', bundleURL)
248
+ const dir = offloadURL(bundleURL)
249
+ const name = path.basename(dir.pathname)
225
250
  const rewrites = new Map()
226
251
 
227
252
  for (const file of offloaded) {
@@ -288,107 +313,6 @@ function isTerminator(input) {
288
313
  return input === '' || input.endsWith('/') || input.endsWith(':')
289
314
  }
290
315
 
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
316
  function encodeBundle(bundle, format, encoding) {
393
317
  const data = bundle.toBuffer()
394
318
 
@@ -405,3 +329,7 @@ function encodeBundle(bundle, format, encoding) {
405
329
  throw new Error(`Unknown format '${format}'`)
406
330
  }
407
331
  }
332
+
333
+ exports.link = require('./lib/link')
334
+
335
+ 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) {
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))
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) {
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 fs.isFile(new URL('package.json', dir))) {
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,96 @@
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 fs = require('./fs')
6
+ const targets = require('./target')
7
+ const languages = require('./language')
8
+ const { select } = require('./provider')
9
+
10
+ exports.providers = function providers(target, opts = {}) {
11
+ let {
12
+ language = 'javascript',
13
+ languageOptions,
14
+ targetOptions,
15
+ hosts,
16
+ resolveTarget,
17
+ resolveLanguage
18
+ } = opts
19
+
20
+ target = targets(target, resolveTarget)
21
+ language = languages(language, resolveLanguage)
22
+
23
+ const harness = select('Target', target, language.name)
24
+
25
+ // A target may pin options that it can't do without, overriding those given.
26
+ const options = { ...language.options(languageOptions), ...harness.options }
27
+
28
+ targetOptions = targets.options(target, targetOptions)
29
+
30
+ target = targets.resolve(target, { language: language.name, options, targetOptions })
31
+
32
+ hosts = resolveHosts(hosts, target.hosts, target.name)
33
+
34
+ return { target, language, harness, options, targetOptions, hosts }
35
+ }
36
+
37
+ function resolveHosts(hosts, supported, name) {
38
+ if (!hosts) return supported
39
+
40
+ for (const host of hosts) {
41
+ if (!supported.includes(host)) {
42
+ throw new Error(`Host '${host}' is not supported by target '${name}'`)
43
+ }
44
+ }
45
+
46
+ return hosts
47
+ }
48
+
49
+ // The graph is traversed the same way when packing it as when linking its
50
+ // addons.
51
+ exports.traversal = function traversal(opts, hosts) {
52
+ return {
53
+ ...opts,
54
+ resolve: resolve.bare,
55
+ aliases: {
56
+ '.ts': '.js',
57
+ '.mts': '.mjs',
58
+ '.cts': '.cjs'
59
+ },
60
+ hosts
61
+ }
62
+ }
63
+
64
+ exports.readModule = function readModule(modules) {
65
+ return async function (url) {
66
+ if (modules.has(url.href)) return modules.get(url.href)
67
+
68
+ const source = await fs.readModule(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, readModule) {
81
+ for await (const resource of bareLink(
82
+ entries.map((url) => fileURLToPath(url)),
83
+ {
84
+ ...traversal,
85
+ out: fileURLToPath(dir),
86
+ alwaysMultiHost: true,
87
+ readModule,
88
+ listPrefix: fs.listPrefix,
89
+ resolveModule: fs.resolveModule
90
+ }
91
+ )) {
92
+ const url = pathToFileURL(resource)
93
+
94
+ if (url.href.startsWith(dir.href)) yield url
95
+ }
96
+ }
@@ -0,0 +1,74 @@
1
+ const path = require('path')
2
+ const { fileURLToPath } = require('url')
3
+
4
+ exports.name = 'javascript'
5
+
6
+ const MODULES = ['esm', 'cjs']
7
+
8
+ exports.options = function options(given = {}) {
9
+ const { module = 'esm', ...unknown } = given
10
+
11
+ for (const name of Object.keys(unknown)) {
12
+ throw new Error(`Unknown option '${name}' for language 'javascript'`)
13
+ }
14
+
15
+ if (!MODULES.includes(module)) {
16
+ throw new Error(`Option 'module' must be one of ${MODULES.join(', ')}, got '${module}'`)
17
+ }
18
+
19
+ return { module }
20
+ }
21
+
22
+ exports.layout = function layout(out) {
23
+ const name = path.posix.basename(out.pathname)
24
+
25
+ if (!/^[a-z0-9~-][a-z0-9._~-]*$/.test(name)) {
26
+ throw new Error(`'${name}' is not a valid package name`)
27
+ }
28
+
29
+ return {
30
+ harness: new URL('index.js', out),
31
+ name,
32
+ manifest: new URL('package.json', out)
33
+ }
34
+ }
35
+
36
+ exports.sibling = function sibling(url, name) {
37
+ return new URL(`${name}.js`, url)
38
+ }
39
+
40
+ exports.types = function types(url) {
41
+ return new URL(path.posix.basename(url.pathname, '.js') + '.d.ts', url)
42
+ }
43
+
44
+ exports.reference = function reference(from, to) {
45
+ if (from.protocol === 'file:' && to.protocol === 'file:') {
46
+ const fromDir = path.dirname(fileURLToPath(from))
47
+ const toPath = fileURLToPath(to)
48
+
49
+ let relative = path.relative(fromDir, toPath).split(path.sep).join('/')
50
+
51
+ if (!relative.startsWith('.')) relative = './' + relative
52
+
53
+ return relative
54
+ }
55
+
56
+ return to.href
57
+ }
58
+
59
+ exports.manifest = function manifest({ name, dependencies, options }) {
60
+ const pkg = {
61
+ name,
62
+ private: true,
63
+ type: options.module === 'esm' ? 'module' : 'commonjs',
64
+ exports: {
65
+ '.': {
66
+ types: './index.d.ts',
67
+ default: './index.js'
68
+ }
69
+ },
70
+ dependencies
71
+ }
72
+
73
+ return JSON.stringify(pkg, null, 2) + '\n'
74
+ }