@friggframework/serverless-plugin 2.0.0-next.48 → 2.0.0-next.49

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/index.js CHANGED
@@ -1,227 +1,121 @@
1
- const { spawn } = require("child_process");
1
+ const { QueueEnvironmentMapper } = require('./lib/queue-environment-mapper');
2
+ const { LocalStackQueueService } = require('./lib/localstack-queue-service');
3
+ const { EsbuildDirectoryManager } = require('./lib/esbuild-directory-manager');
2
4
 
3
- /**
4
- * Frigg Serverless Plugin - Handles AWS discovery and local development setup
5
- */
6
5
  class FriggServerlessPlugin {
7
- /**
8
- * Creates an instance of FriggServerlessPlugin
9
- * @param {Object} serverless - Serverless framework instance
10
- * @param {Object} options - Plugin options
11
- */
12
6
  constructor(serverless, options) {
13
7
  this.serverless = serverless;
14
8
  this.options = options;
15
- this.provider = serverless.getProvider("aws");
9
+ this.provider = serverless.getProvider('aws');
16
10
 
17
- // Create .esbuild/.serverless directory IMMEDIATELY, synchronously,
18
- // before any hooks run. This ensures serverless-esbuild has the
19
- // directory it needs regardless of hook execution order.
20
11
  const fs = require('fs');
21
12
  const path = require('path');
22
- const esbuildDir = path.join(
23
- serverless.config.servicePath || process.cwd(),
24
- '.esbuild',
25
- '.serverless'
26
- );
13
+ const basePath = serverless.config.servicePath || process.cwd();
27
14
 
15
+ const dirManager = new EsbuildDirectoryManager(fs, path);
28
16
  try {
29
- fs.mkdirSync(esbuildDir, { recursive: true });
17
+ const esbuildDir = dirManager.ensureDirectory(basePath);
30
18
  console.log(`✓ Frigg plugin created ${esbuildDir}`);
31
19
  } catch (error) {
32
- console.error(`⚠️ Failed to create ${esbuildDir}:`, error.message);
20
+ console.error(`⚠️ Failed to create esbuild directory:`, error.message);
33
21
  }
34
22
 
35
23
  this.hooks = {
36
24
  initialize: () => this.init(),
37
- "before:package:initialize": () => this.beforePackageInitialize(),
38
- "after:package:package": () => this.afterPackage(),
39
- "before:deploy:deploy": () => this.beforeDeploy(),
25
+ 'before:package:initialize': () => this.beforePackageInitialize(),
26
+ 'after:package:package': () => this.afterPackage(),
27
+ 'before:deploy:deploy': () => this.beforeDeploy(),
40
28
  };
41
29
  }
42
- /**
43
- * Asynchronous initialization for offline mode
44
- * Creates SQS queues in localstack for local development
45
- * @returns {Promise<void>}
46
- */
30
+
47
31
  async asyncInit() {
48
- this.serverless.cli.log("Initializing Frigg Serverless Plugin...");
49
- console.log("Hello from Frigg Serverless Plugin!");
50
-
51
- // CRITICAL: Create .esbuild/.serverless directory before serverless-esbuild needs it
52
- // This prevents ENOENT errors during packaging
32
+ this.serverless.cli.log('Initializing Frigg Serverless Plugin...');
33
+ console.log('Hello from Frigg Serverless Plugin!');
34
+
53
35
  const fs = require('fs');
54
36
  const path = require('path');
55
- const esbuildDir = path.join(this.serverless.config.servicePath || process.cwd(), '.esbuild', '.serverless');
37
+ const basePath = this.serverless.config.servicePath || process.cwd();
56
38
 
57
- if (!fs.existsSync(esbuildDir)) {
58
- fs.mkdirSync(esbuildDir, { recursive: true });
59
- console.log(`✓ Created ${esbuildDir} directory for serverless-esbuild`);
60
- }
61
-
62
- if (this.serverless.processedInput.commands.includes("offline")) {
63
- console.log("Running in offline mode. Making queues!");
64
- const queues = Object.keys(this.serverless.service.custom)
65
- .filter((key) => key.endsWith("Queue"))
66
- .map((key) => {
67
- return {
68
- key,
69
- name: this.serverless.service.custom[key],
70
- };
71
- });
72
- console.log("Queues to be created:", queues);
73
-
74
- const AWS = require("aws-sdk");
75
-
76
- const endpointUrl = process.env.AWS_ENDPOINT || "http://localhost:4566"; // LocalStack SQS endpoint
77
- const region = process.env.AWS_REGION || "us-east-1";
78
- const accessKeyId = process.env.AWS_ACCESS_KEY_ID || "root"; // LocalStack default
79
- const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY || "root"; // LocalStack default
80
-
81
- // Configure AWS SDK for LocalStack
82
- AWS.config.update({
83
- region: region,
84
- endpoint: endpointUrl,
85
- accessKeyId: accessKeyId,
86
- secretAccessKey: secretAccessKey,
87
- s3ForcePathStyle: true, // Required for LocalStack
88
- sslEnabled: false, // Disable SSL for LocalStack
89
- });
90
-
91
- const sqs = new AWS.SQS({
92
- sslEnabled: false, // Disable SSL validation for LocalStack
93
- });
94
- // Find the environment variables that we need to override and create an easy map
95
- const environmentMap = {};
96
- const environment = this.serverless.service.provider.environment;
97
-
98
- for (const [key, value] of Object.entries(environment)) {
99
- if (typeof value === "object" && value.Ref) {
100
- environmentMap[value.Ref] = key;
101
- }
102
- }
103
-
104
- const queueCreationPromises = queues.map((queue) => {
105
- return new Promise((resolve, reject) => {
106
- const params = {
107
- QueueName: queue.name,
108
- };
109
-
110
- sqs.createQueue(params, (err, data) => {
111
- if (err) {
112
- console.error(
113
- `Error creating queue ${queue.name}: ${err.message}`
114
- );
115
- reject(err);
116
- } else {
117
- const queueUrl = data.QueueUrl;
118
- console.log(
119
- `Queue ${queue.name} created successfully. URL: ${queueUrl}`
120
- );
121
-
122
- const environmentKey = environmentMap[queue.key];
123
- this.serverless.extendConfiguration(
124
- ["provider", "environment", environmentKey],
125
- queueUrl
126
- );
127
- console.log(`Set ${environmentKey} to ${queueUrl}`);
128
- resolve(queueUrl);
129
- }
130
- });
131
- });
132
- });
133
-
134
- await Promise.all(queueCreationPromises);
39
+ const dirManager = new EsbuildDirectoryManager(fs, path);
40
+ const esbuildDir = dirManager.ensureDirectory(basePath);
41
+
42
+ if (this.serverless.processedInput.commands.includes('offline')) {
43
+ console.log('Running in offline mode. Making queues!');
44
+ await this.setupOfflineQueues();
135
45
  } else {
136
- console.log("Running in online mode, doing nothing");
46
+ console.log('Running in online mode, doing nothing');
137
47
  }
138
48
  }
139
49
 
140
- /**
141
- * Hook that runs before serverless package initialization
142
- * AWS discovery is now handled in serverless-template.js
143
- * @returns {Promise<void>}
144
- */
50
+ async setupOfflineQueues() {
51
+ const queues = this.extractQueueDefinitions();
52
+ console.log('Queues to be created:', queues);
53
+
54
+ const sqsClient = this.createLocalStackSQSClient();
55
+ const queueService = new LocalStackQueueService(sqsClient);
56
+ const mapper = new QueueEnvironmentMapper();
57
+
58
+ const environmentMap = mapper.createMapping(queues);
59
+ const createdQueues = await queueService.createQueues(queues);
60
+
61
+ createdQueues.forEach(({ key, url }) => {
62
+ const envKey = mapper.getEnvironmentKey(key, environmentMap);
63
+ this.serverless.extendConfiguration(['provider', 'environment', envKey], url);
64
+ console.log(`Set ${envKey} to ${url}`);
65
+ });
66
+ }
67
+
68
+ extractQueueDefinitions() {
69
+ return Object.keys(this.serverless.service.custom)
70
+ .filter((key) => key.endsWith('Queue'))
71
+ .map((key) => ({
72
+ key,
73
+ name: this.serverless.service.custom[key],
74
+ }));
75
+ }
76
+
77
+ createLocalStackSQSClient() {
78
+ const AWS = require('aws-sdk');
79
+
80
+ AWS.config.update({
81
+ region: process.env.AWS_REGION || 'us-east-1',
82
+ endpoint: process.env.AWS_ENDPOINT || 'http://localhost:4566',
83
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID || 'root',
84
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || 'root',
85
+ s3ForcePathStyle: true,
86
+ sslEnabled: false,
87
+ });
88
+
89
+ return new AWS.SQS({ sslEnabled: false });
90
+ }
91
+
145
92
  async beforePackageInitialize() {
146
- // AWS discovery is now handled directly in serverless-template.js
147
- // This hook remains for potential future use or other pre-package tasks
148
- this.serverless.cli.log("Frigg Serverless Plugin: Pre-package hook");
93
+ this.serverless.cli.log('Frigg Serverless Plugin: Pre-package hook');
149
94
 
150
- // Ensure .esbuild/.serverless directory exists to prevent ENOENT errors
151
- // serverless-esbuild may try to access this directory during packaging
152
95
  const fs = require('fs');
153
96
  const path = require('path');
154
- const esbuildDir = path.join(this.serverless.config.servicePath || process.cwd(), '.esbuild', '.serverless');
97
+ const basePath = this.serverless.config.servicePath || process.cwd();
155
98
 
156
- if (!fs.existsSync(esbuildDir)) {
157
- fs.mkdirSync(esbuildDir, { recursive: true });
158
- }
99
+ const dirManager = new EsbuildDirectoryManager(fs, path);
100
+ dirManager.ensureDirectory(basePath);
159
101
  }
160
102
 
161
-
162
- /**
163
- * Initialization hook - runs very early, before packaging
164
- * Create .esbuild/.serverless directory to prevent ENOENT errors
165
- */
166
103
  init() {
167
- // Ensure .esbuild/.serverless directory exists to prevent ENOENT errors
168
- // serverless-esbuild may try to access this directory during packaging
169
104
  const fs = require('fs');
170
105
  const path = require('path');
171
- const esbuildDir = path.join(this.serverless.config.servicePath || process.cwd(), '.esbuild', '.serverless');
106
+ const basePath = this.serverless.config.servicePath || process.cwd();
172
107
 
173
- if (!fs.existsSync(esbuildDir)) {
174
- fs.mkdirSync(esbuildDir, { recursive: true });
175
- console.log(`Created ${esbuildDir} directory for serverless-esbuild`);
176
- }
108
+ const dirManager = new EsbuildDirectoryManager(fs, path);
109
+ const esbuildDir = dirManager.ensureDirectory(basePath);
110
+ console.log(`Created ${esbuildDir} directory for serverless-esbuild`);
177
111
  }
178
- /**
179
- * Hook that runs after serverless package
180
- */
112
+
181
113
  afterPackage() {
182
- console.log("After package hook called");
183
- // // const queues = Object.keys(infrastructure.custom)
184
- // // .filter((key) => key.endsWith('Queue'))
185
- // // .map((key) => infrastructure.custom[key]);
186
- // // console.log('Queues to be created:', queues);
187
- // //
188
- // // const endpointUrl = 'http://localhost:4566'; // Assuming localstack is running on port 4
189
- // // const region = 'us-east-1';
190
- // // const command = 'aws';
191
- // // queues.forEach((queue) => {
192
- // // const args = [
193
- // // '--endpoint-url',
194
- // // endpointUrl,
195
- // // 'sqs',
196
- // // 'create-queue',
197
- // // '--queue-name',
198
- // // queue,
199
- // // '--region',
200
- // // region,
201
- // // '--output',
202
- // // 'table',
203
- // // ];
204
- // //
205
- // // const childProcess = spawn(command, args, {
206
- // // cwd: backendPath,
207
- // // stdio: 'inherit',
208
- // // });
209
- // // childProcess.on('error', (error) => {
210
- // // console.error(`Error executing command: ${error.message}`);
211
- // // });
212
- // //
213
- // // childProcess.on('close', (code) => {
214
- // // if (code !== 0) {
215
- // // console.log(`Child process exited with code ${code}`);
216
- // // }
217
- // // });
218
- // });
114
+ console.log('After package hook called');
219
115
  }
220
- /**
221
- * Hook that runs before serverless deploy
222
- */
116
+
223
117
  beforeDeploy() {
224
- console.log("Before deploy hook called");
118
+ console.log('Before deploy hook called');
225
119
  }
226
120
  }
227
121
 
package/index.test.js CHANGED
@@ -1,307 +1,227 @@
1
1
  const FriggServerlessPlugin = require('./index');
2
- const fs = require('fs');
3
- const path = require('path');
4
2
 
5
- // Mock fs module
6
- jest.mock('fs');
3
+ jest.mock('./lib/queue-environment-mapper');
4
+ jest.mock('./lib/localstack-queue-service');
5
+ jest.mock('./lib/esbuild-directory-manager');
6
+
7
+ const { QueueEnvironmentMapper } = require('./lib/queue-environment-mapper');
8
+ const { LocalStackQueueService } = require('./lib/localstack-queue-service');
9
+ const { EsbuildDirectoryManager } = require('./lib/esbuild-directory-manager');
7
10
 
8
11
  describe('FriggServerlessPlugin', () => {
9
- let plugin;
10
- let mockServerless;
11
- let mockOptions;
12
- let mockServicePath;
13
-
14
- beforeEach(() => {
15
- mockServicePath = '/test/service/path';
16
-
17
- mockServerless = {
18
- config: {
19
- servicePath: mockServicePath,
20
- },
21
- cli: {
22
- log: jest.fn(),
23
- },
24
- service: {
25
- custom: {},
26
- provider: {
27
- environment: {},
28
- },
29
- },
30
- processedInput: {
31
- commands: [],
32
- },
33
- getProvider: jest.fn().mockReturnValue({}),
34
- extendConfiguration: jest.fn(),
35
- };
36
-
37
- mockOptions = {
38
- stage: 'test',
39
- };
40
-
41
- // Clear all mocks before each test
42
- jest.clearAllMocks();
12
+ let plugin;
13
+ let mockServerless;
14
+ let mockOptions;
15
+
16
+ beforeEach(() => {
17
+ mockServerless = {
18
+ config: { servicePath: '/test/path' },
19
+ cli: { log: jest.fn() },
20
+ service: {
21
+ custom: {},
22
+ provider: { environment: {} },
23
+ },
24
+ processedInput: { commands: [] },
25
+ getProvider: jest.fn().mockReturnValue({}),
26
+ extendConfiguration: jest.fn(),
27
+ };
28
+
29
+ mockOptions = { stage: 'test' };
30
+
31
+ EsbuildDirectoryManager.mockImplementation(() => ({
32
+ ensureDirectory: jest.fn().mockReturnValue('/test/path/.esbuild/.serverless'),
33
+ }));
34
+
35
+ jest.clearAllMocks();
36
+ });
37
+
38
+ describe('Constructor', () => {
39
+ it('should initialize with serverless instance and options', () => {
40
+ plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
41
+
42
+ expect(plugin.serverless).toBe(mockServerless);
43
+ expect(plugin.options).toBe(mockOptions);
44
+ expect(plugin.hooks).toBeDefined();
43
45
  });
44
46
 
45
- describe('Constructor', () => {
46
- it('should initialize with serverless instance and options', () => {
47
- plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
48
-
49
- expect(plugin.serverless).toBe(mockServerless);
50
- expect(plugin.options).toBe(mockOptions);
51
- expect(plugin.hooks).toBeDefined();
52
- });
47
+ it('should register required hooks', () => {
48
+ plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
53
49
 
54
- it('should register required hooks', () => {
55
- plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
56
-
57
- expect(plugin.hooks).toHaveProperty('initialize');
58
- expect(plugin.hooks).toHaveProperty('before:package:initialize');
59
- expect(plugin.hooks).toHaveProperty('after:package:package');
60
- expect(plugin.hooks).toHaveProperty('before:deploy:deploy');
61
- });
50
+ expect(plugin.hooks).toHaveProperty('initialize');
51
+ expect(plugin.hooks).toHaveProperty('before:package:initialize');
52
+ expect(plugin.hooks).toHaveProperty('after:package:package');
53
+ expect(plugin.hooks).toHaveProperty('before:deploy:deploy');
62
54
  });
63
55
 
64
- describe('asyncInit - Directory Creation', () => {
65
- it('should create .esbuild/.serverless directory if it does not exist', async () => {
66
- plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
67
-
68
- // Mock fs.existsSync to return false (directory doesn't exist)
69
- fs.existsSync.mockReturnValue(false);
70
- fs.mkdirSync.mockImplementation(() => { });
71
-
72
- // Spy on console.log
73
- const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
74
-
75
- await plugin.asyncInit();
76
-
77
- const expectedPath = path.join(mockServicePath, '.esbuild', '.serverless');
78
-
79
- // Verify directory existence check
80
- expect(fs.existsSync).toHaveBeenCalledWith(expectedPath);
81
-
82
- // Verify directory creation
83
- expect(fs.mkdirSync).toHaveBeenCalledWith(expectedPath, { recursive: true });
84
-
85
- // Verify success message
86
- expect(consoleLogSpy).toHaveBeenCalledWith(
87
- expect.stringContaining('Created')
88
- );
89
- expect(consoleLogSpy).toHaveBeenCalledWith(
90
- expect.stringContaining('.esbuild')
91
- );
92
-
93
- consoleLogSpy.mockRestore();
94
- });
95
-
96
- it('should not create directory if it already exists', async () => {
97
- plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
98
-
99
- // Mock fs.existsSync to return true (directory exists)
100
- fs.existsSync.mockReturnValue(true);
101
- fs.mkdirSync.mockImplementation(() => { });
102
-
103
- const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
104
-
105
- await plugin.asyncInit();
106
-
107
- const expectedPath = path.join(mockServicePath, '.esbuild', '.serverless');
108
-
109
- // Verify directory existence check
110
- expect(fs.existsSync).toHaveBeenCalledWith(expectedPath);
111
-
112
- // Verify directory creation was NOT called
113
- expect(fs.mkdirSync).not.toHaveBeenCalled();
114
-
115
- // Verify success message was NOT logged
116
- expect(consoleLogSpy).not.toHaveBeenCalledWith(
117
- expect.stringContaining('Created')
118
- );
119
-
120
- consoleLogSpy.mockRestore();
121
- });
122
-
123
- it('should use process.cwd() if servicePath is not available', async () => {
124
- // Remove servicePath from config
125
- mockServerless.config.servicePath = undefined;
126
-
127
- plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
128
-
129
- fs.existsSync.mockReturnValue(false);
130
- fs.mkdirSync.mockImplementation(() => { });
131
-
132
- await plugin.asyncInit();
133
-
134
- const expectedPath = path.join(process.cwd(), '.esbuild', '.serverless');
135
-
136
- expect(fs.existsSync).toHaveBeenCalledWith(expectedPath);
137
- expect(fs.mkdirSync).toHaveBeenCalledWith(expectedPath, { recursive: true });
138
- });
56
+ it('should create esbuild directory on construction', () => {
57
+ const mockEnsureDir = jest.fn().mockReturnValue('/test/.esbuild/.serverless');
58
+ EsbuildDirectoryManager.mockImplementation(() => ({
59
+ ensureDirectory: mockEnsureDir,
60
+ }));
139
61
 
140
- it('should log initialization messages', async () => {
141
- plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
62
+ plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
142
63
 
143
- fs.existsSync.mockReturnValue(true);
144
-
145
- const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
146
-
147
- await plugin.asyncInit();
148
-
149
- // Verify plugin initialization messages
150
- expect(mockServerless.cli.log).toHaveBeenCalledWith('Initializing Frigg Serverless Plugin...');
151
- expect(consoleLogSpy).toHaveBeenCalledWith('Hello from Frigg Serverless Plugin!');
152
-
153
- consoleLogSpy.mockRestore();
154
- });
64
+ expect(mockEnsureDir).toHaveBeenCalledWith('/test/path');
155
65
  });
66
+ });
156
67
 
157
- describe('asyncInit - Offline Mode', () => {
158
- it('should not create SQS queues when not in offline mode', async () => {
159
- plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
160
-
161
- fs.existsSync.mockReturnValue(true);
162
-
163
- // Not in offline mode
164
- mockServerless.processedInput.commands = ['deploy'];
165
-
166
- const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
68
+ describe('asyncInit', () => {
69
+ it('should log initialization messages', async () => {
70
+ plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
71
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
167
72
 
168
- await plugin.asyncInit();
73
+ await plugin.asyncInit();
169
74
 
170
- expect(consoleLogSpy).toHaveBeenCalledWith('Running in online mode, doing nothing');
171
- expect(consoleLogSpy).not.toHaveBeenCalledWith(
172
- expect.stringContaining('offline mode')
173
- );
75
+ expect(mockServerless.cli.log).toHaveBeenCalledWith('Initializing Frigg Serverless Plugin...');
76
+ expect(consoleLogSpy).toHaveBeenCalledWith('Hello from Frigg Serverless Plugin!');
174
77
 
175
- consoleLogSpy.mockRestore();
176
- });
177
-
178
- it('should create SQS queues when in offline mode', async () => {
179
- plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
180
-
181
- fs.existsSync.mockReturnValue(true);
182
-
183
- // Set offline mode
184
- mockServerless.processedInput.commands = ['offline'];
185
- mockServerless.service.custom = {
186
- testQueue: 'test-queue-name',
187
- };
188
-
189
- // Mock AWS SDK
190
- const mockCreateQueue = jest.fn((params, callback) => {
191
- callback(null, { QueueUrl: 'http://localhost:4566/queue/test-queue-name' });
192
- });
193
-
194
- jest.mock('aws-sdk', () => ({
195
- SQS: jest.fn(() => ({
196
- createQueue: mockCreateQueue,
197
- })),
198
- config: {
199
- update: jest.fn(),
200
- },
201
- }));
78
+ consoleLogSpy.mockRestore();
79
+ });
202
80
 
203
- const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
81
+ it('should run in online mode when not offline', async () => {
82
+ plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
83
+ mockServerless.processedInput.commands = ['deploy'];
204
84
 
205
- await plugin.asyncInit();
85
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
206
86
 
207
- expect(consoleLogSpy).toHaveBeenCalledWith(
208
- expect.stringContaining('offline mode')
209
- );
87
+ await plugin.asyncInit();
210
88
 
211
- consoleLogSpy.mockRestore();
212
- });
89
+ expect(consoleLogSpy).toHaveBeenCalledWith('Running in online mode, doing nothing');
90
+ consoleLogSpy.mockRestore();
213
91
  });
214
92
 
215
- describe('beforePackageInitialize', () => {
216
- it('should create .esbuild/.serverless directory', () => {
217
- plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
93
+ it('should setup offline queues when in offline mode', async () => {
94
+ plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
95
+ mockServerless.processedInput.commands = ['offline'];
96
+ mockServerless.service.custom = { AsanaQueue: 'test-queue' };
218
97
 
219
- fs.existsSync.mockReturnValue(false);
220
- fs.mkdirSync.mockImplementation(() => { });
98
+ const mockMapper = {
99
+ createMapping: jest.fn().mockReturnValue({ AsanaQueue: 'ASANA_QUEUE_URL' }),
100
+ getEnvironmentKey: jest.fn().mockReturnValue('ASANA_QUEUE_URL'),
101
+ };
102
+ QueueEnvironmentMapper.mockImplementation(() => mockMapper);
221
103
 
222
- plugin.beforePackageInitialize();
104
+ const mockQueueService = {
105
+ createQueues: jest.fn().mockResolvedValue([
106
+ { key: 'AsanaQueue', url: 'http://localhost:4566/queue' },
107
+ ]),
108
+ };
109
+ LocalStackQueueService.mockImplementation(() => mockQueueService);
223
110
 
224
- const expectedPath = path.join(mockServicePath, '.esbuild', '.serverless');
111
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
225
112
 
226
- expect(fs.existsSync).toHaveBeenCalledWith(expectedPath);
227
- expect(fs.mkdirSync).toHaveBeenCalledWith(expectedPath, { recursive: true });
228
- });
113
+ await plugin.asyncInit();
229
114
 
230
- it('should log pre-package hook message', () => {
231
- plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
115
+ expect(consoleLogSpy).toHaveBeenCalledWith('Running in offline mode. Making queues!');
116
+ expect(mockQueueService.createQueues).toHaveBeenCalled();
117
+ expect(mockServerless.extendConfiguration).toHaveBeenCalledWith(
118
+ ['provider', 'environment', 'ASANA_QUEUE_URL'],
119
+ 'http://localhost:4566/queue'
120
+ );
232
121
 
233
- fs.existsSync.mockReturnValue(true);
234
-
235
- plugin.beforePackageInitialize();
236
-
237
- expect(mockServerless.cli.log).toHaveBeenCalledWith('Frigg Serverless Plugin: Pre-package hook');
238
- });
122
+ consoleLogSpy.mockRestore();
123
+ });
124
+ });
125
+
126
+ describe('setupOfflineQueues', () => {
127
+ it('should orchestrate queue creation and environment configuration', async () => {
128
+ plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
129
+ mockServerless.service.custom = {
130
+ AsanaQueue: 'test-asana-queue',
131
+ SlackQueue: 'test-slack-queue',
132
+ };
133
+
134
+ const mockMapper = {
135
+ createMapping: jest.fn().mockReturnValue({
136
+ AsanaQueue: 'ASANA_QUEUE_URL',
137
+ SlackQueue: 'SLACK_QUEUE_URL',
138
+ }),
139
+ getEnvironmentKey: jest.fn()
140
+ .mockReturnValueOnce('ASANA_QUEUE_URL')
141
+ .mockReturnValueOnce('SLACK_QUEUE_URL'),
142
+ };
143
+ QueueEnvironmentMapper.mockImplementation(() => mockMapper);
144
+
145
+ const mockQueueService = {
146
+ createQueues: jest.fn().mockResolvedValue([
147
+ { key: 'AsanaQueue', url: 'http://localhost:4566/asana' },
148
+ { key: 'SlackQueue', url: 'http://localhost:4566/slack' },
149
+ ]),
150
+ };
151
+ LocalStackQueueService.mockImplementation(() => mockQueueService);
152
+
153
+ await plugin.setupOfflineQueues();
154
+
155
+ expect(mockMapper.createMapping).toHaveBeenCalled();
156
+ expect(mockQueueService.createQueues).toHaveBeenCalled();
157
+ expect(mockServerless.extendConfiguration).toHaveBeenCalledTimes(2);
158
+ });
159
+ });
160
+
161
+ describe('extractQueueDefinitions', () => {
162
+ it('should extract queue definitions from custom config', () => {
163
+ plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
164
+ mockServerless.service.custom = {
165
+ AsanaQueue: 'test-asana-queue',
166
+ someOtherConfig: 'something-else',
167
+ SlackQueue: 'test-slack-queue',
168
+ };
169
+
170
+ const queues = plugin.extractQueueDefinitions();
171
+
172
+ expect(queues).toEqual([
173
+ { key: 'AsanaQueue', name: 'test-asana-queue' },
174
+ { key: 'SlackQueue', name: 'test-slack-queue' },
175
+ ]);
239
176
  });
240
177
 
241
- describe('init', () => {
242
- it('should create .esbuild/.serverless directory', () => {
243
- plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
244
-
245
- fs.existsSync.mockReturnValue(false);
246
- fs.mkdirSync.mockImplementation(() => { });
178
+ it('should return empty array when no queues defined', () => {
179
+ plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
180
+ mockServerless.service.custom = { someConfig: 'value' };
247
181
 
248
- const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
182
+ const queues = plugin.extractQueueDefinitions();
249
183
 
250
- plugin.init();
184
+ expect(queues).toEqual([]);
185
+ });
186
+ });
251
187
 
252
- const expectedPath = path.join(mockServicePath, '.esbuild', '.serverless');
188
+ describe('Hooks', () => {
189
+ it('should execute init hook', () => {
190
+ plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
191
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
253
192
 
254
- expect(fs.existsSync).toHaveBeenCalledWith(expectedPath);
255
- expect(fs.mkdirSync).toHaveBeenCalledWith(expectedPath, { recursive: true });
256
- expect(consoleLogSpy).toHaveBeenCalledWith(
257
- expect.stringContaining('Created')
258
- );
193
+ plugin.init();
259
194
 
260
- consoleLogSpy.mockRestore();
261
- });
195
+ expect(consoleLogSpy).toHaveBeenCalled();
196
+ consoleLogSpy.mockRestore();
262
197
  });
263
198
 
264
- describe('afterPackage', () => {
265
- it('should log after package hook message', () => {
266
- plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
199
+ it('should execute beforePackageInitialize hook', async () => {
200
+ plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
267
201
 
268
- const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
202
+ await plugin.beforePackageInitialize();
269
203
 
270
- plugin.afterPackage();
271
-
272
- expect(consoleLogSpy).toHaveBeenCalledWith('After package hook called');
273
-
274
- consoleLogSpy.mockRestore();
275
- });
204
+ expect(mockServerless.cli.log).toHaveBeenCalledWith('Frigg Serverless Plugin: Pre-package hook');
276
205
  });
277
206
 
278
- describe('beforeDeploy', () => {
279
- it('should log before deploy hook message', () => {
280
- plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
281
-
282
- const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
207
+ it('should execute afterPackage hook', () => {
208
+ plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
209
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
283
210
 
284
- plugin.beforeDeploy();
211
+ plugin.afterPackage();
285
212
 
286
- expect(consoleLogSpy).toHaveBeenCalledWith('Before deploy hook called');
287
-
288
- consoleLogSpy.mockRestore();
289
- });
213
+ expect(consoleLogSpy).toHaveBeenCalledWith('After package hook called');
214
+ consoleLogSpy.mockRestore();
290
215
  });
291
216
 
292
- describe('Error Handling', () => {
293
- it('should handle fs.mkdirSync errors gracefully', async () => {
294
- plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
217
+ it('should execute beforeDeploy hook', () => {
218
+ plugin = new FriggServerlessPlugin(mockServerless, mockOptions);
219
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
295
220
 
296
- fs.existsSync.mockReturnValue(false);
297
- fs.mkdirSync.mockImplementation(() => {
298
- throw new Error('Permission denied');
299
- });
221
+ plugin.beforeDeploy();
300
222
 
301
- // Should not throw - error should be caught or allowed to propagate
302
- await expect(plugin.asyncInit()).rejects.toThrow('Permission denied');
303
- });
223
+ expect(consoleLogSpy).toHaveBeenCalledWith('Before deploy hook called');
224
+ consoleLogSpy.mockRestore();
304
225
  });
226
+ });
305
227
  });
306
-
307
-
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Infrastructure Service - ESBuild Directory Management
3
+ *
4
+ * Ensures .esbuild/.serverless directory exists to prevent ENOENT errors.
5
+ */
6
+ class EsbuildDirectoryManager {
7
+ constructor(fs, path) {
8
+ this.fs = fs;
9
+ this.path = path;
10
+ }
11
+
12
+ /**
13
+ * @param {string} basePath - Base service path
14
+ * @returns {string} Created directory path
15
+ */
16
+ ensureDirectory(basePath) {
17
+ const esbuildDir = this.path.join(basePath, '.esbuild', '.serverless');
18
+
19
+ if (!this.fs.existsSync(esbuildDir)) {
20
+ this.fs.mkdirSync(esbuildDir, { recursive: true });
21
+ }
22
+
23
+ return esbuildDir;
24
+ }
25
+ }
26
+
27
+ module.exports = { EsbuildDirectoryManager };
@@ -0,0 +1,65 @@
1
+ const { EsbuildDirectoryManager } = require('./esbuild-directory-manager');
2
+
3
+ describe('EsbuildDirectoryManager', () => {
4
+ let manager;
5
+ let mockFs;
6
+ let mockPath;
7
+
8
+ beforeEach(() => {
9
+ mockFs = {
10
+ existsSync: jest.fn(),
11
+ mkdirSync: jest.fn(),
12
+ };
13
+ mockPath = {
14
+ join: jest.fn((...args) => args.join('/')),
15
+ };
16
+ manager = new EsbuildDirectoryManager(mockFs, mockPath);
17
+ });
18
+
19
+ describe('ensureDirectory', () => {
20
+ it('should create directory if it does not exist', () => {
21
+ mockFs.existsSync.mockReturnValue(false);
22
+
23
+ const result = manager.ensureDirectory('/test/path');
24
+
25
+ expect(mockPath.join).toHaveBeenCalledWith('/test/path', '.esbuild', '.serverless');
26
+ expect(mockFs.existsSync).toHaveBeenCalledWith('/test/path/.esbuild/.serverless');
27
+ expect(mockFs.mkdirSync).toHaveBeenCalledWith('/test/path/.esbuild/.serverless', {
28
+ recursive: true,
29
+ });
30
+ expect(result).toBe('/test/path/.esbuild/.serverless');
31
+ });
32
+
33
+ it('should not create directory if it already exists', () => {
34
+ mockFs.existsSync.mockReturnValue(true);
35
+
36
+ const result = manager.ensureDirectory('/test/path');
37
+
38
+ expect(mockFs.existsSync).toHaveBeenCalledWith('/test/path/.esbuild/.serverless');
39
+ expect(mockFs.mkdirSync).not.toHaveBeenCalled();
40
+ expect(result).toBe('/test/path/.esbuild/.serverless');
41
+ });
42
+
43
+ it('should handle different base paths', () => {
44
+ mockFs.existsSync.mockReturnValue(false);
45
+
46
+ manager.ensureDirectory('/different/base');
47
+
48
+ expect(mockPath.join).toHaveBeenCalledWith('/different/base', '.esbuild', '.serverless');
49
+ expect(mockFs.mkdirSync).toHaveBeenCalledWith('/different/base/.esbuild/.serverless', {
50
+ recursive: true,
51
+ });
52
+ });
53
+
54
+ it('should propagate fs errors', () => {
55
+ mockFs.existsSync.mockReturnValue(false);
56
+ mockFs.mkdirSync.mockImplementation(() => {
57
+ throw new Error('Permission denied');
58
+ });
59
+
60
+ expect(() => {
61
+ manager.ensureDirectory('/test/path');
62
+ }).toThrow('Permission denied');
63
+ });
64
+ });
65
+ });
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Infrastructure Service - LocalStack Queue Management
3
+ *
4
+ * Handles SQS queue creation in LocalStack for offline development.
5
+ */
6
+ class LocalStackQueueService {
7
+ constructor(sqsClient) {
8
+ this.sqs = sqsClient;
9
+ }
10
+
11
+ /**
12
+ * @param {string} queueName - Name of queue to create
13
+ * @returns {Promise<string>} Queue URL
14
+ */
15
+ async createQueue(queueName) {
16
+ return new Promise((resolve, reject) => {
17
+ this.sqs.createQueue({ QueueName: queueName }, (err, data) => {
18
+ if (err) {
19
+ reject(new Error(`Failed to create queue ${queueName}: ${err.message}`));
20
+ } else {
21
+ resolve(data.QueueUrl);
22
+ }
23
+ });
24
+ });
25
+ }
26
+
27
+ /**
28
+ * @param {Array<{key: string, name: string}>} queues - Queue definitions
29
+ * @returns {Promise<Array<{key: string, url: string}>>} Created queues with URLs
30
+ */
31
+ async createQueues(queues) {
32
+ const results = await Promise.all(
33
+ queues.map(async (queue) => {
34
+ const url = await this.createQueue(queue.name);
35
+ console.log(`Queue ${queue.name} created successfully. URL: ${url}`);
36
+ return { key: queue.key, url };
37
+ })
38
+ );
39
+
40
+ return results;
41
+ }
42
+ }
43
+
44
+ module.exports = { LocalStackQueueService };
@@ -0,0 +1,94 @@
1
+ const { LocalStackQueueService } = require('./localstack-queue-service');
2
+
3
+ describe('LocalStackQueueService', () => {
4
+ let service;
5
+ let mockSQS;
6
+
7
+ beforeEach(() => {
8
+ mockSQS = {
9
+ createQueue: jest.fn(),
10
+ };
11
+ service = new LocalStackQueueService(mockSQS);
12
+ });
13
+
14
+ describe('createQueue', () => {
15
+ it('should create queue and return URL on success', async () => {
16
+ const queueUrl = 'http://localhost:4566/000000000000/test-queue';
17
+ mockSQS.createQueue.mockImplementation((params, callback) => {
18
+ callback(null, { QueueUrl: queueUrl });
19
+ });
20
+
21
+ const result = await service.createQueue('test-queue');
22
+
23
+ expect(result).toBe(queueUrl);
24
+ expect(mockSQS.createQueue).toHaveBeenCalledWith(
25
+ { QueueName: 'test-queue' },
26
+ expect.any(Function)
27
+ );
28
+ });
29
+
30
+ it('should reject with error on failure', async () => {
31
+ const error = new Error('SQS Error');
32
+ mockSQS.createQueue.mockImplementation((params, callback) => {
33
+ callback(error);
34
+ });
35
+
36
+ await expect(service.createQueue('test-queue')).rejects.toThrow(
37
+ 'Failed to create queue test-queue: SQS Error'
38
+ );
39
+ });
40
+ });
41
+
42
+ describe('createQueues', () => {
43
+ it('should create multiple queues and return results', async () => {
44
+ mockSQS.createQueue.mockImplementation((params, callback) => {
45
+ const url = `http://localhost:4566/000000000000/${params.QueueName}`;
46
+ callback(null, { QueueUrl: url });
47
+ });
48
+
49
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
50
+
51
+ const queues = [
52
+ { key: 'AsanaQueue', name: 'test-asana-queue' },
53
+ { key: 'SlackQueue', name: 'test-slack-queue' },
54
+ ];
55
+
56
+ const results = await service.createQueues(queues);
57
+
58
+ expect(results).toHaveLength(2);
59
+ expect(results[0]).toEqual({
60
+ key: 'AsanaQueue',
61
+ url: 'http://localhost:4566/000000000000/test-asana-queue',
62
+ });
63
+ expect(results[1]).toEqual({
64
+ key: 'SlackQueue',
65
+ url: 'http://localhost:4566/000000000000/test-slack-queue',
66
+ });
67
+
68
+ expect(consoleLogSpy).toHaveBeenCalledTimes(2);
69
+ consoleLogSpy.mockRestore();
70
+ });
71
+
72
+ it('should handle empty queue array', async () => {
73
+ const results = await service.createQueues([]);
74
+ expect(results).toEqual([]);
75
+ });
76
+
77
+ it('should reject if any queue creation fails', async () => {
78
+ mockSQS.createQueue.mockImplementation((params, callback) => {
79
+ if (params.QueueName === 'failing-queue') {
80
+ callback(new Error('Failed'));
81
+ } else {
82
+ callback(null, { QueueUrl: 'http://localhost:4566/queue' });
83
+ }
84
+ });
85
+
86
+ const queues = [
87
+ { key: 'SuccessQueue', name: 'success-queue' },
88
+ { key: 'FailQueue', name: 'failing-queue' },
89
+ ];
90
+
91
+ await expect(service.createQueues(queues)).rejects.toThrow('Failed to create queue failing-queue');
92
+ });
93
+ });
94
+ });
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Domain Service - Queue to Environment Variable Mapper
3
+ *
4
+ * Maps queue keys to environment variable names using naming convention.
5
+ * Pattern: AsanaQueue -> ASANA_QUEUE_URL
6
+ */
7
+ class QueueEnvironmentMapper {
8
+ /**
9
+ * @param {Array<{key: string, name: string}>} queues - Queue definitions
10
+ * @returns {Object} Map of queue keys to environment variable names
11
+ */
12
+ createMapping(queues) {
13
+ const mapping = {};
14
+
15
+ queues.forEach(queue => {
16
+ const baseName = queue.key.replace(/Queue$/, '');
17
+ const envVarName = `${baseName.toUpperCase()}_QUEUE_URL`;
18
+ mapping[queue.key] = envVarName;
19
+ });
20
+
21
+ return mapping;
22
+ }
23
+
24
+ /**
25
+ * @param {string} queueKey - Queue key to look up
26
+ * @param {Object} mapping - Environment variable mapping
27
+ * @returns {string} Environment variable name
28
+ * @throws {Error} If no mapping found
29
+ */
30
+ getEnvironmentKey(queueKey, mapping) {
31
+ const envKey = mapping[queueKey];
32
+
33
+ if (!envKey) {
34
+ throw new Error(
35
+ `No environment variable mapping found for queue "${queueKey}". ` +
36
+ `Expected pattern: {QueueName}Queue -> {QUEUENAME}_QUEUE_URL`
37
+ );
38
+ }
39
+
40
+ return envKey;
41
+ }
42
+ }
43
+
44
+ module.exports = { QueueEnvironmentMapper };
@@ -0,0 +1,74 @@
1
+ const { QueueEnvironmentMapper } = require('./queue-environment-mapper');
2
+
3
+ describe('QueueEnvironmentMapper', () => {
4
+ let mapper;
5
+
6
+ beforeEach(() => {
7
+ mapper = new QueueEnvironmentMapper();
8
+ });
9
+
10
+ describe('createMapping', () => {
11
+ it('should map single queue key to environment variable name', () => {
12
+ const queues = [{ key: 'AsanaQueue', name: 'test-asana-queue' }];
13
+ const result = mapper.createMapping(queues);
14
+
15
+ expect(result).toEqual({
16
+ AsanaQueue: 'ASANA_QUEUE_URL',
17
+ });
18
+ });
19
+
20
+ it('should map multiple queue keys to environment variable names', () => {
21
+ const queues = [
22
+ { key: 'AsanaQueue', name: 'test-asana-queue' },
23
+ { key: 'SlackQueue', name: 'test-slack-queue' },
24
+ { key: 'HubspotQueue', name: 'test-hubspot-queue' },
25
+ ];
26
+ const result = mapper.createMapping(queues);
27
+
28
+ expect(result).toEqual({
29
+ AsanaQueue: 'ASANA_QUEUE_URL',
30
+ SlackQueue: 'SLACK_QUEUE_URL',
31
+ HubspotQueue: 'HUBSPOT_QUEUE_URL',
32
+ });
33
+ });
34
+
35
+ it('should handle empty queue array', () => {
36
+ const result = mapper.createMapping([]);
37
+ expect(result).toEqual({});
38
+ });
39
+
40
+ it('should preserve case in base name conversion', () => {
41
+ const queues = [{ key: 'MyCustomQueue', name: 'test-queue' }];
42
+ const result = mapper.createMapping(queues);
43
+
44
+ expect(result).toEqual({
45
+ MyCustomQueue: 'MYCUSTOM_QUEUE_URL',
46
+ });
47
+ });
48
+ });
49
+
50
+ describe('getEnvironmentKey', () => {
51
+ it('should return environment variable name for valid queue key', () => {
52
+ const mapping = { AsanaQueue: 'ASANA_QUEUE_URL' };
53
+ const result = mapper.getEnvironmentKey('AsanaQueue', mapping);
54
+
55
+ expect(result).toBe('ASANA_QUEUE_URL');
56
+ });
57
+
58
+ it('should throw error for missing queue key', () => {
59
+ const mapping = { AsanaQueue: 'ASANA_QUEUE_URL' };
60
+
61
+ expect(() => {
62
+ mapper.getEnvironmentKey('InvalidQueue', mapping);
63
+ }).toThrow('No environment variable mapping found for queue "InvalidQueue"');
64
+ });
65
+
66
+ it('should throw error with helpful message pattern', () => {
67
+ const mapping = {};
68
+
69
+ expect(() => {
70
+ mapper.getEnvironmentKey('TestQueue', mapping);
71
+ }).toThrow('Expected pattern: {QueueName}Queue -> {QUEUENAME}_QUEUE_URL');
72
+ });
73
+ });
74
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@friggframework/serverless-plugin",
3
- "version": "2.0.0-next.48",
3
+ "version": "2.0.0-next.49",
4
4
  "description": "Plugin to help dynamically load frigg resources",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -11,5 +11,5 @@
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
14
- "gitHead": "5167dd83884504bd0a4a91298208b102ebced687"
14
+ "gitHead": "ab5b2d2e5f0a24bc4af6b3dc0ac592c358638bfa"
15
15
  }