Skip to content

Commit 6d993e4

Browse files
feat: Support (O)SLS nested stack
1 parent c4c94da commit 6d993e4

File tree

20 files changed

+589
-40
lines changed

20 files changed

+589
-40
lines changed

.github/workflows/common-test.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,56 @@ jobs:
380380
- name: Test - observability mode
381381
run: OBSERVABLE_MODE=true npx vitest --retry 2 test/osls-basic.test.ts
382382

383+
test-osls-nested:
384+
runs-on: ubuntu-latest
385+
concurrency:
386+
group: test-osls-nested
387+
steps:
388+
- uses: actions/checkout@v4
389+
- name: Use Node.js
390+
uses: actions/setup-node@v4
391+
with:
392+
node-version: ${{ env.node_version }}
393+
registry-url: 'https://registry.npmjs.org'
394+
- name: Install dependencies
395+
run: |
396+
node prepareForTest.js osls-nested
397+
npm i
398+
- name: Download build artifact
399+
uses: actions/download-artifact@v4
400+
if: ${{ inputs.mode == 'build' }}
401+
with:
402+
name: dist
403+
path: dist
404+
- name: Install lambda-live-debugger globally
405+
if: ${{ inputs.mode == 'global' }}
406+
run: |
407+
npm i lambda-live-debugger@${{ inputs.version || 'latest' }} -g
408+
npm i osls -g
409+
working-directory: test
410+
- name: Install lambda-live-debugger locally
411+
if: ${{ inputs.mode == 'local' }}
412+
run: |
413+
npm i lambda-live-debugger@${{ inputs.version || 'latest' }}
414+
working-directory: test
415+
- name: Configure AWS Credentials
416+
uses: aws-actions/configure-aws-credentials@v4
417+
with:
418+
aws-region: eu-west-1
419+
role-to-assume: ${{ secrets.AWS_ROLE }}
420+
role-session-name: GitHubActions
421+
- name: Destroy
422+
run: npm run destroy
423+
working-directory: test/osls-nested
424+
continue-on-error: true
425+
- name: Deploy
426+
run: npm run deploy
427+
working-directory: test/osls-nested
428+
- name: Test
429+
run: npx vitest --retry 2 test/osls-nested.test.ts
430+
- name: Test - observability mode
431+
run: OBSERVABLE_MODE=true npx vitest --retry 2 test/osls-nested.test.ts
432+
383433
test-osls-esbuild-cjs:
384434
runs-on: ubuntu-latest
385435
concurrency:

