oneworks 0.1.0-alpha.2 → 0.1.0-beta.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/__tests__/cli.spec.ts +57 -0
- package/__tests__/npm-package.spec.ts +69 -0
- package/__tests__/runtime-package.spec.ts +51 -2
- package/package.json +3 -3
- package/src/npm-package-cache.ts +60 -12
- package/src/npm-package-install.ts +37 -7
- package/src/npm-package.ts +14 -60
- package/src/npm-registry.ts +154 -0
- package/src/package-version-request.ts +121 -0
- package/src/program.ts +35 -4
- package/src/runtime-package.ts +42 -10
package/__tests__/cli.spec.ts
CHANGED
|
@@ -69,6 +69,23 @@ describe('bootstrap cli', () => {
|
|
|
69
69
|
})
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
+
it('routes runtime package cache versions from flags', () => {
|
|
73
|
+
expect(routeBootstrapCommand('runtime', [
|
|
74
|
+
'install',
|
|
75
|
+
'server',
|
|
76
|
+
'--version=2.3.4',
|
|
77
|
+
'--cache-version',
|
|
78
|
+
'dev-local'
|
|
79
|
+
])).toEqual({
|
|
80
|
+
action: 'install',
|
|
81
|
+
cacheVersion: 'dev-local',
|
|
82
|
+
json: false,
|
|
83
|
+
kind: 'runtime-package',
|
|
84
|
+
target: 'server',
|
|
85
|
+
version: '2.3.4'
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
72
89
|
it('routes unknown commands through the CLI package', () => {
|
|
73
90
|
expect(routeBootstrapCommand('hello', [])).toEqual({
|
|
74
91
|
commandName: 'oneworks',
|
|
@@ -197,6 +214,46 @@ describe('bootstrap cli', () => {
|
|
|
197
214
|
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('"requestedVersion":"1.2.3"'))
|
|
198
215
|
})
|
|
199
216
|
|
|
217
|
+
it('dispatches runtime package installation with a cache version', async () => {
|
|
218
|
+
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
|
|
219
|
+
const installRuntimePackage = vi.fn(async () => ({
|
|
220
|
+
cacheVersion: 'dev-local',
|
|
221
|
+
installed: true,
|
|
222
|
+
installedVersion: '1.2.3',
|
|
223
|
+
latestInstalled: true,
|
|
224
|
+
latestVersion: '1.2.3',
|
|
225
|
+
packageName: '@oneworks/server',
|
|
226
|
+
requestedVersion: '1.2.3',
|
|
227
|
+
target: 'server' as const,
|
|
228
|
+
updateAvailable: false
|
|
229
|
+
}))
|
|
230
|
+
const cli = createBootstrapCli({
|
|
231
|
+
checkRuntimePackage: vi.fn(async () => {
|
|
232
|
+
throw new Error('unexpected check')
|
|
233
|
+
}),
|
|
234
|
+
installRuntimePackage,
|
|
235
|
+
launchDesktopApp: vi.fn(async () => {}),
|
|
236
|
+
launchInstalledPackage: vi.fn(async () => 0)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
await cli.parseAsync([
|
|
240
|
+
'node',
|
|
241
|
+
'oneworks',
|
|
242
|
+
'runtime',
|
|
243
|
+
'install',
|
|
244
|
+
'server',
|
|
245
|
+
'--version=1.2.3',
|
|
246
|
+
'--cache-version=dev-local',
|
|
247
|
+
'--json'
|
|
248
|
+
])
|
|
249
|
+
|
|
250
|
+
expect(installRuntimePackage).toHaveBeenCalledWith('server', {
|
|
251
|
+
cacheVersion: 'dev-local',
|
|
252
|
+
version: '1.2.3'
|
|
253
|
+
})
|
|
254
|
+
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('"cacheVersion":"dev-local"'))
|
|
255
|
+
})
|
|
256
|
+
|
|
200
257
|
it('forwards --help after a routed command', async () => {
|
|
201
258
|
const launchDesktopApp = vi.fn(async () => {})
|
|
202
259
|
const launchInstalledPackage = vi.fn(async () => 0)
|
|
@@ -20,6 +20,8 @@ describe('bootstrap npm package env', () => {
|
|
|
20
20
|
vi.stubEnv('ONEWORKS_BOOTSTRAP_DISABLE_BACKGROUND_REFRESH', '1')
|
|
21
21
|
vi.stubEnv('ONEWORKS_BOOTSTRAP_PACKAGE_CACHE_FIRST', undefined)
|
|
22
22
|
vi.stubEnv('ONEWORKS_BOOTSTRAP_PACKAGE_LOOKUP_TIMEOUT_MS', '1000')
|
|
23
|
+
vi.stubEnv('ONEWORKS_BOOTSTRAP_PACKAGE_TAG', undefined)
|
|
24
|
+
vi.stubEnv('ONEWORKS_BOOTSTRAP_PACKAGE_VERSION', undefined)
|
|
23
25
|
vi.stubEnv('NPM_CONFIG_USERCONFIG', undefined)
|
|
24
26
|
vi.stubEnv('npm_config_userconfig', undefined)
|
|
25
27
|
})
|
|
@@ -40,8 +42,25 @@ describe('bootstrap npm package env', () => {
|
|
|
40
42
|
`#!/usr/bin/env node
|
|
41
43
|
const delay = Number.parseInt(process.env.ONEWORKS_TEST_NPM_VIEW_DELAY_MS || '0', 10)
|
|
42
44
|
const version = process.env.ONEWORKS_TEST_NPM_VIEW_VERSION || '1.0.0'
|
|
45
|
+
const exactVersions = JSON.parse(process.env.ONEWORKS_TEST_NPM_VIEW_EXACT_VERSIONS || '{}')
|
|
46
|
+
const versions = JSON.parse(process.env.ONEWORKS_TEST_NPM_VIEW_VERSIONS || '[]')
|
|
43
47
|
if (process.argv[2] === 'view') {
|
|
48
|
+
const spec = process.argv[3]
|
|
49
|
+
const field = process.argv[4]
|
|
44
50
|
setTimeout(() => {
|
|
51
|
+
if (field === 'versions') {
|
|
52
|
+
process.stdout.write(JSON.stringify(versions.length > 0 ? versions : [version]) + '\\n')
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
if (Object.prototype.hasOwnProperty.call(exactVersions, spec)) {
|
|
56
|
+
process.stdout.write(JSON.stringify(exactVersions[spec]) + '\\n')
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
if (process.env.ONEWORKS_TEST_NPM_VIEW_FAIL_EXACT === '1' && /@\\d+\\.\\d+\\.\\d+/.test(spec)) {
|
|
60
|
+
process.stderr.write('not found\\n')
|
|
61
|
+
process.exitCode = 1
|
|
62
|
+
return
|
|
63
|
+
}
|
|
45
64
|
process.stdout.write(JSON.stringify(version) + '\\n')
|
|
46
65
|
}, delay)
|
|
47
66
|
} else {
|
|
@@ -118,6 +137,7 @@ if (process.argv[2] === 'view') {
|
|
|
118
137
|
})
|
|
119
138
|
|
|
120
139
|
it('uses installed package cache before npm view when no metadata exists', async () => {
|
|
140
|
+
vi.stubEnv('ONEWORKS_BOOTSTRAP_PACKAGE_TAG', 'latest')
|
|
121
141
|
await writeCachedPackage('@scope/pkg', '4.5.6')
|
|
122
142
|
|
|
123
143
|
await expect(resolvePublishedPackageVersion('@scope/pkg')).resolves.toBe('4.5.6')
|
|
@@ -139,6 +159,55 @@ if (process.argv[2] === 'view') {
|
|
|
139
159
|
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('timed out after 20ms'))
|
|
140
160
|
})
|
|
141
161
|
|
|
162
|
+
it('prefers the exact bootstrap prerelease version for runtime package resolution', async () => {
|
|
163
|
+
await installFakeNpm()
|
|
164
|
+
vi.stubEnv('ONEWORKS_BOOTSTRAP_PACKAGE_CACHE_FIRST', '0')
|
|
165
|
+
vi.stubEnv('ONEWORKS_BOOTSTRAP_PACKAGE_VERSION', '0.1.0-beta.0')
|
|
166
|
+
vi.stubEnv('ONEWORKS_TEST_NPM_VIEW_VERSION', '0.1.0-alpha.0')
|
|
167
|
+
vi.stubEnv(
|
|
168
|
+
'ONEWORKS_TEST_NPM_VIEW_EXACT_VERSIONS',
|
|
169
|
+
JSON.stringify({
|
|
170
|
+
'@scope/pkg@0.1.0-beta.0': '0.1.0-beta.0'
|
|
171
|
+
})
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
await expect(resolvePublishedPackageVersion('@scope/pkg')).resolves.toBe('0.1.0-beta.0')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('falls back to the highest same-core bootstrap prerelease version', async () => {
|
|
178
|
+
await installFakeNpm()
|
|
179
|
+
vi.stubEnv('ONEWORKS_BOOTSTRAP_PACKAGE_CACHE_FIRST', '0')
|
|
180
|
+
vi.stubEnv('ONEWORKS_BOOTSTRAP_PACKAGE_VERSION', '0.1.0-beta.0')
|
|
181
|
+
vi.stubEnv('ONEWORKS_TEST_NPM_VIEW_FAIL_EXACT', '1')
|
|
182
|
+
vi.stubEnv(
|
|
183
|
+
'ONEWORKS_TEST_NPM_VIEW_VERSIONS',
|
|
184
|
+
JSON.stringify([
|
|
185
|
+
'0.1.0-alpha.9',
|
|
186
|
+
'0.1.0-beta.1',
|
|
187
|
+
'0.1.0-beta.2',
|
|
188
|
+
'0.1.0-rc.0',
|
|
189
|
+
'0.1.0',
|
|
190
|
+
'0.1.1-beta.0'
|
|
191
|
+
])
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
await expect(resolvePublishedPackageVersion('@scope/pkg')).resolves.toBe('0.1.0-beta.2')
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('does not use an installed alpha cache for bootstrap beta resolution', async () => {
|
|
198
|
+
await installFakeNpm()
|
|
199
|
+
await writeCachedPackage('@scope/pkg', '0.1.0-alpha.0')
|
|
200
|
+
vi.stubEnv('ONEWORKS_BOOTSTRAP_PACKAGE_VERSION', '0.1.0-beta.0')
|
|
201
|
+
vi.stubEnv(
|
|
202
|
+
'ONEWORKS_TEST_NPM_VIEW_EXACT_VERSIONS',
|
|
203
|
+
JSON.stringify({
|
|
204
|
+
'@scope/pkg@0.1.0-beta.0': '0.1.0-beta.0'
|
|
205
|
+
})
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
await expect(resolvePublishedPackageVersion('@scope/pkg')).resolves.toBe('0.1.0-beta.0')
|
|
209
|
+
})
|
|
210
|
+
|
|
142
211
|
it('waits for npm view when no cached package version exists yet', async () => {
|
|
143
212
|
await installFakeNpm()
|
|
144
213
|
vi.stubEnv('ONEWORKS_TEST_NPM_VIEW_VERSION', '3.0.0')
|
|
@@ -26,13 +26,13 @@ describe('bootstrap runtime package commands', () => {
|
|
|
26
26
|
await rm(tempDir, { force: true, recursive: true })
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
-
const writeCachedPackage = async (packageName: string, version: string) => {
|
|
29
|
+
const writeCachedPackage = async (packageName: string, version: string, cacheVersion = version) => {
|
|
30
30
|
const sanitizedName = packageName.replace(/^@/, '').replace(/[\\/]/g, '__')
|
|
31
31
|
const packageDir = path.join(
|
|
32
32
|
tempDir,
|
|
33
33
|
'.oneworks/bootstrap/npm',
|
|
34
34
|
sanitizedName,
|
|
35
|
-
|
|
35
|
+
cacheVersion,
|
|
36
36
|
'node_modules',
|
|
37
37
|
...packageName.split('/')
|
|
38
38
|
)
|
|
@@ -190,6 +190,49 @@ process.exit(1)
|
|
|
190
190
|
).resolves.toBeUndefined()
|
|
191
191
|
})
|
|
192
192
|
|
|
193
|
+
it('installs a runtime package under an explicit cache version', async () => {
|
|
194
|
+
vi.stubEnv('ONEWORKS_TEST_NPM_VERSION', '9.9.9')
|
|
195
|
+
|
|
196
|
+
await expect(installRuntimePackage('server', {
|
|
197
|
+
cacheVersion: 'dev-local',
|
|
198
|
+
version: '2.2.0'
|
|
199
|
+
})).resolves.toMatchObject({
|
|
200
|
+
cacheVersion: 'dev-local',
|
|
201
|
+
installedVersion: '2.2.0',
|
|
202
|
+
latestInstalled: true,
|
|
203
|
+
latestVersion: '2.2.0',
|
|
204
|
+
packageName: '@oneworks/server',
|
|
205
|
+
requestedVersion: '2.2.0',
|
|
206
|
+
target: 'server',
|
|
207
|
+
updateAvailable: false
|
|
208
|
+
})
|
|
209
|
+
await expect(
|
|
210
|
+
writeFile(
|
|
211
|
+
path.join(
|
|
212
|
+
tempDir,
|
|
213
|
+
'.oneworks/bootstrap/npm/oneworks__server/dev-local/node_modules/@oneworks/server/probe'
|
|
214
|
+
),
|
|
215
|
+
'ok'
|
|
216
|
+
)
|
|
217
|
+
).resolves.toBeUndefined()
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('uses the runtime package cache version env for check and install', async () => {
|
|
221
|
+
await writeCachedPackage('@oneworks/server', '2.2.0', 'dev-env')
|
|
222
|
+
vi.stubEnv('ONEWORKS_RUNTIME_PACKAGE_CACHE_VERSION', 'dev-env')
|
|
223
|
+
vi.stubEnv('ONEWORKS_TEST_NPM_VERSION', '2.2.0')
|
|
224
|
+
|
|
225
|
+
await expect(checkRuntimePackage('server')).resolves.toMatchObject({
|
|
226
|
+
cacheVersion: 'dev-env',
|
|
227
|
+
installedVersion: '2.2.0',
|
|
228
|
+
latestInstalled: true,
|
|
229
|
+
latestVersion: '2.2.0',
|
|
230
|
+
packageName: '@oneworks/server',
|
|
231
|
+
target: 'server',
|
|
232
|
+
updateAvailable: false
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
193
236
|
it('installs the latest client runtime package target', async () => {
|
|
194
237
|
vi.stubEnv('ONEWORKS_TEST_NPM_VERSION', '2.2.0')
|
|
195
238
|
|
|
@@ -208,4 +251,10 @@ process.exit(1)
|
|
|
208
251
|
'Runtime package version must be an exact semver version'
|
|
209
252
|
)
|
|
210
253
|
})
|
|
254
|
+
|
|
255
|
+
it('rejects unsafe runtime package cache versions', async () => {
|
|
256
|
+
await expect(checkRuntimePackage('cli', { cacheVersion: '../dev', version: '1.0.0' })).rejects.toThrow(
|
|
257
|
+
'Runtime package cache version contains unsupported characters'
|
|
258
|
+
)
|
|
259
|
+
})
|
|
211
260
|
})
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oneworks",
|
|
3
|
-
"version": "0.1.0-
|
|
3
|
+
"version": "0.1.0-beta.1",
|
|
4
|
+
"description": "One Works bootstrap launcher",
|
|
4
5
|
"repository": {
|
|
5
6
|
"type": "git",
|
|
6
7
|
"url": "https://github.com/oneworks-ai/app.git",
|
|
7
8
|
"directory": "apps/bootstrap"
|
|
8
9
|
},
|
|
9
|
-
"description": "One Works bootstrap launcher",
|
|
10
10
|
"oneworks": {
|
|
11
11
|
"runtimeTranspile": true,
|
|
12
12
|
"publishAliases": [
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@clack/prompts": "^0.11.0",
|
|
28
28
|
"commander": "^12.1.0",
|
|
29
|
-
"@oneworks/cli-helper": "0.1.0-
|
|
29
|
+
"@oneworks/cli-helper": "0.1.0-beta.1"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"test": "pnpm -C ../.. exec vitest run --workspace vitest.workspace.ts --project node apps/bootstrap/__tests__"
|
package/src/npm-package-cache.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable max-lines -- package cache resolution keeps shared cache lookup and version fallback policy together. */
|
|
1
2
|
import { createHash } from 'node:crypto'
|
|
2
3
|
import { existsSync } from 'node:fs'
|
|
3
4
|
import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
@@ -9,6 +10,12 @@ import { resolveBootstrapPackageCacheDir, resolveRealHomeDir } from './paths'
|
|
|
9
10
|
const DEFAULT_PACKAGE_TAG = 'latest'
|
|
10
11
|
const DEFAULT_PACKAGE_LOOKUP_TIMEOUT_MS = 1_000
|
|
11
12
|
const DEFAULT_CACHE_FIRST = true
|
|
13
|
+
const PACKAGE_CACHE_VERSION_PATTERN = /^[\w.+-]+$/u
|
|
14
|
+
|
|
15
|
+
export const RUNTIME_PACKAGE_CACHE_VERSION_ENV = '__ONEWORKS_RUNTIME_PACKAGE_CACHE_VERSION__'
|
|
16
|
+
export const PUBLIC_RUNTIME_PACKAGE_CACHE_VERSION_ENV = 'ONEWORKS_RUNTIME_PACKAGE_CACHE_VERSION'
|
|
17
|
+
export const DESKTOP_DEV_RUNTIME_VERSION_ENV = '__ONEWORKS_DESKTOP_DEV_RUNTIME_VERSION__'
|
|
18
|
+
export const PUBLIC_DESKTOP_DEV_RUNTIME_VERSION_ENV = 'ONEWORKS_DESKTOP_DEV_RUNTIME_VERSION'
|
|
12
19
|
|
|
13
20
|
interface PublishedPackageVersionMetadata {
|
|
14
21
|
lookupKey: string
|
|
@@ -58,8 +65,33 @@ export const shouldUseCachedPackageVersionFirst = () => {
|
|
|
58
65
|
return !['0', 'false', 'no', 'off'].includes(rawValue)
|
|
59
66
|
}
|
|
60
67
|
|
|
61
|
-
export const
|
|
62
|
-
|
|
68
|
+
export const normalizePackageCacheVersion = (value: string | undefined) => {
|
|
69
|
+
const normalized = value?.trim()
|
|
70
|
+
if (normalized == null || normalized === '') return undefined
|
|
71
|
+
if (!PACKAGE_CACHE_VERSION_PATTERN.test(normalized) || normalized === '.' || normalized === '..') {
|
|
72
|
+
throw new Error(`Runtime package cache version contains unsupported characters: ${normalized}.`)
|
|
73
|
+
}
|
|
74
|
+
return normalized
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const resolveRuntimePackageCacheVersion = () => (
|
|
78
|
+
normalizePackageCacheVersion(process.env[RUNTIME_PACKAGE_CACHE_VERSION_ENV]) ??
|
|
79
|
+
normalizePackageCacheVersion(process.env[PUBLIC_RUNTIME_PACKAGE_CACHE_VERSION_ENV]) ??
|
|
80
|
+
normalizePackageCacheVersion(process.env[DESKTOP_DEV_RUNTIME_VERSION_ENV]) ??
|
|
81
|
+
normalizePackageCacheVersion(process.env[PUBLIC_DESKTOP_DEV_RUNTIME_VERSION_ENV])
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
export const resolvePackageCacheDir = (
|
|
85
|
+
packageName: string,
|
|
86
|
+
version: string,
|
|
87
|
+
options: { cacheVersion?: string } = {}
|
|
88
|
+
) => (
|
|
89
|
+
path.join(
|
|
90
|
+
resolveBootstrapPackageCacheDir(),
|
|
91
|
+
'npm',
|
|
92
|
+
sanitizePackageName(packageName),
|
|
93
|
+
normalizePackageCacheVersion(options.cacheVersion) ?? version
|
|
94
|
+
)
|
|
63
95
|
)
|
|
64
96
|
|
|
65
97
|
export const resolvePackageCacheRootDir = (packageName: string) => (
|
|
@@ -109,22 +141,29 @@ const readOptionalFile = async (filePath: string | undefined) => {
|
|
|
109
141
|
}
|
|
110
142
|
}
|
|
111
143
|
|
|
112
|
-
const resolvePackageLookupKey = async (
|
|
144
|
+
const resolvePackageLookupKey = async (
|
|
145
|
+
packageName: string,
|
|
146
|
+
options: { lookupScope?: string } = {}
|
|
147
|
+
) => {
|
|
113
148
|
const env = resolvePackageManagerEnv()
|
|
114
149
|
const userConfig = env.npm_config_userconfig ?? env.NPM_CONFIG_USERCONFIG
|
|
115
150
|
const userConfigContent = await readOptionalFile(userConfig)
|
|
151
|
+
const packageTag = options.lookupScope?.trim() || resolvePackageTag()
|
|
116
152
|
|
|
117
153
|
return JSON.stringify({
|
|
118
154
|
packageName,
|
|
119
|
-
packageTag
|
|
155
|
+
packageTag,
|
|
120
156
|
registry: env.npm_config_registry ?? env.NPM_CONFIG_REGISTRY ?? '',
|
|
121
157
|
userConfig: userConfig ?? '',
|
|
122
158
|
userConfigContentHash: userConfigContent == null ? '' : hashValue(userConfigContent)
|
|
123
159
|
})
|
|
124
160
|
}
|
|
125
161
|
|
|
126
|
-
const resolvePackageVersionMetadataPath = async (
|
|
127
|
-
|
|
162
|
+
const resolvePackageVersionMetadataPath = async (
|
|
163
|
+
packageName: string,
|
|
164
|
+
options: { lookupScope?: string } = {}
|
|
165
|
+
) => {
|
|
166
|
+
const lookupKey = await resolvePackageLookupKey(packageName, options)
|
|
128
167
|
return {
|
|
129
168
|
lookupKey,
|
|
130
169
|
metadataPath: path.join(
|
|
@@ -134,8 +173,12 @@ const resolvePackageVersionMetadataPath = async (packageName: string) => {
|
|
|
134
173
|
}
|
|
135
174
|
}
|
|
136
175
|
|
|
137
|
-
export const readPublishedPackageVersionMetadata = async (
|
|
138
|
-
|
|
176
|
+
export const readPublishedPackageVersionMetadata = async (
|
|
177
|
+
packageName: string,
|
|
178
|
+
options: { lookupScope?: string } = {}
|
|
179
|
+
) => {
|
|
180
|
+
const packageTag = options.lookupScope?.trim() || resolvePackageTag()
|
|
181
|
+
const { lookupKey, metadataPath } = await resolvePackageVersionMetadataPath(packageName, options)
|
|
139
182
|
|
|
140
183
|
try {
|
|
141
184
|
const content = await readFile(metadataPath, 'utf8')
|
|
@@ -143,7 +186,7 @@ export const readPublishedPackageVersionMetadata = async (packageName: string) =
|
|
|
143
186
|
if (
|
|
144
187
|
parsed.lookupKey === lookupKey &&
|
|
145
188
|
parsed.packageName === packageName &&
|
|
146
|
-
parsed.packageTag ===
|
|
189
|
+
parsed.packageTag === packageTag &&
|
|
147
190
|
typeof parsed.version === 'string' &&
|
|
148
191
|
parsed.version.trim()
|
|
149
192
|
) {
|
|
@@ -159,15 +202,20 @@ export const readPublishedPackageVersionMetadata = async (packageName: string) =
|
|
|
159
202
|
return undefined
|
|
160
203
|
}
|
|
161
204
|
|
|
162
|
-
export const writePublishedPackageVersionMetadata = async (
|
|
163
|
-
|
|
205
|
+
export const writePublishedPackageVersionMetadata = async (
|
|
206
|
+
packageName: string,
|
|
207
|
+
version: string,
|
|
208
|
+
options: { lookupScope?: string } = {}
|
|
209
|
+
) => {
|
|
210
|
+
const packageTag = options.lookupScope?.trim() || resolvePackageTag()
|
|
211
|
+
const { lookupKey, metadataPath } = await resolvePackageVersionMetadataPath(packageName, options)
|
|
164
212
|
await ensureDirectory(path.dirname(metadataPath))
|
|
165
213
|
|
|
166
214
|
const tempPath = `${metadataPath}.${process.pid}.${Date.now()}.tmp`
|
|
167
215
|
const metadata: PublishedPackageVersionMetadata = {
|
|
168
216
|
lookupKey,
|
|
169
217
|
packageName,
|
|
170
|
-
packageTag
|
|
218
|
+
packageTag,
|
|
171
219
|
resolvedAt: new Date().toISOString(),
|
|
172
220
|
version
|
|
173
221
|
}
|
|
@@ -6,10 +6,12 @@ import {
|
|
|
6
6
|
compareVersionLike,
|
|
7
7
|
ensureDirectory,
|
|
8
8
|
isExistingPath,
|
|
9
|
+
normalizePackageCacheVersion,
|
|
9
10
|
resolvePackageCacheDir,
|
|
10
11
|
resolvePackageCacheRootDir,
|
|
11
12
|
resolvePackageInstallDir,
|
|
12
|
-
resolvePackageManagerEnv
|
|
13
|
+
resolvePackageManagerEnv,
|
|
14
|
+
resolveRuntimePackageCacheVersion
|
|
13
15
|
} from './npm-package-cache'
|
|
14
16
|
import { runBufferedCommand } from './process-utils'
|
|
15
17
|
import { createBootstrapProgress } from './progress'
|
|
@@ -17,6 +19,7 @@ import { createBootstrapProgress } from './progress'
|
|
|
17
19
|
const NPM_BIN = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
18
20
|
|
|
19
21
|
interface InstalledPackageInfo {
|
|
22
|
+
cacheVersion: string
|
|
20
23
|
packageDir: string
|
|
21
24
|
version: string
|
|
22
25
|
}
|
|
@@ -36,7 +39,13 @@ export const readInstalledPackageVersion = async (packageDir: string) => {
|
|
|
36
39
|
}
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
export const findInstalledPublishedPackageVersion = async (
|
|
42
|
+
export const findInstalledPublishedPackageVersion = async (
|
|
43
|
+
packageName: string,
|
|
44
|
+
options: {
|
|
45
|
+
preferredVersion?: string
|
|
46
|
+
versionFilter?: (version: string) => boolean
|
|
47
|
+
} = {}
|
|
48
|
+
) => {
|
|
40
49
|
let versions: string[]
|
|
41
50
|
try {
|
|
42
51
|
versions = (await readdir(resolvePackageCacheRootDir(packageName), { withFileTypes: true }))
|
|
@@ -48,6 +57,9 @@ export const findInstalledPublishedPackageVersion = async (packageName: string)
|
|
|
48
57
|
|
|
49
58
|
const installedVersions: string[] = []
|
|
50
59
|
for (const version of versions) {
|
|
60
|
+
if (options.versionFilter != null && !options.versionFilter(version)) {
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
51
63
|
const packageDir = resolvePackageInstallDir(resolvePackageCacheDir(packageName, version), packageName)
|
|
52
64
|
const installedVersion = await readInstalledPackageVersion(packageDir)
|
|
53
65
|
if (installedVersion === version) {
|
|
@@ -55,6 +67,10 @@ export const findInstalledPublishedPackageVersion = async (packageName: string)
|
|
|
55
67
|
}
|
|
56
68
|
}
|
|
57
69
|
|
|
70
|
+
if (options.preferredVersion != null && installedVersions.includes(options.preferredVersion)) {
|
|
71
|
+
return options.preferredVersion
|
|
72
|
+
}
|
|
73
|
+
|
|
58
74
|
return installedVersions.sort(compareVersionLike).at(-1)
|
|
59
75
|
}
|
|
60
76
|
|
|
@@ -63,12 +79,19 @@ const formatInstallError = (message: string, stderr: string) => {
|
|
|
63
79
|
return detail ? `${message}\n${detail}` : message
|
|
64
80
|
}
|
|
65
81
|
|
|
66
|
-
export const installPublishedPackage = async (
|
|
67
|
-
|
|
82
|
+
export const installPublishedPackage = async (
|
|
83
|
+
packageName: string,
|
|
84
|
+
version: string,
|
|
85
|
+
options: { cacheVersion?: string } = {}
|
|
86
|
+
): Promise<InstalledPackageInfo> => {
|
|
87
|
+
const cacheVersion = normalizePackageCacheVersion(options.cacheVersion) ??
|
|
88
|
+
resolveRuntimePackageCacheVersion() ??
|
|
89
|
+
version
|
|
90
|
+
const cacheDir = resolvePackageCacheDir(packageName, version, { cacheVersion })
|
|
68
91
|
const packageDir = resolvePackageInstallDir(cacheDir, packageName)
|
|
69
92
|
const installedVersion = await readInstalledPackageVersion(packageDir)
|
|
70
93
|
if (installedVersion === version) {
|
|
71
|
-
return { packageDir, version }
|
|
94
|
+
return { cacheVersion, packageDir, version }
|
|
72
95
|
}
|
|
73
96
|
|
|
74
97
|
const stagingDir = `${cacheDir}.tmp-${process.pid}-${Date.now()}`
|
|
@@ -76,7 +99,9 @@ export const installPublishedPackage = async (packageName: string, version: stri
|
|
|
76
99
|
await ensureDirectory(stagingDir)
|
|
77
100
|
|
|
78
101
|
const progress = createBootstrapProgress({
|
|
79
|
-
label:
|
|
102
|
+
label: cacheVersion === version
|
|
103
|
+
? `installing ${packageName}@${version} into bootstrap cache`
|
|
104
|
+
: `installing ${packageName}@${version} into bootstrap cache ${cacheVersion}`
|
|
80
105
|
})
|
|
81
106
|
try {
|
|
82
107
|
const result = await runBufferedCommand({
|
|
@@ -100,7 +125,11 @@ export const installPublishedPackage = async (packageName: string, version: stri
|
|
|
100
125
|
await ensureDirectory(path.dirname(cacheDir))
|
|
101
126
|
await rm(cacheDir, { recursive: true, force: true })
|
|
102
127
|
await rename(stagingDir, cacheDir)
|
|
103
|
-
progress.finish(
|
|
128
|
+
progress.finish(
|
|
129
|
+
cacheVersion === version
|
|
130
|
+
? `cached ${packageName}@${version}`
|
|
131
|
+
: `cached ${packageName}@${version} as ${cacheVersion}`
|
|
132
|
+
)
|
|
104
133
|
} catch (error) {
|
|
105
134
|
progress.fail(`failed to cache ${packageName}@${version}`)
|
|
106
135
|
await rm(stagingDir, { recursive: true, force: true }).catch(() => {})
|
|
@@ -108,6 +137,7 @@ export const installPublishedPackage = async (packageName: string, version: stri
|
|
|
108
137
|
}
|
|
109
138
|
|
|
110
139
|
return {
|
|
140
|
+
cacheVersion,
|
|
111
141
|
packageDir: resolvePackageInstallDir(cacheDir, packageName),
|
|
112
142
|
version
|
|
113
143
|
}
|
package/src/npm-package.ts
CHANGED
|
@@ -8,14 +8,12 @@ import {
|
|
|
8
8
|
readPublishedPackageVersionMetadata,
|
|
9
9
|
resolvePackageLookupTimeoutMs,
|
|
10
10
|
resolvePackageManagerEnv,
|
|
11
|
-
resolvePackageTag,
|
|
12
11
|
shouldUseCachedPackageVersionFirst,
|
|
13
12
|
writePublishedPackageVersionMetadata
|
|
14
13
|
} from './npm-package-cache'
|
|
15
14
|
import { findInstalledPublishedPackageVersion } from './npm-package-install'
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
const NPM_BIN = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
15
|
+
import { resolvePublishedPackageVersionFromRegistry } from './npm-registry'
|
|
16
|
+
import { resolvePackageVersionRequest } from './package-version-request'
|
|
19
17
|
|
|
20
18
|
export { resolvePackageManagerEnv } from './npm-package-cache'
|
|
21
19
|
export { installPublishedPackage, resolvePackageBinEntrypoint } from './npm-package-install'
|
|
@@ -52,68 +50,21 @@ const spawnPackageVersionRefresh = (packageName: string) => {
|
|
|
52
50
|
}
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
const resolvePublishedPackageVersionFromRegistry = async (
|
|
56
|
-
packageName: string,
|
|
57
|
-
options: { timeoutMs?: number } = {}
|
|
58
|
-
) => {
|
|
59
|
-
const spec = `${packageName}@${resolvePackageTag()}`
|
|
60
|
-
const result = await runBufferedCommand({
|
|
61
|
-
command: NPM_BIN,
|
|
62
|
-
args: ['view', spec, 'version', '--json'],
|
|
63
|
-
env: resolvePackageManagerEnv(),
|
|
64
|
-
timeoutMs: options.timeoutMs
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
if (result.timedOut === true) {
|
|
68
|
-
return {
|
|
69
|
-
spec,
|
|
70
|
-
timedOut: true as const
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (result.code !== 0) {
|
|
75
|
-
throw new Error(`Failed to resolve published version for ${spec}:\n${result.stderr.trim()}`)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const normalizedOutput = result.stdout.trim()
|
|
79
|
-
if (!normalizedOutput) {
|
|
80
|
-
throw new Error(`No version was returned for ${spec}.`)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const parsed = JSON.parse(normalizedOutput) as unknown
|
|
85
|
-
if (typeof parsed === 'string' && parsed.trim()) {
|
|
86
|
-
return {
|
|
87
|
-
spec,
|
|
88
|
-
version: parsed.trim()
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
} catch {
|
|
92
|
-
// fall through
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const unquotedOutput = normalizedOutput.replace(/^"|"$/g, '').trim()
|
|
96
|
-
if (!unquotedOutput) {
|
|
97
|
-
throw new Error(`Invalid published version for ${spec}: ${normalizedOutput}`)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
spec,
|
|
102
|
-
version: unquotedOutput
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
53
|
export const resolvePublishedPackageVersion = async (
|
|
107
54
|
packageName: string,
|
|
108
55
|
options: { cacheFirst?: boolean } = {}
|
|
109
56
|
) => {
|
|
110
|
-
const
|
|
57
|
+
const request = resolvePackageVersionRequest(packageName)
|
|
58
|
+
const cachedMetadata = await readPublishedPackageVersionMetadata(packageName, { lookupScope: request.lookupScope })
|
|
111
59
|
if (cachedMetadata != null && (options.cacheFirst ?? shouldUseCachedPackageVersionFirst())) {
|
|
112
60
|
spawnPackageVersionRefresh(packageName)
|
|
113
61
|
return cachedMetadata.version
|
|
114
62
|
}
|
|
115
63
|
|
|
116
|
-
const cachedInstalledVersion = await findInstalledPublishedPackageVersion(packageName
|
|
64
|
+
const cachedInstalledVersion = await findInstalledPublishedPackageVersion(packageName, {
|
|
65
|
+
preferredVersion: request.exactVersion,
|
|
66
|
+
versionFilter: request.versionFilter
|
|
67
|
+
})
|
|
117
68
|
if (cachedInstalledVersion != null && (options.cacheFirst ?? shouldUseCachedPackageVersionFirst())) {
|
|
118
69
|
spawnPackageVersionRefresh(packageName)
|
|
119
70
|
return cachedInstalledVersion
|
|
@@ -121,6 +72,7 @@ export const resolvePublishedPackageVersion = async (
|
|
|
121
72
|
|
|
122
73
|
const registryResult = await resolvePublishedPackageVersionFromRegistry(
|
|
123
74
|
packageName,
|
|
75
|
+
request,
|
|
124
76
|
cachedMetadata == null
|
|
125
77
|
? {}
|
|
126
78
|
: {
|
|
@@ -129,15 +81,17 @@ export const resolvePublishedPackageVersion = async (
|
|
|
129
81
|
)
|
|
130
82
|
|
|
131
83
|
if ('version' in registryResult) {
|
|
132
|
-
await writePublishedPackageVersionMetadata(packageName, registryResult.version
|
|
84
|
+
await writePublishedPackageVersionMetadata(packageName, registryResult.version, {
|
|
85
|
+
lookupScope: request.lookupScope
|
|
86
|
+
})
|
|
133
87
|
return registryResult.version
|
|
134
88
|
}
|
|
135
89
|
|
|
136
90
|
if (cachedMetadata == null) {
|
|
137
91
|
// This is not expected because uncached lookups do not use a timeout.
|
|
138
|
-
const retryResult = await resolvePublishedPackageVersionFromRegistry(packageName)
|
|
92
|
+
const retryResult = await resolvePublishedPackageVersionFromRegistry(packageName, request)
|
|
139
93
|
if ('version' in retryResult) {
|
|
140
|
-
await writePublishedPackageVersionMetadata(packageName, retryResult.version)
|
|
94
|
+
await writePublishedPackageVersionMetadata(packageName, retryResult.version, { lookupScope: request.lookupScope })
|
|
141
95
|
return retryResult.version
|
|
142
96
|
}
|
|
143
97
|
throw new Error(`Failed to resolve published version for ${retryResult.spec}.`)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import { resolvePackageManagerEnv } from './npm-package-cache'
|
|
4
|
+
import { comparePackageVersions } from './package-version-request'
|
|
5
|
+
import type { PackageVersionRequest } from './package-version-request'
|
|
6
|
+
import { runBufferedCommand } from './process-utils'
|
|
7
|
+
|
|
8
|
+
const NPM_BIN = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
9
|
+
|
|
10
|
+
const parseVersionOutput = (spec: string, output: string) => {
|
|
11
|
+
const normalizedOutput = output.trim()
|
|
12
|
+
if (!normalizedOutput) {
|
|
13
|
+
throw new Error(`No version was returned for ${spec}.`)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(normalizedOutput) as unknown
|
|
18
|
+
if (typeof parsed === 'string' && parsed.trim()) {
|
|
19
|
+
return parsed.trim()
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
// fall through
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const unquotedOutput = normalizedOutput.replace(/^"|"$/g, '').trim()
|
|
26
|
+
if (!unquotedOutput) {
|
|
27
|
+
throw new Error(`Invalid published version for ${spec}: ${normalizedOutput}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return unquotedOutput
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const resolvePublishedPackageVersionBySpec = async (
|
|
34
|
+
spec: string,
|
|
35
|
+
options: { allowMissing?: boolean; timeoutMs?: number } = {}
|
|
36
|
+
) => {
|
|
37
|
+
const result = await runBufferedCommand({
|
|
38
|
+
command: NPM_BIN,
|
|
39
|
+
args: ['view', spec, 'version', '--json'],
|
|
40
|
+
env: resolvePackageManagerEnv(),
|
|
41
|
+
timeoutMs: options.timeoutMs
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
if (result.timedOut === true) {
|
|
45
|
+
return {
|
|
46
|
+
spec,
|
|
47
|
+
timedOut: true as const
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (result.code !== 0) {
|
|
52
|
+
if (options.allowMissing === true) {
|
|
53
|
+
return {
|
|
54
|
+
missing: true as const,
|
|
55
|
+
spec
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
throw new Error(`Failed to resolve published version for ${spec}:\n${result.stderr.trim()}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
spec,
|
|
63
|
+
version: parseVersionOutput(spec, result.stdout)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const parseVersionsOutput = (spec: string, output: string) => {
|
|
68
|
+
const normalizedOutput = output.trim()
|
|
69
|
+
if (!normalizedOutput) {
|
|
70
|
+
throw new Error(`No versions were returned for ${spec}.`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let parsed: unknown
|
|
74
|
+
try {
|
|
75
|
+
parsed = JSON.parse(normalizedOutput) as unknown
|
|
76
|
+
} catch {
|
|
77
|
+
parsed = normalizedOutput.replace(/^"|"$/g, '').trim()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const versions = Array.isArray(parsed)
|
|
81
|
+
? parsed.filter((version): version is string => typeof version === 'string' && version.trim() !== '')
|
|
82
|
+
: typeof parsed === 'string' && parsed.trim() !== ''
|
|
83
|
+
? [parsed.trim()]
|
|
84
|
+
: []
|
|
85
|
+
if (versions.length === 0) {
|
|
86
|
+
throw new Error(`Invalid published versions for ${spec}: ${normalizedOutput}`)
|
|
87
|
+
}
|
|
88
|
+
return versions
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const resolvePublishedPackageVersionsFromRegistry = async (
|
|
92
|
+
packageName: string,
|
|
93
|
+
options: { timeoutMs?: number } = {}
|
|
94
|
+
) => {
|
|
95
|
+
const result = await runBufferedCommand({
|
|
96
|
+
command: NPM_BIN,
|
|
97
|
+
args: ['view', packageName, 'versions', '--json'],
|
|
98
|
+
env: resolvePackageManagerEnv(),
|
|
99
|
+
timeoutMs: options.timeoutMs
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
if (result.timedOut === true) {
|
|
103
|
+
return {
|
|
104
|
+
spec: packageName,
|
|
105
|
+
timedOut: true as const
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (result.code !== 0) {
|
|
110
|
+
throw new Error(`Failed to resolve published versions for ${packageName}:\n${result.stderr.trim()}`)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
spec: packageName,
|
|
115
|
+
versions: parseVersionsOutput(packageName, result.stdout)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const resolvePublishedPackageVersionFromRegistry = async (
|
|
120
|
+
packageName: string,
|
|
121
|
+
request: PackageVersionRequest,
|
|
122
|
+
options: { timeoutMs?: number } = {}
|
|
123
|
+
) => {
|
|
124
|
+
if (request.exactVersion == null) {
|
|
125
|
+
return await resolvePublishedPackageVersionBySpec(request.packageSpec, options)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const exactResult = await resolvePublishedPackageVersionBySpec(request.packageSpec, {
|
|
129
|
+
allowMissing: true,
|
|
130
|
+
timeoutMs: options.timeoutMs
|
|
131
|
+
})
|
|
132
|
+
if ('version' in exactResult || 'timedOut' in exactResult) {
|
|
133
|
+
return exactResult
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const versionsResult = await resolvePublishedPackageVersionsFromRegistry(packageName, options)
|
|
137
|
+
if ('timedOut' in versionsResult) {
|
|
138
|
+
return versionsResult
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const matchedVersion = versionsResult.versions
|
|
142
|
+
.filter(version => request.versionFilter == null || request.versionFilter(version))
|
|
143
|
+
.sort(comparePackageVersions)
|
|
144
|
+
.at(-1)
|
|
145
|
+
if (matchedVersion == null) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`No published version for ${packageName} matches bootstrap runtime series ${request.exactVersion}.`
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
spec: `${packageName} versions`,
|
|
152
|
+
version: matchedVersion
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
|
|
4
|
+
import { compareVersionLike, resolvePackageTag } from './npm-package-cache'
|
|
5
|
+
|
|
6
|
+
const BOOTSTRAP_PACKAGE_VERSION_ENV = 'ONEWORKS_BOOTSTRAP_PACKAGE_VERSION'
|
|
7
|
+
|
|
8
|
+
interface ParsedSemver {
|
|
9
|
+
major: number
|
|
10
|
+
minor: number
|
|
11
|
+
patch: number
|
|
12
|
+
prerelease: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PackageVersionRequest {
|
|
16
|
+
exactVersion?: string
|
|
17
|
+
lookupScope: string
|
|
18
|
+
packageSpec: string
|
|
19
|
+
tag?: string
|
|
20
|
+
versionFilter?: (version: string) => boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const parseSemver = (version: string): ParsedSemver | undefined => {
|
|
24
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(version.trim())
|
|
25
|
+
if (match == null) return undefined
|
|
26
|
+
return {
|
|
27
|
+
major: Number(match[1]),
|
|
28
|
+
minor: Number(match[2]),
|
|
29
|
+
patch: Number(match[3]),
|
|
30
|
+
prerelease: match[4]?.split('.') ?? []
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const comparePrereleaseIdentifiers = (left: string, right: string) => {
|
|
35
|
+
const leftNumber = /^\d+$/u.test(left) ? Number(left) : undefined
|
|
36
|
+
const rightNumber = /^\d+$/u.test(right) ? Number(right) : undefined
|
|
37
|
+
if (leftNumber != null && rightNumber != null) return leftNumber - rightNumber
|
|
38
|
+
if (leftNumber != null) return -1
|
|
39
|
+
if (rightNumber != null) return 1
|
|
40
|
+
return left.localeCompare(right)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const compareSemver = (left: ParsedSemver, right: ParsedSemver) => {
|
|
44
|
+
const coreDiff = left.major - right.major || left.minor - right.minor || left.patch - right.patch
|
|
45
|
+
if (coreDiff !== 0) return coreDiff
|
|
46
|
+
if (left.prerelease.length === 0 && right.prerelease.length === 0) return 0
|
|
47
|
+
if (left.prerelease.length === 0) return 1
|
|
48
|
+
if (right.prerelease.length === 0) return -1
|
|
49
|
+
const maxLength = Math.max(left.prerelease.length, right.prerelease.length)
|
|
50
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
51
|
+
const leftPart = left.prerelease[index]
|
|
52
|
+
const rightPart = right.prerelease[index]
|
|
53
|
+
if (leftPart == null) return -1
|
|
54
|
+
if (rightPart == null) return 1
|
|
55
|
+
const diff = comparePrereleaseIdentifiers(leftPart, rightPart)
|
|
56
|
+
if (diff !== 0) return diff
|
|
57
|
+
}
|
|
58
|
+
return 0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const comparePackageVersions = (left: string, right: string) => {
|
|
62
|
+
const leftSemver = parseSemver(left)
|
|
63
|
+
const rightSemver = parseSemver(right)
|
|
64
|
+
if (leftSemver != null && rightSemver != null) return compareSemver(leftSemver, rightSemver)
|
|
65
|
+
return compareVersionLike(left, right)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const hasSameSemverCore = (left: ParsedSemver, right: ParsedSemver) => (
|
|
69
|
+
left.major === right.major && left.minor === right.minor && left.patch === right.patch
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const readBootstrapPackageVersion = () => {
|
|
73
|
+
const envVersion = process.env[BOOTSTRAP_PACKAGE_VERSION_ENV]?.trim()
|
|
74
|
+
if (envVersion) return envVersion
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const requireFromHere = createRequire(import.meta.url)
|
|
78
|
+
const packageJsonPath = requireFromHere.resolve('oneworks/package.json')
|
|
79
|
+
const packageJson = requireFromHere(packageJsonPath) as { version?: unknown }
|
|
80
|
+
return typeof packageJson.version === 'string' && packageJson.version.trim()
|
|
81
|
+
? packageJson.version.trim()
|
|
82
|
+
: undefined
|
|
83
|
+
} catch {
|
|
84
|
+
return undefined
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const resolvePackageVersionRequest = (packageName: string): PackageVersionRequest => {
|
|
89
|
+
const explicitTag = process.env.ONEWORKS_BOOTSTRAP_PACKAGE_TAG?.trim()
|
|
90
|
+
if (explicitTag) {
|
|
91
|
+
return {
|
|
92
|
+
lookupScope: `tag:${explicitTag}`,
|
|
93
|
+
packageSpec: `${packageName}@${explicitTag}`,
|
|
94
|
+
tag: explicitTag
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const bootstrapVersion = readBootstrapPackageVersion()
|
|
99
|
+
const parsedBootstrapVersion = bootstrapVersion == null ? undefined : parseSemver(bootstrapVersion)
|
|
100
|
+
const prereleaseChannel = parsedBootstrapVersion?.prerelease[0]
|
|
101
|
+
if (bootstrapVersion != null && parsedBootstrapVersion != null && prereleaseChannel != null) {
|
|
102
|
+
return {
|
|
103
|
+
exactVersion: bootstrapVersion,
|
|
104
|
+
lookupScope: `bootstrap:${bootstrapVersion}:channel:${prereleaseChannel}`,
|
|
105
|
+
packageSpec: `${packageName}@${bootstrapVersion}`,
|
|
106
|
+
versionFilter: (version) => {
|
|
107
|
+
const parsedVersion = parseSemver(version)
|
|
108
|
+
return parsedVersion != null &&
|
|
109
|
+
hasSameSemverCore(parsedVersion, parsedBootstrapVersion) &&
|
|
110
|
+
parsedVersion.prerelease[0] === prereleaseChannel
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const tag = resolvePackageTag()
|
|
116
|
+
return {
|
|
117
|
+
lookupScope: `tag:${tag}`,
|
|
118
|
+
packageSpec: `${packageName}@${tag}`,
|
|
119
|
+
tag
|
|
120
|
+
}
|
|
121
|
+
}
|
package/src/program.ts
CHANGED
|
@@ -35,6 +35,7 @@ type BootstrapTarget =
|
|
|
35
35
|
}
|
|
36
36
|
| {
|
|
37
37
|
action: RuntimePackageAction
|
|
38
|
+
cacheVersion?: string
|
|
38
39
|
json: boolean
|
|
39
40
|
kind: 'runtime-package'
|
|
40
41
|
target?: string
|
|
@@ -88,6 +89,7 @@ const parseRuntimePackageTarget = (args: string[]): BootstrapTarget => {
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
let version: string | undefined
|
|
92
|
+
let cacheVersion: string | undefined
|
|
91
93
|
for (let index = 0; index < forwardedArgs.length; index += 1) {
|
|
92
94
|
const arg = forwardedArgs[index]
|
|
93
95
|
if (arg === '--version') {
|
|
@@ -109,6 +111,28 @@ const parseRuntimePackageTarget = (args: string[]): BootstrapTarget => {
|
|
|
109
111
|
version = value
|
|
110
112
|
forwardedArgs.splice(index, 1)
|
|
111
113
|
index -= 1
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (arg === '--cache-version') {
|
|
118
|
+
const value = forwardedArgs[index + 1]
|
|
119
|
+
if (value == null || value.trim() === '') {
|
|
120
|
+
throw new Error('Runtime package --cache-version requires a value.')
|
|
121
|
+
}
|
|
122
|
+
cacheVersion = value
|
|
123
|
+
forwardedArgs.splice(index, 2)
|
|
124
|
+
index -= 1
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (arg?.startsWith('--cache-version=')) {
|
|
129
|
+
const value = arg.slice('--cache-version='.length)
|
|
130
|
+
if (value.trim() === '') {
|
|
131
|
+
throw new Error('Runtime package --cache-version requires a value.')
|
|
132
|
+
}
|
|
133
|
+
cacheVersion = value
|
|
134
|
+
forwardedArgs.splice(index, 1)
|
|
135
|
+
index -= 1
|
|
112
136
|
}
|
|
113
137
|
}
|
|
114
138
|
|
|
@@ -130,6 +154,7 @@ const parseRuntimePackageTarget = (args: string[]): BootstrapTarget => {
|
|
|
130
154
|
|
|
131
155
|
return {
|
|
132
156
|
action,
|
|
157
|
+
...(cacheVersion != null ? { cacheVersion } : {}),
|
|
133
158
|
json,
|
|
134
159
|
kind: 'runtime-package',
|
|
135
160
|
target: selectorTarget,
|
|
@@ -216,6 +241,7 @@ Examples:
|
|
|
216
241
|
npx oneworks runtime check cli@0.1.0-alpha.0
|
|
217
242
|
npx oneworks runtime install server
|
|
218
243
|
npx oneworks runtime install server --version 0.1.0-alpha.0
|
|
244
|
+
npx oneworks runtime install server --version 0.1.0-alpha.0 --cache-version dev-local
|
|
219
245
|
npx oneworks app
|
|
220
246
|
npx oneworks app cache
|
|
221
247
|
npx oneworks app --no-cache
|
|
@@ -240,15 +266,20 @@ Examples:
|
|
|
240
266
|
}
|
|
241
267
|
|
|
242
268
|
if (target.kind === 'runtime-package') {
|
|
269
|
+
const runtimeOptions: RuntimePackageOptions = {
|
|
270
|
+
...(target.version == null ? {} : { version: target.version }),
|
|
271
|
+
...(target.cacheVersion == null ? {} : { cacheVersion: target.cacheVersion })
|
|
272
|
+
}
|
|
273
|
+
const hasRuntimeOptions = Object.keys(runtimeOptions).length > 0
|
|
243
274
|
let status: RuntimePackageStatus
|
|
244
275
|
if (target.action === 'install') {
|
|
245
|
-
status =
|
|
276
|
+
status = !hasRuntimeOptions
|
|
246
277
|
? await deps.installRuntimePackage(target.target)
|
|
247
|
-
: await deps.installRuntimePackage(target.target,
|
|
278
|
+
: await deps.installRuntimePackage(target.target, runtimeOptions)
|
|
248
279
|
} else {
|
|
249
|
-
status =
|
|
280
|
+
status = !hasRuntimeOptions
|
|
250
281
|
? await deps.checkRuntimePackage(target.target)
|
|
251
|
-
: await deps.checkRuntimePackage(target.target,
|
|
282
|
+
: await deps.checkRuntimePackage(target.target, runtimeOptions)
|
|
252
283
|
}
|
|
253
284
|
const output = target.json ? JSON.stringify(status) : formatRuntimePackageStatus(status)
|
|
254
285
|
process.stdout.write(`${output}\n`)
|
package/src/runtime-package.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { resolvePublishedPackageVersion } from './npm-package'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
normalizePackageCacheVersion,
|
|
4
|
+
resolvePackageCacheDir,
|
|
5
|
+
resolvePackageInstallDir,
|
|
6
|
+
resolveRuntimePackageCacheVersion
|
|
7
|
+
} from './npm-package-cache'
|
|
3
8
|
import {
|
|
4
9
|
findInstalledPublishedPackageVersion,
|
|
5
10
|
installPublishedPackage,
|
|
@@ -10,10 +15,12 @@ export type RuntimePackageAction = 'check' | 'install'
|
|
|
10
15
|
export type RuntimePackageTarget = 'cli' | 'client' | 'server' | 'web'
|
|
11
16
|
|
|
12
17
|
export interface RuntimePackageOptions {
|
|
18
|
+
cacheVersion?: string
|
|
13
19
|
version?: string
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
export interface RuntimePackageStatus {
|
|
23
|
+
cacheVersion?: string
|
|
17
24
|
installed: boolean
|
|
18
25
|
installedVersion?: string
|
|
19
26
|
latestInstalled: boolean
|
|
@@ -56,8 +63,11 @@ export const resolveRuntimePackageTarget = (value: string | undefined): RuntimeP
|
|
|
56
63
|
)
|
|
57
64
|
}
|
|
58
65
|
|
|
59
|
-
const readVersionInstalledAt = async (packageName: string, version: string) => {
|
|
60
|
-
const packageDir = resolvePackageInstallDir(
|
|
66
|
+
const readVersionInstalledAt = async (packageName: string, version: string, cacheVersion = version) => {
|
|
67
|
+
const packageDir = resolvePackageInstallDir(
|
|
68
|
+
resolvePackageCacheDir(packageName, version, { cacheVersion }),
|
|
69
|
+
packageName
|
|
70
|
+
)
|
|
61
71
|
return await readInstalledPackageVersion(packageDir)
|
|
62
72
|
}
|
|
63
73
|
|
|
@@ -70,12 +80,19 @@ const createRuntimePackageStatus = async (
|
|
|
70
80
|
target: RuntimePackageTarget,
|
|
71
81
|
packageName: string,
|
|
72
82
|
targetVersion: string,
|
|
73
|
-
requestedVersion: string | undefined
|
|
83
|
+
requestedVersion: string | undefined,
|
|
84
|
+
requestedCacheVersion: string | undefined
|
|
74
85
|
): Promise<RuntimePackageStatus> => {
|
|
75
|
-
const
|
|
76
|
-
const
|
|
86
|
+
const cacheVersion = normalizePackageCacheVersion(requestedCacheVersion)
|
|
87
|
+
const resolvedCacheVersion = cacheVersion ?? targetVersion
|
|
88
|
+
const installedVersion = cacheVersion == null
|
|
89
|
+
? await findInstalledPublishedPackageVersion(packageName)
|
|
90
|
+
: await readVersionInstalledAt(packageName, targetVersion, resolvedCacheVersion)
|
|
91
|
+
const targetInstalled =
|
|
92
|
+
await readVersionInstalledAt(packageName, targetVersion, resolvedCacheVersion) === targetVersion
|
|
77
93
|
|
|
78
94
|
return {
|
|
95
|
+
...(cacheVersion != null ? { cacheVersion } : {}),
|
|
79
96
|
installed: installedVersion != null,
|
|
80
97
|
...(installedVersion != null ? { installedVersion } : {}),
|
|
81
98
|
latestInstalled: targetInstalled,
|
|
@@ -83,7 +100,9 @@ const createRuntimePackageStatus = async (
|
|
|
83
100
|
packageName,
|
|
84
101
|
...(requestedVersion != null ? { requestedVersion } : {}),
|
|
85
102
|
target,
|
|
86
|
-
updateAvailable: requestedVersion != null
|
|
103
|
+
updateAvailable: requestedVersion != null || cacheVersion != null
|
|
104
|
+
? !targetInstalled
|
|
105
|
+
: installedVersion !== targetVersion
|
|
87
106
|
}
|
|
88
107
|
}
|
|
89
108
|
|
|
@@ -94,8 +113,9 @@ export const checkRuntimePackage = async (
|
|
|
94
113
|
const target = resolveRuntimePackageTarget(targetValue)
|
|
95
114
|
const packageName = RUNTIME_PACKAGE_NAMES[target]
|
|
96
115
|
const requestedVersion = normalizeRequestedVersion(options.version)
|
|
116
|
+
const cacheVersion = normalizePackageCacheVersion(options.cacheVersion) ?? resolveRuntimePackageCacheVersion()
|
|
97
117
|
const targetVersion = requestedVersion ?? await resolveRuntimePackageVersion(packageName, options)
|
|
98
|
-
return await createRuntimePackageStatus(target, packageName, targetVersion, requestedVersion)
|
|
118
|
+
return await createRuntimePackageStatus(target, packageName, targetVersion, requestedVersion, cacheVersion)
|
|
99
119
|
}
|
|
100
120
|
|
|
101
121
|
export const installRuntimePackage = async (
|
|
@@ -105,19 +125,31 @@ export const installRuntimePackage = async (
|
|
|
105
125
|
const target = resolveRuntimePackageTarget(targetValue)
|
|
106
126
|
const packageName = RUNTIME_PACKAGE_NAMES[target]
|
|
107
127
|
const requestedVersion = normalizeRequestedVersion(options.version)
|
|
128
|
+
const cacheVersion = normalizePackageCacheVersion(options.cacheVersion) ?? resolveRuntimePackageCacheVersion()
|
|
108
129
|
const targetVersion = requestedVersion ?? await resolveRuntimePackageVersion(packageName, options)
|
|
109
|
-
await installPublishedPackage(packageName, targetVersion)
|
|
110
|
-
return await createRuntimePackageStatus(target, packageName, targetVersion, requestedVersion)
|
|
130
|
+
await installPublishedPackage(packageName, targetVersion, cacheVersion == null ? {} : { cacheVersion })
|
|
131
|
+
return await createRuntimePackageStatus(target, packageName, targetVersion, requestedVersion, cacheVersion)
|
|
111
132
|
}
|
|
112
133
|
|
|
113
134
|
export const formatRuntimePackageStatus = (status: RuntimePackageStatus) => {
|
|
114
135
|
const current = status.installedVersion ?? 'not installed'
|
|
115
136
|
if (status.requestedVersion != null) {
|
|
137
|
+
if (status.cacheVersion != null) {
|
|
138
|
+
return status.latestInstalled
|
|
139
|
+
? `${status.packageName}@${status.requestedVersion} cached as ${status.cacheVersion}`
|
|
140
|
+
: `${status.packageName}@${status.requestedVersion} not cached as ${status.cacheVersion} (${current})`
|
|
141
|
+
}
|
|
116
142
|
return status.latestInstalled
|
|
117
143
|
? `${status.packageName}@${status.requestedVersion} cached`
|
|
118
144
|
: `${status.packageName}@${status.requestedVersion} not cached (${current})`
|
|
119
145
|
}
|
|
120
146
|
|
|
147
|
+
if (status.cacheVersion != null) {
|
|
148
|
+
return status.latestInstalled
|
|
149
|
+
? `${status.packageName}@${status.latestVersion} cached as ${status.cacheVersion}`
|
|
150
|
+
: `${status.packageName}@${status.latestVersion} not cached as ${status.cacheVersion} (${current})`
|
|
151
|
+
}
|
|
152
|
+
|
|
121
153
|
const suffix = status.updateAvailable
|
|
122
154
|
? `update available: ${current} -> ${status.latestVersion}`
|
|
123
155
|
: `up to date: ${status.latestVersion}`
|