Skip to content

Commit 3002a11

Browse files
committed
feat: integrate plugins into core runtime
- Auto-register storage, debug, frequency, and banner plugins in ExperienceRuntime constructor - Move shared types to plugins package to resolve circular dependency - Update core to depend on plugins package - Bundle all dependencies in IIFE build for script tag usage - Remove default export to fix named/default export warning - Configure Biome to allow any in specs contracts - Update pre-commit hook to only lint staged files (matches sdk-kit pattern) All plugins now work seamlessly with the runtime. Closes #9
1 parent 720b38e commit 3002a11

32 files changed

+248
-160
lines changed

.changeset/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88
"baseBranch": "main",
99
"updateInternalDependencies": "patch",
1010
"ignore": []
11-
}
11+
}

.husky/pre-commit

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,24 @@
33

44
echo "🔍 Running pre-commit checks..."
55

6-
# Lint and format staged files
7-
echo "📝 Linting and formatting..."
8-
pnpm lint-staged
6+
# Build packages first (needed for typecheck due to workspace dependencies)
7+
echo "🔨 Building packages..."
8+
pnpm build > /dev/null 2>&1
9+
10+
# Get staged files
11+
files=$(git diff --cached --name-only --diff-filter=ACMR "*.ts" "*.tsx" "*.js" "*.jsx" | xargs)
12+
13+
if [ -n "$files" ]; then
14+
echo "📝 Linting and formatting staged files..."
15+
pnpm lint-staged $files
16+
git add $files
17+
fi
918

1019
# Type check entire project
1120
echo "🔎 Type checking..."
1221
pnpm typecheck
1322

23+
# Clean up dist files (they shouldn't be committed)
24+
rm -rf packages/*/dist
25+
1426
echo "✅ Pre-commit checks passed!"

biome.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"$schema": "https://biomejs.dev/schemas/2.3.0/schema.json",
2+
"$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
33
"assist": { "actions": { "source": { "organizeImports": "on" } } },
44
"linter": {
55
"enabled": true,
@@ -30,7 +30,9 @@
3030
{
3131
"includes": [
3232
"packages/core/src/types.ts",
33-
"packages/core/src/runtime.ts"
33+
"packages/core/src/runtime.ts",
34+
"packages/plugins/src/types.ts",
35+
"specs/**/contracts/types.ts"
3436
],
3537
"linter": {
3638
"rules": {

commitlint.config.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@ module.exports = {
22
extends: ['@commitlint/config-conventional'],
33
rules: {
44
'body-max-line-length': [2, 'always', 100],
5-
'subject-case': [
6-
2,
7-
'never',
8-
['start-case', 'pascal-case', 'upper-case'],
9-
],
5+
'subject-case': [2, 'never', ['start-case', 'pascal-case', 'upper-case']],
106
},
11-
};
7+
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,4 @@
5252
"dependencies": {
5353
"@lytics/sdk-kit-plugins": "0.1.2"
5454
}
55-
}
55+
}

packages/core/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,14 @@
4242
"test:watch": "vitest"
4343
},
4444
"dependencies": {
45-
"@lytics/sdk-kit": "^0.1.1"
45+
"@lytics/sdk-kit": "^0.1.1",
46+
"@lytics/sdk-kit-plugins": "^0.1.2",
47+
"@prosdevlab/experience-sdk-plugins": "workspace:*"
4648
},
4749
"devDependencies": {
4850
"@types/node": "^24.0.0",
4951
"tsup": "^8.5.1",
5052
"typescript": "^5.9.3",
5153
"vitest": "^4.0.16"
5254
}
53-
}
55+
}

packages/core/src/index.ts

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,46 +5,45 @@
55
* built on @lytics/sdk-kit.
66
*/
77

8-
// Export all types
9-
export type {
10-
Experience,
11-
TargetingRules,
12-
UrlRule,
13-
FrequencyRule,
14-
FrequencyConfig,
15-
ExperienceContent,
16-
BannerContent,
17-
ModalContent,
18-
TooltipContent,
19-
ModalAction,
20-
Context,
21-
UserContext,
22-
Decision,
23-
TraceStep,
24-
DecisionMetadata,
25-
ExperienceConfig,
26-
RuntimeState,
27-
} from './types';
8+
// Re-export plugins for convenience
9+
export { bannerPlugin, debugPlugin, frequencyPlugin } from '@prosdevlab/experience-sdk-plugins';
2810

