Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/workflows/common-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,56 @@ jobs:
- name: Test - observability mode
run: OBSERVABLE_MODE=true npx vitest --retry 2 test/osls-basic.test.ts

test-osls-nested:
runs-on: ubuntu-latest
concurrency:
group: test-osls-nested
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.node_version }}
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: |
node prepareForTest.js osls-nested
npm i
- name: Download build artifact
uses: actions/download-artifact@v4
if: ${{ inputs.mode == 'build' }}
with:
name: dist
path: dist
- name: Install lambda-live-debugger globally
if: ${{ inputs.mode == 'global' }}
run: |
npm i lambda-live-debugger@${{ inputs.version || 'latest' }} -g
npm i osls -g
working-directory: test
- name: Install lambda-live-debugger locally
if: ${{ inputs.mode == 'local' }}
run: |
npm i lambda-live-debugger@${{ inputs.version || 'latest' }}
working-directory: test
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: eu-west-1
role-to-assume: ${{ secrets.AWS_ROLE }}
role-session-name: GitHubActions
- name: Destroy
run: npm run destroy
working-directory: test/osls-nested
continue-on-error: true
- name: Deploy
run: npm run deploy
working-directory: test/osls-nested
- name: Test
run: npx vitest --retry 2 test/osls-nested.test.ts
- name: Test - observability mode
run: OBSERVABLE_MODE=true npx vitest --retry 2 test/osls-nested.test.ts

