Skip to content

Commit 879675b

Browse files
authored
Updated support for pnmp and yarn (#164)
This PR adds additional updates and fixes to better support pnmp and yarn with `@github/local-action`.
2 parents 91bc911 + 4a646a5 commit 879675b

7 files changed

+101
-141
lines changed

bin/local-action.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,8 @@ function entrypoint() {
2929
// need to be double-escaped so the path resolves correctly.
3030
const bootstrapPath =
3131
process.platform === 'win32'
32-
? path
33-
.join(packagePath, 'src', 'bootstrap.mts')
34-
.replaceAll('\\', '\\\\')
35-
: path.join(packagePath, 'src', 'bootstrap.mts')
32+
? path.join(packagePath, 'src', 'bootstrap.ts').replaceAll('\\', '\\\\')
33+
: path.join(packagePath, 'src', 'bootstrap.ts')
3634

3735
// Require the bootstrap script in NODE_OPTIONS.
3836
process.env.NODE_OPTIONS = process.env.NODE_OPTIONS

jest.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const config: JestConfigWithTsJest = {
77
coverageDirectory: 'coverage',
88
coveragePathIgnorePatterns: [
99
'node_modules',
10-
'src/bootstrap.mts',
10+
'src/bootstrap.ts',
1111
'src/types/quibble.d.ts',
1212
'src/commands/run.ts',
1313
'src/stubs/artifact/artifact.ts'

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@github/local-action",
33
"description": "Local Debugging for GitHub Actions",
4-
"version": "3.0.5",
4+
"version": "3.1.0",
55
"type": "module",
66
"author": "Nick Alteen <[email protected]>",
77
"private": false,

src/bootstrap.mts

-45
This file was deleted.

src/bootstrap.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/* eslint-disable @typescript-eslint/no-require-imports */
2+
3+
/**
4+
* This file is used to bootstrap the environment for the action.
5+
*
6+
* It is added as a --require option to `npx tsx`. The purpose of this file is
7+
* to handle various configuration options. For example, if an action repository
8+
* makes use of TypeScript paths for module resolution, this bootstrap script
9+
* will parse them and register them so that modules can be resolved correctly.
10+
*/
11+
12+
const { existsSync, readFileSync } = require('fs')
13+
const { loadConfig, register } = require('tsconfig-paths')
14+
const { parse } = require('comment-json')
15+
16+
if (process.env.TARGET_ACTION_PATH && process.env.TARGET_ACTION_PATH !== '') {
17+
// Check if the action has a `tsconfig.json` file.
18+
if (existsSync(`${process.env.TARGET_ACTION_PATH}/tsconfig.json`)) {
19+
// Load the `tsconfig.json` from the action directory.
20+
const actionTsConfig = parse(
21+
readFileSync(`${process.env.TARGET_ACTION_PATH}/tsconfig.json`, 'utf-8')
22+
)
23+
24+
// Load the current `tsconfig.json` from the root of this directory.
25+
loadConfig(__dirname)
26+
27+
// Get the paths from the action's `tsconfig.json`, if any.
28+
const paths = actionTsConfig.compilerOptions?.paths ?? {}
29+
30+
// Add any path mappings from the imported action. Replace the base URL with
31+
// the target action path.
32+
// @todo Should this take into account the previous `baseUrl` value?
33+
register({
34+
baseUrl: process.env.TARGET_ACTION_PATH,
35+
paths,
36+
addMatchAll: true
37+
})
38+
}
39+
}

src/commands/run.ts

+56-88
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export async function action(): Promise<void> {
3232
yellow: (msg: string) => console.log(chalk.yellow(msg))
3333
}
3434

35+
const actionType = isESM() ? 'esm' : 'cjs'
36+
3537
// Output the configuration
3638
printTitle(CoreMeta.colors.cyan, 'Configuration')
3739
console.log()
@@ -134,11 +136,23 @@ export async function action(): Promise<void> {
134136
'No package.json file found in the action directory or any parent directories.'
135137
)
136138

137-
// If the package manager is `npm` or `pnpm`, then a `node_modules` directory
138-
// should exist somewhere in the project. If the package manager is `yarn`,
139-
// then we need to first check for `node_modules` (for non-PnP versions), or
140-
// we can try unplugging the dependencies from the yarn cache.
141-
if (process.env.NODE_PACKAGE_MANAGER === 'npm') {
139+
// If the package manager is `npm`, we can assume there is a `node_modules`
140+
// directory somewhere in the project.
141+
//
142+
// Actions that use `pnpm` will also have a `node_modules` directory, however
143+
// in testing it seems that the `node_modules/@actions/<pkg>` dependency can
144+
// only be properly stubbed for CJS actions. For ESM actions, this should
145+
// instead point to the pnpm cache.
146+
//
147+
// If the package manager is `yarn`, then we need to first check for
148+
// `node_modules` (for non-PnP versions), or we can try unplugging the
149+
// dependencies from the yarn cache.
150+
if (
151+
process.env.NODE_PACKAGE_MANAGER === 'npm' ||
152+
(process.env.NODE_PACKAGE_MANAGER === 'pnpm' && actionType === 'cjs') ||
153+
(process.env.NODE_PACKAGE_MANAGER === 'yarn' &&
154+
fs.existsSync(path.join(EnvMeta.actionPath, 'node_modules')))
155+
) {
142156
/**
143157
* Get the path in the npm cache for each package.
144158
* `npm ls <package> --json --long`
@@ -179,7 +193,10 @@ export async function action(): Promise<void> {
179193
Object.keys(stubs).forEach(key => {
180194
stubs[key as keyof typeof stubs].base = npmList.dependencies?.[key]?.path
181195
})
182-
} else if (process.env.NODE_PACKAGE_MANAGER === 'pnpm') {
196+
} else if (
197+
process.env.NODE_PACKAGE_MANAGER === 'pnpm' &&
198+
actionType === 'esm'
199+
) {
183200
/**
184201
* Get the path in the pnpm cache for each package.
185202
* `pnpm list <package> --json`
@@ -217,87 +234,38 @@ export async function action(): Promise<void> {
217234
pnpmList[0].dependencies?.[key]?.path
218235
})
219236
} else if (process.env.NODE_PACKAGE_MANAGER === 'yarn') {
220-
// Depending on the version and configuration for yarn, a `node_modules`
221-
// directory may or may not exist. Also, the CLI commands are different
222-
// across versions for getting the path to a dependency.
223-
224-
// First check if a `node_modules` directory exists in the target action
225-
// path (or a parent path).
226-
if (fs.existsSync(path.join(EnvMeta.actionPath, 'node_modules'))) {
227-
// Get the path in the npm cache for each package. This will work if there
228-
// is a `node_modules` directory (`yarn list` does not provide the path).
229-
// `npm ls <package> --json --long`
230-
231-
/**
232-
* Example Output
233-
*
234-
* {
235-
* "path": "<workspace>/typescript-action",
236-
* "_dependencies": {
237-
* "@actions/core": "^1.11.1",
238-
* "@actions/github": "^6.0.0"
239-
* },
240-
* "dependencies": {
241-
* "@actions/core": {
242-
* "path": "<workspace>/typescript-action/node_modules/@actions/core",
243-
* },
244-
* "@actions/github": {
245-
* "path": "<workspace>/typescript-action/node_modules/@actions/github",
246-
* }
247-
* }
248-
* }
249-
*/
250-
const npmList = JSON.parse(
251-
execSync(
252-
`npm ls ${Object.keys(stubs).join(' ')} --json --long`
253-
).toString()
254-
) as {
255-
path: string
256-
dependencies?: {
257-
[key: string]: { path: string }
258-
}
259-
}
260-
261-
if (Object.keys(npmList.dependencies ?? {}).length === 0)
262-
throw new Error('Something went wrong with npm list')
263-
264-
Object.keys(stubs).forEach(key => {
265-
stubs[key as keyof typeof stubs].base =
266-
npmList.dependencies?.[key]?.path
267-
})
268-
} else {
269-
// At this point, it's likely yarn is running in PnP mode.
270-
printTitle(CoreMeta.colors.magenta, 'Yarn: Unplugging Dependencies')
271-
console.log()
272-
273-
// For now, we need to `unplug` each dependency to get the path to the
274-
// package.
275-
// TODO: Is there a better way to do this without unplugging?
276-
needsReplug = true
277-
278-
for (const key of Object.keys(stubs)) {
279-
// This may fail if the package is not a dependency for this action.
280-
try {
281-
const output = execSync(`yarn unplug ${key}`).toString()
282-
console.log(`Unplugged: ${key}`)
283-
284-
// Next, get the path to the package. Unfortunately using the `--json`
285-
// flag with `yarn unplug` does not output the target path, so we need
286-
// to parse it from the plaintext output.
287-
const packagePath = output.match(
288-
/Will unpack .* to (?<packagePath>.*)/
289-
)?.groups?.packagePath
290-
291-
if (!packagePath) throw new Error(`Could not unplug ${key}`)
292-
293-
stubs[key as keyof typeof stubs].base = path.join(
294-
packagePath,
295-
'node_modules',
296-
key
297-
)
298-
} catch {
299-
// This is fine...
300-
}
237+
// Note: The CLI commands are different across yarn versions for getting the
238+
// path to a dependency.
239+
240+
// At this point, it's likely yarn is running in PnP mode.
241+
printTitle(CoreMeta.colors.magenta, 'Yarn: Unplugging Dependencies')
242+
console.log()
243+
244+
// For now, we need to `unplug` each dependency to get the path to the
245+
// package.
246+
needsReplug = true
247+
248+
for (const key of Object.keys(stubs)) {
249+
// This may fail if the package is not a dependency for this action.
250+
try {
251+
const output = execSync(`yarn unplug ${key}`).toString()
252+
console.log(`Unplugged: ${key}`)
253+
254+
// Next, get the path to the package. Unfortunately using the `--json`
255+
// flag with `yarn unplug` does not output the target path, so we need
256+
// to parse it from the plaintext output.
257+
const packagePath = output.match(/Will unpack .* to (?<packagePath>.*)/)
258+
?.groups?.packagePath
259+
260+
if (!packagePath) throw new Error(`Could not unplug ${key}`)
261+
262+
stubs[key as keyof typeof stubs].base = path.join(
263+
packagePath,
264+
'node_modules',
265+
key
266+
)
267+
} catch {
268+
// This is fine...
301269
}
302270
}
303271
}
@@ -317,7 +285,7 @@ export async function action(): Promise<void> {
317285
// Stub the `@actions/toolkit` libraries and run the action. Quibble and
318286
// local-action require a different approach depending on if the called action
319287
// is written in ESM.
320-
if (isESM()) {
288+
if (actionType === 'esm') {
321289
await quibble.esm(
322290
path.resolve(
323291
stubs['@actions/github'].base ?? '',

0 commit comments

Comments
 (0)