apify-test-tools 0.8.3 → 0.8.5-beta.0
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.
- package/.github/scripts/before-beta-release.js +3 -1
- package/.github/workflows/manual_publish_to_npm.yaml +3 -1
- package/CHANGELOG.md +12 -0
- package/README.md +21 -0
- package/bin/build-from-local.ts +204 -0
- package/bin/build.ts +67 -21
- package/bin/main.ts +26 -0
- package/bin/utils.ts +64 -0
- package/dist/bin/build-from-local.d.ts +14 -0
- package/dist/bin/build-from-local.d.ts.map +1 -0
- package/dist/bin/build-from-local.js +153 -0
- package/dist/bin/build-from-local.js.map +1 -0
- package/dist/bin/build.d.ts +25 -1
- package/dist/bin/build.d.ts.map +1 -1
- package/dist/bin/build.js +54 -17
- package/dist/bin/build.js.map +1 -1
- package/dist/bin/main.js +20 -0
- package/dist/bin/main.js.map +1 -1
- package/dist/bin/utils.d.ts +18 -0
- package/dist/bin/utils.d.ts.map +1 -1
- package/dist/bin/utils.js +51 -0
- package/dist/bin/utils.js.map +1 -1
- package/dist/test/unit/bin/build-from-local.test.d.ts +2 -0
- package/dist/test/unit/bin/build-from-local.test.d.ts.map +1 -0
- package/dist/test/unit/bin/build-from-local.test.js +152 -0
- package/dist/test/unit/bin/build-from-local.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +61 -61
- package/test/unit/bin/build-from-local.test.ts +233 -0
package/package.json
CHANGED
|
@@ -1,63 +1,63 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
2
|
+
"name": "apify-test-tools",
|
|
3
|
+
"version": "0.8.5-beta.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "TBD",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/apify/apify-test-tools.git"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18.0.0"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"apify-test-tools": "dist/bin/main.js"
|
|
15
|
+
},
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"main": "./dist/lib/lib.js",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@apify/consts": "^2.43.0",
|
|
20
|
+
"@slack/web-api": "^7.9.2",
|
|
21
|
+
"apify-client": "^2.22.2",
|
|
22
|
+
"yargs": "^18.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@apify/eslint-config": "^1.0.0",
|
|
26
|
+
"@apify/tsconfig": "^0.1.1",
|
|
27
|
+
"@types/node": "^24.0.0",
|
|
28
|
+
"@types/yargs": "^17.0.33",
|
|
29
|
+
"eslint": "^9.29.0",
|
|
30
|
+
"eslint-config-prettier": "^10.1.5",
|
|
31
|
+
"globals": "^17.0.0",
|
|
32
|
+
"prettier": "^3.5.3",
|
|
33
|
+
"tsx": "^4.20.3",
|
|
34
|
+
"typescript": "^5.9.3",
|
|
35
|
+
"typescript-eslint": "^8.34.1",
|
|
36
|
+
"vitest": "^3.2.4",
|
|
37
|
+
"knip": "^5.65.0",
|
|
38
|
+
"husky": "^9.0.11",
|
|
39
|
+
"lint-staged": "^15.2.2"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"vitest": ">=3.2.4"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"start": "npm run dist/bin/main.js",
|
|
46
|
+
"start:dev": "GITHUB_WORKSPACE=local-clone tsx bin/main.ts",
|
|
47
|
+
"build": "tsc && chmod +x ./dist/bin/main.js",
|
|
48
|
+
"lint": "eslint",
|
|
49
|
+
"lint:fix": "eslint --fix",
|
|
50
|
+
"format": "prettier --write .",
|
|
51
|
+
"format:check": "prettier --check .",
|
|
52
|
+
"type-check": "tsc --noEmit",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"check-unused": "npx knip",
|
|
55
|
+
"prepare": "husky .husky || true"
|
|
56
|
+
},
|
|
57
|
+
"lint-staged": {
|
|
58
|
+
"*.js": "eslint",
|
|
59
|
+
"*.ts": "eslint"
|
|
60
|
+
},
|
|
61
|
+
"author": "oklinov",
|
|
62
|
+
"license": "ISC"
|
|
63
63
|
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import type * as ChildProcessModule from 'node:child_process';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
10
|
+
// @ts-ignore: editor-only TS6059 — test/tsconfig.json's rootDir doesn't span bin/, but the root
|
|
11
|
+
// tsconfig (used for the real build and for eslint's type-aware linting) has no such restriction.
|
|
12
|
+
import {
|
|
13
|
+
collectNonIgnoredFiles,
|
|
14
|
+
flattenMonorepoContext,
|
|
15
|
+
rewriteActorJsonPaths,
|
|
16
|
+
} from '../../../bin/build-from-local.js';
|
|
17
|
+
import * as Utils from '../../../bin/utils.js';
|
|
18
|
+
|
|
19
|
+
// Defaults to the real spawnSync so `git init`/`git ls-files` calls made by the code under test
|
|
20
|
+
// (and by test setup below) actually run — individual tests override this via mockReturnValue
|
|
21
|
+
// where they need to fake git's output, and vi.restoreAllMocks() reverts back to this passthrough.
|
|
22
|
+
vi.mock('node:child_process', async (importOriginal) => {
|
|
23
|
+
const actual = await importOriginal<typeof ChildProcessModule>();
|
|
24
|
+
return { ...actual, spawnSync: vi.fn(actual.spawnSync) };
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const mkTempDir = async (prefix: string) => fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
28
|
+
|
|
29
|
+
const initGitRepo = (dir: string) => {
|
|
30
|
+
spawnSync('git', ['init', '-q'], { cwd: dir });
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe('build-from-local helpers', () => {
|
|
34
|
+
const tempDirs: string[] = [];
|
|
35
|
+
|
|
36
|
+
afterEach(async () => {
|
|
37
|
+
vi.restoreAllMocks();
|
|
38
|
+
await Promise.all(tempDirs.splice(0).map(async (dir) => fs.rm(dir, { recursive: true, force: true })));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('collectNonIgnoredFiles', () => {
|
|
42
|
+
it('drops secret-pattern files and gitignored files, keeps everything else', async () => {
|
|
43
|
+
const rootDir = await mkTempDir('apify-test-tools-collect-');
|
|
44
|
+
tempDirs.push(rootDir);
|
|
45
|
+
initGitRepo(rootDir);
|
|
46
|
+
|
|
47
|
+
await fs.writeFile(path.join(rootDir, 'main.js'), 'console.log(1)');
|
|
48
|
+
await fs.writeFile(path.join(rootDir, '.env'), 'SECRET=1');
|
|
49
|
+
await fs.mkdir(path.join(rootDir, 'sub'));
|
|
50
|
+
await fs.writeFile(path.join(rootDir, 'sub', 'ignored.log'), 'log');
|
|
51
|
+
|
|
52
|
+
// Only the gitignore side is mocked — isSecretFile runs for real, so this also
|
|
53
|
+
// proves the secret-pattern backstop applies independently of .gitignore.
|
|
54
|
+
vi.spyOn(Utils, 'getGitignoredPaths').mockImplementation(
|
|
55
|
+
(relativePaths) => new Set(relativePaths.filter((p) => p.endsWith('.log'))),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const result = collectNonIgnoredFiles(rootDir, rootDir);
|
|
59
|
+
|
|
60
|
+
expect(result).toStrictEqual([path.join(rootDir, 'main.js')]);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('flattenMonorepoContext', () => {
|
|
65
|
+
it('filters the .actor/ overlay through the same secret-pattern check as the rest of the context', async () => {
|
|
66
|
+
const repoRoot = await mkTempDir('apify-test-tools-repo-');
|
|
67
|
+
tempDirs.push(repoRoot);
|
|
68
|
+
initGitRepo(repoRoot);
|
|
69
|
+
|
|
70
|
+
const absActorDir = path.join(repoRoot, 'actors', 'owner_actor');
|
|
71
|
+
await fs.mkdir(path.join(absActorDir, '.actor'), { recursive: true });
|
|
72
|
+
await fs.writeFile(
|
|
73
|
+
path.join(absActorDir, '.actor', 'actor.json'),
|
|
74
|
+
JSON.stringify({ actorSpecification: 1, name: 'actor' }),
|
|
75
|
+
);
|
|
76
|
+
// A stray secret file inside .actor/ must never survive into the build.
|
|
77
|
+
await fs.writeFile(path.join(absActorDir, '.actor', '.env'), 'SECRET=leaked');
|
|
78
|
+
await fs.writeFile(path.join(absActorDir, '.actor', 'INPUT_SCHEMA.json'), '{}');
|
|
79
|
+
|
|
80
|
+
const keptContextFile = path.join(repoRoot, 'package.json');
|
|
81
|
+
await fs.writeFile(keptContextFile, '{}');
|
|
82
|
+
|
|
83
|
+
// Nothing is gitignored here — isolates the assertion to the secret-pattern filter.
|
|
84
|
+
vi.spyOn(Utils, 'getGitignoredPaths').mockReturnValue(new Set());
|
|
85
|
+
|
|
86
|
+
const actorJson = JSON.parse(
|
|
87
|
+
await fs.readFile(path.join(absActorDir, '.actor', 'actor.json'), 'utf8'),
|
|
88
|
+
) as Record<string, unknown>;
|
|
89
|
+
|
|
90
|
+
const { tempDir: flattenedDir, filePaths } = await flattenMonorepoContext(
|
|
91
|
+
'test/actor',
|
|
92
|
+
absActorDir,
|
|
93
|
+
repoRoot,
|
|
94
|
+
actorJson,
|
|
95
|
+
[keptContextFile],
|
|
96
|
+
repoRoot,
|
|
97
|
+
);
|
|
98
|
+
tempDirs.push(flattenedDir);
|
|
99
|
+
|
|
100
|
+
await expect(fs.access(path.join(flattenedDir, '.actor', '.env'))).rejects.toThrow();
|
|
101
|
+
await expect(fs.access(path.join(flattenedDir, '.actor', 'INPUT_SCHEMA.json'))).resolves.toBeUndefined();
|
|
102
|
+
await expect(fs.access(path.join(flattenedDir, '.actor', 'actor.json'))).resolves.toBeUndefined();
|
|
103
|
+
await expect(fs.access(path.join(flattenedDir, 'package.json'))).resolves.toBeUndefined();
|
|
104
|
+
|
|
105
|
+
// The returned filePaths must match what actually landed on disk — no .env, everything else present.
|
|
106
|
+
expect(new Set(filePaths)).toStrictEqual(
|
|
107
|
+
new Set([
|
|
108
|
+
path.join(flattenedDir, 'package.json'),
|
|
109
|
+
path.join(flattenedDir, '.actor', 'INPUT_SCHEMA.json'),
|
|
110
|
+
path.join(flattenedDir, '.actor', 'actor.json'),
|
|
111
|
+
]),
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("keeps another actor's .actor/ directory intact at its original nested path", async () => {
|
|
116
|
+
const repoRoot = await mkTempDir('apify-test-tools-repo-');
|
|
117
|
+
tempDirs.push(repoRoot);
|
|
118
|
+
initGitRepo(repoRoot);
|
|
119
|
+
|
|
120
|
+
const absActorDir = path.join(repoRoot, 'actors', 'owner_actor');
|
|
121
|
+
await fs.mkdir(path.join(absActorDir, '.actor'), { recursive: true });
|
|
122
|
+
await fs.writeFile(
|
|
123
|
+
path.join(absActorDir, '.actor', 'actor.json'),
|
|
124
|
+
JSON.stringify({ actorSpecification: 1, name: 'actor' }),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const otherActorDir = path.join(repoRoot, 'actors', 'owner_other-actor');
|
|
128
|
+
await fs.mkdir(path.join(otherActorDir, '.actor'), { recursive: true });
|
|
129
|
+
const otherActorSchemaFile = path.join(otherActorDir, '.actor', 'input_schema.json');
|
|
130
|
+
await fs.writeFile(otherActorSchemaFile, JSON.stringify({ schema: 'other' }));
|
|
131
|
+
|
|
132
|
+
vi.spyOn(Utils, 'getGitignoredPaths').mockReturnValue(new Set());
|
|
133
|
+
|
|
134
|
+
const actorJson = JSON.parse(
|
|
135
|
+
await fs.readFile(path.join(absActorDir, '.actor', 'actor.json'), 'utf8'),
|
|
136
|
+
) as Record<string, unknown>;
|
|
137
|
+
|
|
138
|
+
const { tempDir: flattenedDir, filePaths } = await flattenMonorepoContext(
|
|
139
|
+
'test/actor',
|
|
140
|
+
absActorDir,
|
|
141
|
+
repoRoot,
|
|
142
|
+
actorJson,
|
|
143
|
+
[otherActorSchemaFile],
|
|
144
|
+
repoRoot,
|
|
145
|
+
);
|
|
146
|
+
tempDirs.push(flattenedDir);
|
|
147
|
+
|
|
148
|
+
const preservedPath = path.join(flattenedDir, 'actors', 'owner_other-actor', '.actor', 'input_schema.json');
|
|
149
|
+
await expect(fs.access(preservedPath)).resolves.toBeUndefined();
|
|
150
|
+
expect(filePaths).toContain(preservedPath);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('rewriteActorJsonPaths', () => {
|
|
155
|
+
it('rewrites path fields that escape .actor/ relative to the new flattened location', async () => {
|
|
156
|
+
const repoRoot = await mkTempDir('apify-test-tools-rewrite-');
|
|
157
|
+
tempDirs.push(repoRoot);
|
|
158
|
+
const flattenedDir = await mkTempDir('apify-test-tools-flattened-');
|
|
159
|
+
tempDirs.push(flattenedDir);
|
|
160
|
+
|
|
161
|
+
const absActorDir = path.join(repoRoot, 'actors', 'owner_actor');
|
|
162
|
+
await fs.mkdir(path.join(flattenedDir, '.actor'), { recursive: true });
|
|
163
|
+
|
|
164
|
+
const actorJson = {
|
|
165
|
+
actorSpecification: 1,
|
|
166
|
+
name: 'my-actor',
|
|
167
|
+
dockerfile: '../../../Dockerfile', // repo root, outside .actor/
|
|
168
|
+
dockerContextDir: '../../..', // repo root itself
|
|
169
|
+
changelog: './CHANGELOG.md', // stays inside .actor/
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
await rewriteActorJsonPaths(absActorDir, repoRoot, flattenedDir, actorJson);
|
|
173
|
+
|
|
174
|
+
const rewritten = JSON.parse(
|
|
175
|
+
await fs.readFile(path.join(flattenedDir, '.actor', 'actor.json'), 'utf8'),
|
|
176
|
+
) as Record<string, string>;
|
|
177
|
+
|
|
178
|
+
expect(rewritten.dockerfile).toBe('../Dockerfile');
|
|
179
|
+
expect(rewritten.dockerContextDir).toBe('..');
|
|
180
|
+
expect(rewritten.changelog).toBe('./CHANGELOG.md');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('getGitignoredPaths', () => {
|
|
186
|
+
beforeEach(() => {
|
|
187
|
+
vi.mocked(spawnSync).mockReset();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('returns an empty set without calling git when given no paths', () => {
|
|
191
|
+
const result = Utils.getGitignoredPaths([]);
|
|
192
|
+
|
|
193
|
+
expect(result).toStrictEqual(new Set());
|
|
194
|
+
expect(spawnSync).not.toHaveBeenCalled();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('returns the paths git reports as ignored, feeding all candidates via stdin', () => {
|
|
198
|
+
vi.mocked(spawnSync).mockReturnValue({
|
|
199
|
+
status: 0,
|
|
200
|
+
stdout: 'node_modules/foo.js\n.env\n',
|
|
201
|
+
stderr: '',
|
|
202
|
+
} as unknown as ReturnType<typeof spawnSync>);
|
|
203
|
+
|
|
204
|
+
const result = Utils.getGitignoredPaths(['node_modules/foo.js', 'bin/build.ts', '.env']);
|
|
205
|
+
|
|
206
|
+
expect(result).toStrictEqual(new Set(['node_modules/foo.js', '.env']));
|
|
207
|
+
expect(spawnSync).toHaveBeenCalledWith(
|
|
208
|
+
'git',
|
|
209
|
+
['check-ignore', '--stdin'],
|
|
210
|
+
expect.objectContaining({ input: 'node_modules/foo.js\nbin/build.ts\n.env' }),
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('returns an empty set when git reports nothing is ignored (exit code 1)', () => {
|
|
215
|
+
vi.mocked(spawnSync).mockReturnValue({
|
|
216
|
+
status: 1,
|
|
217
|
+
stdout: '',
|
|
218
|
+
stderr: '',
|
|
219
|
+
} as unknown as ReturnType<typeof spawnSync>);
|
|
220
|
+
|
|
221
|
+
expect(Utils.getGitignoredPaths(['bin/build.ts'])).toStrictEqual(new Set());
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('throws on an unexpected git failure instead of silently including/excluding files', () => {
|
|
225
|
+
vi.mocked(spawnSync).mockReturnValue({
|
|
226
|
+
status: 128,
|
|
227
|
+
stdout: '',
|
|
228
|
+
stderr: 'fatal: not a git repository',
|
|
229
|
+
} as unknown as ReturnType<typeof spawnSync>);
|
|
230
|
+
|
|
231
|
+
expect(() => Utils.getGitignoredPaths(['bin/build.ts'])).toThrow('git check-ignore');
|
|
232
|
+
});
|
|
233
|
+
});
|