Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support to run multi script commands specified with regex selector #5871

Merged
merged 32 commits into from Feb 2, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
413fb09
refactor: extract the process to run script command into a separated …
Shinyaigeek Dec 31, 2022
fd2ebe5
feat: run script command parallel if multiple script commands is spec…
Shinyaigeek Jan 1, 2023
25fae54
feat: run script command parallel if multiple script command is speci…
Shinyaigeek Jan 2, 2023
776dd24
test: run unit test for multi script commands selector
Shinyaigeek Jan 2, 2023
8424515
fixup! test: run unit test for multi script commands selector
Shinyaigeek Jan 3, 2023
f256571
feat: run selected scripts parallel with the limits specified with wo…
Shinyaigeek Jan 3, 2023
b92e3bb
fixup! test: run unit test for multi script commands selector
Shinyaigeek Jan 3, 2023
514986f
feat: run multi scripts parallel per 4 as a default behavior
Shinyaigeek Jan 3, 2023
bd0aef2
fix: use regexp for multi scripts wildcard selector
Shinyaigeek Jan 7, 2023
b7e6428
chore: add changeset
Shinyaigeek Jan 7, 2023
7608448
Revert "fix: use regexp for multi scripts wildcard selector"
Shinyaigeek Jan 9, 2023
ec8e4ee
fixup! test: run unit test for multi script commands selector
Shinyaigeek Jan 12, 2023
bc23e6b
fix: use regexp selector instaed of original syntax
Shinyaigeek Jan 17, 2023
274a076
fixup! fix: use regexp selector instaed of original syntax
Shinyaigeek Jan 19, 2023
671a063
fixup! fix: use regexp selector instaed of original syntax
Shinyaigeek Jan 27, 2023
3864f03
fix: update help message of --sequential option more simple
Shinyaigeek Jan 27, 2023
e140531
fixup! fix: use regexp selector instaed of original syntax
Shinyaigeek Jan 27, 2023
f6fe3cf
fixup! fix: use regexp selector instaed of original syntax
Shinyaigeek Jan 27, 2023
8a70297
fixup! fix: use regexp selector instaed of original syntax
Shinyaigeek Jan 27, 2023
f917b1f
fix: make the process in map callback to invoke runScript more simple
Shinyaigeek Jan 27, 2023
c02be84
fixup! fixup! fix: use regexp selector instaed of original syntax
Shinyaigeek Jan 28, 2023
4802e46
fixup! chore: add changeset
Shinyaigeek Jan 29, 2023
421a693
fixup! fix: use regexp selector instaed of original syntax
Shinyaigeek Jan 29, 2023
7db6957
fixup! fix: use regexp selector instaed of original syntax
Shinyaigeek Jan 29, 2023
acf55fd
fixup! fixup! fix: use regexp selector instaed of original syntax
Shinyaigeek Jan 29, 2023
cbc19e7
fixup! fixup! fixup! fix: use regexp selector instaed of original syntax
Shinyaigeek Jan 30, 2023
0d81ca4
fixup! fixup! fixup! fix: use regexp selector instaed of original syntax
Shinyaigeek Jan 30, 2023
2b337da
fix: update test description for regexp script selector
Shinyaigeek Jan 31, 2023
67f62a1
fix: report error if passed regexp script command includes flag
Shinyaigeek Jan 31, 2023
d9bd169
refactor: update text
zkochan Feb 1, 2023
46522b3
fixup! fixup! fixup! fixup! fix: use regexp selector instaed of origi…
Shinyaigeek Feb 1, 2023
1485f79
fixup! refactor: update text
Shinyaigeek Feb 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silly-flies-care.md
@@ -0,0 +1,5 @@
---
"@pnpm/plugin-commands-script-runners": minor
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also add pnpm: minor

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4802e46 👍

---

feat: support wildcard selector to specify multiple scripts to execute"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add more details. This text is going to be on the release page.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4802e46 👍

