@outputai/core 0.7.1-next.de30052.0 → 0.7.1-next.ed233ce.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/bin/worker.sh +6 -0
- package/package.json +1 -1
- package/src/consts.js +0 -4
- package/src/errors.js +6 -2
- package/src/interface/evaluator.js +7 -20
- package/src/interface/evaluator.spec.js +117 -1
- package/src/interface/step.js +8 -9
- package/src/interface/step.spec.js +124 -0
- package/src/interface/validations/index.js +108 -0
- package/src/interface/validations/index.spec.js +182 -0
- package/src/interface/validations/schemas.js +113 -0
- package/src/interface/validations/schemas.spec.js +209 -0
- package/src/interface/webhook.js +1 -1
- package/src/interface/webhook.spec.js +1 -1
- package/src/interface/workflow.d.ts +10 -9
- package/src/interface/workflow.js +76 -164
- package/src/interface/workflow.spec.js +637 -521
- package/src/interface/workflow_activity_options.js +16 -0
- package/src/interface/workflow_utils.js +1 -1
- package/src/interface/zod_integration.spec.js +2 -2
- package/src/internal_utils/aggregations.js +0 -10
- package/src/internal_utils/aggregations.spec.js +1 -48
- package/src/internal_utils/errors.js +14 -8
- package/src/internal_utils/errors.spec.js +73 -27
- package/src/utils/index.d.ts +19 -0
- package/src/utils/utils.js +53 -0
- package/src/utils/utils.spec.js +105 -1
- package/src/worker/bundle.js +26 -0
- package/src/worker/bundle.spec.js +53 -0
- package/src/worker/bundler_options.js +1 -1
- package/src/worker/bundler_options.spec.js +1 -1
- package/src/worker/catalog_workflow/catalog_job.js +148 -0
- package/src/worker/catalog_workflow/catalog_job.spec.js +232 -0
- package/src/worker/check.js +24 -0
- package/src/worker/connection_monitor.js +112 -0
- package/src/worker/connection_monitor.spec.js +199 -0
- package/src/worker/index.js +146 -41
- package/src/worker/index.spec.js +281 -109
- package/src/worker/interceptors/activity.js +7 -24
- package/src/worker/interceptors/activity.spec.js +97 -66
- package/src/worker/interceptors/index.js +4 -7
- package/src/worker/interceptors/modules.js +15 -0
- package/src/worker/interceptors/workflow.js +6 -8
- package/src/worker/interceptors/workflow.spec.js +49 -42
- package/src/worker/interruption.js +33 -0
- package/src/worker/interruption.spec.js +86 -0
- package/src/worker/loader/activities.js +75 -0
- package/src/worker/loader/activities.spec.js +213 -0
- package/src/worker/loader/hooks.js +28 -0
- package/src/worker/loader/hooks.spec.js +64 -0
- package/src/worker/loader/matchers.js +46 -0
- package/src/worker/loader/matchers.spec.js +140 -0
- package/src/worker/{loader_tools.js → loader/tools.js} +19 -67
- package/src/worker/{loader_tools.spec.js → loader/tools.spec.js} +53 -85
- package/src/worker/loader/workflows.js +82 -0
- package/src/worker/loader/workflows.spec.js +256 -0
- package/src/worker/{setup_telemetry.js → telemetry.js} +9 -4
- package/src/worker/{setup_telemetry.spec.js → telemetry.spec.js} +3 -3
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +5 -109
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +31 -103
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +5 -6
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +11 -83
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +8 -11
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +9 -9
- package/src/interface/validations/runtime.js +0 -20
- package/src/interface/validations/runtime.spec.js +0 -29
- package/src/interface/validations/schema_utils.js +0 -8
- package/src/interface/validations/schema_utils.spec.js +0 -67
- package/src/interface/validations/static.js +0 -137
- package/src/interface/validations/static.spec.js +0 -397
- package/src/interface/workflow.replay_compatibility.spec.js +0 -254
- package/src/worker/loader.js +0 -202
- package/src/worker/loader.spec.js +0 -498
- package/src/worker/shutdown.js +0 -26
- package/src/worker/shutdown.spec.js +0 -82
- package/src/worker/start_catalog.js +0 -96
- package/src/worker/start_catalog.spec.js +0 -179
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock( '#consts', () => ( {
|
|
4
|
+
ACTIVITY_SEND_HTTP_REQUEST: '__internal#sendHttpRequest',
|
|
5
|
+
ACTIVITY_GET_TRACE_DESTINATIONS: '__internal#getTraceDestinations',
|
|
6
|
+
WORKFLOWS_INDEX_FILENAME: '__workflows_entrypoint.js',
|
|
7
|
+
WORKFLOW_CATALOG: 'catalog',
|
|
8
|
+
ACTIVITY_OPTIONS_FILENAME: '__activity_options.js',
|
|
9
|
+
SHARED_STEP_PREFIX: '$shared'
|
|
10
|
+
} ) );
|
|
11
|
+
|
|
12
|
+
const sendHttpRequestMock = vi.fn();
|
|
13
|
+
const getTraceDestinationsMock = vi.fn();
|
|
14
|
+
vi.mock( '#internal_activities', () => ( {
|
|
15
|
+
sendHttpRequest: sendHttpRequestMock,
|
|
16
|
+
getTraceDestinations: getTraceDestinationsMock
|
|
17
|
+
} ) );
|
|
18
|
+
|
|
19
|
+
const { importComponentsMock, findSharedActivitiesFromWorkflowsMock, matchFilesMock, writeFileInTempDirMock } = vi.hoisted( () => ( {
|
|
20
|
+
importComponentsMock: vi.fn(),
|
|
21
|
+
findSharedActivitiesFromWorkflowsMock: vi.fn(),
|
|
22
|
+
matchFilesMock: vi.fn(),
|
|
23
|
+
writeFileInTempDirMock: vi.fn()
|
|
24
|
+
} ) );
|
|
25
|
+
|
|
26
|
+
const { buildActivityMatcherMock, activityMatcherMock, sharedStepsDirMock, sharedEvaluatorsDirMock } = vi.hoisted( () => ( {
|
|
27
|
+
buildActivityMatcherMock: vi.fn(),
|
|
28
|
+
activityMatcherMock: vi.fn(),
|
|
29
|
+
sharedStepsDirMock: vi.fn(),
|
|
30
|
+
sharedEvaluatorsDirMock: vi.fn()
|
|
31
|
+
} ) );
|
|
32
|
+
|
|
33
|
+
vi.mock( './tools.js', () => ( {
|
|
34
|
+
importComponents: importComponentsMock,
|
|
35
|
+
findSharedActivitiesFromWorkflows: findSharedActivitiesFromWorkflowsMock,
|
|
36
|
+
matchFiles: matchFilesMock,
|
|
37
|
+
writeFileInTempDir: writeFileInTempDirMock
|
|
38
|
+
} ) );
|
|
39
|
+
|
|
40
|
+
vi.mock( './matchers.js', () => ( {
|
|
41
|
+
buildActivityMatcher: buildActivityMatcherMock,
|
|
42
|
+
staticMatchers: {
|
|
43
|
+
sharedStepsDir: sharedStepsDirMock,
|
|
44
|
+
sharedEvaluatorsDir: sharedEvaluatorsDirMock
|
|
45
|
+
}
|
|
46
|
+
} ) );
|
|
47
|
+
|
|
48
|
+
describe( 'loadActivities', () => {
|
|
49
|
+
beforeEach( () => {
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
importComponentsMock.mockReset();
|
|
52
|
+
importComponentsMock.mockImplementation( async function *() {} );
|
|
53
|
+
findSharedActivitiesFromWorkflowsMock.mockReset();
|
|
54
|
+
findSharedActivitiesFromWorkflowsMock.mockReturnValue( [] );
|
|
55
|
+
matchFilesMock.mockReset();
|
|
56
|
+
matchFilesMock.mockReturnValue( [] );
|
|
57
|
+
writeFileInTempDirMock.mockReset();
|
|
58
|
+
writeFileInTempDirMock.mockReturnValue( '/tmp/__activity_options.js' );
|
|
59
|
+
buildActivityMatcherMock.mockReset();
|
|
60
|
+
buildActivityMatcherMock.mockReturnValue( activityMatcherMock );
|
|
61
|
+
} );
|
|
62
|
+
|
|
63
|
+
it( 'loadActivities returns map including system activity and writes options file', async () => {
|
|
64
|
+
const { loadActivities } = await import( './activities.js' );
|
|
65
|
+
|
|
66
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
67
|
+
yield {
|
|
68
|
+
fn: () => {},
|
|
69
|
+
metadata: { name: 'Act1', options: { activityOptions: { retry: { maximumAttempts: 3 } } } },
|
|
70
|
+
path: '/a/steps.js'
|
|
71
|
+
};
|
|
72
|
+
} );
|
|
73
|
+
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
74
|
+
|
|
75
|
+
const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
|
|
76
|
+
const { activities, optionsFile } = await loadActivities( '/root', workflows );
|
|
77
|
+
expect( activities['A#Act1'] ).toBeTypeOf( 'function' );
|
|
78
|
+
expect( activities['__internal#sendHttpRequest'] ).toBe( sendHttpRequestMock );
|
|
79
|
+
expect( optionsFile ).toBe( '/tmp/__activity_options.js' );
|
|
80
|
+
|
|
81
|
+
expect( writeFileInTempDirMock ).toHaveBeenCalledTimes( 1 );
|
|
82
|
+
const [ contents, filename ] = writeFileInTempDirMock.mock.calls[0];
|
|
83
|
+
expect( filename ).toBe( '__activity_options.js' );
|
|
84
|
+
expect( contents ).toContain( 'export default' );
|
|
85
|
+
expect( JSON.parse( contents.replace( /^export default\s*/, '' ).replace( /;\s*$/, '' ) ) ).toEqual( {
|
|
86
|
+
'A#Act1': { retry: { maximumAttempts: 3 } }
|
|
87
|
+
} );
|
|
88
|
+
} );
|
|
89
|
+
|
|
90
|
+
it( 'loadActivities omits activity options when component has no options or no activityOptions', async () => {
|
|
91
|
+
const { loadActivities } = await import( './activities.js' );
|
|
92
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
93
|
+
yield { fn: () => {}, metadata: { name: 'NoOptions' }, path: '/a/steps.js' };
|
|
94
|
+
yield { fn: () => {}, metadata: { name: 'EmptyOptions', options: {} }, path: '/a/steps2.js' };
|
|
95
|
+
} );
|
|
96
|
+
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
97
|
+
|
|
98
|
+
await loadActivities( '/root', [ { name: 'A', path: '/a/workflow.js' } ] );
|
|
99
|
+
const written = JSON.parse(
|
|
100
|
+
writeFileInTempDirMock.mock.calls[0][0].replace( /^export default\s*/, '' ).replace( /;\s*$/, '' )
|
|
101
|
+
);
|
|
102
|
+
expect( written['A#NoOptions'] ).toBeUndefined();
|
|
103
|
+
expect( written['A#EmptyOptions'] ).toBeUndefined();
|
|
104
|
+
} );
|
|
105
|
+
|
|
106
|
+
it( 'loadActivities throws when two activities in the same workflow share a name', async () => {
|
|
107
|
+
const { loadActivities } = await import( './activities.js' );
|
|
108
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
109
|
+
yield { fn: () => {}, metadata: { name: 'DuplicateActivity' }, path: '/a/steps.js' };
|
|
110
|
+
yield { fn: () => {}, metadata: { name: 'DuplicateActivity' }, path: '/a/evaluators.js' };
|
|
111
|
+
} );
|
|
112
|
+
|
|
113
|
+
await expect( loadActivities( '/root', [ { name: 'A', path: '/a/workflow.js' } ] ) ).rejects.toThrow(
|
|
114
|
+
'Activity "DuplicateActivity" in workflow "A" conflicts with another activity in the same workflow. \
|
|
115
|
+
Activity names must be unique within a workflow.'
|
|
116
|
+
);
|
|
117
|
+
} );
|
|
118
|
+
|
|
119
|
+
it( 'loadActivities throws when two shared activities share a name', async () => {
|
|
120
|
+
const { loadActivities } = await import( './activities.js' );
|
|
121
|
+
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
122
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
123
|
+
yield { fn: () => {}, metadata: { name: 'DuplicateShared' }, path: '/root/shared/steps/a.js' };
|
|
124
|
+
yield { fn: () => {}, metadata: { name: 'DuplicateShared' }, path: '/root/shared/evaluators/a.js' };
|
|
125
|
+
} );
|
|
126
|
+
|
|
127
|
+
await expect( loadActivities( '/root', [ { name: 'A', path: '/a/workflow.js' } ] ) ).rejects.toThrow(
|
|
128
|
+
'Shared activity "DuplicateShared" conflicts with another shared activity. Shared activity names must be unique.'
|
|
129
|
+
);
|
|
130
|
+
} );
|
|
131
|
+
|
|
132
|
+
it( 'loadActivities uses activity and shared matchers for workflow and shared scans', async () => {
|
|
133
|
+
const { loadActivities } = await import( './activities.js' );
|
|
134
|
+
const workflowFiles = [ { path: '/a/steps/foo.js' } ];
|
|
135
|
+
const sharedFiles = [ { path: '/root/shared/steps/baz.js' } ];
|
|
136
|
+
matchFilesMock.mockReturnValueOnce( workflowFiles );
|
|
137
|
+
matchFilesMock.mockReturnValueOnce( sharedFiles );
|
|
138
|
+
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
139
|
+
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
140
|
+
|
|
141
|
+
const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
|
|
142
|
+
await loadActivities( '/root', workflows );
|
|
143
|
+
|
|
144
|
+
expect( matchFilesMock ).toHaveBeenCalledTimes( 2 );
|
|
145
|
+
expect( buildActivityMatcherMock ).toHaveBeenCalledWith( '/a' );
|
|
146
|
+
expect( matchFilesMock ).toHaveBeenNthCalledWith( 1, '/a', [ activityMatcherMock ] );
|
|
147
|
+
expect( matchFilesMock ).toHaveBeenNthCalledWith( 2, '/root', [ sharedStepsDirMock, sharedEvaluatorsDirMock ] );
|
|
148
|
+
|
|
149
|
+
expect( importComponentsMock ).toHaveBeenCalledTimes( 2 );
|
|
150
|
+
expect( importComponentsMock ).toHaveBeenNthCalledWith( 1, workflowFiles );
|
|
151
|
+
expect( importComponentsMock ).toHaveBeenNthCalledWith( 2, sharedFiles );
|
|
152
|
+
} );
|
|
153
|
+
|
|
154
|
+
it( 'loads shared activities from external workflow packages', async () => {
|
|
155
|
+
const { loadActivities } = await import( './activities.js' );
|
|
156
|
+
const externalSharedFiles = [ { path: '/root/node_modules/pkg/shared/steps/prepare.js' } ];
|
|
157
|
+
const localWorkflow = { name: 'Local', path: '/root/workflows/local/workflow.js' };
|
|
158
|
+
const externalWorkflow = { name: 'External', path: '/root/node_modules/pkg/workflows/a/workflow.js', external: true };
|
|
159
|
+
findSharedActivitiesFromWorkflowsMock.mockReturnValue( externalSharedFiles );
|
|
160
|
+
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
161
|
+
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
162
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
163
|
+
yield {
|
|
164
|
+
fn: () => {},
|
|
165
|
+
metadata: { name: 'ExternalShared', options: { activityOptions: { retry: { maximumAttempts: 2 } } } },
|
|
166
|
+
path: '/root/node_modules/pkg/shared/steps/prepare.js'
|
|
167
|
+
};
|
|
168
|
+
} );
|
|
169
|
+
|
|
170
|
+
const { activities } = await loadActivities( '/root', [ localWorkflow, externalWorkflow ] );
|
|
171
|
+
|
|
172
|
+
expect( findSharedActivitiesFromWorkflowsMock ).toHaveBeenCalledWith( [ externalWorkflow ] );
|
|
173
|
+
expect( importComponentsMock ).toHaveBeenNthCalledWith( 3, externalSharedFiles );
|
|
174
|
+
expect( activities['$shared#ExternalShared'] ).toBeTypeOf( 'function' );
|
|
175
|
+
const written = JSON.parse(
|
|
176
|
+
writeFileInTempDirMock.mock.calls[0][0].replace( /^export default\s*/, '' ).replace( /;\s*$/, '' )
|
|
177
|
+
);
|
|
178
|
+
expect( written['$shared#ExternalShared'] ).toEqual( { retry: { maximumAttempts: 2 } } );
|
|
179
|
+
} );
|
|
180
|
+
|
|
181
|
+
it( 'loadActivities includes nested workflow steps and shared evaluators', async () => {
|
|
182
|
+
const { loadActivities } = await import( './activities.js' );
|
|
183
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
184
|
+
yield { fn: () => {}, metadata: { name: 'ActNested' }, path: '/a/steps/foo.js' };
|
|
185
|
+
} );
|
|
186
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
187
|
+
yield { fn: () => {}, metadata: { name: 'SharedEval' }, path: '/root/shared/evaluators/bar.js' };
|
|
188
|
+
} );
|
|
189
|
+
|
|
190
|
+
const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
|
|
191
|
+
const { activities } = await loadActivities( '/root', workflows );
|
|
192
|
+
expect( activities['A#ActNested'] ).toBeTypeOf( 'function' );
|
|
193
|
+
expect( activities['$shared#SharedEval'] ).toBeTypeOf( 'function' );
|
|
194
|
+
} );
|
|
195
|
+
|
|
196
|
+
it( 'collects shared nested steps and evaluators across multiple subfolders', async () => {
|
|
197
|
+
const { loadActivities } = await import( './activities.js' );
|
|
198
|
+
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
199
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
200
|
+
yield { fn: () => {}, metadata: { name: 'SharedStepPrimary' }, path: '/root/shared/steps/primary/a.js' };
|
|
201
|
+
yield { fn: () => {}, metadata: { name: 'SharedStepSecondary' }, path: '/root/shared/steps/secondary/b.js' };
|
|
202
|
+
yield { fn: () => {}, metadata: { name: 'SharedEvalPrimary' }, path: '/root/shared/evaluators/primary/c.js' };
|
|
203
|
+
yield { fn: () => {}, metadata: { name: 'SharedEvalSecondary' }, path: '/root/shared/evaluators/secondary/d.js' };
|
|
204
|
+
} );
|
|
205
|
+
|
|
206
|
+
const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
|
|
207
|
+
const { activities } = await loadActivities( '/root', workflows );
|
|
208
|
+
expect( activities['$shared#SharedStepPrimary'] ).toBeTypeOf( 'function' );
|
|
209
|
+
expect( activities['$shared#SharedStepSecondary'] ).toBeTypeOf( 'function' );
|
|
210
|
+
expect( activities['$shared#SharedEvalPrimary'] ).toBeTypeOf( 'function' );
|
|
211
|
+
expect( activities['$shared#SharedEvalSecondary'] ).toBeTypeOf( 'function' );
|
|
212
|
+
} );
|
|
213
|
+
} );
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { createChildLogger } from '#logger';
|
|
4
|
+
|
|
5
|
+
const log = createChildLogger( 'Hooks Loader' );
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Loads the hook files from package.json's "outputai" section.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} rootDir
|
|
11
|
+
* @returns {void}
|
|
12
|
+
*/
|
|
13
|
+
export async function loadHooks( rootDir ) {
|
|
14
|
+
const packageFile = join( rootDir, 'package.json' );
|
|
15
|
+
if ( existsSync( packageFile ) ) {
|
|
16
|
+
const pkg = await import( packageFile, { with: { type: 'json' } } );
|
|
17
|
+
const content = pkg.default;
|
|
18
|
+
const hooks = [];
|
|
19
|
+
// @DEPRECATED: "output" is the legacy namespace for configs, can be removed after couple version (this is being added in 0.3.x)
|
|
20
|
+
hooks.push( ...( content['output']?.hookFiles ?? [] ) );
|
|
21
|
+
hooks.push( ...( content['outputai']?.hookFiles ?? [] ) );
|
|
22
|
+
for ( const path of hooks ) {
|
|
23
|
+
const hookFile = join( rootDir, path );
|
|
24
|
+
await import( hookFile );
|
|
25
|
+
log.info( 'Hook file loaded', { path } );
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
const fsMocks = vi.hoisted( () => ( {
|
|
6
|
+
existsSync: vi.fn().mockReturnValue( false )
|
|
7
|
+
} ) );
|
|
8
|
+
vi.mock( 'node:fs', () => ( {
|
|
9
|
+
existsSync: fsMocks.existsSync
|
|
10
|
+
} ) );
|
|
11
|
+
|
|
12
|
+
describe( 'loadHooks', () => {
|
|
13
|
+
beforeEach( () => {
|
|
14
|
+
vi.clearAllMocks();
|
|
15
|
+
fsMocks.existsSync.mockReturnValue( false );
|
|
16
|
+
} );
|
|
17
|
+
|
|
18
|
+
it( 'resolves without importing when package.json does not exist', async () => {
|
|
19
|
+
fsMocks.existsSync.mockReturnValue( false );
|
|
20
|
+
const { loadHooks } = await import( './hooks.js' );
|
|
21
|
+
await expect( loadHooks( '/root' ) ).resolves.toBeUndefined();
|
|
22
|
+
expect( fsMocks.existsSync ).toHaveBeenCalledWith( join( '/root', 'package.json' ) );
|
|
23
|
+
} );
|
|
24
|
+
|
|
25
|
+
it( 'imports hook files listed in package.json outputai.hookFiles', async () => {
|
|
26
|
+
vi.doUnmock( 'node:fs' );
|
|
27
|
+
vi.resetModules();
|
|
28
|
+
const fs = await import( 'node:fs' );
|
|
29
|
+
const tmpDir = fs.mkdtempSync( join( tmpdir(), 'loader-spec-' ) );
|
|
30
|
+
try {
|
|
31
|
+
fs.writeFileSync( join( tmpDir, 'package.json' ), JSON.stringify( {
|
|
32
|
+
outputai: { hookFiles: [ 'hook.js' ] }
|
|
33
|
+
} ) );
|
|
34
|
+
fs.writeFileSync( join( tmpDir, 'hook.js' ), 'globalThis.__loadHooksTestLoaded = true;' );
|
|
35
|
+
|
|
36
|
+
const { loadHooks } = await import( './hooks.js' );
|
|
37
|
+
await loadHooks( tmpDir );
|
|
38
|
+
expect( globalThis.__loadHooksTestLoaded ).toBe( true );
|
|
39
|
+
} finally {
|
|
40
|
+
delete globalThis.__loadHooksTestLoaded;
|
|
41
|
+
fs.rmSync( tmpDir, { recursive: true, force: true } );
|
|
42
|
+
}
|
|
43
|
+
} );
|
|
44
|
+
|
|
45
|
+
it( 'imports hook files from legacy package.json output.hookFiles', async () => {
|
|
46
|
+
vi.doUnmock( 'node:fs' );
|
|
47
|
+
vi.resetModules();
|
|
48
|
+
const fs = await import( 'node:fs' );
|
|
49
|
+
const tmpDir = fs.mkdtempSync( join( tmpdir(), 'loader-spec-' ) );
|
|
50
|
+
try {
|
|
51
|
+
fs.writeFileSync( join( tmpDir, 'package.json' ), JSON.stringify( {
|
|
52
|
+
output: { hookFiles: [ 'legacy_hook.js' ] }
|
|
53
|
+
} ) );
|
|
54
|
+
fs.writeFileSync( join( tmpDir, 'legacy_hook.js' ), 'globalThis.__loadHooksLegacyTestLoaded = true;' );
|
|
55
|
+
|
|
56
|
+
const { loadHooks } = await import( './hooks.js' );
|
|
57
|
+
await loadHooks( tmpDir );
|
|
58
|
+
expect( globalThis.__loadHooksLegacyTestLoaded ).toBe( true );
|
|
59
|
+
} finally {
|
|
60
|
+
delete globalThis.__loadHooksLegacyTestLoaded;
|
|
61
|
+
fs.rmSync( tmpDir, { recursive: true, force: true } );
|
|
62
|
+
}
|
|
63
|
+
} );
|
|
64
|
+
} );
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { sep } from 'node:path';
|
|
2
|
+
import { rxEscape } from '#utils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a matcher function that based on "path", matches:
|
|
6
|
+
* - path/steps.js
|
|
7
|
+
* - path/evaluators.js
|
|
8
|
+
* - path/steps/*
|
|
9
|
+
* - path/evaluators/*
|
|
10
|
+
* @param {string} path
|
|
11
|
+
* @returns {function(string): boolean}
|
|
12
|
+
*/
|
|
13
|
+
export const buildActivityMatcher = path => {
|
|
14
|
+
const exp = new RegExp( `^${rxEscape( `${path}${sep}` )}(?:steps|evaluators)(?:\\.js$|${rxEscape( sep )})` );
|
|
15
|
+
return v => exp.test( v );
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Matchers that can be used to access conditions without initializing them
|
|
20
|
+
*/
|
|
21
|
+
export const staticMatchers = {
|
|
22
|
+
/**
|
|
23
|
+
* Matches a workflow.js file
|
|
24
|
+
* @param {string} path - Path to test
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
27
|
+
workflowFile: v => v.endsWith( `${sep}workflow.js` ),
|
|
28
|
+
/**
|
|
29
|
+
* Matches a workflow.js that is inside a shared folder: eg foo/shared/workflow.js
|
|
30
|
+
* @param {string} path - Path to test
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
workflowPathHasShared: v => v.endsWith( `${sep}shared${sep}workflow.js` ),
|
|
34
|
+
/**
|
|
35
|
+
* Matches the shared folder for steps src/shared/steps/../step_file.js
|
|
36
|
+
* @param {string} path - Path to test
|
|
37
|
+
* @returns {boolean}
|
|
38
|
+
*/
|
|
39
|
+
sharedStepsDir: v => v.includes( `${sep}shared${sep}steps${sep}` ) && v.endsWith( '.js' ),
|
|
40
|
+
/**
|
|
41
|
+
* Matches the shared folder for evaluators src/shared/evaluators/../evaluator_file.js
|
|
42
|
+
* @param {string} path - Path to test
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
sharedEvaluatorsDir: v => v.includes( `${sep}shared${sep}evaluators${sep}` ) && v.endsWith( '.js' )
|
|
46
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { sep } from 'node:path';
|
|
3
|
+
import { buildActivityMatcher, staticMatchers } from './matchers.js';
|
|
4
|
+
|
|
5
|
+
describe( 'buildActivityMatcher', () => {
|
|
6
|
+
const base = `${sep}app${sep}proj`;
|
|
7
|
+
|
|
8
|
+
it( 'matches steps.js at the base path', () => {
|
|
9
|
+
const matchActivity = buildActivityMatcher( base );
|
|
10
|
+
|
|
11
|
+
expect( matchActivity( `${base}${sep}steps.js` ) ).toBe( true );
|
|
12
|
+
} );
|
|
13
|
+
|
|
14
|
+
it( 'matches evaluators.js at the base path', () => {
|
|
15
|
+
const matchActivity = buildActivityMatcher( base );
|
|
16
|
+
|
|
17
|
+
expect( matchActivity( `${base}${sep}evaluators.js` ) ).toBe( true );
|
|
18
|
+
} );
|
|
19
|
+
|
|
20
|
+
it( 'matches files under the steps directory', () => {
|
|
21
|
+
const matchActivity = buildActivityMatcher( base );
|
|
22
|
+
|
|
23
|
+
expect( matchActivity( `${base}${sep}steps${sep}a.js` ) ).toBe( true );
|
|
24
|
+
expect( matchActivity( `${base}${sep}steps${sep}sub${sep}b.js` ) ).toBe( true );
|
|
25
|
+
} );
|
|
26
|
+
|
|
27
|
+
it( 'matches files under the evaluators directory', () => {
|
|
28
|
+
const matchActivity = buildActivityMatcher( base );
|
|
29
|
+
|
|
30
|
+
expect( matchActivity( `${base}${sep}evaluators${sep}x.js` ) ).toBe( true );
|
|
31
|
+
expect( matchActivity( `${base}${sep}evaluators${sep}y${sep}z.js` ) ).toBe( true );
|
|
32
|
+
} );
|
|
33
|
+
|
|
34
|
+
it( 'rejects activity filenames outside the base path', () => {
|
|
35
|
+
const matchActivity = buildActivityMatcher( base );
|
|
36
|
+
|
|
37
|
+
expect( matchActivity( `${base}${sep}nested${sep}steps.js` ) ).toBe( false );
|
|
38
|
+
expect( matchActivity( `${base}${sep}sub${sep}evaluators.js` ) ).toBe( false );
|
|
39
|
+
expect( matchActivity( `${sep}app${sep}other${sep}steps.js` ) ).toBe( false );
|
|
40
|
+
} );
|
|
41
|
+
|
|
42
|
+
it( 'rejects non-activity names at the base path', () => {
|
|
43
|
+
const matchActivity = buildActivityMatcher( base );
|
|
44
|
+
|
|
45
|
+
expect( matchActivity( `${base}${sep}workflow.js` ) ).toBe( false );
|
|
46
|
+
expect( matchActivity( `${base}${sep}shared${sep}steps${sep}a.js` ) ).toBe( false );
|
|
47
|
+
expect( matchActivity( `${base}${sep}other${sep}a.js` ) ).toBe( false );
|
|
48
|
+
} );
|
|
49
|
+
|
|
50
|
+
it( 'rejects similarly named files and directories', () => {
|
|
51
|
+
const matchActivity = buildActivityMatcher( base );
|
|
52
|
+
|
|
53
|
+
expect( matchActivity( `${base}${sep}stepsXjs` ) ).toBe( false );
|
|
54
|
+
expect( matchActivity( `${base}${sep}evaluatorsXjs` ) ).toBe( false );
|
|
55
|
+
expect( matchActivity( `${base}${sep}steps-extra.js` ) ).toBe( false );
|
|
56
|
+
expect( matchActivity( `${base}${sep}evaluators-extra.js` ) ).toBe( false );
|
|
57
|
+
expect( matchActivity( `${base}${sep}steps_extra${sep}a.js` ) ).toBe( false );
|
|
58
|
+
expect( matchActivity( `${base}${sep}evaluators_extra${sep}a.js` ) ).toBe( false );
|
|
59
|
+
} );
|
|
60
|
+
|
|
61
|
+
it( 'rejects directory names without a nested path', () => {
|
|
62
|
+
const matchActivity = buildActivityMatcher( base );
|
|
63
|
+
|
|
64
|
+
expect( matchActivity( `${base}${sep}steps` ) ).toBe( false );
|
|
65
|
+
expect( matchActivity( `${base}${sep}evaluators` ) ).toBe( false );
|
|
66
|
+
} );
|
|
67
|
+
|
|
68
|
+
it( 'rejects exact files when additional path segments follow', () => {
|
|
69
|
+
const matchActivity = buildActivityMatcher( base );
|
|
70
|
+
|
|
71
|
+
expect( matchActivity( `${base}${sep}steps.js${sep}extra.js` ) ).toBe( false );
|
|
72
|
+
expect( matchActivity( `${base}${sep}evaluators.js${sep}extra.js` ) ).toBe( false );
|
|
73
|
+
} );
|
|
74
|
+
|
|
75
|
+
it( 'escapes regular expression characters in the base path', () => {
|
|
76
|
+
const specialBase = `${sep}app${sep}proj.with-symbols+(test)`;
|
|
77
|
+
const matchActivity = buildActivityMatcher( specialBase );
|
|
78
|
+
|
|
79
|
+
expect( matchActivity( `${specialBase}${sep}steps.js` ) ).toBe( true );
|
|
80
|
+
expect( matchActivity( `${sep}app${sep}projXwith-symbols+(test)${sep}steps.js` ) ).toBe( false );
|
|
81
|
+
} );
|
|
82
|
+
} );
|
|
83
|
+
|
|
84
|
+
describe( 'staticMatchers', () => {
|
|
85
|
+
describe( 'workflowFile', () => {
|
|
86
|
+
it( 'matches paths ending with path separator and workflow.js', () => {
|
|
87
|
+
expect( staticMatchers.workflowFile( `${sep}x${sep}y${sep}workflow.js` ) ).toBe( true );
|
|
88
|
+
} );
|
|
89
|
+
|
|
90
|
+
it( 'rejects workflow.ts', () => {
|
|
91
|
+
expect( staticMatchers.workflowFile( `${sep}a${sep}workflow.ts` ) ).toBe( false );
|
|
92
|
+
} );
|
|
93
|
+
} );
|
|
94
|
+
|
|
95
|
+
describe( 'workflowPathHasShared', () => {
|
|
96
|
+
it( 'matches workflow.js under a shared folder segment', () => {
|
|
97
|
+
expect( staticMatchers.workflowPathHasShared( `${sep}foo${sep}shared${sep}workflow.js` ) ).toBe( true );
|
|
98
|
+
} );
|
|
99
|
+
|
|
100
|
+
it( 'rejects workflow.js not under shared', () => {
|
|
101
|
+
expect( staticMatchers.workflowPathHasShared( `${sep}foo${sep}workflow.js` ) ).toBe( false );
|
|
102
|
+
} );
|
|
103
|
+
} );
|
|
104
|
+
|
|
105
|
+
describe( 'sharedStepsDir', () => {
|
|
106
|
+
it( 'matches .js files inside shared/steps/', () => {
|
|
107
|
+
expect( staticMatchers.sharedStepsDir( `${sep}app${sep}dist${sep}shared${sep}steps${sep}tools.js` ) ).toBe( true );
|
|
108
|
+
} );
|
|
109
|
+
|
|
110
|
+
it( 'matches .js files in nested subdirectories of shared/steps/', () => {
|
|
111
|
+
expect( staticMatchers.sharedStepsDir( `${sep}app${sep}dist${sep}shared${sep}steps${sep}utils${sep}helper.js` ) ).toBe( true );
|
|
112
|
+
} );
|
|
113
|
+
|
|
114
|
+
it( 'rejects .ts files inside shared/steps/', () => {
|
|
115
|
+
expect( staticMatchers.sharedStepsDir( `${sep}app${sep}src${sep}shared${sep}steps${sep}tools.ts` ) ).toBe( false );
|
|
116
|
+
} );
|
|
117
|
+
|
|
118
|
+
it( 'rejects non-.js files inside shared/steps/', () => {
|
|
119
|
+
expect( staticMatchers.sharedStepsDir( `${sep}app${sep}dist${sep}shared${sep}steps${sep}readme.md` ) ).toBe( false );
|
|
120
|
+
} );
|
|
121
|
+
} );
|
|
122
|
+
|
|
123
|
+
describe( 'sharedEvaluatorsDir', () => {
|
|
124
|
+
it( 'matches .js files inside shared/evaluators/', () => {
|
|
125
|
+
expect( staticMatchers.sharedEvaluatorsDir( `${sep}app${sep}dist${sep}shared${sep}evaluators${sep}quality.js` ) ).toBe( true );
|
|
126
|
+
} );
|
|
127
|
+
|
|
128
|
+
it( 'matches .js files in nested subdirectories of shared/evaluators/', () => {
|
|
129
|
+
expect( staticMatchers.sharedEvaluatorsDir( `${sep}app${sep}dist${sep}shared${sep}evaluators${sep}utils${sep}helper.js` ) ).toBe( true );
|
|
130
|
+
} );
|
|
131
|
+
|
|
132
|
+
it( 'rejects .ts files inside shared/evaluators/', () => {
|
|
133
|
+
expect( staticMatchers.sharedEvaluatorsDir( `${sep}app${sep}src${sep}shared${sep}evaluators${sep}quality.ts` ) ).toBe( false );
|
|
134
|
+
} );
|
|
135
|
+
|
|
136
|
+
it( 'rejects non-.js files inside shared/evaluators/', () => {
|
|
137
|
+
expect( staticMatchers.sharedEvaluatorsDir( `${sep}app${sep}dist${sep}shared${sep}evaluators${sep}readme.md` ) ).toBe( false );
|
|
138
|
+
} );
|
|
139
|
+
} );
|
|
140
|
+
} );
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { dirname, resolve
|
|
2
|
-
import { pathToFileURL } from 'url';
|
|
1
|
+
import { join, dirname, resolve } from 'node:path';
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
3
3
|
import { METADATA_ACCESS_SYMBOL } from '#consts';
|
|
4
|
-
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from 'fs';
|
|
4
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { hashElement } from 'folder-hash';
|
|
6
|
+
import { staticMatchers } from './matchers.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Returns the real path for symlink
|
|
@@ -20,69 +21,6 @@ export const resolveSymlink = link => {
|
|
|
20
21
|
}
|
|
21
22
|
};
|
|
22
23
|
|
|
23
|
-
/**
|
|
24
|
-
* Returns matchers that need to be built using a relative path
|
|
25
|
-
*
|
|
26
|
-
* @param {string} path
|
|
27
|
-
* @returns {object} The object containing the matchers
|
|
28
|
-
*/
|
|
29
|
-
export const activityMatchersBuilder = path => ( {
|
|
30
|
-
/**
|
|
31
|
-
* Matches a file called steps.js, located at the path
|
|
32
|
-
* @param {string} path - Path to test
|
|
33
|
-
* @returns {boolean}
|
|
34
|
-
*/
|
|
35
|
-
stepsFile: v => v === `${path}${sep}steps.js`,
|
|
36
|
-
/**
|
|
37
|
-
* Matches a file called evaluators.js, located at the path
|
|
38
|
-
* @param {string} path - Path to test
|
|
39
|
-
* @returns {boolean}
|
|
40
|
-
*/
|
|
41
|
-
evaluatorsFile: v => v === `${path}${sep}evaluators.js`,
|
|
42
|
-
/**
|
|
43
|
-
* Matches all files on any levels inside a folder called steps/, located at the path
|
|
44
|
-
* @param {string} path - Path to test
|
|
45
|
-
* @returns {boolean}
|
|
46
|
-
*/
|
|
47
|
-
stepsDir: v => v.startsWith( `${path}${sep}steps${sep}` ),
|
|
48
|
-
/**
|
|
49
|
-
* Matches all files on any levels inside a folder called evaluators/, located at the path
|
|
50
|
-
* @param {string} path - Path to test
|
|
51
|
-
* @returns {boolean}
|
|
52
|
-
*/
|
|
53
|
-
evaluatorsDir: v => v.startsWith( `${path}${sep}evaluators${sep}` )
|
|
54
|
-
} );
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Matchers that can be used to access conditions without initializing them
|
|
58
|
-
*/
|
|
59
|
-
export const staticMatchers = {
|
|
60
|
-
/**
|
|
61
|
-
* Matches a workflow.js file
|
|
62
|
-
* @param {string} path - Path to test
|
|
63
|
-
* @returns {boolean}
|
|
64
|
-
*/
|
|
65
|
-
workflowFile: v => v.endsWith( `${sep}workflow.js` ),
|
|
66
|
-
/**
|
|
67
|
-
* Matches a workflow.js that is inside a shared folder: eg foo/shared/workflow.js
|
|
68
|
-
* @param {string} path - Path to test
|
|
69
|
-
* @returns {boolean}
|
|
70
|
-
*/
|
|
71
|
-
workflowPathHasShared: v => v.endsWith( `${sep}shared${sep}workflow.js` ),
|
|
72
|
-
/**
|
|
73
|
-
* Matches the shared folder for steps src/shared/steps/../step_file.js
|
|
74
|
-
* @param {string} path - Path to test
|
|
75
|
-
* @returns {boolean}
|
|
76
|
-
*/
|
|
77
|
-
sharedStepsDir: v => v.includes( `${sep}shared${sep}steps${sep}` ) && v.endsWith( '.js' ),
|
|
78
|
-
/**
|
|
79
|
-
* Matches the shared folder for evaluators src/shared/evaluators/../evaluator_file.js
|
|
80
|
-
* @param {string} path - Path to test
|
|
81
|
-
* @returns {boolean}
|
|
82
|
-
*/
|
|
83
|
-
sharedEvaluatorsDir: v => v.includes( `${sep}shared${sep}evaluators${sep}` ) && v.endsWith( '.js' )
|
|
84
|
-
};
|
|
85
|
-
|
|
86
24
|
/**
|
|
87
25
|
* @typedef {object} File
|
|
88
26
|
* @property {string} path - The file path
|
|
@@ -314,7 +252,7 @@ export const hashSourceCode = async rootDir => {
|
|
|
314
252
|
try {
|
|
315
253
|
const { hash } = await hashElement( rootDir, {
|
|
316
254
|
folders: {
|
|
317
|
-
exclude: [ '.*', 'node_modules', 'test_coverage', 'vendor', 'test' ],
|
|
255
|
+
exclude: [ '.*', 'node_modules', 'test_coverage', 'vendor', 'test', 'logs', 'dist' ],
|
|
318
256
|
ignoreRootName: true
|
|
319
257
|
},
|
|
320
258
|
files: {
|
|
@@ -327,3 +265,17 @@ export const hashSourceCode = async rootDir => {
|
|
|
327
265
|
throw new Error( `Error calculating hash from "${error}": ${error.message}`, { cause: error } );
|
|
328
266
|
}
|
|
329
267
|
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Creates a file in the temp folder and returns its path
|
|
271
|
+
* @returns {string} Folder
|
|
272
|
+
*/
|
|
273
|
+
export const writeFileInTempDir = ( content, name ) => {
|
|
274
|
+
const here = dirname( fileURLToPath( import.meta.url ) );
|
|
275
|
+
const tempDir = join( here, '..', 'temp' );
|
|
276
|
+
mkdirSync( tempDir, { recursive: true } );
|
|
277
|
+
|
|
278
|
+
const filename = join( tempDir, name );
|
|
279
|
+
writeFileSync( filename, content, 'utf-8' );
|
|
280
|
+
return filename;
|
|
281
|
+
};
|