.vscode/launch.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,26 @@
170170
"type": "node",
171171
"cwd": "${workspaceRoot}/test/osls-basic"
172172
},
173+
{
174+
"name": "LLDebugger - OSLS nested",
175+
"program": "${workspaceRoot}/node_modules/tsx/dist/cli.mjs",
176+
"args": ["../../src/lldebugger.ts", "--stage=test"],
177+
"request": "launch",
178+
"skipFiles": ["<node_internals>/**"],
179+
"console": "integratedTerminal",
180+
"type": "node",
181+
"cwd": "${workspaceRoot}/test/osls-nested"
182+
},
183+
{
184+
"name": "LLDebugger - OSLS nested - observability",
185+
"program": "${workspaceRoot}/node_modules/tsx/dist/cli.mjs",
186+
"args": ["../../src/lldebugger.ts", "--stage=test", "-o"],
187+
"request": "launch",
188+
"skipFiles": ["<node_internals>/**"],
189+
"console": "integratedTerminal",
190+
"type": "node",
191+
"cwd": "${workspaceRoot}/test/osls-nested"
192+
},
173193
{
174194
"name": "LLDebugger - OSLS EsBuild CJS",
175195
"program": "${workspaceRoot}/node_modules/tsx/dist/cli.mjs",

package-lock.json

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@
6464
"test-sls-esbuild-esm-observable": "npm run build && RUN_TEST_FROM_CLI=true OBSERVABLE_MODE=true vitest run test/sls-esbuild-esm.test.ts",
6565
"test-osls-basic": "npm run build && RUN_TEST_FROM_CLI=true vitest run test/osls-basic.test.ts",
6666
"test-osls-basic-observable": "npm run build && RUN_TEST_FROM_CLI=true OBSERVABLE_MODE=true vitest run test/osls-basic.test.ts",
67+
"test-osls-nested": "npm run build && RUN_TEST_FROM_CLI=true vitest run test/osls-nested.test.ts",
68+
"test-osls-nested-observable": "npm run build && RUN_TEST_FROM_CLI=true OBSERVABLE_MODE=true vitest run test/osls-nested.test.ts",
6769
"test-osls-esbuild-cjs": "npm run build && RUN_TEST_FROM_CLI=true vitest run test/osls-esbuild-cjs.test.ts",
6870
"test-osls-esbuild-cjs-observable": "npm run build && RUN_TEST_FROM_CLI=true OBSERVABLE_MODE=true vitest run test/osls-esbuild-cjs.test.ts",
6971
"test-osls-esbuild-esm": "npm run build && RUN_TEST_FROM_CLI=true vitest run test/osls-esbuild-esm.test.ts",
@@ -160,6 +162,7 @@
160162
"test/sls-esbuild",
161163
"test/sls-esbuild-cjs",
162164
"test/osls-basic",
165+
"test/osls-nested",
163166
"test/osls-esbuild",
164167
"test/osls-esbuild-cjs",
165168
"test/sam-basic",

src/frameworks/slsFramework.ts

Lines changed: 153 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -182,60 +182,173 @@ export class SlsFramework implements IFramework {
182182
config,
183183
);
184184

185+
// Get functions from main configuration
185186
const lambdas = serverless.service.functions;
186187

187188
Logger.verbose(`[SLS] Found Lambdas:`, JSON.stringify(lambdas, null, 2));
188189

190+
// Process main stack functions
189191
for (const func in lambdas) {
190-
const lambda = lambdas[func] as Serverless.FunctionDefinitionHandler;
191-
const handlerFull = lambda.handler;
192-
const handlerParts = handlerFull.split('.');
193-
const handler = handlerParts[1];
194-
195-
const possibleCodePaths = [
196-
`${handlerParts[0]}.ts`,
197-
`${handlerParts[0]}.js`,
198-
`${handlerParts[0]}.cjs`,
199-
`${handlerParts[0]}.mjs`,
200-
];
201-
let codePath: string | undefined;
202-
for (const cp of possibleCodePaths) {
203-
try {
204-
await fs.access(cp, constants.F_OK);
205-
codePath = cp;
206-
break;
207-
} catch {
208-
// ignore, file not found
209-
}
210-
}
192+
const lambdaResource = await this.processFunction(
193+
func,
194+
lambdas[func] as Serverless.FunctionDefinitionHandler,
195+
esBuildOptions,
196+
);
197+
lambdasDiscovered.push(lambdaResource);
198+
}
211199

212-
if (!codePath) {
213-
throw new Error(`Code path not found for handler: ${handlerFull}`);
200+
// Check for nested stacks (serverless-nested-stack plugin)
201+
const nestedStacks = (serverless.service as any).nestedStacks;
202+
if (nestedStacks) {
203+
Logger.verbose(
204+
`[SLS] Found nested stacks configuration:`,
205+
JSON.stringify(nestedStacks, null, 2),
206+
);
207+
208+
const nestedLambdas = await this.parseNestedStacks(
209+
nestedStacks,
210+
esBuildOptions,
211+
);
212+
lambdasDiscovered.push(...nestedLambdas);
213+
}
214+
215+
return lambdasDiscovered;
216+
}
217+
218+
/**
219+
* Process a single Lambda function
220+
*/
221+
private async processFunction(
222+
funcName: string,
223+
lambda: Serverless.FunctionDefinitionHandler,
224+
esBuildOptions: EsBuildOptions | undefined,
225+
): Promise<LambdaResource> {
226+
const handlerFull = lambda.handler;
227+
const handlerParts = handlerFull.split('.');
228+
const handler = handlerParts[1];
229+
230+
const possibleCodePaths = [
231+
`${handlerParts[0]}.ts`,
232+
`${handlerParts[0]}.js`,
233+
`${handlerParts[0]}.cjs`,
234+
`${handlerParts[0]}.mjs`,
235+
];
236+
let codePath: string | undefined;
237+
for (const cp of possibleCodePaths) {
238+
try {
239+
await fs.access(cp, constants.F_OK);
240+
codePath = cp;
241+
break;
242+
} catch {
243+
// ignore, file not found
214244
}
245+
}
215246

216-
const functionName = lambda.name;
217-
if (!functionName) {
218-
throw new Error(`Function name not found for handler: ${handlerFull}`);
247+
if (!codePath) {
248+
throw new Error(`Code path not found for handler: ${handlerFull}`);
249+
}
250+
251+
const functionName = lambda.name;
252+
if (!functionName) {
253+
throw new Error(`Function name not found for handler: ${handlerFull}`);
254+
}
255+
256+
const packageJsonPath = await findPackageJson(codePath);
257+
Logger.verbose(`[SLS] package.json path: ${packageJsonPath}`);
258+
259+
const lambdaResource: LambdaResource = {
260+
functionName,
261+
codePath,
262+
handler,
263+
packageJsonPath,
264+
esBuildOptions,
265+
metadata: {
266+
framework: 'sls',
267+
},
268+
};
269+
270+
return lambdaResource;
271+
}
272+
273+
/**
274+
* Parse nested stacks recursively
275+
*/
276+
private async parseNestedStacks(
277+
nestedStacks: any,
278+
esBuildOptions: EsBuildOptions | undefined,
279+
currentDir: string = process.cwd(),
280+
): Promise<LambdaResource[]> {
281+
const lambdas: LambdaResource[] = [];
282+
283+
for (const stackName in nestedStacks) {
284+
const stackConfig = nestedStacks[stackName];
285+
const templatePath = stackConfig.template;
286+
287+
if (!templatePath) {
288+
Logger.verbose(
289+
`[SLS] Nested stack ${stackName} has no template property`,
290+
);
291+
continue;
219292
}
220293

221-
const packageJsonPath = await findPackageJson(codePath);
222-
Logger.verbose(`[SLS] package.json path: ${packageJsonPath}`);
294+
const resolvedTemplatePath = path.resolve(currentDir, templatePath);
295+
Logger.verbose(
296+
`[SLS] Parsing nested stack ${stackName}: ${resolvedTemplatePath}`,
297+
);
223298

224-
const lambdaResource: LambdaResource = {
225-
functionName,
226-
codePath,
227-
handler,
228-
packageJsonPath,
229-
esBuildOptions,
230-
metadata: {
231-
framework: 'sls',
232-
},
233-
};
299+
try {
300+
const templateContent = await fs.readFile(
301+
resolvedTemplatePath,
302+
'utf-8',
303+
);
304+
const yaml = await import('yaml');
305+
const nestedConfig = yaml.parse(templateContent);
306+
307+
// Process functions in nested stack
308+
if (nestedConfig.functions) {
309+
Logger.verbose(
310+
`[SLS] Found functions in nested stack ${stackName}:`,
311+
JSON.stringify(nestedConfig.functions, null, 2),
312+
);
313+
314+
for (const funcName in nestedConfig.functions) {
315+
const func = nestedConfig.functions[funcName];
316+
const lambdaResource = await this.processFunction(
317+
funcName,
318+
func as Serverless.FunctionDefinitionHandler,
319+
esBuildOptions,
320+
);
321+
lambdas.push(lambdaResource);
322+
}
323+
}
234324

235-
lambdasDiscovered.push(lambdaResource);
325+
// Recursively process nested stacks within this stack
326+
if (nestedConfig.nestedStacks) {
327+
Logger.verbose(
328+
`[SLS] Found nested stacks within ${stackName}:`,
329+
JSON.stringify(nestedConfig.nestedStacks, null, 2),
330+
);
331+
332+
const templateDir = path.dirname(resolvedTemplatePath);
333+
const deeperNestedLambdas = await this.parseNestedStacks(
334+
nestedConfig.nestedStacks,
335+
esBuildOptions,
336+
templateDir,
337+
);
338+
lambdas.push(...deeperNestedLambdas);
339+
}
340+
} catch (err: any) {
341+
Logger.warn(
342+
`[SLS] Could not parse nested stack at ${resolvedTemplatePath}: ${err.message}`,
343+
);
344+
}
236345
}
237346

238-
return lambdasDiscovered;
347+
Logger.verbose(
348+
`[SLS] Finished parsing nested stacks, found ${lambdas.length} Lambda function(s)${lambdas.length > 0 ? `:\n${lambdas.map((l) => ` - ${l.functionName}`).join('\n')}` : ''}`,
349+
);
350+
351+
return lambdas;
239352
}
240353

241354
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
service: lls-osls-basic-nested-nested
2+
frameworkVersion: '3'
3+
4+
provider:
5+
name: aws
6+
runtime: nodejs22.x
7+
region: eu-west-1
8+
9+
functions:
10+
testJsCommonJsNested:
11+
handler: services/testJsCommonJs/lambda.lambdaHandler
12+
13+
testJsEsModuleNested:
14+
handler: services/testJsEsModule/lambda.lambdaHandler

test/osls-basic/nested-stack-sam.yml

Whitespace-only changes.

0 commit comments

Comments
 (0)