diff --git a/ark/attest/__tests__/utils.ts b/ark/attest/__tests__/utils.ts index 5daf26e8ef..3496ddfe75 100644 --- a/ark/attest/__tests__/utils.ts +++ b/ark/attest/__tests__/utils.ts @@ -7,7 +7,7 @@ export const runThenGetContents = (templatePath: string): string => { const tempPath = templatePath + ".temp.ts" copyFileSync(templatePath, tempPath) try { - shell(`node --import=tsx ${tempPath}`, { + shell("node", ["--import=tsx", tempPath], { cwd: dirName(), env: { ATTEST_failOnMissingSnapshots: "0" diff --git a/ark/attest/cache/snapshots.ts b/ark/attest/cache/snapshots.ts index 7421292bc6..6330eb955e 100644 --- a/ark/attest/cache/snapshots.ts +++ b/ark/attest/cache/snapshots.ts @@ -198,6 +198,9 @@ const runFormatterIfAvailable = (queuedUpdates: QueuedUpdate[]) => { const { formatCmd: formatter, shouldFormat } = getConfig() if (!shouldFormat) return + if (formatter.length === 0) + throw new Error("config formatCmd must be at least length 1") + try { const updatedPaths = [ ...new Set( @@ -206,7 +209,9 @@ const runFormatterIfAvailable = (queuedUpdates: QueuedUpdate[]) => { ) ) ] - shell(`${formatter} ${updatedPaths.join(" ")}`) + const command = formatter[0] + const args = formatter.slice(1) + shell(command, [...args, "--", ...updatedPaths]) } catch { // If formatter is unavailable or skipped, do nothing. } diff --git a/ark/attest/cli/trace.ts b/ark/attest/cli/trace.ts index e32a098dcf..1f199ec41d 100644 --- a/ark/attest/cli/trace.ts +++ b/ark/attest/cli/trace.ts @@ -266,7 +266,8 @@ const generateTraceData = ( ): string => { try { const output = getShellOutput( - `${baseDiagnosticTscCmd} --project ${tsconfigPath} --generateTrace ${traceDir}`, + baseDiagnosticTscCmd, + ["--project", tsconfigPath, "--generateTrace", traceDir], { cwd: packageDir } ) process.stdout.write(output) // Display tsc output directly diff --git a/ark/attest/config.ts b/ark/attest/config.ts index ccdf804b03..69aefabf26 100644 --- a/ark/attest/config.ts +++ b/ark/attest/config.ts @@ -44,7 +44,7 @@ type BaseAttestConfig = { benchErrorOnThresholdExceeded: BenchErrorConfig filter: string | undefined testDeclarationAliases: string[] - formatCmd: string + formatCmd: string[] shouldFormat: boolean /** * Provided options will override the following defaults. @@ -75,7 +75,7 @@ export const getDefaultAttestConfig = (): BaseAttestConfig => ({ benchErrorOnThresholdExceeded: true, filter: undefined, testDeclarationAliases: ["bench", "it", "test"], - formatCmd: `npm exec --no -- prettier --write`, + formatCmd: ["npm", "exec", "--no", "--", "prettier", "--write"], shouldFormat: true, typeToStringFormat: {} }) diff --git a/ark/attest/fixtures.ts b/ark/attest/fixtures.ts index 031f2e2b8a..9b509664de 100644 --- a/ark/attest/fixtures.ts +++ b/ark/attest/fixtures.ts @@ -27,9 +27,9 @@ export const setup = (options?: Partial): typeof teardown => { ) // if we're in our own repo, we need to pnpm to use the root script to execute ts directly if (fileName().endsWith("ts")) - shell(`pnpm attest precache ${precachePath}`) + shell("pnpm", ["attest", "precache", precachePath]) // otherwise, just use npm to run the CLI command from build output - else shell(`npm exec -c "attest precache ${precachePath}"`) + else shell("npm", ["exec", "-c", "attest", "precache", precachePath]) }) } return teardown diff --git a/ark/fs/fs.ts b/ark/fs/fs.ts index 98729c0887..24aea2a337 100644 --- a/ark/fs/fs.ts +++ b/ark/fs/fs.ts @@ -174,7 +174,12 @@ export const readPackageJson = (startDir = dirOfCaller()): any => export const getSourceControlPaths = (): string[] => // include tracked and untracked files as long as they are not ignored - getShellOutput("git ls-files --exclude-standard --cached --others") + getShellOutput("git", [ + "ls-files", + "--exclude-standard", + "--cached", + "--others" + ]) .split("\n") .filter(path => existsSync(path) && statSync(path).isFile()) diff --git a/ark/fs/shell.ts b/ark/fs/shell.ts index 17e14765ff..cabe017bac 100644 --- a/ark/fs/shell.ts +++ b/ark/fs/shell.ts @@ -1,29 +1,36 @@ -import { execSync, type ExecSyncOptions } from "node:child_process" +import { spawnSync, type SpawnSyncOptions } from "node:child_process" import * as process from "node:process" -export type ShellOptions = Omit & { +export type ShellOptions = Omit & { env?: Record } /** Run the cmd synchronously. Output goes to terminal. */ export const shell = ( cmd: string, + args: string[], { env, ...otherOptions }: ShellOptions = {} ): void => { - execSync(cmd, { + const result = spawnSync(cmd, args, { env: { ...process.env, ...env }, ...otherOptions, stdio: "inherit" }) + if (result.error) throw result.error } /** Run the cmd synchronously, returning output as a string */ export const getShellOutput = ( cmd: string, + args: string[], { env, ...otherOptions }: ShellOptions = {} -): string => - execSync(cmd, { +): string => { + const result = spawnSync(cmd, args, { env: { ...process.env, ...env }, ...otherOptions, stdio: "pipe" - })!.toString() + }) + if (result.error) throw result.error + + return result.toString() +} diff --git a/ark/repo/build.ts b/ark/repo/build.ts index 2196c17a05..061e0d4a66 100644 --- a/ark/repo/build.ts +++ b/ark/repo/build.ts @@ -19,9 +19,11 @@ const outDir = fromCwd("out") const packageName = readPackageJson(process.cwd()).name const buildCurrentProject = () => - shell( - `node ${fromHere("node_modules", "typescript", "lib", "tsc.js")} --project tsconfig.build.json` - ) + shell("node", [ + fromHere("node_modules", "typescript", "lib", "tsc.js"), + "--project", + "tsconfig.build.json" + ]) try { rmRf(outDir) diff --git a/ark/repo/dtsGen.ts b/ark/repo/dtsGen.ts index dfc0f00a9b..a1d0f4a809 100644 --- a/ark/repo/dtsGen.ts +++ b/ark/repo/dtsGen.ts @@ -14,7 +14,16 @@ export const dtsGen = () => { console.log(`✍️ Generating DTS bundle for ${pkg.name}...`) - shell("pnpm tsup index.ts --dts-only --dts-resolve --format esm --out-dir .") + shell("pnpm", [ + "tsup", + "index.ts", + "--dts-only", + "--dts-resolve", + "--format", + "esm", + "--out-dir", + "." + ]) const expectedDtsBundlePath = join(pkg.path, "index.d.ts") diff --git a/ark/repo/publish.ts b/ark/repo/publish.ts index 020f5d4b8d..dd83676d97 100644 --- a/ark/repo/publish.ts +++ b/ark/repo/publish.ts @@ -3,7 +3,7 @@ import { packages, type ArkPackage } from "./shared.ts" const tagsToPublish: string[] = [] -const existingTags = getShellOutput("git tag").split("\n") +const existingTags = getShellOutput("git", ["tag"]).split("\n") const publishPackage = (pkg: ArkPackage, alias?: string) => { const tagName = `${alias ?? pkg.name}@${pkg.version}` @@ -11,9 +11,9 @@ const publishPackage = (pkg: ArkPackage, alias?: string) => { if (!existingTags.includes(tagName)) { if (alias) rewritePackageJsonName(pkg.packageJsonPath, alias) - shell(`git tag ${tagName}`) + shell("git", ["tag", tagName]) tagsToPublish.push(tagName) - shell("pnpm publish --no-git-checks", { cwd: pkg.path }) + shell("pnpm", ["publish", "--no-git-checks"], { cwd: pkg.path }) if (alias) rewritePackageJsonName(pkg.packageJsonPath, pkg.name) } @@ -34,7 +34,7 @@ for (const pkg of packages) { } } -shell("git push --tags") +shell("git", ["push", "--tags"]) for (const tagName of tagsToPublish) - shell(`gh release create ${tagName} --latest`) + shell("gh", ["release", "create", tagName, "--latest"]) diff --git a/ark/repo/testPackage.ts b/ark/repo/testPackage.ts index 8bb2c966d5..7ef33643a0 100644 --- a/ark/repo/testPackage.ts +++ b/ark/repo/testPackage.ts @@ -1,7 +1,8 @@ import { fromHere, shell } from "@ark/fs" -shell( - `pnpm mocha --config ${fromHere("mocha.package.jsonc")} ${process.argv - .slice(2) - .join(" ")}` -) +shell("pnpm", [ + "mocha", + "--config", + fromHere("mocha.package.jsonc"), + process.argv.slice(2).join(" ") +])