onework 0.1.0-beta.0 → 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__/npm-package.spec.ts +69 -0
- package/package.json +2 -2
- package/src/npm-package-cache.ts +26 -10
- package/src/npm-package-install.ts +14 -1
- package/src/npm-package.ts +14 -60
- package/src/npm-registry.ts +154 -0
- package/src/package-version-request.ts +121 -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')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "onework",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.1",
|
|
4
4
|
"description": "One Works bootstrap launcher",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@clack/prompts": "^0.11.0",
|
|
21
21
|
"commander": "^12.1.0",
|
|
22
|
-
"@oneworks/cli-helper": "0.1.0-beta.
|
|
22
|
+
"@oneworks/cli-helper": "0.1.0-beta.1"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"test": "pnpm -C ../.. exec vitest run --workspace vitest.workspace.ts --project node apps/bootstrap/__tests__"
|
package/src/npm-package-cache.ts
CHANGED
|
@@ -141,22 +141,29 @@ const readOptionalFile = async (filePath: string | undefined) => {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
const resolvePackageLookupKey = async (
|
|
144
|
+
const resolvePackageLookupKey = async (
|
|
145
|
+
packageName: string,
|
|
146
|
+
options: { lookupScope?: string } = {}
|
|
147
|
+
) => {
|
|
145
148
|
const env = resolvePackageManagerEnv()
|
|
146
149
|
const userConfig = env.npm_config_userconfig ?? env.NPM_CONFIG_USERCONFIG
|
|
147
150
|
const userConfigContent = await readOptionalFile(userConfig)
|
|
151
|
+
const packageTag = options.lookupScope?.trim() || resolvePackageTag()
|
|
148
152
|
|
|
149
153
|
return JSON.stringify({
|
|
150
154
|
packageName,
|
|
151
|
-
packageTag
|
|
155
|
+
packageTag,
|
|
152
156
|
registry: env.npm_config_registry ?? env.NPM_CONFIG_REGISTRY ?? '',
|
|
153
157
|
userConfig: userConfig ?? '',
|
|
154
158
|
userConfigContentHash: userConfigContent == null ? '' : hashValue(userConfigContent)
|
|
155
159
|
})
|
|
156
160
|
}
|
|
157
161
|
|
|
158
|
-
const resolvePackageVersionMetadataPath = async (
|
|
159
|
-
|
|
162
|
+
const resolvePackageVersionMetadataPath = async (
|
|
163
|
+
packageName: string,
|
|
164
|
+
options: { lookupScope?: string } = {}
|
|
165
|
+
) => {
|
|
166
|
+
const lookupKey = await resolvePackageLookupKey(packageName, options)
|
|
160
167
|
return {
|
|
161
168
|
lookupKey,
|
|
162
169
|
metadataPath: path.join(
|
|
@@ -166,8 +173,12 @@ const resolvePackageVersionMetadataPath = async (packageName: string) => {
|
|
|
166
173
|
}
|
|
167
174
|
}
|
|
168
175
|
|
|
169
|
-
export const readPublishedPackageVersionMetadata = async (
|
|
170
|
-
|
|
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)
|
|
171
182
|
|
|
172
183
|
try {
|
|
173
184
|
const content = await readFile(metadataPath, 'utf8')
|
|
@@ -175,7 +186,7 @@ export const readPublishedPackageVersionMetadata = async (packageName: string) =
|
|
|
175
186
|
if (
|
|
176
187
|
parsed.lookupKey === lookupKey &&
|
|
177
188
|
parsed.packageName === packageName &&
|
|
178
|
-
parsed.packageTag ===
|
|
189
|
+
parsed.packageTag === packageTag &&
|
|
179
190
|
typeof parsed.version === 'string' &&
|
|
180
191
|
parsed.version.trim()
|
|
181
192
|
) {
|
|
@@ -191,15 +202,20 @@ export const readPublishedPackageVersionMetadata = async (packageName: string) =
|
|
|
191
202
|
return undefined
|
|
192
203
|
}
|
|
193
204
|
|
|
194
|
-
export const writePublishedPackageVersionMetadata = async (
|
|
195
|
-
|
|
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)
|
|
196
212
|
await ensureDirectory(path.dirname(metadataPath))
|
|
197
213
|
|
|
198
214
|
const tempPath = `${metadataPath}.${process.pid}.${Date.now()}.tmp`
|
|
199
215
|
const metadata: PublishedPackageVersionMetadata = {
|
|
200
216
|
lookupKey,
|
|
201
217
|
packageName,
|
|
202
|
-
packageTag
|
|
218
|
+
packageTag,
|
|
203
219
|
resolvedAt: new Date().toISOString(),
|
|
204
220
|
version
|
|
205
221
|
}
|
|
@@ -39,7 +39,13 @@ export const readInstalledPackageVersion = async (packageDir: string) => {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
export const findInstalledPublishedPackageVersion = async (
|
|
42
|
+
export const findInstalledPublishedPackageVersion = async (
|
|
43
|
+
packageName: string,
|
|
44
|
+
options: {
|
|
45
|
+
preferredVersion?: string
|
|
46
|
+
versionFilter?: (version: string) => boolean
|
|
47
|
+
} = {}
|
|
48
|
+
) => {
|
|
43
49
|
let versions: string[]
|
|
44
50
|
try {
|
|
45
51
|
versions = (await readdir(resolvePackageCacheRootDir(packageName), { withFileTypes: true }))
|
|
@@ -51,6 +57,9 @@ export const findInstalledPublishedPackageVersion = async (packageName: string)
|
|
|
51
57
|
|
|
52
58
|
const installedVersions: string[] = []
|
|
53
59
|
for (const version of versions) {
|
|
60
|
+
if (options.versionFilter != null && !options.versionFilter(version)) {
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
54
63
|
const packageDir = resolvePackageInstallDir(resolvePackageCacheDir(packageName, version), packageName)
|
|
55
64
|
const installedVersion = await readInstalledPackageVersion(packageDir)
|
|
56
65
|
if (installedVersion === version) {
|
|
@@ -58,6 +67,10 @@ export const findInstalledPublishedPackageVersion = async (packageName: string)
|
|
|
58
67
|
}
|
|
59
68
|
}
|
|
60
69
|
|
|
70
|
+
if (options.preferredVersion != null && installedVersions.includes(options.preferredVersion)) {
|
|
71
|
+
return options.preferredVersion
|
|
72
|
+
}
|
|
73
|
+
|
|
61
74
|
return installedVersions.sort(compareVersionLike).at(-1)
|
|
62
75
|
}
|
|
63
76
|
|
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
|
+
}
|