2911
// Export runtime class and functions
3012
export {
31-
ExperienceRuntime,
3213
buildContext,
14+
ExperienceRuntime,
3315
evaluateExperience,
3416
evaluateUrlRule,
3517
} from './runtime';
3618

3719
// Export singleton API
3820
export {
3921
createInstance,
40-
init,
41-
register,
22+
destroy,
4223
evaluate,
4324
explain,
4425
getState,
26+
init,
4527
on,
46-
destroy,
47-
experiences as default,
28+
register,
4829
} from './singleton';
49-
50-
30+
// Export all types
31+
export type {
32+
BannerContent,
33+
Context,
34+
Decision,
35+
DecisionMetadata,
36+
Experience,
37+
ExperienceConfig,
38+
ExperienceContent,
39+
FrequencyConfig,
40+
FrequencyRule,
41+
ModalAction,
42+
ModalContent,
43+
RuntimeState,
44+
TargetingRules,
45+
TooltipContent,
46+
TraceStep,
47+
UrlRule,
48+
UserContext,
49+
} from './types';

packages/core/src/runtime.test.ts

Lines changed: 45 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -93,17 +93,17 @@ describe('ExperienceRuntime', () => {
9393
});
9494

9595
it('should allow multiple experiences', () => {
96-
runtime.register('exp1', {
97-
type: 'banner',
98-
targeting: {},
99-
content: { title: 'Exp 1', message: 'Message 1' },
100-
});
96+
runtime.register('exp1', {
97+
type: 'banner',
98+
targeting: {},
99+
content: { title: 'Exp 1', message: 'Message 1' },
100+
});
101101

102-
runtime.register('exp2', {
103-
type: 'banner',
104-
targeting: {},
105-
content: { title: 'Exp 2', message: 'Message 2' },
106-
});
102+
runtime.register('exp2', {
103+
type: 'banner',
104+
targeting: {},
105+
content: { title: 'Exp 2', message: 'Message 2' },
106+
});
107107

108108
const state = runtime.getState();
109109
expect(state.experiences.size).toBe(2);
@@ -112,13 +112,13 @@ describe('ExperienceRuntime', () => {
112112

113113
describe('evaluate()', () => {
114114
beforeEach(() => {
115-
runtime.register('test', {
116-
type: 'banner',
117-
targeting: {
118-
url: { contains: '/products' },
119-
},
120-
content: { title: 'Test', message: 'Test message' },
121-
});
115+
runtime.register('test', {
116+
type: 'banner',
117+
targeting: {
118+
url: { contains: '/products' },
119+
},
120+
content: { title: 'Test', message: 'Test message' },
121+
});
122122
});
123123

124124
it('should return decision with matched experience', () => {
@@ -199,13 +199,13 @@ describe('ExperienceRuntime', () => {
199199
});
200200

201201
it('should match first experience only', () => {
202-
runtime.register('test2', {
203-
type: 'banner',
204-
targeting: {
205-
url: { contains: '/products' },
206-
},
207-
content: { title: 'Test 2', message: 'Test 2 message' },
208-
});
202+
runtime.register('test2', {
203+
type: 'banner',
204+
targeting: {
205+
url: { contains: '/products' },
206+
},
207+
content: { title: 'Test 2', message: 'Test 2 message' },
208+
});
209209

210210
const decision = runtime.evaluate({
211211
url: 'https://example.com/products',
@@ -218,13 +218,13 @@ describe('ExperienceRuntime', () => {
218218

219219
describe('explain()', () => {
220220
it('should explain specific experience', () => {
221-
runtime.register('test', {
222-
type: 'banner',
223-
targeting: {
224-
url: { contains: '/test' },
225-
},
226-
content: { title: 'Test', message: 'Test message' },
227-
});
221+
runtime.register('test', {
222+
type: 'banner',
223+
targeting: {
224+
url: { contains: '/test' },
225+
},
226+
content: { title: 'Test', message: 'Test message' },
227+
});
228228

229229
const explanation = runtime.explain('test');
230230

@@ -286,12 +286,12 @@ describe('ExperienceRuntime', () => {
286286

287287
describe('destroy()', () => {
288288
it('should clean up runtime', async () => {
289-
await runtime.init();
290-
runtime.register('test', {
291-
type: 'banner',
292-
targeting: {},
293-
content: { title: 'Test', message: 'Test message' },
294-
});
289+
await runtime.init();
290+
runtime.register('test', {
291+
type: 'banner',
292+
targeting: {},
293+
content: { title: 'Test', message: 'Test message' },
294+
});
295295

296296
await runtime.destroy();
297297

@@ -398,26 +398,22 @@ describe('ExperienceRuntime', () => {
398398

399399
describe('evaluateUrlRule', () => {
400400
it('should match with equals rule', () => {
401-
expect(evaluateUrlRule({ equals: 'https://example.com' }, 'https://example.com')).toBe(
402-
true
403-
);
401+
expect(evaluateUrlRule({ equals: 'https://example.com' }, 'https://example.com')).toBe(true);
404402
expect(evaluateUrlRule({ equals: 'https://example.com' }, 'https://other.com')).toBe(false);
405403
});
406404

407405
it('should match with contains rule', () => {
408-
expect(evaluateUrlRule({ contains: '/products' }, 'https://example.com/products')).toBe(
409-
true
410-
);
406+
expect(evaluateUrlRule({ contains: '/products' }, 'https://example.com/products')).toBe(true);
411407
expect(evaluateUrlRule({ contains: '/products' }, 'https://example.com/about')).toBe(false);
412408
});
413409

414410
it('should match with regex rule', () => {
415-
expect(evaluateUrlRule({ matches: /\/product\/\d+/ }, 'https://example.com/product/123')).toBe(
416-
true
417-
);
418-
expect(evaluateUrlRule({ matches: /\/product\/\d+/ }, 'https://example.com/product/abc')).toBe(
419-
false
420-
);
411+
expect(
412+
evaluateUrlRule({ matches: /\/product\/\d+/ }, 'https://example.com/product/123')
413+
).toBe(true);
414+
expect(
415+
evaluateUrlRule({ matches: /\/product\/\d+/ }, 'https://example.com/product/abc')
416+
).toBe(false);
421417
});
422418

423419
it('should return true for empty rule', () => {
@@ -465,4 +461,3 @@ describe('ExperienceRuntime', () => {
465461
});
466462
});
467463
});
468-

packages/core/src/runtime.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { SDK } from '@lytics/sdk-kit';
2+
import { storagePlugin } from '@lytics/sdk-kit-plugins';
3+
import { bannerPlugin, debugPlugin, frequencyPlugin } from '@prosdevlab/experience-sdk-plugins';
24
import type {
35
Context,
46
Decision,
@@ -32,6 +34,12 @@ export class ExperienceRuntime {
3234
name: 'experience-sdk',
3335
...config,
3436
});
37+
38+
// Auto-register plugins
39+
this.sdk.use(storagePlugin);
40+
this.sdk.use(debugPlugin);
41+
this.sdk.use(frequencyPlugin);
42+
this.sdk.use(bannerPlugin);
3543
}
3644

3745
/**
@@ -263,4 +271,3 @@ export function evaluateUrlRule(rule: UrlRule, url: string = ''): boolean {
263271
// No rules specified = match all
264272
return true;
265273
}
266-

packages/core/src/singleton.test.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { beforeEach, describe, expect, it } from 'vitest';
22
import {
33
createInstance,
4-
init,
5-
register,
4+
destroy,
65
evaluate,
6+
experiences as experiencesDefault,
77
explain,
88
getState,
9+
init,
910
on,
10-
destroy,
11-
experiences as experiencesDefault,
11+
register,
1212
} from './singleton';
1313

1414
describe('Export Pattern', () => {
@@ -234,4 +234,3 @@ describe('Export Pattern', () => {
234234
});
235235
});
236236
});
237-

0 commit comments

Comments
 (0)