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.
@@ -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
+ }
@@ -0,0 +1,139 @@
1
+ const path = require('path')
2
+
3
+ exports.name = 'kotlin'
4
+
5
+ exports.options = function options(given = {}) {
6
+ const { package: name, platform = 'android', ...unknown } = given
7
+
8
+ for (const option of Object.keys(unknown)) {
9
+ throw new Error(`Unknown option '${option}' for language 'kotlin'`)
10
+ }
11
+
12
+ if (name === undefined) throw new Error("Option 'package' is required for language 'kotlin'")
13
+
14
+ if (!/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$/.test(name)) {
15
+ throw new Error(`'${name}' is not a valid Kotlin package name`)
16
+ }
17
+
18
+ if (platform !== 'android' && platform !== 'jvm') {
19
+ throw new Error(`Option 'platform' must be 'android' or 'jvm', got '${platform}'`)
20
+ }
21
+
22
+ return { package: name, platform }
23
+ }
24
+
25
+ // The bundle is an asset or resource of the module, under the directory of its
26
+ // package as those of every module of the application share one namespace. A
27
+ // JVM module can't read its resources as files, so it lists the files beside
28
+ // the bundle for the harness to extract.
29
+ exports.layout = function layout(out, options) {
30
+ const dir = options.package.replace(/\./g, '/')
31
+
32
+ const module = {
33
+ harness: new URL(`src/main/kotlin/${dir}/Harness.kt`, out),
34
+ name: options.package,
35
+ manifest: new URL('build.gradle', out)
36
+ }
37
+
38
+ if (options.platform === 'jvm') {
39
+ return { ...module, artifacts: new URL(`src/main/resources/${dir}/`, out), listing: true }
40
+ }
41
+
42
+ return {
43
+ ...module,
44
+ artifacts: new URL(`src/main/assets/${dir}/`, out),
45
+ addons: new URL('src/main/addons/', out),
46
+ resources: new URL('src/main/assets/', out)
47
+ }
48
+ }
49
+
50
+ exports.sibling = function sibling(url, name) {
51
+ return new URL(capitalize(name) + '.kt', url)
52
+ }
53
+
54
+ // Assets and resources are referenced by their path within those of the
55
+ // application.
56
+ exports.reference = function reference(from, to) {
57
+ for (const dir of ['/src/main/assets/', '/src/main/resources/']) {
58
+ const index = to.pathname.lastIndexOf(dir)
59
+
60
+ if (index !== -1) return to.pathname.slice(index + dir.length)
61
+ }
62
+
63
+ return path.posix.relative(path.posix.dirname(from.pathname), to.pathname)
64
+ }
65
+
66
+ // The dependencies of Kotlin generators map Maven coordinates without a version
67
+ // to the version that the module uses. They are part of the API of the module,
68
+ // as the harness exposes their types.
69
+ exports.manifest = function manifest({ name, dependencies, options = {}, addons = [] }) {
70
+ const apis = Object.entries(dependencies).map(
71
+ ([coordinates, version]) => ` api ${JSON.stringify(coordinates + ':' + version)}`
72
+ )
73
+
74
+ if (options.platform === 'jvm') {
75
+ return `\
76
+ plugins {
77
+ id "java-library"
78
+ id "org.jetbrains.kotlin.jvm"
79
+ }
80
+
81
+ dependencies {
82
+ ${apis.join('\n')}
83
+ }
84
+ `
85
+ }
86
+
87
+ // Linked addons are native libraries in a directory per ABI, along with the
88
+ // archives of any Java classes they use.
89
+ const libraries = [
90
+ ...new Set(
91
+ addons
92
+ .filter((addon) => addon.endsWith('.so'))
93
+ .map((addon) => path.posix.dirname(path.posix.dirname(addon)))
94
+ )
95
+ ]
96
+
97
+ for (const addon of addons) {
98
+ if (addon.endsWith('.jar')) apis.push(` api files(${JSON.stringify(addon)})`)
99
+ }
100
+
101
+ const sourceSets = libraries.length
102
+ ? `
103
+
104
+ sourceSets {
105
+ main {
106
+ ${libraries.map((dir) => ` jniLibs.directories.add(${JSON.stringify(dir)})`).join('\n')}
107
+ }
108
+ }`
109
+ : ''
110
+
111
+ // Projects that opt out of the Kotlin support built into AGP 9, such as those
112
+ // of Expo, need the Kotlin plugin to compile the harness.
113
+ return `\
114
+ plugins {
115
+ id "com.android.library"
116
+ }
117
+
118
+ if (findProperty("android.builtInKotlin") == "false") {
119
+ apply plugin: "org.jetbrains.kotlin.android"
120
+ }
121
+
122
+ android {
123
+ namespace = ${JSON.stringify(name)}
124
+ compileSdk = 36
125
+
126
+ defaultConfig {
127
+ minSdk = 29
128
+ }${sourceSets}
129
+ }
130
+
131
+ dependencies {
132
+ ${apis.join('\n')}
133
+ }
134
+ `
135
+ }
136
+
137
+ function capitalize(name) {
138
+ return name.charAt(0).toUpperCase() + name.slice(1)
139
+ }
@@ -0,0 +1,65 @@
1
+ const path = require('path')
2
+
3
+ exports.name = 'python'
4
+
5
+ exports.options = function options(given = {}) {
6
+ for (const name of Object.keys(given)) {
7
+ throw new Error(`Unknown option '${name}' for language 'python'`)
8
+ }
9
+
10
+ return {}
11
+ }
12
+
13
+ exports.layout = function layout(out) {
14
+ const name = path.posix.basename(out.pathname).replace(/-/g, '_')
15
+
16
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
17
+ throw new Error(`'${name}' is not a valid Python package name`)
18
+ }
19
+
20
+ return {
21
+ harness: new URL(`${name}/__init__.py`, out),
22
+ name,
23
+ manifest: new URL('pyproject.toml', out)
24
+ }
25
+ }
26
+
27
+ exports.sibling = function sibling(url, name) {
28
+ return new URL(`${name}.py`, url)
29
+ }
30
+
31
+ exports.reference = function reference(from, to) {
32
+ const relative = path.posix.relative(path.posix.dirname(from.pathname), to.pathname)
33
+
34
+ if (relative.endsWith('.py') && !relative.includes('/')) {
35
+ return '.' + path.posix.basename(relative, '.py')
36
+ }
37
+
38
+ return relative
39
+ }
40
+
41
+ exports.manifest = function manifest({ name, dependencies, artifacts }) {
42
+ const requirements = Object.entries(dependencies).map(
43
+ ([dependency, version]) => ` ${JSON.stringify(dependency + version)},`
44
+ )
45
+
46
+ // The bundle is listed as an artifact so that it is included in the wheel
47
+ // even when ignored by version control.
48
+ return `\
49
+ [build-system]
50
+ requires = ["hatchling"]
51
+ build-backend = "hatchling.build"
52
+
53
+ [project]
54
+ name = ${JSON.stringify(name)}
55
+ version = "0.0.0"
56
+ requires-python = ">=3.10"
57
+ dependencies = [
58
+ ${requirements.join('\n')}
59
+ ]
60
+
61
+ [tool.hatch.build.targets.wheel]
62
+ packages = [${JSON.stringify(name)}]
63
+ artifacts = [${artifacts.map((artifact) => JSON.stringify(artifact)).join(', ')}]
64
+ `
65
+ }
@@ -0,0 +1,111 @@
1
+ const path = require('path')
2
+
3
+ exports.name = 'swift'
4
+
5
+ exports.options = function options(given = {}) {
6
+ for (const name of Object.keys(given)) {
7
+ throw new Error(`Unknown option '${name}' for language 'swift'`)
8
+ }
9
+
10
+ return {}
11
+ }
12
+
13
+ exports.layout = function layout(out) {
14
+ const name = path.posix.basename(out.pathname).split(/[-_.]/).map(capitalize).join('')
15
+
16
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
17
+ throw new Error(`'${name}' is not a valid Swift module name`)
18
+ }
19
+
20
+ return {
21
+ harness: new URL(`Sources/${name}/Harness.swift`, out),
22
+ name,
23
+ manifest: new URL('Package.swift', out),
24
+ addons: new URL('Frameworks/', out),
25
+ resources: out
26
+ }
27
+ }
28
+
29
+ exports.sibling = function sibling(url, name) {
30
+ return new URL(capitalize(name) + '.swift', url)
31
+ }
32
+
33
+ exports.reference = function reference(from, to) {
34
+ return path.posix.relative(path.posix.dirname(from.pathname), to.pathname)
35
+ }
36
+
37
+ // The oldest versions of the Apple platforms that the Swift runtimes support.
38
+ const platforms = {
39
+ darwin: '.macOS(.v13)',
40
+ ios: '.iOS(.v15)'
41
+ }
42
+
43
+ // The dependencies of Swift generators map package identities to the URL, the
44
+ // minimum version, and the products of the package that the module uses. Like
45
+ // npm, a minor release of a package before 1.0.0 may break compatibility.
46
+ exports.manifest = function manifest({ name, dependencies, hosts, artifacts, addons = [] }) {
47
+ const supported = Object.entries(platforms)
48
+ .filter(([platform]) => hosts.some((host) => host.startsWith(platform + '-')))
49
+ .map(([, minimum]) => minimum)
50
+
51
+ const packages = Object.values(dependencies).map(({ url, from }) => {
52
+ const version = from.startsWith('0.')
53
+ ? `.upToNextMinor(from: ${JSON.stringify(from)})`
54
+ : `from: ${JSON.stringify(from)}`
55
+
56
+ return ` .package(url: ${JSON.stringify(url)}, ${version}),`
57
+ })
58
+
59
+ const products = Object.entries(dependencies).flatMap(([identity, { products }]) =>
60
+ products.map(
61
+ (product) =>
62
+ ` .product(name: ${JSON.stringify(product)}, package: ${JSON.stringify(identity)}),`
63
+ )
64
+ )
65
+
66
+ // Linked addons are binary targets that the module depends on.
67
+ const frameworks = addons
68
+ .filter((addon) => addon.endsWith('.xcframework'))
69
+ .map((addon) => ({ name: path.posix.basename(addon, '.xcframework'), path: addon }))
70
+
71
+ for (const framework of frameworks) products.push(` ${JSON.stringify(framework.name)},`)
72
+
73
+ const binaries = frameworks.map(
74
+ (framework) =>
75
+ `,\n .binaryTarget(name: ${JSON.stringify(framework.name)}, path: ${JSON.stringify(framework.path)})`
76
+ )
77
+
78
+ // Resources are named relative to the directory of the module.
79
+ const resources = artifacts.map(
80
+ (artifact) => `.copy(${JSON.stringify(path.posix.relative(`Sources/${name}`, artifact))})`
81
+ )
82
+
83
+ return `\
84
+ // swift-tools-version: 5.10
85
+ import PackageDescription
86
+
87
+ let package = Package(
88
+ name: ${JSON.stringify(name)},
89
+ platforms: [${supported.join(', ')}],
90
+ products: [
91
+ .library(name: ${JSON.stringify(name)}, targets: [${JSON.stringify(name)}])
92
+ ],
93
+ dependencies: [
94
+ ${packages.join('\n')}
95
+ ],
96
+ targets: [
97
+ .target(
98
+ name: ${JSON.stringify(name)},
99
+ dependencies: [
100
+ ${products.join('\n')}
101
+ ],
102
+ resources: [${resources.join(', ')}]
103
+ )${binaries.join('')}
104
+ ]
105
+ )
106
+ `
107
+ }
108
+
109
+ function capitalize(name) {
110
+ return name.charAt(0).toUpperCase() + name.slice(1)
111
+ }
@@ -0,0 +1,18 @@
1
+ const { registry } = require('./provider')
2
+
3
+ const providers = {
4
+ javascript: () => require('./language/javascript'),
5
+ kotlin: () => require('./language/kotlin'),
6
+ python: () => require('./language/python'),
7
+ swift: () => require('./language/swift')
8
+ }
9
+
10
+ module.exports = registry('language', providers, function validate(language) {
11
+ for (const method of ['options', 'layout', 'sibling', 'reference', 'manifest']) {
12
+ if (typeof language?.[method] !== 'function') {
13
+ throw new Error(`Language must provide a ${method}() function`)
14
+ }
15
+ }
16
+
17
+ return language
18
+ })
package/lib/link.js ADDED
@@ -0,0 +1,149 @@
1
+ const pack = require('bare-pack')
2
+ const fs = require('./fs')
3
+ const assets = require('./assets')
4
+ const shim = require('./shim')
5
+ const graph = require('./graph')
6
+ const rpcs = require('./rpc')
7
+
8
+ module.exports = async function* link(entries, target, out, opts = {}) {
9
+ if (!target) throw new Error("'target' is required")
10
+ if (!out) throw new Error("'out' is required")
11
+
12
+ let {
13
+ language,
14
+ languageOptions,
15
+ targetOptions,
16
+ server,
17
+ hosts,
18
+ resolveTarget,
19
+ resolveRPC,
20
+ resolveLanguage,
21
+ readModule = fs.readModule,
22
+ listPrefix = fs.listPrefix,
23
+ resolveModule = fs.resolveModule,
24
+ ...packOpts
25
+ } = opts
26
+
27
+ entries = await Promise.all([].concat(entries).map((entry) => resolveModule(new URL(entry))))
28
+
29
+ if (entries.length === 0) throw new Error("'entries' must not be empty")
30
+ out = new URL(out)
31
+
32
+ if (!out.pathname.endsWith('/')) out.pathname += '/'
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 (!target.linked) throw new Error(`Target '${target.name}' does not link addons`)
50
+
51
+ if (typeof harness.link !== 'function') {
52
+ throw new Error(
53
+ `Target '${target.name}' does not support linking for language '${language.name}'`
54
+ )
55
+ }
56
+
57
+ if (server) server = rpcs(server, resolveRPC)
58
+
59
+ const layout = language.layout(out, options)
60
+
61
+ if (!layout.addons) throw new Error(`Language '${language.name}' does not support linked addons`)
62
+
63
+ // Every shim imports the same runtime and RPC server, so one covers them all.
64
+ const main = shim(entries[0], target.isolation, server)
65
+
66
+ const traversal = graph.traversal(packOpts, hosts)
67
+
68
+ const hooks = {
69
+ readModule: graph.readModule(main.modules, readModule),
70
+ listPrefix,
71
+ resolveModule
72
+ }
73
+
74
+ const addons = []
75
+
76
+ for await (const url of graph.link(
77
+ [main.url, ...entries.slice(1)],
78
+ layout.addons,
79
+ traversal,
80
+ hooks
81
+ )) {
82
+ addons.push(url)
83
+ yield { url }
84
+ }
85
+
86
+ const linked = await collectAssets(entries, traversal, hooks)
87
+
88
+ if (linked.length > 0) {
89
+ if (!layout.resources) {
90
+ throw new Error(`Language '${language.name}' does not support linked assets`)
91
+ }
92
+
93
+ for await (const url of assets.write(layout.resources, assets.app, linked)) yield { url }
94
+ }
95
+
96
+ const generated = harness.link({ options })
97
+
98
+ const files = []
99
+
100
+ if (generated.source) files.push({ url: layout.harness, source: generated.source })
101
+
102
+ files.push({
103
+ url: layout.manifest,
104
+ source: language.manifest({
105
+ name: layout.name,
106
+ dependencies: generated.dependencies || {},
107
+ options,
108
+ hosts,
109
+ artifacts: [],
110
+ addons: addons.map((url) => language.reference(layout.manifest, url))
111
+ })
112
+ })
113
+
114
+ for (const file of files) {
115
+ await fs.writeFile(file.url, file.source)
116
+ yield { url: file.url }
117
+ }
118
+ }
119
+
120
+ async function collectAssets(entries, traversal, hooks) {
121
+ const collected = new Map()
122
+
123
+ for (const entry of entries) {
124
+ const bundle = await pack(
125
+ entry,
126
+ { ...traversal, linked: true },
127
+ hooks.readModule,
128
+ hooks.listPrefix,
129
+ hooks.resolveModule
130
+ )
131
+
132
+ for (const key of bundle.assets) {
133
+ const url = new URL(key)
134
+ const relative = await assets.path(url, hooks.readModule)
135
+ const source = bundle.read(key)
136
+ const existing = collected.get(relative)
137
+
138
+ if (existing && !existing.source.equals(source)) {
139
+ throw new Error(
140
+ `Assets '${existing.url.href}' and '${url.href}' both link as '${relative}'`
141
+ )
142
+ }
143
+
144
+ collected.set(relative, { path: relative, url, source })
145
+ }
146
+ }
147
+
148
+ return [...collected.values()]
149
+ }
@@ -0,0 +1,33 @@
1
+ exports.registry = function registry(kind, providers, validate) {
2
+ return function provider(name, resolve) {
3
+ if (typeof name !== 'string') return validate(name)
4
+
5
+ const load = providers[name]
6
+
7
+ if (load) return load()
8
+
9
+ if (resolve) return validate(resolve(name))
10
+
11
+ throw new Error(`Unknown ${kind} '${name}'`)
12
+ }
13
+ }
14
+
15
+ exports.validateLanguages = function validateLanguages(kind, languages) {
16
+ if (typeof languages !== 'object' || languages === null) {
17
+ throw new Error(`${kind} must provide a languages object`)
18
+ }
19
+
20
+ for (const [name, entry] of Object.entries(languages)) {
21
+ if (!entry || typeof entry.generate !== 'function') {
22
+ throw new Error(`${kind} must provide a generate() function for language '${name}'`)
23
+ }
24
+ }
25
+ }
26
+
27
+ exports.select = function select(kind, provider, language) {
28
+ if (!Object.hasOwn(provider.languages, language)) {
29
+ throw new Error(`${kind} '${provider.name}' does not support language '${language}'`)
30
+ }
31
+
32
+ return provider.languages[language]
33
+ }