15 changes: 15 additions & 0 deletions exec/plugin-commands-script-runners/src/regexpCommand.ts
@@ -0,0 +1,15 @@
export function tryBuildRegExpFromCommand (
command: string
): RegExp | null {
if (command.length < 3) {
return null
}
if (command[0] !== '/' || command.lastIndexOf('/') < 1) {
return null
}
try {
return new RegExp(command.slice(0, command.lastIndexOf('/')).slice(1))
} catch {
return null
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Support /x/imgy.

Suggested change
if (command.length < 3) {
return null
}
if (command[0] !== '/' || command.lastIndexOf('/') < 1) {
return null
}
try {
return new RegExp(command.slice(0, command.lastIndexOf('/')).slice(1))
} catch {
return null
}
// https://github.com/stdlib-js/regexp-regexp/blob/6428051ac9ef7c9d03468b19bdbb1dc6fc2a5509/lib/regexp.js
const isRegExpStr = /^\/((?:\\\/|[^\/])+)\/([imgy]*)$/.test(command)
if (!isRegExpStr) {
return null
}
// https://stackoverflow.com/a/874742/6596777
const match = command.match(new RegExp('^/(.*?)/([imgy]*)$'))
if (!match) {
return null
}
try {
return new RegExp(match[1], match[2])
} catch {
return null
}

Ref:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are other flac chalactor to present flag https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/flags other than imy, and these are defined in EcmaScript RegExp specification. I think it is hard to keep up with RegExp flag for ecmascript starndart so it is better to simply split wih /

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I have modified the code:

  // https://github.com/stdlib-js/regexp-regexp/blob/6428051ac9ef7c9d03468b19bdbb1dc6fc2a5509/lib/regexp.js
  const isRegExpStr = /^\/((?:\\\/|[^\/])+)\/([dgimuys]*)$/.test(command)
  if (!isRegExpStr) {
    return null
  }
  
  // https://stackoverflow.com/a/874742/6596777
  const match = command.match(new RegExp('^/(.*?)/([dgimuys]*)$'))
  if (!match) {
    return null
  }
  
  try {
    return new RegExp(match[1], match[2])
  } catch {
    return null
  }

Result:

console.log(/build:.*/dgimuys.test("build:web")) // true

Excess flags do not affect the results.

Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#advanced_searching_with_flags

Copy link
Member

@zkochan zkochan Jan 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what case would regex flags be useful? A regex is already more powerful than it needs to be for this simple task of selecting scripts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, only the i flags are useful. But if flags are not supported, I think it's fine.

We need to discuss what will happen if the user types pnpm run /build:*/ig:

  1. Ingore flags and print warning
  2. Ignore flags
  3. Report an error
  4. Consider this as scriptName
  5. Consider this as Regex flags.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • If ignore flags:
// https://github.com/stdlib-js/regexp-regexp/blob/6428051ac9ef7c9d03468b19bdbb1dc6fc2a5509/lib/regexp.js
const reRegExp = new RegExp(/^\/((?:\\\/|[^\/])+)\/([dgimuys]*)$/)
const match = command.match(reRegExp)

if (!match) {
  return null
}

// if need print warning
if (match[2]) {
  logger.warn("not support flags and will ignore")
}

try {
  return new RegExp(match[1])
} catch {
  return null
}
  • If support flags:
// https://github.com/stdlib-js/regexp-regexp/blob/6428051ac9ef7c9d03468b19bdbb1dc6fc2a5509/lib/regexp.js
const reRegExp = new RegExp(/^\/((?:\\\/|[^\/])+)\/([dgimuys]*)$/)
const match = command.match(reRegExp)

if (!match) {
  return null
}

try {
  return new RegExp(match[1], match[2])
} catch {
  return null
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd vote for either reporting an error or consider it as script name. Although the second one may be confusing

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to vote for reporting an error or ignoring flag and warning it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd vote for ignoring the flags and warning it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the tryBuildRegExpFromCommand to report the error if RegExp flag is specified in the passed script command 67f62a1 👍

}
BlackHole1 marked this conversation as resolved.
Show resolved Hide resolved
91 changes: 72 additions & 19 deletions exec/plugin-commands-script-runners/src/run.ts
@@ -1,4 +1,5 @@
import path from 'path'
import pLimit from 'p-limit'
import {
docsUrl,
readProjectManifestOnly,
Expand All @@ -13,13 +14,14 @@ import {
makeNodeRequireOption,
RunLifecycleHookOptions,
} from '@pnpm/lifecycle'
import { ProjectManifest } from '@pnpm/types'
import { PackageScripts, ProjectManifest } from '@pnpm/types'
import pick from 'ramda/src/pick'
import realpathMissing from 'realpath-missing'
import renderHelp from 'render-help'
import { runRecursive, RecursiveRunOpts } from './runRecursive'
import { existsInDir } from './existsInDir'
import { handler as exec } from './exec'
import { tryBuildRegExpFromCommand } from './regexpCommand'

export const IF_PRESENT_OPTION = {
'if-present': Boolean,
Expand All @@ -43,13 +45,21 @@ export const RESUME_FROM_OPTION_HELP = {
name: '--resume-from',
}

export const SEQUENTIAL_OPTION_HELP = {
description: 'Run specified scripts one after one.',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
description: 'Run specified scripts one after one.',
description: 'Run specified scripts one by one',

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Thanks!

name: '--sequential',
}

export const shorthands = {
parallel: [
'--workspace-concurrency=Infinity',
'--no-sort',
'--stream',
'--recursive',
],
sequential: [
'--workspace-concurrency=1',
],
}

export function rcOptionsTypes () {
Expand Down Expand Up @@ -112,6 +122,7 @@ For options that may be used with `-r`, see "pnpm help recursive"',
PARALLEL_OPTION_HELP,
RESUME_FROM_OPTION_HELP,
...UNIVERSAL_OPTIONS,
SEQUENTIAL_OPTION_HELP,
],
},
FILTERING,
Expand Down Expand Up @@ -159,7 +170,10 @@ export async function handler (
: undefined
return printProjectCommands(manifest, rootManifest ?? undefined)
}
if (scriptName !== 'start' && !manifest.scripts?.[scriptName]) {

const specifiedScripts = getSpecifiedScripts(manifest.scripts ?? {}, scriptName)

if (specifiedScripts.length < 1) {
if (opts.ifPresent) return
if (opts.fallbackCommandUsed) {
if (opts.argv == null) throw new Error('Could not fallback because opts.argv.original was not passed to the script runner')
Expand All @@ -170,9 +184,9 @@ export async function handler (
}
if (opts.workspaceDir) {
const { manifest: rootManifest } = await tryReadProjectManifest(opts.workspaceDir, opts)
if (rootManifest?.scripts?.[scriptName]) {
if (getSpecifiedScripts(rootManifest?.scripts ?? {}, scriptName).length > 0 && specifiedScripts.length < 1) {
throw new PnpmError('NO_SCRIPT', `Missing script: ${scriptName}`, {
hint: `But ${scriptName} is present in the root of the workspace,
hint: `But script matched with ${scriptName} is present in the root of the workspace,
so you may run "pnpm -w run ${scriptName}"`,
})
}
Expand Down Expand Up @@ -203,21 +217,11 @@ so you may run "pnpm -w run ${scriptName}"`,
}
}
try {
if (
opts.enablePrePostScripts &&
manifest.scripts?.[`pre${scriptName}`] &&
!manifest.scripts[scriptName].includes(`pre${scriptName}`)
) {
await runLifecycleHook(`pre${scriptName}`, manifest, lifecycleOpts)
}
await runLifecycleHook(scriptName, manifest, { ...lifecycleOpts, args: passedThruArgs })
if (
opts.enablePrePostScripts &&
manifest.scripts?.[`post${scriptName}`] &&
!manifest.scripts[scriptName].includes(`post${scriptName}`)
) {
await runLifecycleHook(`post${scriptName}`, manifest, lifecycleOpts)
}
const limitRun = pLimit(opts.workspaceConcurrency ?? 4)

const _runScript = runScript.bind(null, { manifest, lifecycleOpts, runScriptOptions: { enablePrePostScripts: opts.enablePrePostScripts ?? false }, passedThruArgs })

await Promise.all(specifiedScripts.map(script => limitRun(() => _runScript(script))))
} catch (err: any) { // eslint-disable-line
if (opts.bail !== false) {
throw err
Expand Down Expand Up @@ -300,6 +304,55 @@ ${renderCommands(rootScripts)}`
return output
}

export interface RunScriptOptions {
enablePrePostScripts: boolean
}

export const runScript: (opts: {
manifest: ProjectManifest
lifecycleOpts: RunLifecycleHookOptions
runScriptOptions: RunScriptOptions
passedThruArgs: string[]
}, scriptName: string) => Promise<void> = async function (opts, scriptName) {
if (
opts.runScriptOptions.enablePrePostScripts &&
opts.manifest.scripts?.[`pre${scriptName}`] &&
!opts.manifest.scripts[scriptName].includes(`pre${scriptName}`)
) {
await runLifecycleHook(`pre${scriptName}`, opts.manifest, opts.lifecycleOpts)
}
await runLifecycleHook(scriptName, opts.manifest, { ...opts.lifecycleOpts, args: opts.passedThruArgs })
if (
opts.runScriptOptions.enablePrePostScripts &&
opts.manifest.scripts?.[`post${scriptName}`] &&
!opts.manifest.scripts[scriptName].includes(`post${scriptName}`)
) {
await runLifecycleHook(`post${scriptName}`, opts.manifest, opts.lifecycleOpts)
}
}

function renderCommands (commands: string[][]) {
return commands.map(([scriptName, script]) => ` ${scriptName}\n ${script}`).join('\n')
}

function getSpecifiedScripts (scripts: PackageScripts, scriptName: string) {
const scriptSelector = tryBuildRegExpFromCommand(scriptName)

// if scriptName which a user passes is RegExp (like /build:.*/), multiple scripts to execute will be selected with RegExp
if (scriptSelector) {
const scriptKeys = Object.keys(scripts)
return scriptKeys.filter(script => script.match(scriptSelector))
}

// if scripts in package.json has script which is equal to scriptName a user passes, return it.
if (scripts[scriptName]) {
return [scriptName]
}

// if a user passes start command as scriptName, `node server.js` will be executed as a fallback, so return start command even if start command is not defined in package.json
if (scriptName === 'start') {
return [scriptName]
}

return []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should do the complicated stuff last because in the vast majority of cases it is scripts[scriptName].

Suggested change
const scriptSelector = tryBuildRegExpFromCommand(scriptName)
// if scriptName which a user passes is RegExp (like /build:.*/), multiple scripts to execute will be selected with RegExp
if (scriptSelector) {
const scriptKeys = Object.keys(scripts)
return scriptKeys.filter(script => script.match(scriptSelector))
}
// if scripts in package.json has script which is equal to scriptName a user passes, return it.
if (scripts[scriptName]) {
return [scriptName]
}
// if a user passes start command as scriptName, `node server.js` will be executed as a fallback, so return start command even if start command is not defined in package.json
if (scriptName === 'start') {
return [scriptName]
}
return []
// if scripts in package.json has script which is equal to scriptName a user passes, return it.
if (scripts[scriptName]) {
return [scriptName]
}
// if a user passes start command as scriptName, `node server.js` will be executed as a fallback, so return start command even if start command is not defined in package.json
if (scriptName === 'start') {
return [scriptName]
}
// if scriptName which a user passes is RegExp (like /build:.*/), multiple scripts to execute will be selected with RegExp
const scriptSelector = tryBuildRegExpFromCommand(scriptName)
if (scriptSelector) {
return Object.keys(scripts).filter(script => script.match(scriptSelector))
}
return []

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. acf55fd

}
48 changes: 30 additions & 18 deletions exec/plugin-commands-script-runners/src/runRecursive.ts
Expand Up @@ -3,7 +3,6 @@ import { RecursiveSummary, throwOnCommandFail } from '@pnpm/cli-utils'
import { Config } from '@pnpm/config'
import { PnpmError } from '@pnpm/error'
import {
runLifecycleHook,
makeNodeRequireOption,
RunLifecycleHookOptions,
} from '@pnpm/lifecycle'
Expand All @@ -13,6 +12,9 @@ import pLimit from 'p-limit'
import realpathMissing from 'realpath-missing'
import { existsInDir } from './existsInDir'
import { getResumedPackageChunks } from './exec'
import { runScript } from './run'
import { tryBuildRegExpFromCommand } from './regexpCommand'
import { PackageScripts } from '@pnpm/types'

export type RecursiveRunOpts = Pick<Config,
| 'enablePrePostScripts'
Expand Down Expand Up @@ -73,15 +75,22 @@ export async function runRecursive (
const missingScriptPackages: string[] = packageChunks
.flat()
.map((prefix) => opts.selectedProjectsGraph[prefix])
.filter((pkg) => !pkg.package.manifest.scripts?.[scriptName])
.filter((pkg) => getSpecifiedScripts(pkg.package.manifest.scripts ?? {}, scriptName).length < 1)
.map((pkg) => pkg.package.manifest.name ?? pkg.package.dir)
if (missingScriptPackages.length) {
throw new PnpmError('RECURSIVE_RUN_NO_SCRIPT', `Missing script "${scriptName}" in packages: ${missingScriptPackages.join(', ')}`)
}
}

for (const chunk of packageChunks) {
await Promise.all(chunk.map(async (prefix: string) =>
const selectedScripts = chunk.map(prefix => {
const pkg = opts.selectedProjectsGraph[prefix]
const specifiedScripts = getSpecifiedScripts(pkg.package.manifest.scripts ?? {}, scriptName)

return specifiedScripts.map(script => ({ prefix, scriptName: script }))
}).flat()

await Promise.all(selectedScripts.map(async ({ prefix, scriptName }) =>
limitRun(async () => {
const pkg = opts.selectedProjectsGraph[prefix]
if (
Expand Down Expand Up @@ -113,21 +122,9 @@ export async function runRecursive (
...makeNodeRequireOption(pnpPath),
}
}
if (
opts.enablePrePostScripts &&
pkg.package.manifest.scripts?.[`pre${scriptName}`] &&
!pkg.package.manifest.scripts[scriptName].includes(`pre${scriptName}`)
) {
await runLifecycleHook(`pre${scriptName}`, pkg.package.manifest, lifecycleOpts)
}
await runLifecycleHook(scriptName, pkg.package.manifest, { ...lifecycleOpts, args: passedThruArgs })
if (
opts.enablePrePostScripts &&
pkg.package.manifest.scripts?.[`post${scriptName}`] &&
!pkg.package.manifest.scripts[scriptName].includes(`post${scriptName}`)
) {
await runLifecycleHook(`post${scriptName}`, pkg.package.manifest, lifecycleOpts)
}

const _runScript = runScript.bind(null, { manifest: pkg.package.manifest, lifecycleOpts, runScriptOptions: { enablePrePostScripts: opts.enablePrePostScripts ?? false }, passedThruArgs })
await _runScript(scriptName)
result.passes++
} catch (err: any) { // eslint-disable-line
logger.info(err)
Expand Down Expand Up @@ -164,3 +161,18 @@ export async function runRecursive (

throwOnCommandFail('pnpm recursive run', result)
}

function getSpecifiedScripts (scripts: PackageScripts, scriptName: string) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this function not the same as in run.ts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my understanding, pnpm executes node server.js in pnpm run start even if start script is not defined in package.json. I can find the logic to do so in run.ts but I cannot in runRecursive.ts. This diff is occurred from this diff

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we could add a function parameter for controlling this behavior?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No parameter is needed. It is better to write another wrapper for run.st, which will include the logic for start.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is also a solution.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed the code to wrap runRecursive getSpecifiedScripts in `run.tsP

const scriptSelector = tryBuildRegExpFromCommand(scriptName)

if (scriptSelector) {
const scriptKeys = Object.keys(scripts)
return scriptKeys.filter(script => script.match(scriptSelector))
}

if (scripts[scriptName]) {
return [scriptName]
}

return []
}
99 changes: 98 additions & 1 deletion exec/plugin-commands-script-runners/test/index.ts
Expand Up @@ -411,7 +411,7 @@ test('if a script is not found but is present in the root, print an info message
}

expect(err).toBeTruthy()
expect(err.hint).toMatch(/But build is present in the root/)
expect(err.hint).toMatch(/But script matched with build is present in the root/)
})

test('scripts work with PnP', async () => {
Expand Down Expand Up @@ -465,3 +465,100 @@ test('pnpm run with custom shell', async () => {

expect((await import(path.resolve('shell-input.json'))).default).toStrictEqual(['-c', 'foo bar'])
})

test('pnpm run with multiple script selector should work', async () => {
prepare({
scripts: {
'build:a': 'node -e "require(\'fs\').writeFileSync(\'./output-build-a.txt\', \'a\', \'utf8\')"',
'build:b': 'node -e "require(\'fs\').writeFileSync(\'./output-build-b.txt\', \'b\', \'utf8\')"',
'build:c': 'node -e "require(\'fs\').writeFileSync(\'./output-build-c.txt\', \'c\', \'utf8\')"',
build: 'node -e "require(\'fs\').writeFileSync(\'./output-build-a.txt\', \'should not run\', \'utf8\')"',
'lint:a': 'node -e "require(\'fs\').writeFileSync(\'./output-lint-a.txt\', \'a\', \'utf8\')"',
'lint:b': 'node -e "require(\'fs\').writeFileSync(\'./output-lint-b.txt\', \'b\', \'utf8\')"',
'lint:c': 'node -e "require(\'fs\').writeFileSync(\'./output-lint-c.txt\', \'c\', \'utf8\')"',
lint: 'node -e "require(\'fs\').writeFileSync(\'./output-lint-a.txt\', \'should not run\', \'utf8\')"',
},
})

await run.handler({
dir: process.cwd(),
extraBinPaths: [],
extraEnv: {},
rawConfig: {},
}, ['/^(lint|build):.*/'])

expect(await fs.readFile('output-build-a.txt', { encoding: 'utf-8' })).toEqual('a')
expect(await fs.readFile('output-build-b.txt', { encoding: 'utf-8' })).toEqual('b')
expect(await fs.readFile('output-build-c.txt', { encoding: 'utf-8' })).toEqual('c')

expect(await fs.readFile('output-lint-a.txt', { encoding: 'utf-8' })).toEqual('a')
expect(await fs.readFile('output-lint-b.txt', { encoding: 'utf-8' })).toEqual('b')
expect(await fs.readFile('output-lint-c.txt', { encoding: 'utf-8' })).toEqual('c')
})

test('pnpm run with multiple script selector should work also for pre/post script', async () => {
prepare({
scripts: {
'build:a': 'node -e "require(\'fs\').writeFileSync(\'./output-a.txt\', \'a\', \'utf8\')"',
'prebuild:a': 'node -e "require(\'fs\').writeFileSync(\'./output-pre-a.txt\', \'pre-a\', \'utf8\')"',
},
})

await run.handler({
dir: process.cwd(),
extraBinPaths: [],
extraEnv: {},
rawConfig: {},
enablePrePostScripts: true,
}, ['/build:.*/'])

expect(await fs.readFile('output-a.txt', { encoding: 'utf-8' })).toEqual('a')
expect(await fs.readFile('output-pre-a.txt', { encoding: 'utf-8' })).toEqual('pre-a')
})

test('pnpm run with multiple script selector should work parallel as a default behavior (parallel execution limits number is four)', async () => {
prepare({
scripts: {
'build:a': 'node -e "let i = 20;setInterval(() => {if (!--i) process.exit(0); require(\'json-append\').append(Date.now(),\'./output-a.json\');},50)"',
'build:b': 'node -e "let i = 40;setInterval(() => {if (!--i) process.exit(0); require(\'json-append\').append(Date.now(),\'./output-b.json\');},25)"',
},
})

await execa('pnpm', ['add', 'json-append@1'])

await run.handler({
dir: process.cwd(),
extraBinPaths: [],
extraEnv: {},
rawConfig: {},
}, ['/build:.*/'])

const { default: outputsA } = await import(path.resolve('output-a.json'))
const { default: outputsB } = await import(path.resolve('output-b.json'))

expect(Math.max(outputsA[0], outputsB[0]) < Math.min(outputsA[outputsA.length - 1], outputsB[outputsB.length - 1])).toBeTruthy()
})

test('pnpm run with multiple script selector should work sequentially with --workspace-concurrency=1', async () => {
prepare({
scripts: {
'build:a': 'node -e "let i = 2;setInterval(() => {if (!i--) process.exit(0); require(\'json-append\').append(Date.now(),\'./output-a.json\');},16)"',
'build:b': 'node -e "let i = 2;setInterval(() => {if (!i--) process.exit(0); require(\'json-append\').append(Date.now(),\'./output-b.json\');},16)"',
},
})

await execa('pnpm', ['add', 'json-append@1'])

await run.handler({
dir: process.cwd(),
extraBinPaths: [],
extraEnv: {},
rawConfig: {},
workspaceConcurrency: 1,
}, ['/build:.*/'])

const { default: outputsA } = await import(path.resolve('output-a.json'))
const { default: outputsB } = await import(path.resolve('output-b.json'))

expect(outputsA[0] < outputsB[0] && outputsA[1] < outputsB[1]).toBeTruthy()
})