test-osls-esbuild-cjs:
runs-on: ubuntu-latest
concurrency:
Expand Down
20 changes: 20 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,26 @@
"type": "node",
"cwd": "${workspaceRoot}/test/osls-basic"
},
{
"name": "LLDebugger - OSLS nested",
"program": "${workspaceRoot}/node_modules/tsx/dist/cli.mjs",
"args": ["../../src/lldebugger.ts", "--stage=test"],
"request": "launch",
"skipFiles": ["<node_internals>/**"],
"console": "integratedTerminal",
"type": "node",
"cwd": "${workspaceRoot}/test/osls-nested"
},
{
"name": "LLDebugger - OSLS nested - observability",
"program": "${workspaceRoot}/node_modules/tsx/dist/cli.mjs",
"args": ["../../src/lldebugger.ts", "--stage=test", "-o"],
"request": "launch",
"skipFiles": ["<node_internals>/**"],
"console": "integratedTerminal",
"type": "node",
"cwd": "${workspaceRoot}/test/osls-nested"
},
{
"name": "LLDebugger - OSLS EsBuild CJS",
"program": "${workspaceRoot}/node_modules/tsx/dist/cli.mjs",
Expand Down
37 changes: 37 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
"test-sls-esbuild-esm-observable": "npm run build && RUN_TEST_FROM_CLI=true OBSERVABLE_MODE=true vitest run test/sls-esbuild-esm.test.ts",
"test-osls-basic": "npm run build && RUN_TEST_FROM_CLI=true vitest run test/osls-basic.test.ts",
"test-osls-basic-observable": "npm run build && RUN_TEST_FROM_CLI=true OBSERVABLE_MODE=true vitest run test/osls-basic.test.ts",
"test-osls-nested": "npm run build && RUN_TEST_FROM_CLI=true vitest run test/osls-nested.test.ts",
"test-osls-nested-observable": "npm run build && RUN_TEST_FROM_CLI=true OBSERVABLE_MODE=true vitest run test/osls-nested.test.ts",
"test-osls-esbuild-cjs": "npm run build && RUN_TEST_FROM_CLI=true vitest run test/osls-esbuild-cjs.test.ts",
"test-osls-esbuild-cjs-observable": "npm run build && RUN_TEST_FROM_CLI=true OBSERVABLE_MODE=true vitest run test/osls-esbuild-cjs.test.ts",
"test-osls-esbuild-esm": "npm run build && RUN_TEST_FROM_CLI=true vitest run test/osls-esbuild-esm.test.ts",
Expand Down Expand Up @@ -160,6 +162,7 @@
"test/sls-esbuild",
"test/sls-esbuild-cjs",
"test/osls-basic",
"test/osls-nested",
"test/osls-esbuild",
"test/osls-esbuild-cjs",
"test/sam-basic",
Expand Down
193 changes: 153 additions & 40 deletions src/frameworks/slsFramework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,60 +182,173 @@ export class SlsFramework implements IFramework {
config,
);

// Get functions from main configuration
const lambdas = serverless.service.functions;

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

// Process main stack functions
for (const func in lambdas) {
const lambda = lambdas[func] as Serverless.FunctionDefinitionHandler;
const handlerFull = lambda.handler;
const handlerParts = handlerFull.split('.');
const handler = handlerParts[1];

const possibleCodePaths = [
`${handlerParts[0]}.ts`,
`${handlerParts[0]}.js`,
`${handlerParts[0]}.cjs`,
`${handlerParts[0]}.mjs`,
];
let codePath: string | undefined;
for (const cp of possibleCodePaths) {
try {
await fs.access(cp, constants.F_OK);
codePath = cp;
break;
} catch {
// ignore, file not found
}
}
const lambdaResource = await this.processFunction(
func,
lambdas[func] as Serverless.FunctionDefinitionHandler,
esBuildOptions,
);
lambdasDiscovered.push(lambdaResource);
}

if (!codePath) {
throw new Error(`Code path not found for handler: ${handlerFull}`);
// Check for nested stacks (serverless-nested-stack plugin)
const nestedStacks = (serverless.service as any).nestedStacks;
if (nestedStacks) {
Logger.verbose(
`[SLS] Found nested stacks configuration:`,
JSON.stringify(nestedStacks, null, 2),
);

const nestedLambdas = await this.parseNestedStacks(
nestedStacks,
esBuildOptions,
);
lambdasDiscovered.push(...nestedLambdas);
}

return lambdasDiscovered;
}

/**
* Process a single Lambda function
*/
private async processFunction(
funcName: string,
lambda: Serverless.FunctionDefinitionHandler,
esBuildOptions: EsBuildOptions | undefined,
): Promise<LambdaResource> {
const handlerFull = lambda.handler;
const handlerParts = handlerFull.split('.');
const handler = handlerParts[1];

const possibleCodePaths = [
`${handlerParts[0]}.ts`,
`${handlerParts[0]}.js`,
`${handlerParts[0]}.cjs`,
`${handlerParts[0]}.mjs`,
];
let codePath: string | undefined;
for (const cp of possibleCodePaths) {
try {
await fs.access(cp, constants.F_OK);
codePath = cp;
break;
} catch {
// ignore, file not found
}
}

const functionName = lambda.name;
if (!functionName) {
throw new Error(`Function name not found for handler: ${handlerFull}`);
if (!codePath) {
throw new Error(`Code path not found for handler: ${handlerFull}`);
}

const functionName = lambda.name;
if (!functionName) {
throw new Error(`Function name not found for handler: ${handlerFull}`);
}

const packageJsonPath = await findPackageJson(codePath);
Logger.verbose(`[SLS] package.json path: ${packageJsonPath}`);

const lambdaResource: LambdaResource = {
functionName,
codePath,
handler,
packageJsonPath,
esBuildOptions,
metadata: {
framework: 'sls',
},
};

return lambdaResource;
}

/**
* Parse nested stacks recursively
*/
private async parseNestedStacks(
nestedStacks: any,
esBuildOptions: EsBuildOptions | undefined,
currentDir: string = process.cwd(),
): Promise<LambdaResource[]> {
const lambdas: LambdaResource[] = [];

for (const stackName in nestedStacks) {
const stackConfig = nestedStacks[stackName];
const templatePath = stackConfig.template;

if (!templatePath) {
Logger.verbose(
`[SLS] Nested stack ${stackName} has no template property`,
);
continue;
}

const packageJsonPath = await findPackageJson(codePath);
Logger.verbose(`[SLS] package.json path: ${packageJsonPath}`);
const resolvedTemplatePath = path.resolve(currentDir, templatePath);
Logger.verbose(
`[SLS] Parsing nested stack ${stackName}: ${resolvedTemplatePath}`,
);

const lambdaResource: LambdaResource = {
functionName,
codePath,
handler,
packageJsonPath,
esBuildOptions,
metadata: {
framework: 'sls',
},
};
try {
const templateContent = await fs.readFile(
resolvedTemplatePath,
'utf-8',
);
const yaml = await import('yaml');
const nestedConfig = yaml.parse(templateContent);

// Process functions in nested stack
if (nestedConfig.functions) {
Logger.verbose(
`[SLS] Found functions in nested stack ${stackName}:`,
JSON.stringify(nestedConfig.functions, null, 2),
);

for (const funcName in nestedConfig.functions) {
const func = nestedConfig.functions[funcName];
const lambdaResource = await this.processFunction(
funcName,
func as Serverless.FunctionDefinitionHandler,
esBuildOptions,
);
lambdas.push(lambdaResource);
}
}

lambdasDiscovered.push(lambdaResource);
// Recursively process nested stacks within this stack
if (nestedConfig.nestedStacks) {
Logger.verbose(
`[SLS] Found nested stacks within ${stackName}:`,
JSON.stringify(nestedConfig.nestedStacks, null, 2),
);

const templateDir = path.dirname(resolvedTemplatePath);
const deeperNestedLambdas = await this.parseNestedStacks(
nestedConfig.nestedStacks,
esBuildOptions,
templateDir,
);
lambdas.push(...deeperNestedLambdas);
}
} catch (err: any) {
Logger.warn(
`[SLS] Could not parse nested stack at ${resolvedTemplatePath}: ${err.message}`,
);
}
}

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

return lambdas;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down
14 changes: 14 additions & 0 deletions test/osls-basic/nested-nested-stack.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
service: lls-osls-basic-nested-nested
frameworkVersion: '3'

provider:
name: aws
runtime: nodejs22.x
region: eu-west-1

functions:
testJsCommonJsNested:
handler: services/testJsCommonJs/lambda.lambdaHandler

testJsEsModuleNested:
handler: services/testJsEsModule/lambda.lambdaHandler
Empty file.
Loading
Loading