bare-stow 0.1.9 → 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.
@@ -0,0 +1,109 @@
1
+ const path = require('path')
2
+
3
+ exports.name = 'kotlin'
4
+
5
+ exports.options = function options(given = {}) {
6
+ const { package: name, ...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
+ return { package: name }
19
+ }
20
+
21
+ // The bundle is an asset of the module, under the directory of its package as
22
+ // the assets of every module of the application share one namespace.
23
+ exports.layout = function layout(out, options) {
24
+ const dir = options.package.replace(/\./g, '/')
25
+
26
+ return {
27
+ harness: new URL(`src/main/kotlin/${dir}/Harness.kt`, out),
28
+ artifacts: new URL(`src/main/assets/${dir}/`, out),
29
+ name: options.package,
30
+ manifest: new URL('build.gradle', out),
31
+ addons: new URL('src/main/addons/', out),
32
+ resources: new URL('src/main/assets/', out)
33
+ }
34
+ }
35
+
36
+ exports.sibling = function sibling(url, name) {
37
+ return new URL(capitalize(name) + '.kt', url)
38
+ }
39
+
40
+ // Assets are referenced by their path within the assets of the application.
41
+ exports.reference = function reference(from, to) {
42
+ const assets = to.pathname.lastIndexOf('/src/main/assets/')
43
+
44
+ if (assets !== -1) return to.pathname.slice(assets + '/src/main/assets/'.length)
45
+
46
+ return path.posix.relative(path.posix.dirname(from.pathname), to.pathname)
47
+ }
48
+
49
+ // The dependencies of Kotlin generators map Maven coordinates without a version
50
+ // to the version that the module uses. They are part of the API of the module,
51
+ // as the harness exposes their types.
52
+ exports.manifest = function manifest({ name, dependencies, addons = [] }) {
53
+ const apis = Object.entries(dependencies).map(
54
+ ([coordinates, version]) => ` api ${JSON.stringify(coordinates + ':' + version)}`
55
+ )
56
+
57
+ // Linked addons are native libraries in a directory per ABI, along with the
58
+ // archives of any Java classes they use.
59
+ const libraries = [
60
+ ...new Set(
61
+ addons
62
+ .filter((addon) => addon.endsWith('.so'))
63
+ .map((addon) => path.posix.dirname(path.posix.dirname(addon)))
64
+ )
65
+ ]
66
+
67
+ for (const addon of addons) {
68
+ if (addon.endsWith('.jar')) apis.push(` api files(${JSON.stringify(addon)})`)
69
+ }
70
+
71
+ const sourceSets = libraries.length
72
+ ? `
73
+
74
+ sourceSets {
75
+ main {
76
+ ${libraries.map((dir) => ` jniLibs.directories.add(${JSON.stringify(dir)})`).join('\n')}
77
+ }
78
+ }`
79
+ : ''
80
+
81
+ // Projects that opt out of the Kotlin support built into AGP 9, such as those
82
+ // of Expo, need the Kotlin plugin to compile the harness.
83
+ return `\
84
+ plugins {
85
+ id "com.android.library"
86
+ }
87
+
88
+ if (findProperty("android.builtInKotlin") == "false") {
89
+ apply plugin: "org.jetbrains.kotlin.android"
90
+ }
91
+
92
+ android {
93
+ namespace = ${JSON.stringify(name)}
94
+ compileSdk = 36
95
+
96
+ defaultConfig {
97
+ minSdk = 29
98
+ }${sourceSets}
99
+ }
100
+
101
+ dependencies {
102
+ ${apis.join('\n')}
103
+ }
104
+ `
105
+ }
106
+
107
+ function capitalize(name) {
108
+ return name.charAt(0).toUpperCase() + name.slice(1)
109
+ }
@@ -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,141 @@
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
+ ...packOpts
22
+ } = opts
23
+
24
+ entries = await Promise.all([].concat(entries).map((entry) => fs.resolveModule(new URL(entry))))
25
+
26
+ if (entries.length === 0) throw new Error("'entries' must not be empty")
27
+ out = new URL(out)
28
+
29
+ if (!out.pathname.endsWith('/')) out.pathname += '/'
30
+
31
+ const resolved = graph.providers(target, {
32
+ language,
33
+ languageOptions,
34
+ targetOptions,
35
+ hosts,
36
+ resolveTarget,
37
+ resolveLanguage
38
+ })
39
+
40
+ const { harness, options } = resolved
41
+
42
+ target = resolved.target
43
+ language = resolved.language
44
+ hosts = resolved.hosts
45
+
46
+ if (!target.linked) throw new Error(`Target '${target.name}' does not link addons`)
47
+
48
+ if (typeof harness.link !== 'function') {
49
+ throw new Error(
50
+ `Target '${target.name}' does not support linking for language '${language.name}'`
51
+ )
52
+ }
53
+
54
+ if (server) server = rpcs(server, resolveRPC)
55
+
56
+ const layout = language.layout(out, options)
57
+
58
+ if (!layout.addons) throw new Error(`Language '${language.name}' does not support linked addons`)
59
+
60
+ // Every shim imports the same runtime and RPC server, so one covers them all.
61
+ const main = shim(entries[0], target.isolation, server)
62
+
63
+ const traversal = graph.traversal(packOpts, hosts)
64
+ const readModule = graph.readModule(main.modules)
65
+
66
+ const addons = []
67
+
68
+ for await (const url of graph.link(
69
+ [main.url, ...entries.slice(1)],
70
+ layout.addons,
71
+ traversal,
72
+ readModule
73
+ )) {
74
+ addons.push(url)
75
+ yield { url }
76
+ }
77
+
78
+ const linked = await collectAssets(entries, traversal, readModule)
79
+
80
+ if (linked.length > 0) {
81
+ if (!layout.resources) {
82
+ throw new Error(`Language '${language.name}' does not support linked assets`)
83
+ }
84
+
85
+ for await (const url of assets.write(layout.resources, assets.app, linked)) yield { url }
86
+ }
87
+
88
+ const generated = harness.link({ options })
89
+
90
+ const files = []
91
+
92
+ if (generated.source) files.push({ url: layout.harness, source: generated.source })
93
+
94
+ files.push({
95
+ url: layout.manifest,
96
+ source: language.manifest({
97
+ name: layout.name,
98
+ dependencies: generated.dependencies || {},
99
+ options,
100
+ hosts,
101
+ artifacts: [],
102
+ addons: addons.map((url) => language.reference(layout.manifest, url))
103
+ })
104
+ })
105
+
106
+ for (const file of files) {
107
+ await fs.writeFile(file.url, file.source)
108
+ yield { url: file.url }
109
+ }
110
+ }
111
+
112
+ async function collectAssets(entries, traversal, readModule) {
113
+ const collected = new Map()
114
+
115
+ for (const entry of entries) {
116
+ const bundle = await pack(
117
+ entry,
118
+ { ...traversal, linked: true },
119
+ readModule,
120
+ fs.listPrefix,
121
+ fs.resolveModule
122
+ )
123
+
124
+ for (const key of bundle.assets) {
125
+ const url = new URL(key)
126
+ const relative = await assets.path(url)
127
+ const source = bundle.read(key)
128
+ const existing = collected.get(relative)
129
+
130
+ if (existing && !existing.source.equals(source)) {
131
+ throw new Error(
132
+ `Assets '${existing.url.href}' and '${url.href}' both link as '${relative}'`
133
+ )
134
+ }
135
+
136
+ collected.set(relative, { path: relative, url, source })
137
+ }
138
+ }
139
+
140
+ return [...collected.values()]
141
+ }
@@ -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
+ }