bulk-release 2.2.13

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,101 @@
1
+ import {$, ctx, fs, path, tempy, copy} from 'zx-extra'
2
+ import {log} from './log.js'
3
+ import {memoizeBy} from './util.js'
4
+
5
+ export const fetchRepo = memoizeBy(async ({cwd: _cwd, branch, origin: _origin, basicAuth}) => ctx(async ($) => {
6
+ const origin = _origin || (await getRepo(_cwd, {basicAuth})).repoAuthedUrl
7
+ const cwd = tempy.temporaryDirectory()
8
+ $.cwd = cwd
9
+ try {
10
+ await $`git clone --single-branch --branch ${branch} --depth 1 ${origin} .`
11
+ } catch (e) {
12
+ log({level: 'warn'})(`ref '${branch}' does not exist in ${origin}`)
13
+ await $`git init . &&
14
+ git remote add origin ${origin}`
15
+ }
16
+
17
+ return cwd
18
+ }), async ({cwd, branch}) => `${await getRoot(cwd)}:${branch}`)
19
+
20
+ export const pushCommit = async ({cwd, from, to, branch, origin, msg, ignoreFiles, files = [], basicAuth, gitCommitterEmail, gitCommitterName}) => ctx(async ($) => {
21
+ let retries = 3
22
+
23
+ const _cwd = await fetchRepo({cwd, branch, origin, basicAuth})
24
+ $.cwd = _cwd
25
+
26
+ for (let {relpath, contents} of files) {
27
+ const _contents = typeof contents === 'string' ? contents : JSON.stringify(contents, null, 2)
28
+ await fs.outputFile(path.resolve(_cwd, to, relpath), _contents)
29
+ }
30
+ if (from) await copy({baseFrom: cwd, from, baseTo: _cwd, to, ignoreFiles, cwd})
31
+
32
+ try {
33
+ await $`git config user.name ${gitCommitterName} &&
34
+ git config user.email ${gitCommitterEmail} &&
35
+ git add . &&
36
+ git commit -m ${msg}`
37
+ } catch {
38
+ log({level: 'warn'})(`no changes to commit to ${branch}`)
39
+ return
40
+ }
41
+
42
+ while (retries > 0) {
43
+ try {
44
+ return await $.raw`git push origin HEAD:refs/heads/${branch}`
45
+ } catch (e) {
46
+ retries -= 1
47
+ log({level: 'error'})('git push failed', 'branch', branch, 'retries left', retries, e)
48
+
49
+ if (retries === 0) {
50
+ throw e
51
+ }
52
+
53
+ await $`git fetch origin ${branch} &&
54
+ git rebase origin/${branch}`
55
+ }
56
+ }
57
+ })
58
+
59
+ export const getSha = async (cwd) => (await $.o({cwd})`git rev-parse HEAD`).toString().trim()
60
+
61
+ export const getRoot = memoizeBy(async (cwd) => (await $.o({cwd})`git rev-parse --show-toplevel`).toString().trim())
62
+
63
+ export const getRepo = memoizeBy(async (cwd, {basicAuth} = {}) => {
64
+ const originUrl = await getOrigin(cwd)
65
+ const [, , repoHost, repoName] = originUrl.replace(':', '/').replace(/\.git/, '').match(/.+(@|\/\/)([^/]+)\/(.+)$/) || []
66
+ const repoPublicUrl = `https://${repoHost}/${repoName}`
67
+ const repoAuthedUrl = basicAuth && repoHost && repoName
68
+ ? `https://${basicAuth}@${repoHost}/${repoName}.git`
69
+ : originUrl
70
+
71
+ return {
72
+ repoName,
73
+ repoHost,
74
+ repoPublicUrl,
75
+ repoAuthedUrl,
76
+ originUrl,
77
+ }
78
+ }, getRoot)
79
+
80
+ export const getOrigin = memoizeBy(async (cwd) =>
81
+ $.o({cwd})`git config --get remote.origin.url`.then(r => r.toString().trim())
82
+ )
83
+
84
+ export const getCommits = async (cwd, from, to = 'HEAD') => ctx(async ($) => {
85
+ const ref = from ? `${from}..${to}` : to
86
+
87
+ $.cwd = cwd
88
+ return (await $.raw`git log ${ref} --format=+++%s__%b__%h__%H -- .`)
89
+ .toString()
90
+ .split('+++')
91
+ .filter(Boolean)
92
+ .map(msg => {
93
+ const [subj, body, short, hash] = msg.split('__').map(raw => raw.trim())
94
+ return {subj, body, short, hash}
95
+ })
96
+ })
97
+
98
+ export const getTags = async (cwd, ref = '') =>
99
+ (await $.o({cwd})`git tag -l ${ref}`)
100
+ .toString()
101
+ .split('\n')
@@ -0,0 +1 @@
1
+ export {run} from './processor.js'
@@ -0,0 +1,63 @@
1
+ import {$, fs} from 'zx-extra'
2
+ import {get, set, tpl} from './util.js'
3
+
4
+ export const log = (ctx) =>
5
+ $.report
6
+ ? $.report.log(ctx)
7
+ : console.log
8
+
9
+ export const createReport = ({logger = console, packages = {}, queue = [], flags} = {}) => ({
10
+ logger,
11
+ flags,
12
+ file: flags.report || flags.file,
13
+ status: 'initial',
14
+ events: [],
15
+ queue,
16
+ packages: Object.entries(packages).reduce((acc, [name, {manifest: {version}, absPath, relPath}]) => {
17
+ acc[name] = {
18
+ status: 'initial',
19
+ name,
20
+ version,
21
+ path: absPath,
22
+ relPath
23
+ }
24
+ return acc
25
+ }, {}),
26
+ get(key, pkgName) {
27
+ return get(
28
+ pkgName ? this.packages[pkgName] : this,
29
+ key
30
+ )
31
+ },
32
+ set(key, value, pkgName) {
33
+ set(
34
+ pkgName ? this.packages[pkgName] : this,
35
+ key,
36
+ value
37
+ )
38
+ return this
39
+ },
40
+ setStatus(status, name) {
41
+ this.set('status', status, name)
42
+ this.save()
43
+ return this
44
+ },
45
+ getStatus(status, name) {
46
+ return this.get('status', name)
47
+ },
48
+ log(ctx = {}) {
49
+ return function (...chunks) {
50
+ const {pkg, scope = pkg?.name || $.scope || '~', level = 'info'} = ctx
51
+ const msg = chunks.map(c => typeof c === 'string' ? tpl(c, ctx) : c)
52
+ const event = {msg, scope, date: Date.now(), level}
53
+ this.events.push(event)
54
+ logger[level](`[${scope}]`, ...msg)
55
+
56
+ return this
57
+ }.bind(this)
58
+ },
59
+ save() {
60
+ this.file && fs.outputJsonSync(this.file, this)
61
+ return this
62
+ }
63
+ })
@@ -0,0 +1,171 @@
1
+ // Semantic tags processing
2
+
3
+ import {Buffer} from 'node:buffer'
4
+ import {queuefy} from 'queuefy'
5
+ import {ctx, semver, $, fs, path} from 'zx-extra'
6
+ import {log} from './log.js'
7
+ import {fetchRepo, pushCommit, getTags as getGitTags} from './git.js'
8
+ import {fetchManifest} from './npm.js'
9
+
10
+ export const pushTag = (pkg) => ctx(async ($) => {
11
+ const {absPath: cwd, name, version, config: {gitCommitterEmail, gitCommitterName}} = pkg
12
+ const tag = formatTag({name, version})
13
+
14
+ pkg.context.git.tag = tag
15
+ log({pkg})(`push release tag ${tag}`)
16
+
17
+ $.cwd = cwd
18
+ await $`git config user.name ${gitCommitterName}`
19
+ await $`git config user.email ${gitCommitterEmail}`
20
+ await $`git tag -m ${tag} ${tag}`
21
+ await $`git push origin ${tag}`
22
+ })
23
+
24
+ export const pushMeta = queuefy(async (pkg) => {
25
+ log({pkg})('push artifact to branch \'meta\'')
26
+
27
+ const {name, version, absPath: cwd, config: {gitCommitterEmail, gitCommitterName, ghBasicAuth: basicAuth}} = pkg
28
+ const tag = formatTag({name, version})
29
+ const to = '.'
30
+ const branch = 'meta'
31
+ const msg = `chore: release meta ${name} ${version}`
32
+ const hash = (await $.o({cwd})`git rev-parse HEAD`).toString().trim()
33
+ const meta = {
34
+ META_VERSION: '1',
35
+ name: pkg.name,
36
+ hash,
37
+ version: pkg.version,
38
+ dependencies: pkg.dependencies,
39
+ devDependencies: pkg.devDependencies,
40
+ peerDependencies: pkg.peerDependencies,
41
+ optionalDependencies: pkg.optionalDependencies,
42
+ }
43
+ const files = [{relpath: `${getArtifactPath(tag)}.json`, contents: meta}]
44
+
45
+ await pushCommit({cwd, to, branch, msg, files, gitCommitterEmail, gitCommitterName, basicAuth})
46
+ })
47
+
48
+ export const getLatest = async (pkg) => {
49
+ const {absPath: cwd, name, config: {ghBasicAuth: basicAuth}} = pkg
50
+ const tag = await getLatestTag(cwd, name)
51
+ const meta = await getLatestMeta(cwd, tag?.ref, basicAuth) || await fetchManifest(pkg, {nothrow: true})
52
+
53
+ return {
54
+ tag,
55
+ meta
56
+ }
57
+ }
58
+
59
+ const f0 = {
60
+ parse(tag) {
61
+ if (!tag.endsWith('-f0')) return null
62
+
63
+ const pattern = /^(\d{4}\.(?:[1-9]|1[012])\.(?:[1-9]|[12]\d|30|31))-((?:[a-z0-9-]+\.)?[a-z0-9-]+)\.(v?\d+\.\d+\.\d+.*)-f0$/
64
+ const matched = pattern.exec(tag) || []
65
+ const [, _date, _name, version] = matched
66
+
67
+ if (!semver.valid(version)) return null
68
+
69
+ const date = parseDateTag(_date)
70
+ const name = _name.includes('.') ? `@${_name.replace('.', '/')}` : _name
71
+
72
+ return {date, name, version, format: 'f0', ref: tag}
73
+ },
74
+ format({name, date = new Date(), version}) {
75
+ if (!/^(@?[a-z0-9-]+\/)?[a-z0-9-]+$/.test(name) || !semver.valid(version)) return null
76
+
77
+ const d = formatDateTag(date)
78
+ const n = name.replace('@', '').replace('/', '.')
79
+
80
+ return `${d}-${n}.${version}-f0`
81
+ }
82
+ }
83
+
84
+ const f1 = {
85
+ parse(tag) {
86
+ if (!tag.endsWith('-f1')) return null
87
+
88
+ const pattern = /^(\d{4}\.(?:[1-9]|1[012])\.(?:[1-9]|[12]\d|30|31))-[a-z0-9-]+\.(v?\d+\.\d+\.\d+.*)\.([^.]+)-f1$/
89
+ const matched = pattern.exec(tag) || []
90
+ const [, _date, version, b64] = matched
91
+
92
+ if (!semver.valid(version)) return null
93
+
94
+ const date = parseDateTag(_date)
95
+ const name = Buffer.from(b64, 'base64url').toString('utf8')
96
+
97
+ return {date, name, version, format: 'f1', ref: tag}
98
+ },
99
+ format({name, date = new Date(), version}) {
100
+ if (!semver.valid(version)) return null
101
+
102
+ const b64 = Buffer.from(name).toString('base64url')
103
+ const d = formatDateTag(date)
104
+ const n = name.replace(/[^a-z0-9-]/ig, '')
105
+
106
+ return `${d}-${n}.${version}.${b64}-f1`
107
+ }
108
+ }
109
+
110
+ const lerna = {
111
+ parse(tag) {
112
+ const pattern = /^(@?[a-z0-9-]+(?:\/[a-z0-9-]+)?)@(v?\d+\.\d+\.\d+.*)/
113
+ const [, name, version] = pattern.exec(tag) || []
114
+
115
+ if (!semver.valid(version)) return null
116
+
117
+ return {name, version, format: 'lerna', ref: tag}
118
+ },
119
+ // format({name, version}) {
120
+ // if (!semver.valid(version)) return null
121
+ //
122
+ // return `${name}@${version}`
123
+ // }
124
+ }
125
+
126
+ // TODO
127
+ // const variants = [f0, f1]
128
+ // export const parseTag = (tag) => {
129
+ // for (const variant of variants) {
130
+ // const parsed = variant.parse(tag)
131
+ // if (parsed) return parsed
132
+ // }
133
+ //
134
+ // return null
135
+ // }
136
+
137
+ export const parseTag = (tag) => f0.parse(tag) || f1.parse(tag) || lerna.parse(tag) || null
138
+
139
+ export const formatTag = (tag) => f0.format(tag) || f1.format(tag) || null
140
+
141
+ export const getTags = async (cwd, ref = '') =>
142
+ (await getGitTags(cwd, ref))
143
+ .map(tag => parseTag(tag.trim()))
144
+ .filter(Boolean)
145
+ .sort((a, b) => semver.rcompare(a.version, b.version))
146
+
147
+ export const getLatestTag = async (cwd, name) =>
148
+ (await getTags(cwd)).find(tag => tag.name === name) || null
149
+
150
+ export const getLatestTaggedVersion = async (cwd, name) =>
151
+ (await getLatestTag(cwd, name))?.version || null
152
+
153
+ export const formatDateTag = (date = new Date()) => `${date.getUTCFullYear()}.${date.getUTCMonth() + 1}.${date.getUTCDate()}`
154
+
155
+ export const parseDateTag = (date) => new Date(date.replaceAll('.', '-')+'Z')
156
+
157
+ export const getArtifactPath = (tag) => tag.toLowerCase().replace(/[^a-z0-9-]/g, '-')
158
+
159
+ export const getLatestMeta = async (cwd, tag, basicAuth) => {
160
+ if (!tag) return null
161
+
162
+ try {
163
+ const _cwd = await fetchRepo({cwd, branch: 'meta', basicAuth})
164
+ return await Promise.any([
165
+ fs.readJson(path.resolve(_cwd, `${getArtifactPath(tag)}.json`)),
166
+ fs.readJson(path.resolve(_cwd, getArtifactPath(tag), 'meta.json'))
167
+ ])
168
+ } catch {}
169
+
170
+ return null
171
+ }
@@ -0,0 +1,65 @@
1
+ import {log} from './log.js'
2
+ import {$, ctx, fs, path, INI, fetch} from 'zx-extra'
3
+
4
+ export const fetchPkg = async (pkg) => {
5
+ const id = `${pkg.name}@${pkg.version}`
6
+
7
+ try {
8
+ log({pkg})(`fetching '${id}'`)
9
+ const cwd = pkg.absPath
10
+ const {npmRegistry, npmToken, npmConfig} = pkg.config
11
+ const bearerToken = getBearerToken(npmRegistry, npmToken, npmConfig)
12
+ const tarball = getTarballUrl(npmRegistry, pkg.name, pkg.version)
13
+ await $.raw`wget --timeout=10 --header='Authorization: ${bearerToken}' -qO- ${tarball} | tar -xvz --strip-components=1 --exclude='package.json' -C ${cwd}`
14
+
15
+ pkg.fetched = true
16
+ } catch (e) {
17
+ log({pkg, level: 'warn'})(`fetching '${id}' failed`, e)
18
+ }
19
+ }
20
+
21
+ export const fetchManifest = async (pkg, {nothrow} = {}) => {
22
+ const {npmRegistry, npmToken, npmConfig} = pkg.config
23
+ const bearerToken = getBearerToken(npmRegistry, npmToken, npmConfig)
24
+ const url = getManifestUrl(npmRegistry, pkg.name, pkg.version)
25
+
26
+ try {
27
+ const res = await fetch(url, {authorization: bearerToken})
28
+ if (!res.ok) throw res
29
+
30
+ return res.json() // NOTE .json() is async too
31
+ } catch (e) {
32
+ if (nothrow) return null
33
+ throw e
34
+ }
35
+ }
36
+
37
+ export const npmPublish = (pkg) => ctx(async ($) => {
38
+ const {absPath: cwd, name, version, manifest, config} = pkg
39
+ if (manifest.private || config?.npmPublish === false) return
40
+ const {npmRegistry, npmToken, npmConfig} = config
41
+ const npmrc = npmConfig ? npmConfig : path.resolve(cwd, '.npmrc')
42
+
43
+ log({pkg})(`publish npm package ${name} ${version} to ${npmRegistry}`)
44
+ $.cwd = cwd
45
+ if (!npmConfig) {
46
+ await $.raw`echo ${npmRegistry.replace(/https?:/, '')}/:_authToken=${npmToken} >> ${npmrc}`
47
+ }
48
+ await $`npm publish --no-git-tag-version --registry=${npmRegistry} --userconfig ${npmrc} --no-workspaces`
49
+ })
50
+
51
+ // $`npm view ${name}@${version} dist.tarball`
52
+ export const getTarballUrl = (registry, name, version) => `${registry}/${name}/-/${name.replace(/^.+(%2f|\/)/,'')}-${version}.tgz`
53
+
54
+ export const getManifestUrl = (registry, name, version) => `${registry}/${name}/${version}`
55
+
56
+ export const getBearerToken = async (npmRegistry, npmToken, npmConfig) => {
57
+ const token = npmConfig
58
+ ? getAuthToken(npmRegistry, INI.parse(await fs.readFile(npmConfig, 'utf8')))
59
+ : npmToken
60
+ return `Bearer ${token}`
61
+ }
62
+
63
+ // NOTE registry-auth-token does not work with localhost:4873
64
+ export const getAuthToken = (registry, npmrc) =>
65
+ (Object.entries(npmrc).find(([reg]) => reg.startsWith(registry.replace(/^https?/, ''))) || [])[1]
@@ -0,0 +1,155 @@
1
+ import os from 'node:os'
2
+ import {$, fs, within} from 'zx-extra'
3
+ import {queuefy} from 'queuefy'
4
+ import {analyze} from './analyze.js'
5
+ import {pushChangelog} from './changelog.js'
6
+ import {getPkgConfig} from './config.js'
7
+ import {topo, traverseDeps, traverseQueue} from './deps.js'
8
+ import {ghPages, ghRelease} from './gh.js'
9
+ import {getRoot, getSha} from './git.js'
10
+ import {log, createReport} from './log.js'
11
+ import {getLatest, pushMeta, pushTag} from './meta.js'
12
+ import {fetchPkg, npmPublish} from './npm.js'
13
+ import {memoizeBy, tpl} from './util.js'
14
+
15
+ export const run = async ({cwd = process.cwd(), env, flags = {}} = {}) => within(async () => {
16
+ const context = await createContext({flags, env, cwd})
17
+ const {report, packages, queue, prev, graphs} = context
18
+ const _runCmd = queuefy(runCmd, flags.concurrency || os.cpus().length)
19
+
20
+ report
21
+ .log()('zx-bulk-release')
22
+ .log()('queue:', queue)
23
+ .log()('graphs', graphs)
24
+
25
+ try {
26
+ await traverseQueue({queue, prev, async cb(name) {
27
+ report.setStatus('analyzing', name)
28
+ const pkg = packages[name]
29
+ await contextify(pkg, context)
30
+ await analyze(pkg)
31
+ report
32
+ .set('config', pkg.config, name)
33
+ .set('version', pkg.version, name)
34
+ .set('prevVersion', pkg.latest.tag?.version || pkg.manifest.version, name)
35
+ .set('releaseType', pkg.releaseType, name)
36
+ .set('tag', pkg.tag, name)
37
+ }})
38
+
39
+ report.setStatus('pending')
40
+
41
+ await traverseQueue({queue, prev, async cb(name) {
42
+ const pkg = packages[name]
43
+
44
+ if (!pkg.releaseType) {
45
+ report.setStatus('skipped', name)
46
+ return
47
+ }
48
+ if (!flags.noBuild) {
49
+ report.setStatus('building', name)
50
+ await build(pkg, _runCmd)
51
+ }
52
+ if (!flags.dryRun && !flags.noPublish) {
53
+ report.setStatus('publishing', name)
54
+ await publish(pkg, _runCmd)
55
+ }
56
+
57
+ report.setStatus('success', name)
58
+ }})
59
+ } catch (e) {
60
+ report
61
+ .log({level: 'error'})(e, e.stack)
62
+ .set('error', e)
63
+ .setStatus('failure')
64
+ throw e
65
+ }
66
+ report
67
+ .setStatus('success')
68
+ .log()('Great success!')
69
+ })
70
+
71
+ export const runCmd = async (pkg, name) => {
72
+ const cmd = tpl(pkg.config[name], {...pkg, ...pkg.context})
73
+
74
+ if (cmd) {
75
+ log({pkg})(`run ${name} '${cmd}'`)
76
+ return $.o({cwd: pkg.absPath, quote: v => v, preferLocal: true})`${cmd}`
77
+ }
78
+ }
79
+
80
+ const createContext = async ({flags, env, cwd}) => {
81
+ const { packages, queue, root, prev, graphs } = await topo({cwd, flags})
82
+ const report = createReport({packages, queue, flags})
83
+
84
+ $.report = report
85
+ $.env = {...process.env, ...env}
86
+ $.verbose = !!(flags.debug || $.env.DEBUG ) || $.verbose
87
+
88
+ return {
89
+ report,
90
+ packages,
91
+ root,
92
+ queue,
93
+ prev,
94
+ graphs,
95
+ flags
96
+ }
97
+ }
98
+
99
+ // Inspired by https://docs.github.com/en/actions/learn-github-actions/contexts
100
+ const contextify = async (pkg, {packages, root}) => {
101
+ pkg.config = await getPkgConfig(pkg.absPath, root.absPath)
102
+ pkg.latest = await getLatest(pkg)
103
+ pkg.context = {
104
+ git: {
105
+ sha: await getSha(pkg.absPath),
106
+ root: await getRoot(pkg.absPath)
107
+ },
108
+ env: $.env,
109
+ packages
110
+ }
111
+ }
112
+
113
+ const build = memoizeBy(async (pkg, run = runCmd, flags = {}, self = build) => within(async () => {
114
+ $.scope = pkg.name
115
+
116
+ await Promise.all([
117
+ traverseDeps(pkg, pkg.context.packages, async (_, {pkg}) => self(pkg, run, flags, self)),
118
+ pkg.changes.length === 0 && pkg.config.npmFetch && !flags.noNpmFetch
119
+ ? fetchPkg(pkg)
120
+ : Promise.resolve()
121
+ ])
122
+
123
+ if (!pkg.fetched) {
124
+ await run(pkg, 'buildCmd')
125
+ await run(pkg, 'testCmd')
126
+ }
127
+
128
+ pkg.built = true
129
+ }))
130
+
131
+ const publish = memoizeBy(async (pkg, run = runCmd) => within(async () => {
132
+ $.scope = pkg.name
133
+
134
+ // Debug
135
+ // https://www.npmjs.com/package/@packasso/preset-ts-tsc-uvu/v/0.0.0?activeTab=code
136
+ // https://github.com/qiwi/packasso/actions/runs/4514909191/jobs/7951564982#step:7:817
137
+ // https://github.com/qiwi/packasso/blob/meta/2023-3-24-packasso-preset-ts-tsc-uvu-0-21-0-f0.json
138
+ if (pkg.version !== pkg.manifest.version) {
139
+ throw new Error('package.json version not synced')
140
+ }
141
+
142
+ fs.writeJsonSync(pkg.manifestPath, pkg.manifest, {spaces: 2})
143
+ await pushTag(pkg)
144
+
145
+ await Promise.all([
146
+ pushMeta(pkg),
147
+ pushChangelog(pkg),
148
+ npmPublish(pkg),
149
+ ghRelease(pkg),
150
+ ghPages(pkg),
151
+ run(pkg, 'publishCmd')
152
+ ])
153
+
154
+ pkg.published = true
155
+ }))
@@ -0,0 +1,46 @@
1
+ export const tpl = (str, context) =>
2
+ str?.replace(/\$\{\{\s*([.a-z0-9]+)\s*}}/gi, (matched, key) => get(context, key) ?? '')
3
+
4
+ export const get = (obj, path = '.') => {
5
+ const chunks = path.split('.').filter(Boolean)
6
+ let result = obj
7
+
8
+ for (let i = 0, len = chunks.length; i < len && result !== undefined && result !== null; i++) {
9
+ result = result[chunks[i]]
10
+ }
11
+
12
+ return result
13
+ }
14
+
15
+ export const set = (obj, path, value) => {
16
+ const chunks = path.split('.').filter(Boolean)
17
+ let result = obj
18
+
19
+ for (let i = 0, len = chunks.length; i < len && result !== undefined && result !== null; i++) {
20
+ if (i === len - 1) {
21
+ result[chunks[i]] = value
22
+ } else {
23
+ result[chunks[i]] = result[chunks[i]] || {}
24
+ result = result[chunks[i]]
25
+ }
26
+ }
27
+
28
+ return result
29
+ }
30
+
31
+ export const msgJoin = (rest, context, def) => tpl(rest.filter(Boolean).join(' ') || def, context)
32
+
33
+ export const keyByValue = (obj, value) => Object.keys(obj).find((key) => obj[key] === value)
34
+
35
+ export const memoizeBy = (fn, getKey = v => v, memo = new Map()) => async (...args) => {
36
+ const key = await getKey(...args)
37
+ if (memo.has(key)) {
38
+ return memo.get(key)
39
+ }
40
+
41
+ const value = fn(...args)
42
+ memo.set(key, value)
43
+ return value
44
+ }
45
+
46
+ export const camelize = s => s.replace(/-./g, x => x[1].toUpperCase())
@@ -0,0 +1,63 @@
1
+ import {ctx, fs, path, tempy, $, sleep} from 'zx-extra'
2
+ import {fileURLToPath} from 'node:url'
3
+
4
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
5
+ export const fixtures = path.resolve(__dirname, '../fixtures')
6
+
7
+ export const createNpmRegistry = () => {
8
+ let p
9
+
10
+ return {
11
+ address: $.env.NPM_REGISTRY,
12
+ async start() {
13
+ fs.removeSync(path.resolve(__dirname, '../../../storage'))
14
+ const config = path.resolve(__dirname, '../../../verdaccio.config.yaml')
15
+ ctx(($) => {
16
+ $.preferLocal = true
17
+ p = $`verdaccio --config ${config}`
18
+ })
19
+
20
+ return sleep(1000)
21
+ },
22
+ async stop() {
23
+ p._nothrow = true
24
+ return p?.kill()
25
+ }
26
+ }
27
+ }
28
+
29
+ export const createFakeRepo = async ({cwd = tempy.temporaryDirectory(), commits = []} = {}) =>
30
+ ctx(async ($) => {
31
+ $.cwd = cwd
32
+ await $`git init`
33
+
34
+ await addCommits({cwd, commits})
35
+
36
+ const bare = tempy.temporaryDirectory()
37
+ await $`git init --bare ${bare}`
38
+ await $`git remote add origin ${bare}`
39
+
40
+ return cwd
41
+ })
42
+
43
+ export const addCommits = async ({cwd, commits = []}) => ctx(async ($) => {
44
+ $.cwd = cwd
45
+
46
+ for (let {msg, files, name = 'Semrel-extra Bot', email = 'semrel-extra-bot@hotmail.com', tags = []} of commits) {
47
+ await $`git config user.name ${name}`
48
+ await $`git config user.email ${email}`
49
+ for (let {relpath, contents} of files) {
50
+ const _contents = typeof contents === 'string' ? contents : JSON.stringify(contents, null, 2)
51
+ const file = path.resolve(cwd, relpath)
52
+ await fs.outputFile(file, _contents)
53
+
54
+ await $`git add ${file}`
55
+ }
56
+
57
+ await $`git commit -m ${msg}`
58
+
59
+ for (let tag of tags) {
60
+ await $`git tag ${tag} -m ${tag}`
61
+ }
62
+ }
63
+ })