Skip to content

Commit fd8f376

Browse files
authored
Switch to ESM for source and test, use CommonJS for dist (#806)
1 parent f9070de commit fd8f376

23 files changed

Lines changed: 212263 additions & 231169 deletions

‎.github/workflows/test.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
npm run all
3939
- name: Check all jobs are in all-tests-passed.needs
4040
run: |
41-
tsc check-all-tests-passed-needs.ts
41+
tsc --module nodenext --moduleResolution nodenext --target es2022 check-all-tests-passed-needs.ts
4242
node check-all-tests-passed-needs.js
4343
working-directory: .github/scripts
4444
- name: Make sure no changes from linters are detected

‎.github/workflows/update-known-checksums.yml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ jobs:
2020
persist-credentials: true
2121
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
2222
with:
23-
node-version: "20"
23+
node-version-file: .nvmrc
24+
cache: npm
2425
- name: Update known checksums
2526
id: update-known-checksums
2627
run:
27-
node dist/update-known-checksums/index.js
28+
node dist/update-known-checksums/index.cjs
2829
src/download/checksum/known-checksums.ts
2930
- name: Check for changes
3031
id: changes-exist

‎AGENTS.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,9 @@ This repository is a TypeScript-based GitHub Action for installing `uv` in GitHu
1010
- User-facing changes are usually multi-file changes. If you add or change inputs, outputs, or behavior, update `action.yml`, the implementation in `src/`, tests in `__tests__/`, relevant docs/README, and then re-package.
1111
- The easiest areas to regress are version resolution and caching. When touching them, add or update tests for precedence, cache invalidation, and cross-platform path behavior.
1212
- Workflow edits have extra CI-only checks (`actionlint` and `zizmor`); `npm run all` does not cover them.
13+
- Source is authored with bundler-friendly TypeScript, but published action artifacts in `dist/` are bundled as CommonJS for maximum GitHub Actions runtime compatibility with `@actions/*` dependencies.
14+
- Keep these concerns separate when changing module formats:
15+
- `src/` and tests may use modern ESM-friendly TypeScript patterns.
16+
- `dist/` should prioritize runtime reliability over format purity.
17+
- Do not switch published bundles to ESM without validating the actual committed artifacts under the target Node runtime.
1318
- Before finishing, make sure validation does not leave generated or formatting-only diffs behind.

‎__tests__/download/download-version.test.ts‎

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
2+
import * as semver from "semver";
23

34
const mockInfo = jest.fn();
45
const mockWarning = jest.fn();
56

6-
jest.mock("@actions/core", () => ({
7+
jest.unstable_mockModule("@actions/core", () => ({
78
debug: jest.fn(),
89
info: mockInfo,
910
warning: mockWarning,
@@ -18,20 +19,17 @@ const mockExtractZip = jest.fn<any>();
1819
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
1920
const mockCacheDir = jest.fn<any>();
2021

21-
jest.mock("@actions/tool-cache", () => {
22-
const actual = jest.requireActual("@actions/tool-cache") as Record<
23-
string,
24-
unknown
25-
>;
26-
27-
return {
28-
...actual,
29-
cacheDir: mockCacheDir,
30-
downloadTool: mockDownloadTool,
31-
extractTar: mockExtractTar,
32-
extractZip: mockExtractZip,
33-
};
34-
});
22+
jest.unstable_mockModule("@actions/tool-cache", () => ({
23+
cacheDir: mockCacheDir,
24+
downloadTool: mockDownloadTool,
25+
evaluateVersions: (versions: string[], range: string) =>
26+
semver.maxSatisfying(versions, range) ?? "",
27+
extractTar: mockExtractTar,
28+
extractZip: mockExtractZip,
29+
find: () => "",
30+
findAllVersions: () => [],
31+
isExplicitVersion: (version: string) => semver.valid(version) !== null,
32+
}));
3533

3634
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
3735
const mockGetLatestVersionFromNdjson = jest.fn<any>();
@@ -40,7 +38,7 @@ const mockGetAllVersionsFromNdjson = jest.fn<any>();
4038
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
4139
const mockGetArtifactFromNdjson = jest.fn<any>();
4240

43-
jest.mock("../../src/download/versions-client", () => ({
41+
jest.unstable_mockModule("../../src/download/versions-client", () => ({
4442
getAllVersions: mockGetAllVersionsFromNdjson,
4543
getArtifact: mockGetArtifactFromNdjson,
4644
getLatestVersion: mockGetLatestVersionFromNdjson,
@@ -53,7 +51,7 @@ const mockGetLatestVersionInManifest = jest.fn<any>();
5351
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
5452
const mockGetManifestArtifact = jest.fn<any>();
5553

56-
jest.mock("../../src/download/version-manifest", () => ({
54+
jest.unstable_mockModule("../../src/download/version-manifest", () => ({
5755
getAllVersions: mockGetAllManifestVersions,
5856
getLatestKnownVersion: mockGetLatestVersionInManifest,
5957
getManifestArtifact: mockGetManifestArtifact,
@@ -62,15 +60,15 @@ jest.mock("../../src/download/version-manifest", () => ({
6260
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
6361
const mockValidateChecksum = jest.fn<any>();
6462

65-
jest.mock("../../src/download/checksum/checksum", () => ({
63+
jest.unstable_mockModule("../../src/download/checksum/checksum", () => ({
6664
validateChecksum: mockValidateChecksum,
6765
}));
6866

69-
import {
67+
const {
7068
downloadVersionFromManifest,
7169
downloadVersionFromNdjson,
7270
resolveVersion,
73-
} from "../../src/download/download-version";
71+
} = await import("../../src/download/download-version");
7472

7573
describe("download-version", () => {
7674
beforeEach(() => {

‎__tests__/download/version-manifest.test.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,24 @@ import { beforeEach, describe, expect, it, jest } from "@jest/globals";
22

33
const mockWarning = jest.fn();
44

5-
jest.mock("@actions/core", () => ({
5+
jest.unstable_mockModule("@actions/core", () => ({
66
debug: jest.fn(),
77
info: jest.fn(),
88
warning: mockWarning,
99
}));
1010

1111
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
1212
const mockFetch = jest.fn<any>();
13-
jest.mock("../../src/utils/fetch", () => ({
13+
jest.unstable_mockModule("../../src/utils/fetch", () => ({
1414
fetch: mockFetch,
1515
}));
1616

17-
import {
17+
const {
1818
clearManifestCache,
1919
getAllVersions,
2020
getLatestKnownVersion,
2121
getManifestArtifact,
22-
} from "../../src/download/version-manifest";
22+
} = await import("../../src/download/version-manifest");
2323

2424
const legacyManifestResponse = JSON.stringify([
2525
{

‎__tests__/download/versions-client.test.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,19 @@ import { beforeEach, describe, expect, it, jest } from "@jest/globals";
22

33
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
44
const mockFetch = jest.fn<any>();
5-
jest.mock("../../src/utils/fetch", () => ({
5+
6+
jest.unstable_mockModule("../../src/utils/fetch", () => ({
67
fetch: mockFetch,
78
}));
89

9-
import {
10+
const {
1011
clearCache,
1112
fetchVersionData,
1213
getAllVersions,
1314
getArtifact,
1415
getLatestVersion,
1516
parseVersionData,
16-
} from "../../src/download/versions-client";
17+
} = await import("../../src/download/versions-client");
1718

1819
const sampleNdjsonResponse = `{"version":"0.9.26","artifacts":[{"platform":"aarch64-apple-darwin","variant":"default","url":"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-aarch64-apple-darwin.tar.gz","archive_format":"tar.gz","sha256":"fcf0a9ea6599c6ae28a4c854ac6da76f2c889354d7c36ce136ef071f7ab9721f"},{"platform":"x86_64-pc-windows-msvc","variant":"default","url":"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-pc-windows-msvc.zip","archive_format":"zip","sha256":"eb02fd95d8e0eed462b4a67ecdd320d865b38c560bffcda9a0b87ec944bdf036"}]}
1920
{"version":"0.9.25","artifacts":[{"platform":"aarch64-apple-darwin","variant":"default","url":"https://github.com/astral-sh/uv/releases/download/0.9.25/uv-aarch64-apple-darwin.tar.gz","archive_format":"tar.gz","sha256":"606b3c6949d971709f2526fa0d9f0fd23ccf60e09f117999b406b424af18a6a6"}]}`;

‎__tests__/utils/inputs.test.ts‎

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,3 @@
1-
jest.mock("@actions/core", () => {
2-
return {
3-
debug: jest.fn(),
4-
getBooleanInput: jest.fn(
5-
(name: string) => (mockInputs[name] ?? "") === "true",
6-
),
7-
getInput: jest.fn((name: string) => mockInputs[name] ?? ""),
8-
warning: jest.fn(),
9-
};
10-
});
11-
121
import {
132
afterEach,
143
beforeEach,
@@ -22,6 +11,26 @@ import {
2211
let mockInputs: Record<string, string> = {};
2312
const ORIGINAL_HOME = process.env.HOME;
2413

14+
const mockDebug = jest.fn();
15+
const mockGetBooleanInput = jest.fn(
16+
(name: string) => (mockInputs[name] ?? "") === "true",
17+
);
18+
const mockGetInput = jest.fn((name: string) => mockInputs[name] ?? "");
19+
const mockInfo = jest.fn();
20+
const mockWarning = jest.fn();
21+
22+
jest.unstable_mockModule("@actions/core", () => ({
23+
debug: mockDebug,
24+
getBooleanInput: mockGetBooleanInput,
25+
getInput: mockGetInput,
26+
info: mockInfo,
27+
warning: mockWarning,
28+
}));
29+
30+
async function importInputsModule() {
31+
return await import("../../src/utils/inputs");
32+
}
33+
2534
describe("cacheDependencyGlob", () => {
2635
beforeEach(() => {
2736
jest.resetModules();
@@ -36,29 +45,29 @@ describe("cacheDependencyGlob", () => {
3645

3746
it("returns empty string when input not provided", async () => {
3847
mockInputs["working-directory"] = "/workspace";
39-
const { cacheDependencyGlob } = await import("../../src/utils/inputs");
48+
const { cacheDependencyGlob } = await importInputsModule();
4049
expect(cacheDependencyGlob).toBe("");
4150
});
4251

4352
it("resolves a single relative path", async () => {
4453
mockInputs["working-directory"] = "/workspace";
4554
mockInputs["cache-dependency-glob"] = "requirements.txt";
46-
const { cacheDependencyGlob } = await import("../../src/utils/inputs");
55+
const { cacheDependencyGlob } = await importInputsModule();
4756
expect(cacheDependencyGlob).toBe("/workspace/requirements.txt");
4857
});
4958

5059
it("strips leading ./ from relative path", async () => {
5160
mockInputs["working-directory"] = "/workspace";
5261
mockInputs["cache-dependency-glob"] = "./uv.lock";
53-
const { cacheDependencyGlob } = await import("../../src/utils/inputs");
62+
const { cacheDependencyGlob } = await importInputsModule();
5463
expect(cacheDependencyGlob).toBe("/workspace/uv.lock");
5564
});
5665

5766
it("handles multiple lines, trimming whitespace, tilde expansion and absolute paths", async () => {
5867
mockInputs["working-directory"] = "/workspace";
5968
mockInputs["cache-dependency-glob"] =
6069
" ~/.cache/file1\n ./rel/file2 \nfile3.txt";
61-
const { cacheDependencyGlob } = await import("../../src/utils/inputs");
70+
const { cacheDependencyGlob } = await importInputsModule();
6271
expect(cacheDependencyGlob).toBe(
6372
[
6473
"/home/testuser/.cache/file1", // expanded tilde, absolute path unchanged
@@ -71,7 +80,7 @@ describe("cacheDependencyGlob", () => {
7180
it("keeps absolute path unchanged in multiline input", async () => {
7281
mockInputs["working-directory"] = "/workspace";
7382
mockInputs["cache-dependency-glob"] = "/abs/path.lock\nrelative.lock";
74-
const { cacheDependencyGlob } = await import("../../src/utils/inputs");
83+
const { cacheDependencyGlob } = await importInputsModule();
7584
expect(cacheDependencyGlob).toBe(
7685
["/abs/path.lock", "/workspace/relative.lock"].join("\n"),
7786
);
@@ -80,7 +89,7 @@ describe("cacheDependencyGlob", () => {
8089
it("handles exclusions in relative paths correct", async () => {
8190
mockInputs["working-directory"] = "/workspace";
8291
mockInputs["cache-dependency-glob"] = "!/abs/path.lock\n!relative.lock";
83-
const { cacheDependencyGlob } = await import("../../src/utils/inputs");
92+
const { cacheDependencyGlob } = await importInputsModule();
8493
expect(cacheDependencyGlob).toBe(
8594
["!/abs/path.lock", "!/workspace/relative.lock"].join("\n"),
8695
);
@@ -104,7 +113,7 @@ describe("tool directories", () => {
104113
mockInputs["tool-bin-dir"] = "~/tool-bin-dir";
105114
mockInputs["tool-dir"] = "~/tool-dir";
106115

107-
const { toolBinDir, toolDir } = await import("../../src/utils/inputs");
116+
const { toolBinDir, toolDir } = await importInputsModule();
108117

109118
expect(toolBinDir).toBe("/home/testuser/tool-bin-dir");
110119
expect(toolDir).toBe("/home/testuser/tool-dir");
@@ -127,9 +136,7 @@ describe("cacheLocalPath", () => {
127136
mockInputs["working-directory"] = "/workspace";
128137
mockInputs["cache-local-path"] = "~/uv-cache/cache-local-path";
129138

130-
const { CacheLocalSource, cacheLocalPath } = await import(
131-
"../../src/utils/inputs"
132-
);
139+
const { CacheLocalSource, cacheLocalPath } = await importInputsModule();
133140

134141
expect(cacheLocalPath).toEqual({
135142
path: "/home/testuser/uv-cache/cache-local-path",
@@ -152,58 +159,51 @@ describe("venvPath", () => {
152159

153160
it("defaults to .venv in the working directory", async () => {
154161
mockInputs["working-directory"] = "/workspace";
155-
const { venvPath } = await import("../../src/utils/inputs");
162+
const { venvPath } = await importInputsModule();
156163
expect(venvPath).toBe("/workspace/.venv");
157164
});
158165

159166
it("resolves a relative venv-path", async () => {
160167
mockInputs["working-directory"] = "/workspace";
161168
mockInputs["activate-environment"] = "true";
162169
mockInputs["venv-path"] = "custom-venv";
163-
const { venvPath } = await import("../../src/utils/inputs");
170+
const { venvPath } = await importInputsModule();
164171
expect(venvPath).toBe("/workspace/custom-venv");
165172
});
166173

167174
it("normalizes venv-path with trailing slash", async () => {
168175
mockInputs["working-directory"] = "/workspace";
169176
mockInputs["activate-environment"] = "true";
170177
mockInputs["venv-path"] = "custom-venv/";
171-
const { venvPath } = await import("../../src/utils/inputs");
178+
const { venvPath } = await importInputsModule();
172179
expect(venvPath).toBe("/workspace/custom-venv");
173180
});
174181

175182
it("keeps an absolute venv-path unchanged", async () => {
176183
mockInputs["working-directory"] = "/workspace";
177184
mockInputs["activate-environment"] = "true";
178185
mockInputs["venv-path"] = "/tmp/custom-venv";
179-
const { venvPath } = await import("../../src/utils/inputs");
186+
const { venvPath } = await importInputsModule();
180187
expect(venvPath).toBe("/tmp/custom-venv");
181188
});
182189

183190
it("expands tilde in venv-path", async () => {
184191
mockInputs["working-directory"] = "/workspace";
185192
mockInputs["activate-environment"] = "true";
186193
mockInputs["venv-path"] = "~/.venv";
187-
const { venvPath } = await import("../../src/utils/inputs");
194+
const { venvPath } = await importInputsModule();
188195
expect(venvPath).toBe("/home/testuser/.venv");
189196
});
190197

191198
it("warns when venv-path is set but activate-environment is false", async () => {
192199
mockInputs["working-directory"] = "/workspace";
193200
mockInputs["venv-path"] = "custom-venv";
194201

195-
const { activateEnvironment, venvPath } = await import(
196-
"../../src/utils/inputs"
197-
);
202+
const { activateEnvironment, venvPath } = await importInputsModule();
198203

199204
expect(activateEnvironment).toBe(false);
200205
expect(venvPath).toBe("/workspace/custom-venv");
201-
202-
const mockedCore = jest.requireMock("@actions/core") as {
203-
warning: jest.Mock;
204-
};
205-
206-
expect(mockedCore.warning).toHaveBeenCalledWith(
206+
expect(mockWarning).toHaveBeenCalledWith(
207207
"venv-path is only used when activate-environment is true",
208208
);
209209
});

0 commit comments

Comments
 (0)