@outputai/core 0.10.0 → 0.10.1-next.09ed166.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/package.json +1 -1
- package/src/bus.js +58 -20
- package/src/bus.spec.js +135 -77
- package/src/consts.js +2 -1
- package/src/helpers/object.js +48 -35
- package/src/helpers/object.spec.js +123 -75
- package/src/hooks/index.d.ts +48 -45
- package/src/hooks/index.js +49 -44
- package/src/hooks/index.spec.js +74 -102
- package/src/interface/workflow.js +92 -85
- package/src/interface/workflow.spec.js +135 -142
- package/src/sdk/helpers/objects.d.ts +24 -19
- package/src/sdk/runtime/events.d.ts +4 -6
- package/src/sdk/runtime/events.js +2 -15
- package/src/sdk/runtime/events.spec.js +14 -92
- package/src/worker/global_functions.js +2 -2
- package/src/worker/global_functions.spec.js +3 -3
- package/src/worker/index.js +3 -3
- package/src/worker/index.spec.js +6 -6
- package/src/worker/interceptors/activity.js +4 -4
- package/src/worker/interceptors/activity.spec.js +9 -9
- package/src/worker/interceptors/workflow.js +5 -5
- package/src/worker/interceptors/workflow.spec.js +7 -7
- package/src/worker/loader/activities.js +56 -40
- package/src/worker/loader/activities.spec.js +21 -22
- package/src/worker/loader/workflows.spec.js +1 -2
- package/src/worker/log_hooks.js +9 -9
- package/src/worker/log_hooks.spec.js +2 -2
- package/src/worker/sinks.js +8 -8
- package/src/worker/sinks.spec.js +9 -9
- package/src/worker/webpack_loaders/consts.js +6 -0
- package/src/worker/webpack_loaders/tools.js +1 -146
- package/src/worker/webpack_loaders/tools.spec.js +0 -23
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +24 -32
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +82 -279
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +6 -7
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +57 -119
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.js +88 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.spec.js +165 -0
- package/src/worker/webpack_loaders/workflow_validator/index.mjs +96 -29
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +110 -583
- package/src/worker/webpack_loaders/{npm_workflow_export_resolve.js → workflow_validator/npm_workflow_export_resolve.js} +1 -1
- package/src/worker/webpack_loaders/{npm_workflow_export_resolve.spec.js → workflow_validator/npm_workflow_export_resolve.spec.js} +1 -1
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +0 -193
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +0 -123
package/package.json
CHANGED
package/src/bus.js
CHANGED
|
@@ -1,30 +1,68 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import {
|
|
3
|
+
import { Storage } from '#async_storage';
|
|
4
|
+
import { WORKFLOW_CATALOG } from '#consts';
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
+
const mainEventBus = new EventEmitter();
|
|
7
|
+
const stepEventBus = new EventEmitter();
|
|
8
|
+
|
|
9
|
+
const getContext = () => {
|
|
10
|
+
const ctx = Storage.load();
|
|
11
|
+
if ( ctx ) {
|
|
12
|
+
const { outputActivityKind, activityInfo, workflowDetails } = ctx;
|
|
13
|
+
return { outputActivityKind, activityInfo, workflowDetails };
|
|
14
|
+
}
|
|
15
|
+
return {};
|
|
16
|
+
};
|
|
6
17
|
|
|
7
18
|
/**
|
|
8
|
-
*
|
|
19
|
+
* Attaches information and filter out events from the bus by proxying .emit().
|
|
20
|
+
*
|
|
21
|
+
* If createEnvelope=true, wrap payload:
|
|
22
|
+
* ```js
|
|
23
|
+
* {
|
|
24
|
+
* eventId,
|
|
25
|
+
* eventDate,
|
|
26
|
+
* outputActivityKind,
|
|
27
|
+
* activityInfo,
|
|
28
|
+
* workflowDetails,
|
|
29
|
+
* payload: <original payload>
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* if createEnvelope=false, adds eventId and eventDate to the original payload object
|
|
34
|
+
*
|
|
35
|
+
* @param {object} args
|
|
36
|
+
* @param {object} args.bus The event emitter to proxy
|
|
37
|
+
* @param {boolean} args.createEnvelope Whether to wrap event around an envelope
|
|
38
|
+
* @param {function} args.filter Filter function to drop events
|
|
9
39
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
40
|
+
const proxyBus = ( { bus, createEnvelope = false, filter } ) => {
|
|
41
|
+
bus.emit = new Proxy( bus.emit, {
|
|
42
|
+
apply( target, thisArg, args ) {
|
|
43
|
+
const [ eventName, payload, ...rest ] = args;
|
|
44
|
+
|
|
45
|
+
const shouldEmit = filter?.( eventName, payload ) ?? true;
|
|
46
|
+
if ( !shouldEmit ) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const eventFields = {
|
|
19
51
|
eventId: randomUUID(),
|
|
20
|
-
eventDate: Date.now()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
52
|
+
eventDate: Date.now()
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const newPayload = createEnvelope ? { ...eventFields, ...getContext(), payload } : { ...eventFields, ...payload };
|
|
56
|
+
return Reflect.apply( target, thisArg, [ eventName, newPayload, ...rest ] );
|
|
24
57
|
}
|
|
58
|
+
} );
|
|
59
|
+
};
|
|
25
60
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
61
|
+
const catalogWfFilter = ( _, payload ) => payload?.workflowDetails?.workflowType !== WORKFLOW_CATALOG;
|
|
62
|
+
|
|
63
|
+
// Main bus is not used inside steps, so it doesn't need context
|
|
64
|
+
proxyBus( { bus: mainEventBus, createEnvelope: false, filter: catalogWfFilter } );
|
|
65
|
+
// This receives SDK and user events from within steps, so add context to it
|
|
66
|
+
proxyBus( { bus: stepEventBus, createEnvelope: true } );
|
|
29
67
|
|
|
30
|
-
export
|
|
68
|
+
export { mainEventBus, stepEventBus };
|
package/src/bus.spec.js
CHANGED
|
@@ -1,135 +1,193 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const loadMock = vi.hoisted( () => vi.fn() );
|
|
4
|
+
|
|
5
|
+
vi.mock( '#async_storage', () => ( {
|
|
6
|
+
Storage: { load: loadMock }
|
|
7
|
+
} ) );
|
|
8
|
+
|
|
9
|
+
import { mainEventBus, stepEventBus } from './bus.js';
|
|
3
10
|
|
|
4
11
|
const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
12
|
+
const ACTIVITY_CONTEXT = {
|
|
13
|
+
activityInfo: { activityId: 'activity-id' },
|
|
14
|
+
workflowDetails: { workflowId: 'workflow-id' },
|
|
15
|
+
outputActivityKind: 'step'
|
|
16
|
+
};
|
|
5
17
|
|
|
6
|
-
describe( '
|
|
18
|
+
describe( 'event buses', () => {
|
|
7
19
|
beforeEach( () => {
|
|
8
|
-
|
|
20
|
+
mainEventBus.removeAllListeners();
|
|
21
|
+
stepEventBus.removeAllListeners();
|
|
22
|
+
loadMock.mockReset();
|
|
9
23
|
} );
|
|
10
24
|
|
|
11
|
-
|
|
12
|
-
|
|
25
|
+
afterEach( () => {
|
|
26
|
+
vi.useRealTimers();
|
|
27
|
+
} );
|
|
28
|
+
|
|
29
|
+
describe( 'mainEventBus', () => {
|
|
30
|
+
it( 'adds event metadata to payload fields', () => {
|
|
13
31
|
const handler = vi.fn();
|
|
14
|
-
|
|
32
|
+
const now = new Date( '2026-06-02T12:00:00.000Z' );
|
|
33
|
+
vi.useFakeTimers();
|
|
34
|
+
vi.setSystemTime( now );
|
|
35
|
+
mainEventBus.on( 'test:event', handler );
|
|
15
36
|
|
|
16
|
-
|
|
37
|
+
mainEventBus.emit( 'test:event', { foo: 'bar' } );
|
|
17
38
|
|
|
18
|
-
expect( handler ).toHaveBeenCalledWith(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
39
|
+
expect( handler ).toHaveBeenCalledWith( {
|
|
40
|
+
eventId: expect.stringMatching( UUID_V4_REGEX ),
|
|
41
|
+
eventDate: now.getTime(),
|
|
42
|
+
foo: 'bar'
|
|
43
|
+
} );
|
|
22
44
|
} );
|
|
23
45
|
|
|
24
|
-
it( '
|
|
46
|
+
it( 'preserves caller-supplied event metadata', () => {
|
|
25
47
|
const handler = vi.fn();
|
|
26
|
-
|
|
48
|
+
mainEventBus.on( 'test:event', handler );
|
|
27
49
|
|
|
28
|
-
|
|
29
|
-
messageBus.emit( 'test:event', { i: 2 } );
|
|
50
|
+
mainEventBus.emit( 'test:event', { eventId: 'fixed-id', eventDate: 1234 } );
|
|
30
51
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
expect( first ).not.toBe( second );
|
|
52
|
+
expect( handler ).toHaveBeenCalledWith( {
|
|
53
|
+
eventId: 'fixed-id',
|
|
54
|
+
eventDate: 1234
|
|
55
|
+
} );
|
|
36
56
|
} );
|
|
37
57
|
|
|
38
|
-
it( '
|
|
58
|
+
it( 'does not mutate the caller-supplied payload', () => {
|
|
39
59
|
const handler = vi.fn();
|
|
40
|
-
|
|
60
|
+
const payload = { foo: 'bar' };
|
|
61
|
+
mainEventBus.on( 'test:event', handler );
|
|
41
62
|
|
|
42
|
-
|
|
63
|
+
mainEventBus.emit( 'test:event', payload );
|
|
43
64
|
|
|
44
|
-
expect( handler ).
|
|
45
|
-
|
|
46
|
-
|
|
65
|
+
expect( handler.mock.calls[0][0] ).not.toBe( payload );
|
|
66
|
+
expect( payload ).toEqual( { foo: 'bar' } );
|
|
67
|
+
} );
|
|
68
|
+
|
|
69
|
+
it( 'does not attach activity context', () => {
|
|
70
|
+
const handler = vi.fn();
|
|
71
|
+
loadMock.mockReturnValue( {
|
|
72
|
+
activityInfo: { activityId: 'activity-id' },
|
|
73
|
+
workflowDetails: { workflowId: 'workflow-id' },
|
|
74
|
+
outputActivityKind: 'step'
|
|
75
|
+
} );
|
|
76
|
+
mainEventBus.on( 'test:event', handler );
|
|
77
|
+
|
|
78
|
+
mainEventBus.emit( 'test:event', { foo: 'bar' } );
|
|
79
|
+
|
|
80
|
+
expect( handler ).toHaveBeenCalledWith( expect.not.objectContaining( {
|
|
81
|
+
activityInfo: expect.anything(),
|
|
82
|
+
workflowDetails: expect.anything(),
|
|
83
|
+
outputActivityKind: expect.anything()
|
|
47
84
|
} ) );
|
|
85
|
+
expect( loadMock ).not.toHaveBeenCalled();
|
|
48
86
|
} );
|
|
49
87
|
|
|
50
|
-
it( '
|
|
88
|
+
it( 'filters catalog workflow events', () => {
|
|
51
89
|
const handler = vi.fn();
|
|
52
|
-
|
|
53
|
-
vi.useFakeTimers();
|
|
54
|
-
vi.setSystemTime( now );
|
|
55
|
-
messageBus.on( 'test:event', handler );
|
|
90
|
+
mainEventBus.on( 'workflow:start', handler );
|
|
56
91
|
|
|
57
|
-
|
|
92
|
+
const emitted = mainEventBus.emit( 'workflow:start', {
|
|
93
|
+
workflowDetails: { workflowType: '$catalog' }
|
|
94
|
+
} );
|
|
58
95
|
|
|
59
|
-
expect(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
96
|
+
expect( emitted ).toBe( false );
|
|
97
|
+
expect( handler ).not.toHaveBeenCalled();
|
|
98
|
+
} );
|
|
99
|
+
} );
|
|
63
100
|
|
|
64
|
-
|
|
101
|
+
describe( 'stepEventBus', () => {
|
|
102
|
+
beforeEach( () => {
|
|
103
|
+
loadMock.mockReturnValue( ACTIVITY_CONTEXT );
|
|
65
104
|
} );
|
|
66
105
|
|
|
67
|
-
it( '
|
|
106
|
+
it( 'wraps payloads with event metadata and activity context', () => {
|
|
68
107
|
const handler = vi.fn();
|
|
69
|
-
|
|
108
|
+
const payload = { foo: 'bar' };
|
|
109
|
+
stepEventBus.on( 'test:event', handler );
|
|
70
110
|
|
|
71
|
-
|
|
111
|
+
stepEventBus.emit( 'test:event', payload );
|
|
72
112
|
|
|
73
|
-
expect( handler ).toHaveBeenCalledWith(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
113
|
+
expect( handler ).toHaveBeenCalledWith( {
|
|
114
|
+
eventId: expect.stringMatching( UUID_V4_REGEX ),
|
|
115
|
+
eventDate: expect.any( Number ),
|
|
116
|
+
...ACTIVITY_CONTEXT,
|
|
117
|
+
payload
|
|
118
|
+
} );
|
|
77
119
|
} );
|
|
78
120
|
|
|
79
|
-
it( '
|
|
121
|
+
it( 'omits activity fields outside activity context', () => {
|
|
80
122
|
const handler = vi.fn();
|
|
81
|
-
|
|
123
|
+
loadMock.mockReturnValue( undefined );
|
|
124
|
+
stepEventBus.on( 'test:event', handler );
|
|
82
125
|
|
|
83
|
-
|
|
84
|
-
messageBus.emit( 'test:event', payload );
|
|
126
|
+
stepEventBus.emit( 'test:event', { foo: 'bar' } );
|
|
85
127
|
|
|
86
|
-
expect(
|
|
87
|
-
|
|
128
|
+
expect( handler ).toHaveBeenCalledWith( {
|
|
129
|
+
eventId: expect.stringMatching( UUID_V4_REGEX ),
|
|
130
|
+
eventDate: expect.any( Number ),
|
|
131
|
+
payload: { foo: 'bar' }
|
|
132
|
+
} );
|
|
88
133
|
} );
|
|
89
|
-
} );
|
|
90
134
|
|
|
91
|
-
|
|
92
|
-
it( 'passes primitive payloads through unchanged', () => {
|
|
135
|
+
it( 'preserves arbitrary payloads unchanged inside the envelope', () => {
|
|
93
136
|
const handler = vi.fn();
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
137
|
+
const array = [ 1, 2, 3 ];
|
|
138
|
+
const instance = new Date();
|
|
139
|
+
stepEventBus.on( 'test:event', handler );
|
|
140
|
+
|
|
141
|
+
stepEventBus.emit( 'test:event', 'value' );
|
|
142
|
+
stepEventBus.emit( 'test:event', array );
|
|
143
|
+
stepEventBus.emit( 'test:event', instance );
|
|
144
|
+
stepEventBus.emit( 'test:event' );
|
|
145
|
+
|
|
146
|
+
expect( handler.mock.calls[0][0].payload ).toBe( 'value' );
|
|
147
|
+
expect( handler.mock.calls[1][0].payload ).toBe( array );
|
|
148
|
+
expect( handler.mock.calls[2][0].payload ).toBe( instance );
|
|
149
|
+
expect( handler.mock.calls[3][0] ).toHaveProperty( 'payload', undefined );
|
|
103
150
|
} );
|
|
104
151
|
|
|
105
|
-
it( '
|
|
152
|
+
it( 'keeps payload metadata separate from envelope metadata', () => {
|
|
106
153
|
const handler = vi.fn();
|
|
107
|
-
|
|
154
|
+
const payload = { eventId: 'payload-id', eventDate: 1234 };
|
|
155
|
+
stepEventBus.on( 'test:event', handler );
|
|
108
156
|
|
|
109
|
-
|
|
110
|
-
messageBus.emit( 'test:event' );
|
|
157
|
+
stepEventBus.emit( 'test:event', payload );
|
|
111
158
|
|
|
112
|
-
|
|
113
|
-
expect(
|
|
159
|
+
const envelope = handler.mock.calls[0][0];
|
|
160
|
+
expect( envelope.eventId ).toMatch( UUID_V4_REGEX );
|
|
161
|
+
expect( envelope.eventId ).not.toBe( payload.eventId );
|
|
162
|
+
expect( envelope.eventDate ).not.toBe( payload.eventDate );
|
|
163
|
+
expect( envelope.payload ).toBe( payload );
|
|
114
164
|
} );
|
|
115
165
|
|
|
116
|
-
it( '
|
|
166
|
+
it( 'gives distinct emits distinct eventIds', () => {
|
|
117
167
|
const handler = vi.fn();
|
|
118
|
-
|
|
168
|
+
stepEventBus.on( 'test:event', handler );
|
|
119
169
|
|
|
120
|
-
|
|
170
|
+
stepEventBus.emit( 'test:event', { i: 1 } );
|
|
171
|
+
stepEventBus.emit( 'test:event', { i: 2 } );
|
|
121
172
|
|
|
122
|
-
|
|
173
|
+
const first = handler.mock.calls[0][0].eventId;
|
|
174
|
+
const second = handler.mock.calls[1][0].eventId;
|
|
175
|
+
expect( first ).toMatch( UUID_V4_REGEX );
|
|
176
|
+
expect( second ).toMatch( UUID_V4_REGEX );
|
|
177
|
+
expect( first ).not.toBe( second );
|
|
123
178
|
} );
|
|
124
179
|
|
|
125
|
-
it( 'forwards additional positional
|
|
180
|
+
it( 'forwards additional positional arguments unchanged', () => {
|
|
126
181
|
const handler = vi.fn();
|
|
127
|
-
|
|
182
|
+
stepEventBus.on( 'test:event', handler );
|
|
128
183
|
|
|
129
|
-
|
|
184
|
+
stepEventBus.emit( 'test:event', { foo: 'bar' }, 'extra', 99 );
|
|
130
185
|
|
|
131
186
|
expect( handler ).toHaveBeenCalledWith(
|
|
132
|
-
expect.objectContaining( {
|
|
187
|
+
expect.objectContaining( {
|
|
188
|
+
eventId: expect.stringMatching( UUID_V4_REGEX ),
|
|
189
|
+
payload: { foo: 'bar' }
|
|
190
|
+
} ),
|
|
133
191
|
'extra',
|
|
134
192
|
99
|
|
135
193
|
);
|
package/src/consts.js
CHANGED
|
@@ -4,11 +4,12 @@ export const ACTIVITY_SEND_HTTP_REQUEST = '__internal#sendHttpRequest';
|
|
|
4
4
|
export const ACTIVITY_WRAPPER_VERSION_FIELD = '__output_activity_wrapper_version';
|
|
5
5
|
export const ACTIVITY_LOGGER_SYMBOL = Symbol.for( '__activity_logger' );
|
|
6
6
|
export const METADATA_ACCESS_SYMBOL = Symbol( '__metadata' );
|
|
7
|
-
export const SHARED_STEP_PREFIX = '$shared';
|
|
8
7
|
export const WORKFLOW_CATALOG = '$catalog';
|
|
9
8
|
export const WORKFLOWS_INDEX_FILENAME = '__workflows_entrypoint.js';
|
|
10
9
|
export const WORKFLOW_WRAPPER_VERSION_FIELD = '__output_workflow_wrapper_version';
|
|
11
10
|
|
|
11
|
+
export const INVOKE_ACTIVITY_SYMBOL = Symbol.for( '@outputai/core:__invoke_activity' );
|
|
12
|
+
|
|
12
13
|
export const ComponentType = {
|
|
13
14
|
EVALUATOR: 'evaluator',
|
|
14
15
|
INTERNAL_STEP: 'internal_step',
|
package/src/helpers/object.js
CHANGED
|
@@ -24,58 +24,71 @@ export const clone = v => {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Creates a new object merging object
|
|
28
|
-
*
|
|
29
|
-
* - Object "
|
|
30
|
-
* - Object "
|
|
27
|
+
* Creates a new object recursively merging the rightmost object over the previous one using a resolver function.
|
|
28
|
+
* Give two objects, (L)eft and (R)right
|
|
29
|
+
* - Object "R" will overwrite fields on object "L" based on the result of the resolver function;
|
|
30
|
+
* - Object "R" fields that don't exist on object "L" will be added;
|
|
31
|
+
* - Object "L" fields that don't exist on object "R" will be preserved;
|
|
31
32
|
*
|
|
32
|
-
* If "
|
|
33
|
+
* If "R" isn't an object, a new object equal to "L" is returned.
|
|
33
34
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
35
|
+
* The resolver function will define the final value of the merge, it receives two args value "L" and "R".
|
|
36
|
+
*
|
|
37
|
+
*
|
|
38
|
+
* @param {object} base - The base object
|
|
39
|
+
* @param {...(object|function)} args - Target objects followed by the resolver function
|
|
37
40
|
* @returns {object} A new object
|
|
38
41
|
*/
|
|
39
|
-
export const deepMergeWithResolver = (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
export const deepMergeWithResolver = ( base, ...args ) => {
|
|
43
|
+
const objects = args.slice( 0, -1 );
|
|
44
|
+
const resolver = args.at( -1 );
|
|
45
|
+
|
|
46
|
+
if ( !isPlainObject( base ) ) {
|
|
47
|
+
throw new Error( 'First argument (base object) is not an object.' );
|
|
42
48
|
}
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
|
|
50
|
+
if ( typeof resolver !== 'function' ) {
|
|
51
|
+
throw new Error( 'Last argument (resolver) is not a function.' );
|
|
45
52
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
|
|
54
|
+
return objects.reduce( ( merged, object ) => {
|
|
55
|
+
if ( !isPlainObject( object ) ) {
|
|
56
|
+
return merged;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for ( const [ k, v ] of Object.entries( object ) ) {
|
|
60
|
+
if ( isPlainObject( v ) && isPlainObject( merged[k] ) ) {
|
|
61
|
+
merged[k] = deepMergeWithResolver( merged[k], v, resolver );
|
|
62
|
+
} else if ( Object.hasOwn( merged, k ) ) {
|
|
63
|
+
merged[k] = resolver( merged[k], v );
|
|
64
|
+
} else {
|
|
65
|
+
merged[k] = v;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return merged;
|
|
69
|
+
}, clone( base ) );
|
|
59
70
|
};
|
|
60
71
|
|
|
61
72
|
/**
|
|
62
|
-
* Creates a new object merging
|
|
63
|
-
*
|
|
64
|
-
* - Object "
|
|
65
|
-
* - Object "
|
|
73
|
+
* Creates a new object recursively merging the rightmost object over the previous one.
|
|
74
|
+
* Give two objects, (L)eft and (R)right
|
|
75
|
+
* - Object "R" will overwrite fields on object "L";
|
|
76
|
+
* - Object "R" fields that don't exist on object "L" will be added;
|
|
77
|
+
* - Object "L" fields that don't exist on object "R" will be preserved;
|
|
66
78
|
*
|
|
67
|
-
* If "
|
|
79
|
+
* If "R" isn't an object, a new object equal to "L" is returned.
|
|
68
80
|
*
|
|
69
|
-
* @param {object}
|
|
70
|
-
* @param {object}
|
|
81
|
+
* @param {object} base - The base object
|
|
82
|
+
* @param {...object} rest - The target objects
|
|
71
83
|
* @returns {object} A new object
|
|
72
84
|
*/
|
|
73
|
-
export const deepMerge = (
|
|
85
|
+
export const deepMerge = ( base, ...rest ) =>
|
|
86
|
+
deepMergeWithResolver( base, ...rest, ( _, b ) => b );
|
|
74
87
|
|
|
75
88
|
/**
|
|
76
89
|
* Adds an non-writable, non-configurable and non-enumerable property to an object
|
|
77
90
|
* @param {object} obj
|
|
78
|
-
* @param {string} key
|
|
91
|
+
* @param {string|Symbol} key
|
|
79
92
|
* @param {any} value
|
|
80
93
|
* @returns
|
|
81
94
|
*/
|