├── .nvmrc ├── .eslintignore ├── .env.example ├── .prettierrc ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml └── workflows │ ├── publish.yml │ └── ci.yml ├── jest.preset.js ├── examples ├── complete │ ├── libs │ │ └── lib1 │ │ │ └── lib1.ts │ ├── src │ │ ├── my-image.jpeg │ │ ├── app4.ts │ │ ├── deeply │ │ │ └── nested │ │ │ │ └── somewhat │ │ │ │ └── app5.ts │ │ └── App3.ts │ ├── app2.py │ ├── app1.ts │ ├── serverless-offline │ │ ├── README.md │ │ └── offline-invoke.mjs │ ├── tsconfig.json │ ├── project.json │ ├── package.json │ └── serverless.yml ├── esm │ ├── app2.py │ ├── src │ │ ├── App3.ts │ │ ├── app4.ts │ │ └── deeply │ │ │ └── nested │ │ │ └── somewhat │ │ │ └── app5.ts │ ├── project.json │ ├── package.json │ ├── serverless.yml │ └── app1.ts ├── config │ ├── app2.py │ ├── app1.ts │ ├── src │ │ ├── App3.ts │ │ ├── app4.ts │ │ └── deeply │ │ │ └── nested │ │ │ └── somewhat │ │ │ └── app5.ts │ ├── package.json │ ├── project.json │ ├── serverless.yml │ └── rspack.config.js └── partial-config │ ├── app2.py │ ├── app1.ts │ ├── src │ ├── app4.ts │ ├── deeply │ │ └── nested │ │ │ └── somewhat │ │ │ └── app5.ts │ └── App3.ts │ ├── package.json │ ├── rspack.config.js │ ├── project.json │ └── serverless.yml ├── libs └── serverless-rspack │ ├── src │ ├── lib │ │ ├── constants.ts │ │ ├── hooks │ │ │ ├── deploy-function │ │ │ │ ├── after-package-function.ts │ │ │ │ └── before-package-function.ts │ │ │ ├── package │ │ │ │ ├── before-create-deployment-artifacts.ts │ │ │ │ └── after-create-deployment-artifacts.ts │ │ │ ├── invoke-local │ │ │ │ └── before-invoke.ts │ │ │ ├── offline │ │ │ │ └── start-init.ts │ │ │ └── initialize.ts │ │ ├── helpers.ts │ │ ├── pack.ts │ │ ├── types.ts │ │ ├── scripts.ts │ │ ├── bundle.ts │ │ └── serverless-rspack.ts │ ├── index.ts │ └── test │ │ ├── test-utils.ts │ │ ├── hooks │ │ ├── deploy-function │ │ │ ├── after-package-function.spec.ts │ │ │ └── before-package-function.spec.ts │ │ ├── package │ │ │ ├── after-create-deployment-artifacts.spec.ts │ │ │ └── before-create-deployment-artifacts.spec.ts │ │ ├── invoke-local │ │ │ └── before-invoke.spec.ts │ │ ├── offline │ │ │ └── start-init.spec.ts │ │ └── initialize.spec.ts │ │ ├── helpers.spec.ts │ │ ├── serverless-rspack.spec.ts │ │ ├── pack.spec.ts │ │ ├── scripts.spec.ts │ │ └── bundle.spec.ts │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── jest.config.ts │ ├── tsconfig.json │ ├── .eslintrc.json │ ├── project.json │ └── package.json ├── .prettierignore ├── jest.config.ts ├── .vscode ├── extensions.json └── launch.json ├── e2e ├── project.json ├── tsconfig.spec.json ├── jest.config.ts ├── tsconfig.json ├── lib │ └── utilities.ts ├── esm.test.ts ├── config.test.ts ├── partial-config.test.ts ├── complete.test.ts └── __snapshots__ │ ├── config.test.ts.snap │ ├── esm.test.ts.snap │ └── complete.test.ts.snap ├── .editorconfig ├── project.json ├── tsconfig.base.json ├── .verdaccio └── config.yml ├── .gitignore ├── .eslintrc.json ├── .eslintrc.base.json ├── docs └── DEVELOPER.md ├── package.json ├── nx.json ├── scripts └── release.js ├── CHANGELOG.md └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.13.1 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_TOKEN=github_123 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset').default; 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /examples/complete/libs/lib1/lib1.ts: -------------------------------------------------------------------------------- 1 | export function lib1Export(valid: boolean) { 2 | return valid ? 200 : 400; 3 | } 4 | -------------------------------------------------------------------------------- /examples/complete/src/my-image.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kitchenshelf/serverless-rspack/HEAD/examples/complete/src/my-image.jpeg -------------------------------------------------------------------------------- /libs/serverless-rspack/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const SERVERLESS_FOLDER = '.serverless'; 2 | export const WORK_FOLDER = '.rspack'; 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | /dist 3 | /coverage 4 | /.nx/cache 5 | /CHANGELOG.md 6 | /README.md -------------------------------------------------------------------------------- /libs/serverless-rspack/src/index.ts: -------------------------------------------------------------------------------- 1 | import { RspackServerlessPlugin } from './lib/serverless-rspack.js'; 2 | export default RspackServerlessPlugin; 3 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { getJestProjectsAsync } from '@nx/jest'; 2 | 3 | export default async () => ({ 4 | projects: await getJestProjectsAsync(), 5 | }); 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "esbenp.prettier-vscode", 5 | "firsttris.vscode-jest-runner" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /examples/esm/app2.py: -------------------------------------------------------------------------------- 1 | def lambda_handler(event, context): 2 | message = 'Hello {} {}!'.format(event['first_name'], event['last_name']) 3 | return { 4 | 'message' : message 5 | } -------------------------------------------------------------------------------- /examples/complete/app2.py: -------------------------------------------------------------------------------- 1 | def lambda_handler(event, context): 2 | message = 'Hello {} {}!'.format(event['first_name'], event['last_name']) 3 | return { 4 | 'message' : message 5 | } -------------------------------------------------------------------------------- /examples/config/app2.py: -------------------------------------------------------------------------------- 1 | def lambda_handler(event, context): 2 | message = 'Hello {} {}!'.format(event['first_name'], event['last_name']) 3 | return { 4 | 'message' : message 5 | } -------------------------------------------------------------------------------- /examples/partial-config/app2.py: -------------------------------------------------------------------------------- 1 | def lambda_handler(event, context): 2 | message = 'Hello {} {}!'.format(event['first_name'], event['last_name']) 3 | return { 4 | 'message' : message 5 | } -------------------------------------------------------------------------------- /e2e/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e-tests", 3 | "$schema": "../node_modules/nx/schemas/project-schema.json", 4 | "implicitDependencies": [ 5 | "serverless-rspack", 6 | "complete", 7 | "config", 8 | "esm", 9 | "partial-config" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/lib/hooks/deploy-function/after-package-function.ts: -------------------------------------------------------------------------------- 1 | import type { RspackServerlessPlugin } from '../../serverless-rspack.js'; 2 | 3 | export async function AfterDeployFunctionPackageFunction( 4 | this: RspackServerlessPlugin 5 | ) { 6 | this.log.verbose('[sls-rspack] after:deploy:function:packageFunction'); 7 | await this.cleanup(); 8 | } 9 | -------------------------------------------------------------------------------- /examples/complete/app1.ts: -------------------------------------------------------------------------------- 1 | import validateIsin from 'isin-validator'; 2 | 3 | export async function handler(event: string) { 4 | const isInvalid = validateIsin(event); 5 | 6 | return { 7 | statusCode: 200, 8 | body: JSON.stringify({ 9 | message: isInvalid ? 'ISIN is invalid!' : 'ISIN is fine!', 10 | input: event, 11 | }), 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /examples/config/app1.ts: -------------------------------------------------------------------------------- 1 | import validateIsin from 'isin-validator'; 2 | 3 | export async function handler(event: string) { 4 | const isInvalid = validateIsin(event); 5 | 6 | return { 7 | statusCode: 200, 8 | body: JSON.stringify({ 9 | message: isInvalid ? 'ISIN is invalid!' : 'ISIN is fine!', 10 | input: event, 11 | }), 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /examples/esm/src/App3.ts: -------------------------------------------------------------------------------- 1 | import validateIsin from 'isin-validator'; 2 | 3 | export async function handler(event: string) { 4 | const isInvalid = validateIsin(event); 5 | 6 | return { 7 | statusCode: 200, 8 | body: JSON.stringify({ 9 | message: isInvalid ? 'ISIN is invalid!' : 'ISIN is fine!', 10 | input: event, 11 | }), 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /examples/esm/src/app4.ts: -------------------------------------------------------------------------------- 1 | import validateIsin from 'isin-validator'; 2 | 3 | export async function handler(event: string) { 4 | const isInvalid = validateIsin(event); 5 | 6 | return { 7 | statusCode: 200, 8 | body: JSON.stringify({ 9 | message: isInvalid ? 'ISIN is invalid!' : 'ISIN is fine!', 10 | input: event, 11 | }), 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /e2e/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "src/**/*.test.ts", 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts", 13 | "src/test/*.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/complete/src/app4.ts: -------------------------------------------------------------------------------- 1 | import validateIsin from 'isin-validator'; 2 | 3 | export async function handler(event: string) { 4 | const isInvalid = validateIsin(event); 5 | 6 | return { 7 | statusCode: 200, 8 | body: JSON.stringify({ 9 | message: isInvalid ? 'ISIN is invalid!' : 'ISIN is fine!', 10 | input: event, 11 | }), 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /examples/config/src/App3.ts: -------------------------------------------------------------------------------- 1 | import validateIsin from 'isin-validator'; 2 | 3 | export async function handler(event: string) { 4 | const isInvalid = validateIsin(event); 5 | 6 | return { 7 | statusCode: 200, 8 | body: JSON.stringify({ 9 | message: isInvalid ? 'ISIN is invalid!' : 'ISIN is fine!', 10 | input: event, 11 | }), 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /examples/config/src/app4.ts: -------------------------------------------------------------------------------- 1 | import validateIsin from 'isin-validator'; 2 | 3 | export async function handler(event: string) { 4 | const isInvalid = validateIsin(event); 5 | 6 | return { 7 | statusCode: 200, 8 | body: JSON.stringify({ 9 | message: isInvalid ? 'ISIN is invalid!' : 'ISIN is fine!', 10 | input: event, 11 | }), 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /examples/partial-config/app1.ts: -------------------------------------------------------------------------------- 1 | import validateIsin from 'isin-validator'; 2 | 3 | export async function handler(event: string) { 4 | const isInvalid = validateIsin(event); 5 | 6 | return { 7 | statusCode: 200, 8 | body: JSON.stringify({ 9 | message: isInvalid ? 'ISIN is invalid!' : 'ISIN is fine!', 10 | input: event, 11 | }), 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /examples/partial-config/src/app4.ts: -------------------------------------------------------------------------------- 1 | import validateIsin from 'isin-validator'; 2 | 3 | export async function handler(event: string) { 4 | const isInvalid = validateIsin(event); 5 | 6 | return { 7 | statusCode: 200, 8 | body: JSON.stringify({ 9 | message: isInvalid ? 'ISIN is invalid!' : 'ISIN is fine!', 10 | input: event, 11 | }), 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /examples/complete/serverless-offline/README.md: -------------------------------------------------------------------------------- 1 | # Serverless-offline commands 2 | 3 | ## By AWS Cli invoke: 4 | 5 | aws lambda invoke ./app3-logs.txt \ 6 | --endpoint-url http://localhost:3002 \ 7 | --function-name complete-example-dev-app3 8 | 9 | ## By aws sdk: 10 | 11 | node offline-invoke.mjs 12 | 13 | ## By mocked Rest API 14 | 15 | curl http://localhost:3000/dev/helloApp3 16 | -------------------------------------------------------------------------------- /libs/serverless-rspack/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "src/**/*.test.ts", 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts", 13 | "src/test/*.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/config/src/deeply/nested/somewhat/app5.ts: -------------------------------------------------------------------------------- 1 | import validateIsin from 'isin-validator'; 2 | 3 | export async function handler(event: string) { 4 | const isInvalid = validateIsin(event); 5 | 6 | return { 7 | statusCode: 200, 8 | body: JSON.stringify({ 9 | message: isInvalid ? 'ISIN is invalid!' : 'ISIN is fine!', 10 | input: event, 11 | }), 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /examples/esm/src/deeply/nested/somewhat/app5.ts: -------------------------------------------------------------------------------- 1 | import validateIsin from 'isin-validator'; 2 | 3 | export async function handler(event: string) { 4 | const isInvalid = validateIsin(event); 5 | 6 | return { 7 | statusCode: 200, 8 | body: JSON.stringify({ 9 | message: isInvalid ? 'ISIN is invalid!' : 'ISIN is fine!', 10 | input: event, 11 | }), 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /libs/serverless-rspack/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": ["node"] 7 | }, 8 | "include": ["src/**/*.ts"], 9 | "exclude": [ 10 | "jest.config.ts", 11 | "src/**/*.spec.ts", 12 | "src/**/*.test.ts", 13 | "src/test/**/*.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/complete/src/deeply/nested/somewhat/app5.ts: -------------------------------------------------------------------------------- 1 | import validateIsin from 'isin-validator'; 2 | 3 | export async function handler(event: string) { 4 | const isInvalid = validateIsin(event); 5 | 6 | return { 7 | statusCode: 200, 8 | body: JSON.stringify({ 9 | message: isInvalid ? 'ISIN is invalid!' : 'ISIN is fine!', 10 | input: event, 11 | }), 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverless-rspack/source", 3 | "$schema": "node_modules/nx/schemas/project-schema.json", 4 | "targets": { 5 | "local-registry": { 6 | "executor": "@nx/js:verdaccio", 7 | "options": { 8 | "port": 4873, 9 | "config": ".verdaccio/config.yml", 10 | "storage": "tmp/local-registry/storage" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/partial-config/src/deeply/nested/somewhat/app5.ts: -------------------------------------------------------------------------------- 1 | import validateIsin from 'isin-validator'; 2 | 3 | export async function handler(event: string) { 4 | const isInvalid = validateIsin(event); 5 | 6 | return { 7 | statusCode: 200, 8 | body: JSON.stringify({ 9 | message: isInvalid ? 'ISIN is invalid!' : 'ISIN is fine!', 10 | input: event, 11 | }), 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /e2e/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'examples-e2e', 4 | preset: '../jest.preset.js', 5 | testEnvironment: 'node', 6 | transform: { 7 | '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], 8 | }, 9 | moduleFileExtensions: ['ts', 'js', 'html'], 10 | coverageDirectory: '../coverage/examples/e2e', 11 | collectCoverage: true, 12 | }; 13 | -------------------------------------------------------------------------------- /libs/serverless-rspack/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'serverless-rspack', 4 | preset: '../../jest.preset.js', 5 | testEnvironment: 'node', 6 | transform: { 7 | '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], 8 | }, 9 | moduleFileExtensions: ['ts', 'js', 'html'], 10 | coverageDirectory: '../../coverage/libs/serverless-rspack', 11 | collectCoverage: true, 12 | }; 13 | -------------------------------------------------------------------------------- /examples/partial-config/src/App3.ts: -------------------------------------------------------------------------------- 1 | declare const __SIGNER_SECRET__: string; 2 | 3 | import validateIsin from 'isin-validator'; 4 | 5 | export async function handler(event: string) { 6 | const isInvalid = validateIsin(event); 7 | 8 | return { 9 | statusCode: 200, 10 | body: JSON.stringify({ 11 | message: isInvalid ? 'ISIN is invalid!' : 'ISIN is fine!', 12 | input: event, 13 | signer: __SIGNER_SECRET__, 14 | }), 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*.ts", "libs/**/*.ts"], 3 | "baseUrl": "./", 4 | "compilerOptions": { 5 | "rootDir": "./", 6 | "module": "commonjs", 7 | "baseUrl": ".", 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "sourceMap": true, 13 | "paths": { 14 | "@kitchenshelf/path-import-libs/*": ["./libs/*"] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/complete/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*.ts", "libs/**/*.ts"], 3 | "baseUrl": "./", 4 | "compilerOptions": { 5 | "rootDir": "./", 6 | "module": "commonjs", 7 | "baseUrl": ".", 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "sourceMap": true, 13 | "paths": { 14 | "@kitchenshelf/path-import-libs/*": ["./libs/*"] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug Examples", 9 | "type": "node", 10 | "request": "attach", 11 | "port": 9229, 12 | "sourceMaps": true, 13 | "outFiles": ["${workspaceFolder}/examples/**/*.js"] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/lib/hooks/package/before-create-deployment-artifacts.ts: -------------------------------------------------------------------------------- 1 | import type { RspackServerlessPlugin } from '../../serverless-rspack.js'; 2 | 3 | export async function BeforePackageCreateDeploymentArtifacts( 4 | this: RspackServerlessPlugin 5 | ) { 6 | this.log.verbose('[sls-rspack] before:package:createDeploymentArtifacts'); 7 | this.timings.set('before:package:createDeploymentArtifacts', Date.now()); 8 | await this.bundle(this.functionEntries); 9 | await this.scripts(); 10 | await this.pack(); 11 | } 12 | -------------------------------------------------------------------------------- /examples/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverless-rspack/complete", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "package": "sls package --verbose" 6 | }, 7 | "dependencies": { 8 | "isin-validator": "^1.1.1" 9 | }, 10 | "devDependencies": { 11 | "@kitchenshelf/serverless-rspack": "../../dist/libs/serverless-rspack", 12 | "@rspack/core": "1.4.10", 13 | "@types/node": "^18.7.21", 14 | "serverless": "^4.4.7", 15 | "serverless-offline": "^14.3.4", 16 | "ts-node": "^10.9.1", 17 | "typescript": "^4.8.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/partial-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverless-rspack/complete", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "package": "sls package --verbose" 6 | }, 7 | "dependencies": { 8 | "isin-validator": "^1.1.1" 9 | }, 10 | "devDependencies": { 11 | "@kitchenshelf/serverless-rspack": "../../dist/libs/serverless-rspack", 12 | "@rspack/core": "1.4.10", 13 | "@types/node": "^18.7.21", 14 | "serverless": "^4.4.7", 15 | "serverless-offline": "^14.3.4", 16 | "ts-node": "^10.9.1", 17 | "typescript": "^4.8.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/partial-config/rspack.config.js: -------------------------------------------------------------------------------- 1 | const { rspack } = require('@rspack/core'); 2 | 3 | /** @type {import('@rspack/cli').Configuration} */ 4 | const config = (serverless) => { 5 | const SIGNER_SECRET = serverless.service.custom.SIGNER_SECRET; 6 | return { 7 | mode: 'production', 8 | plugins: [ 9 | new rspack.DefinePlugin({ 10 | 'process.env.NODE_ENV': JSON.stringify(process.env['NODE_ENV']), 11 | __SIGNER_SECRET__: JSON.stringify(`${SIGNER_SECRET}`), 12 | }), 13 | ].filter(Boolean), 14 | }; 15 | }; 16 | module.exports = config; 17 | -------------------------------------------------------------------------------- /examples/esm/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esm", 3 | "$schema": "../node_modules/nx/schemas/project-schema.json", 4 | "targets": { 5 | "install": { 6 | "executor": "nx:run-commands", 7 | "options": { 8 | "command": "cd ./examples/esm && npm i --no-audit --no-vunerabilities --no-fund" 9 | } 10 | }, 11 | "package": { 12 | "executor": "nx:run-commands", 13 | "options": { 14 | "command": "cd ./examples/esm && npm run package" 15 | }, 16 | "dependsOn": [ 17 | { 18 | "target": "install" 19 | } 20 | ] 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/lib/hooks/package/after-create-deployment-artifacts.ts: -------------------------------------------------------------------------------- 1 | import type { RspackServerlessPlugin } from '../../serverless-rspack.js'; 2 | 3 | export async function AfterPackageCreateDeploymentArtifacts( 4 | this: RspackServerlessPlugin 5 | ) { 6 | this.log.verbose('[sls-rspack] after:package:createDeploymentArtifacts'); 7 | await this.cleanup(); 8 | this.log.verbose( 9 | `[Performance] Hook createDeploymentArtifacts ${ 10 | this.serverless.service.service 11 | } [${ 12 | Date.now() - this.timings.get('before:package:createDeploymentArtifacts')! 13 | } ms]` 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /libs/serverless-rspack/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "esModuleInterop": true 12 | }, 13 | "files": [], 14 | "include": [], 15 | "references": [ 16 | { 17 | "path": "./tsconfig.lib.json" 18 | }, 19 | { 20 | "path": "./tsconfig.spec.json" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /examples/config/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "config", 3 | "$schema": "../node_modules/nx/schemas/project-schema.json", 4 | "targets": { 5 | "install": { 6 | "executor": "nx:run-commands", 7 | "options": { 8 | "command": "cd ./examples/config && npm i --no-audit --no-vunerabilities --no-fund" 9 | } 10 | }, 11 | "package": { 12 | "executor": "nx:run-commands", 13 | "options": { 14 | "command": "cd ./examples/config && npm run package" 15 | }, 16 | "dependsOn": [ 17 | { 18 | "target": "install" 19 | } 20 | ] 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/complete/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "complete", 3 | "$schema": "../node_modules/nx/schemas/project-schema.json", 4 | "targets": { 5 | "install": { 6 | "executor": "nx:run-commands", 7 | "options": { 8 | "command": "cd ./examples/complete && npm i --no-audit --no-vunerabilities --no-fund" 9 | } 10 | }, 11 | "package": { 12 | "executor": "nx:run-commands", 13 | "options": { 14 | "command": "cd ./examples/complete && npm run package" 15 | }, 16 | "dependsOn": [ 17 | { 18 | "target": "install" 19 | } 20 | ] 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverless-rspack/complete", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "package": "sls package --verbose" 6 | }, 7 | "dependencies": { 8 | "@aws-sdk/client-dynamodb": "^3.577.0", 9 | "@aws-sdk/lib-dynamodb": "^3.577.0", 10 | "isin-validator": "^1.1.1" 11 | }, 12 | "devDependencies": { 13 | "@kitchenshelf/serverless-rspack": "../../dist/libs/serverless-rspack", 14 | "@rspack/core": "1.4.10", 15 | "@types/node": "^18.7.21", 16 | "serverless": "^4.4.7", 17 | "serverless-offline": "^14.3.4", 18 | "ts-node": "^10.9.1", 19 | "typescript": "^4.8.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2020", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@kitchenshelf/serverless-rspack": ["libs/serverless-rspack/src/index.ts"] 19 | } 20 | }, 21 | "exclude": ["node_modules", "tmp"] 22 | } 23 | -------------------------------------------------------------------------------- /examples/partial-config/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "partial-config", 3 | "$schema": "../node_modules/nx/schemas/project-schema.json", 4 | "targets": { 5 | "install": { 6 | "executor": "nx:run-commands", 7 | "options": { 8 | "command": "cd ./examples/partial-config && npm i --no-audit --no-vunerabilities --no-fund" 9 | } 10 | }, 11 | "package": { 12 | "executor": "nx:run-commands", 13 | "options": { 14 | "command": "cd ./examples/partial-config && npm run package" 15 | }, 16 | "dependsOn": [ 17 | { 18 | "target": "install" 19 | } 20 | ] 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /libs/serverless-rspack/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "../../.eslintrc.base.json", 4 | "plugin:require-extensions/recommended" 5 | ], 6 | "plugins": ["require-extensions"], 7 | "ignorePatterns": ["!**/*"], 8 | "overrides": [ 9 | { 10 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.ts", "*.tsx"], 15 | "rules": {} 16 | }, 17 | { 18 | "files": ["*.js", "*.jsx"], 19 | "rules": {} 20 | }, 21 | { 22 | "files": ["*.json"], 23 | "parser": "jsonc-eslint-parser", 24 | "rules": { 25 | "@nx/dependency-checks": "error" 26 | } 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.verdaccio/config.yml: -------------------------------------------------------------------------------- 1 | # path to a directory with all packages 2 | storage: ../tmp/local-registry/storage 3 | 4 | # a list of other known repositories we can talk to 5 | uplinks: 6 | npmjs: 7 | url: https://registry.npmjs.org/ 8 | maxage: 60m 9 | 10 | packages: 11 | '**': 12 | # give all users (including non-authenticated users) full access 13 | # because it is a local registry 14 | access: $all 15 | publish: $all 16 | unpublish: $all 17 | 18 | # if package is not available locally, proxy requests to npm registry 19 | proxy: npmjs 20 | 21 | # log settings 22 | logs: 23 | type: stdout 24 | format: pretty 25 | level: warn 26 | 27 | publish: 28 | allow_offline: true # set offline to true to allow publish offline 29 | -------------------------------------------------------------------------------- /examples/config/serverless.yml: -------------------------------------------------------------------------------- 1 | service: complete-example 2 | 3 | plugins: 4 | - '@kitchenshelf/serverless-rspack' 5 | - serverless-offline 6 | 7 | build: 8 | esbuild: false 9 | 10 | custom: 11 | rspack: 12 | keepOutputDirectory: true 13 | stats: true 14 | config: 15 | path: './rspack.config.js' 16 | 17 | provider: 18 | name: aws 19 | runtime: nodejs20.x 20 | 21 | package: 22 | individually: true 23 | 24 | functions: 25 | App1: 26 | handler: app1.handler 27 | runtime: 'provided.al2023' 28 | rspack: true 29 | app2: 30 | handler: app2.lambda_handler 31 | runtime: python3.9 32 | app3: 33 | handler: src/App3.handler 34 | runtime: nodejs20.x 35 | app4: 36 | handler: src/app4.handler 37 | app5: 38 | handler: src/deeply/nested/somewhat/app5.handler 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | dist 5 | tmp 6 | /out-tsc 7 | .rspack 8 | .serverless 9 | .rsdoctor 10 | 11 | # dependencies 12 | node_modules 13 | 14 | # IDEs and editors 15 | /.idea 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # IDE - VSCode 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | 30 | # misc 31 | /.sass-cache 32 | /connect.lock 33 | /coverage 34 | /libpeerconnection.log 35 | npm-debug.log 36 | yarn-error.log 37 | testem.log 38 | /typings 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | 44 | # env 45 | .env* 46 | !.env.example 47 | 48 | .nx/cache 49 | 50 | # logs 51 | *-logs.txt 52 | -------------------------------------------------------------------------------- /examples/complete/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverless-rspack/complete", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "package": "sls package --verbose", 6 | "deploy": "sls deploy --verbose", 7 | "deploy:func:app3": "sls deploy function -f app3 --verbose", 8 | "debug": "export SLS_DEBUG=* && node --inspect node_modules/serverless/bin/serverless offline start --noTimeout --verbose" 9 | }, 10 | "dependencies": { 11 | "isin-validator": "^1.1.1", 12 | "sharp": "^0.33.5" 13 | }, 14 | "devDependencies": { 15 | "@kitchenshelf/serverless-rspack": "../../dist/libs/serverless-rspack", 16 | "@rspack/core": "1.4.10", 17 | "@types/node": "^18.7.21", 18 | "serverless": "^4.4.7", 19 | "serverless-offline": "^14.3.4", 20 | "ts-node": "^10.9.1", 21 | "typescript": "^4.8.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/esm/serverless.yml: -------------------------------------------------------------------------------- 1 | service: esm-example 2 | 3 | plugins: 4 | - '@kitchenshelf/serverless-rspack' 5 | 6 | build: 7 | esbuild: false 8 | 9 | custom: 10 | rspack: 11 | keepOutputDirectory: true 12 | esm: true 13 | externals: 14 | - '^@aws-sdk/.*$' 15 | - '^@smithy/.*$' 16 | - '^isin-validator$' 17 | 18 | provider: 19 | name: aws 20 | runtime: nodejs20.x 21 | 22 | package: 23 | individually: true 24 | 25 | functions: 26 | App1: 27 | handler: app1.handler 28 | runtime: 'provided.al2023' 29 | rspack: true 30 | app2: 31 | handler: app2.lambda_handler 32 | runtime: python3.9 33 | app3: 34 | handler: src/App3.handler 35 | runtime: nodejs20.x 36 | app4: 37 | handler: src/app4.handler 38 | app5: 39 | handler: src/deeply/nested/somewhat/app5.handler 40 | -------------------------------------------------------------------------------- /examples/complete/src/App3.ts: -------------------------------------------------------------------------------- 1 | import { lib1Export } from '@kitchenshelf/path-import-libs/lib1/lib1'; 2 | import validateIsin from 'isin-validator'; 3 | import { readFileSync } from 'node:fs'; 4 | import path from 'node:path'; 5 | import sharp from 'sharp'; 6 | 7 | type MyEventPayload = { 8 | isin: string; 9 | }; 10 | 11 | export async function handler(event: MyEventPayload) { 12 | const isInvalid = validateIsin(event.isin); 13 | const imagePath = path.join(__dirname, '../my-image.jpeg'); 14 | const imageBuffer = readFileSync(imagePath); 15 | 16 | const { info } = await sharp(imageBuffer) 17 | .raw() 18 | .toBuffer({ resolveWithObject: true }); 19 | 20 | return { 21 | statusCode: lib1Export(!!isInvalid), 22 | body: JSON.stringify({ 23 | handler: 'App3', 24 | message: isInvalid ? 'ISIN is invalid!' : 'ISIN is fine!', 25 | input: event, 26 | info, 27 | }), 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /examples/esm/app1.ts: -------------------------------------------------------------------------------- 1 | import validateIsin from 'isin-validator'; 2 | import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 3 | import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb'; 4 | 5 | const client = new DynamoDBClient({}); 6 | const docClient = DynamoDBDocumentClient.from(client); 7 | 8 | export async function handler(event: string) { 9 | const isInvalid = validateIsin(event); 10 | 11 | const command = new GetCommand({ 12 | TableName: 'AngryAnimals', 13 | Key: { 14 | CommonName: 'Shoebill', 15 | }, 16 | }); 17 | 18 | try { 19 | const response = await docClient.send(command); 20 | console.log(response); 21 | } catch (error) { 22 | console.log(error); 23 | } 24 | 25 | return { 26 | statusCode: 200, 27 | body: JSON.stringify({ 28 | message: isInvalid ? 'ISIN is invalid!' : 'ISIN is fine!', 29 | input: event, 30 | }), 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /examples/partial-config/serverless.yml: -------------------------------------------------------------------------------- 1 | service: partial-config 2 | 3 | plugins: 4 | - '@kitchenshelf/serverless-rspack' 5 | - serverless-offline 6 | 7 | build: 8 | esbuild: false 9 | 10 | custom: 11 | SIGNER_SECRET: '___MY_SUPER_SECRET_SIGNER___' 12 | rspack: 13 | keepOutputDirectory: true 14 | stats: true 15 | doctor: true 16 | config: 17 | path: './rspack.config.js' 18 | strategy: 'combine' 19 | 20 | provider: 21 | name: aws 22 | runtime: nodejs20.x 23 | 24 | package: 25 | individually: true 26 | 27 | functions: 28 | App1: 29 | handler: app1.handler 30 | runtime: 'provided.al2023' 31 | rspack: true 32 | app2: 33 | handler: app2.lambda_handler 34 | runtime: python3.9 35 | app3: 36 | handler: src/App3.handler 37 | runtime: nodejs20.x 38 | app4: 39 | handler: src/app4.handler 40 | app5: 41 | handler: src/deeply/nested/somewhat/app5.handler 42 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePatterns": ["**/*"], 3 | "overrides": [ 4 | { 5 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 6 | "rules": { 7 | "@nx/enforce-module-boundaries": [ 8 | "error", 9 | { 10 | "enforceBuildableLibDependency": true, 11 | "allow": [], 12 | "depConstraints": [ 13 | { 14 | "sourceTag": "*", 15 | "onlyDependOnLibsWithTags": ["*"] 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | }, 22 | { 23 | "files": ["*.ts", "*.tsx"], 24 | "rules": {} 25 | }, 26 | { 27 | "files": ["*.js", "*.jsx"], 28 | "rules": {} 29 | }, 30 | { 31 | "files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"], 32 | "env": { 33 | "jest": true 34 | }, 35 | "rules": {} 36 | } 37 | ], 38 | "extends": ["./.eslintrc.base.json"] 39 | } 40 | -------------------------------------------------------------------------------- /libs/serverless-rspack/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-rspack", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/serverless-rspack/src", 5 | "projectType": "library", 6 | "release": { 7 | "version": { 8 | "generatorOptions": { 9 | "packageRoot": "dist/{projectRoot}", 10 | "currentVersionResolver": "git-tag" 11 | } 12 | } 13 | }, 14 | "tags": [], 15 | "targets": { 16 | "build": { 17 | "executor": "@nx/js:tsc", 18 | "outputs": ["{options.outputPath}"], 19 | "options": { 20 | "outputPath": "dist/libs/serverless-rspack", 21 | "main": "libs/serverless-rspack/src/index.ts", 22 | "tsConfig": "libs/serverless-rspack/tsconfig.lib.json", 23 | "assets": ["libs/serverless-rspack/*.md", "README.md"] 24 | } 25 | }, 26 | "nx-release-publish": { 27 | "options": { 28 | "packageRoot": "dist/{projectRoot}" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/lib/hooks/invoke-local/before-invoke.ts: -------------------------------------------------------------------------------- 1 | import type { RspackServerlessPlugin } from '../../serverless-rspack.js'; 2 | import { isInvokeOptions } from '../../types.js'; 3 | 4 | export async function BeforeInvokeLocalInvoke(this: RspackServerlessPlugin) { 5 | this.log.verbose('[sls-rspack] before:invoke:local:invoke'); 6 | 7 | if (!isInvokeOptions(this.options)) { 8 | throw new this.serverless.classes.Error( 9 | 'This hook only supports invoke options' 10 | ); 11 | } 12 | 13 | const invokeFunc = this.options.function; 14 | 15 | if (!(invokeFunc in this.functionEntries)) { 16 | throw new this.serverless.classes.Error( 17 | `Function ${invokeFunc} not found in function entries` 18 | ); 19 | } 20 | 21 | await this.bundle({ 22 | [invokeFunc]: this.functionEntries[invokeFunc], 23 | }); 24 | 25 | await this.scripts(); 26 | 27 | this.serverless.config.servicePath = 28 | this.serviceDirPath + '/' + this.buildOutputFolder + '/' + invokeFunc; 29 | } 30 | -------------------------------------------------------------------------------- /examples/complete/serverless-offline/offline-invoke.mjs: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer'; 2 | import aws from 'aws-sdk'; 3 | 4 | const { stringify } = JSON; 5 | 6 | const lambda = new aws.Lambda({ 7 | apiVersion: '2015-03-31', 8 | endpoint: 'http://localhost:3002', 9 | region: 'eu-west-1', 10 | }); 11 | 12 | export async function handler() { 13 | const clientContextData = stringify({ 14 | foo: 'foo', 15 | }); 16 | 17 | const payload = stringify({ 18 | isin: 'XX000A1G0AE8', 19 | }); 20 | 21 | const params = { 22 | ClientContext: Buffer.from(clientContextData).toString('base64'), 23 | // FunctionName is composed of: service name - stage - function name, e.g. 24 | FunctionName: 'complete-example-dev-app3', 25 | InvocationType: 'RequestResponse', 26 | Payload: payload, 27 | }; 28 | 29 | const response = await lambda.invoke(params).promise(); 30 | console.log(response); 31 | 32 | return { 33 | body: stringify(response), 34 | statusCode: 200, 35 | }; 36 | } 37 | 38 | handler(); 39 | -------------------------------------------------------------------------------- /.eslintrc.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "files": ["*.ts", "*.tsx"], 26 | "extends": ["plugin:@nx/typescript"], 27 | "rules": {} 28 | }, 29 | { 30 | "files": ["*.js", "*.jsx"], 31 | "extends": ["plugin:@nx/javascript"], 32 | "rules": {} 33 | }, 34 | { 35 | "files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"], 36 | "env": { 37 | "jest": true 38 | }, 39 | "rules": {} 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this project. 3 | body: 4 | - type: textarea 5 | id: problem-statement 6 | attributes: 7 | label: Problem statement 8 | description: A clear and concise description of what problem you are having. 9 | validations: 10 | required: true 11 | - type: textarea 12 | id: description 13 | attributes: 14 | label: Describe the feature/solution 15 | description: | 16 | A clear and concise description of the behavior to be added. 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: alternatives 21 | attributes: 22 | label: Describe the alternatives you have thought of 23 | description: | 24 | A clear and concise description of any alternative solutions or features you've considered. 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: addition 29 | attributes: 30 | label: Additional context 31 | placeholder: Add any other context about the feature here. 32 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/lib/hooks/deploy-function/before-package-function.ts: -------------------------------------------------------------------------------- 1 | import type { RspackServerlessPlugin } from '../../serverless-rspack.js'; 2 | import { isDeployFunctionOptions } from '../../types.js'; 3 | 4 | export async function BeforeDeployFunctionPackageFunction( 5 | this: RspackServerlessPlugin 6 | ) { 7 | this.log.verbose('[sls-rspack] before:deploy:function:packageFunction'); 8 | 9 | if (!isDeployFunctionOptions(this.options)) { 10 | throw new this.serverless.classes.Error( 11 | 'This hook only supports deploy function options' 12 | ); 13 | } 14 | 15 | const deployFunc = this.options.function; 16 | 17 | if (!(deployFunc in this.functionEntries)) { 18 | throw new this.serverless.classes.Error( 19 | `Function ${deployFunc} not found in function entries` 20 | ); 21 | } 22 | 23 | const entry = this.functionEntries[deployFunc]; 24 | 25 | await this.bundle({ 26 | [deployFunc]: entry, 27 | }); 28 | 29 | this.functionEntries = {}; 30 | this.functionEntries[deployFunc] = entry; 31 | 32 | await this.scripts(); 33 | await this.pack(); 34 | } 35 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/lib/hooks/offline/start-init.ts: -------------------------------------------------------------------------------- 1 | import type { RspackServerlessPlugin } from '../../serverless-rspack.js'; 2 | import { RsPackFunctionDefinitionHandler } from '../../types.js'; 3 | 4 | export async function BeforeOfflineStartInit(this: RspackServerlessPlugin) { 5 | this.log.verbose('[sls-rspack] before:offline:start:init'); 6 | this.offlineMode = true; 7 | 8 | this.pluginOptions = { 9 | ...this.pluginOptions, 10 | sourcemap: 'source-map', 11 | }; 12 | 13 | await this.bundle(this.functionEntries); 14 | await this.scripts(); 15 | 16 | if (this.serverless.service.custom?.['serverless-offline']) { 17 | this.serverless.service.custom['serverless-offline'].location = 18 | this.buildOutputFolderPath; 19 | } else { 20 | this.serverless.service.custom = { 21 | ...this.serverless.service.custom, 22 | 'serverless-offline': { location: this.buildOutputFolderPath }, 23 | }; 24 | } 25 | 26 | this.serverless.service.getAllFunctions().forEach((functionName) => { 27 | const functionDefinitionHandler = this.serverless.service.getFunction( 28 | functionName 29 | ) as RsPackFunctionDefinitionHandler; 30 | 31 | functionDefinitionHandler.handler = 32 | functionName + '/' + functionDefinitionHandler.handler; 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /libs/serverless-rspack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kitchenshelf/serverless-rspack", 3 | "version": "1.0.0-beta.0", 4 | "license": "MIT", 5 | "author": { 6 | "name": "Chris Williams", 7 | "url": "https://github.com/codingnuclei" 8 | }, 9 | "keywords": [ 10 | "serverless", 11 | "serverless plugin", 12 | "plugin", 13 | "rspack", 14 | "aws lambda", 15 | "aws", 16 | "lambda", 17 | "bundler", 18 | "minifier", 19 | "typescript" 20 | ], 21 | "type": "module", 22 | "main": "./src/index.js", 23 | "typings": "./src/index.d.ts", 24 | "module": "./src/index.js", 25 | "dependencies": { 26 | "archiver": "^7.0.1", 27 | "p-map": "^7.0.2", 28 | "tslib": "^2.3.0", 29 | "webpack-merge": "^6.0.1", 30 | "zod": "^3.23.6" 31 | }, 32 | "peerDependencies": { 33 | "@rsdoctor/rspack-plugin": "1.1.10", 34 | "@rspack/core": "1.4.10", 35 | "serverless": "3.x || 4.x" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/kitchenshelf/serverless-rspack.git", 40 | "directory": "libs/serverless-rspack" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/kitchenshelf/serverless-rspack/issues" 44 | }, 45 | "engines": { 46 | "node": ">=20.13.1" 47 | }, 48 | "publishConfig": { 49 | "access": "public" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | # Run manually using the GitHub UI 5 | workflow_dispatch: 6 | inputs: 7 | version: 8 | description: 'Version to publish' 9 | required: false 10 | default: '' 11 | # ...or whenever a GitHub release gets created 12 | release: 13 | types: [published] 14 | 15 | jobs: 16 | publish: 17 | name: Publish to npm 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: read 21 | id-token: write # needed for provenance data generation 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 # need to fetch all the tags 27 | 28 | - name: Use Node.js v20 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: 20 32 | registry-url: 'https://registry.npmjs.org' 33 | 34 | - name: Install dependencies 35 | run: npm ci 36 | 37 | - name: Apply updated version to packages and publish 38 | run: | 39 | # Use the version from the workflow input if it's set, otherwise use the tag name from the release 40 | VERSION=${{ github.event.inputs.version || github.ref_name }} 41 | npx nx release version --specifier $VERSION 42 | npx nx release publish 43 | env: 44 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 45 | NPM_CONFIG_PROVENANCE: true 46 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | permissions: 10 | actions: read 11 | contents: read 12 | 13 | jobs: 14 | main: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | # Connect your workspace on nx.app and uncomment this to enable task distribution. 22 | # The "--stop-agents-after" is optional, but allows idle agents to shut down once the "e2e-ci" targets have been requested 23 | # - run: npx nx-cloud start-ci-run --distribute-on="5 linux-medium-js" --stop-agents-after="e2e-ci" 24 | 25 | # Cache node_modules 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version: 20 29 | cache: 'npm' 30 | - run: npm ci 31 | - uses: nrwl/nx-set-shas@v4 32 | 33 | - run: git branch --track main origin/main 34 | if: ${{ github.event_name == 'pull_request' }} 35 | 36 | - run: npx nx-cloud record -- nx format:check 37 | - run: npx nx affected -t lint test build 38 | - run: npx nx affected --parallel 1 -t e2e-local 39 | env: 40 | SERVERLESS_ACCESS_KEY: ${{ secrets.SERVERLESS_ACCESS_KEY }} 41 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 42 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 43 | AWS_DEFAULT_REGION: us-west-2 44 | -------------------------------------------------------------------------------- /docs/DEVELOPER.md: -------------------------------------------------------------------------------- 1 | ## Development 2 | 3 | 1. `cd` to the repository root 4 | 2. `npm install` to install all dependencies 5 | 3. `npx nx run serverless-rspack:build --watch` will build the plugin in watch mode 6 | 7 | The example projects point to the local dist of the build. To use the example projects, in a new terminal 8 | 9 | 1. `cd` into an example project i.e. `/examples/complete` 10 | 2. `npm install` to install all dependencies 11 | 3. `npx sls package --verbose` 12 | 13 | To try the plugin in another local repo then 14 | 15 | 1. `npx nx run @serverless-rspack/source:local-registry` to run a local registry 16 | 2. `npx nx run serverless-rspack:build --watch` to build the plugin in watch mode 17 | 3. Edit the `package.json` version in the dist folder to a non-existent version i.e. `1.0.0-my-local.0` 18 | 4. `npx nx release publish` to publish the plugin to the local registry 19 | 5. Follow the [Install Instructions](../README.md#install) as normal in your other local repository, replacing the version with the one you just published. 20 | 6. If you make changes to the plugin, repeat steps 3&4 changing the version number. You will also need to update the `package.json` in your local repo to point to the new version. And perform a fresh install of the plugin in your local repo. 21 | 22 | Note: To test the deploy functionality, you will need to have an AWS account and [AWS credentials configured](https://www.serverless.com/framework/docs/providers/aws/guide/credentials). 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Bug report for sls-rspack behavior. 3 | body: 4 | - type: textarea 5 | id: description 6 | attributes: 7 | label: Describe the bug 8 | description: | 9 | A clear and concise description of the behavior. 10 | validations: 11 | required: true 12 | - type: textarea 13 | id: expected-behavior 14 | attributes: 15 | label: Expected behavior 16 | description: A clear and concise description of what you expect to happen. 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: code 21 | attributes: 22 | label: Reproduction code 23 | description: Code to create a minimal reproduction. 24 | render: typescript 25 | - type: input 26 | id: repro-link 27 | attributes: 28 | label: Reproduction URL 29 | description: Use [Stackblitz](https://stackblitz.com/fork/serverless-rspack) or a git repo to show a minimal reproduction of the issue. Please also paste the example code in the "Reproduction code" section above. 30 | - type: input 31 | id: version 32 | attributes: 33 | label: serverless-rspack version 34 | validations: 35 | required: true 36 | - type: textarea 37 | id: environment 38 | attributes: 39 | label: Environment 40 | placeholder: Version of runtime environment, build configuration, etc, that can affect behavior of serverless-rspack. 41 | - type: textarea 42 | id: addition 43 | attributes: 44 | label: Additional context 45 | placeholder: Add any other context about the problem here. 46 | -------------------------------------------------------------------------------- /e2e/lib/utilities.ts: -------------------------------------------------------------------------------- 1 | import decompress from 'decompress'; 2 | import fs from 'fs-extra'; 3 | import path from 'path'; 4 | 5 | export const validateLambdaFunction = async ( 6 | appName: string, 7 | cloudformation: any, 8 | testArtifactPath: string, 9 | tempAssertDir: string, 10 | esm: boolean = true 11 | ) => { 12 | const zipFile = `${appName}.zip`; 13 | const extractPath = path.join(tempAssertDir, appName); 14 | const functionName = appName.charAt(0).toUpperCase() + appName.slice(1); 15 | const resourceKey = `${functionName}LambdaFunction`; 16 | const outputsKey = `${functionName}LambdaFunctionQualifiedArn`; 17 | 18 | // Test CloudFormation resource 19 | expect(cloudformation.Resources[resourceKey]).toMatchSnapshot({ 20 | Properties: { 21 | Code: { S3Key: expect.stringContaining(zipFile) }, 22 | }, 23 | }); 24 | 25 | // Test CloudFormation output 26 | expect(cloudformation.Outputs[outputsKey]).toMatchSnapshot({ 27 | Value: { 28 | Ref: expect.stringContaining(`${functionName}LambdaVersion`), 29 | }, 30 | }); 31 | 32 | // Test zip contents 33 | const zipFilePath = path.join(testArtifactPath, zipFile); 34 | await decompress(zipFilePath, extractPath); 35 | 36 | const handlerBasePath = 37 | cloudformation.Resources[resourceKey].Properties['Handler'].split('.')[0]; 38 | const handlerPath = path.join( 39 | extractPath, 40 | `${handlerBasePath}.${esm ? 'mjs' : 'js'}` 41 | ); 42 | expect(await fs.pathExists(handlerPath)).toBe(true); 43 | 44 | const indexContents = fs.readFileSync(handlerPath).toString(); 45 | 46 | expect(indexContents).toMatchSnapshot(); 47 | }; 48 | -------------------------------------------------------------------------------- /examples/complete/serverless.yml: -------------------------------------------------------------------------------- 1 | service: complete-example 2 | 3 | plugins: 4 | - '@kitchenshelf/serverless-rspack' 5 | - serverless-offline 6 | 7 | build: 8 | esbuild: false 9 | 10 | custom: 11 | rspack: 12 | keepOutputDirectory: true 13 | sourcemap: 'source-map' 14 | stats: true 15 | esm: true 16 | doctor: 17 | enable: true 18 | outputDirectory: ./ 19 | mode: production 20 | tsConfig: './tsconfig.json' 21 | externals: 22 | - '^@aws-sdk/.*$' 23 | - '^@smithy/.*$' 24 | - '^isin-validator$' 25 | - '^sharp$' 26 | scripts: 27 | - 'echo "First global script"' 28 | - 'echo "Last global script"' 29 | 30 | provider: 31 | name: aws 32 | runtime: nodejs20.x 33 | 34 | package: 35 | individually: true 36 | 37 | functions: 38 | App1: 39 | handler: app1.handler 40 | runtime: 'provided.al2023' 41 | rspack: true 42 | app2: 43 | handler: app2.lambda_handler 44 | runtime: python3.9 45 | app3: 46 | events: 47 | - http: 48 | method: get 49 | path: helloApp3 50 | handler: src/App3.handler 51 | runtime: nodejs20.x 52 | rspack: 53 | enable: true 54 | scripts: 55 | - 'echo "First function script"' 56 | - 'npx npm init -y && npm install --force --os=linux --cpu=x64 --include=optional sharp @img/sharp-linux-x64' 57 | - 'cp $KS_SERVICE_DIR/src/my-image.jpeg ./' 58 | - 'echo "Last function script"' 59 | app4: 60 | handler: src/app4.handler 61 | app5: 62 | handler: src/deeply/nested/somewhat/app5.handler 63 | app6: 64 | handler: src/deeply/nested/somewhat/app5.handler 65 | rspack: false 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverless-rspack/source", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "release": "node --env-file=.env.local ./scripts/release.js" 7 | }, 8 | "private": true, 9 | "devDependencies": { 10 | "@nx/esbuild": "19.0.2", 11 | "@nx/eslint": "19.0.2", 12 | "@nx/eslint-plugin": "19.0.2", 13 | "@nx/jest": "19.0.2", 14 | "@nx/js": "19.0.2", 15 | "@nx/workspace": "19.0.2", 16 | "@swc-node/register": "~1.8.0", 17 | "@swc/core": "~1.3.85", 18 | "@swc/helpers": "~0.5.2", 19 | "@types/archiver": "^6.0.2", 20 | "@types/decompress": "^4.2.7", 21 | "@types/jest": "^29.4.0", 22 | "@types/node": "18.16.9", 23 | "@types/serverless": "^3.12.23", 24 | "@typescript-eslint/eslint-plugin": "^7.3.0", 25 | "@typescript-eslint/parser": "^7.3.0", 26 | "decompress": "^4.2.1", 27 | "esbuild": "^0.19.2", 28 | "eslint": "~8.57.0", 29 | "eslint-config-prettier": "^9.0.0", 30 | "eslint-plugin-require-extensions": "^0.1.3", 31 | "i": "^0.3.7", 32 | "jest": "^29.4.1", 33 | "jest-environment-node": "^29.4.1", 34 | "npm": "^10.7.0", 35 | "nx": "19.0.2", 36 | "prettier": "^2.6.2", 37 | "ts-jest": "^29.1.0", 38 | "ts-node": "10.9.1", 39 | "typescript": "~5.4.2", 40 | "verdaccio": "^5.0.4" 41 | }, 42 | "dependencies": { 43 | "archiver": "^7.0.1", 44 | "p-map": "^7.0.2", 45 | "serverless": "^4.4.7", 46 | "tslib": "^2.3.0", 47 | "webpack-merge": "^6.0.1", 48 | "zod": "^3.23.6" 49 | }, 50 | "peerDependencies": { 51 | "@rsdoctor/rspack-plugin": "1.1.10", 52 | "@rspack/core": "1.4.10" 53 | }, 54 | "nx": { 55 | "includedScripts": [] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/lib/hooks/initialize.ts: -------------------------------------------------------------------------------- 1 | import type { RspackServerlessPlugin } from '../serverless-rspack.js'; 2 | import path from 'node:path'; 3 | 4 | export async function Initialize(this: RspackServerlessPlugin) { 5 | this.pluginOptions = this.getPluginOptions(); 6 | 7 | if (this.pluginOptions.config?.path) { 8 | const configPath = path.join( 9 | this.serviceDirPath, 10 | this.pluginOptions.config.path 11 | ); 12 | if (!this.serverless.utils.fileExistsSync(configPath)) { 13 | throw new this.serverless.classes.Error( 14 | `Rspack config does not exist at path: ${configPath}` 15 | ); 16 | } 17 | const configFn = (await import(configPath)).default; 18 | 19 | if (typeof configFn !== 'function') { 20 | throw new this.serverless.classes.Error( 21 | `Config located at ${configPath} does not return a function. See for reference: https://github.com/kitchenshelf/serverless-rspack/blob/main/README.md#config-file` 22 | ); 23 | } 24 | 25 | this.providedRspackConfig = await configFn(this.serverless); 26 | } 27 | 28 | const functions = this.serverless.service.getAllFunctions(); 29 | this.functionEntries = this.buildFunctionEntries(functions); 30 | if ( 31 | !this.functionEntries || 32 | Object.entries(this.functionEntries).length === 0 33 | ) { 34 | throw new this.serverless.classes.Error( 35 | `No functions detected in service - you can remove this plugin from your service` 36 | ); 37 | } 38 | this.log.verbose('[sls-rspack] Function Entries:', this.functionEntries); 39 | 40 | this.functionScripts = this.buildFunctionScripts(functions); 41 | this.log.verbose('[sls-rspack] Function Scripts:', this.functionScripts); 42 | } 43 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/lib/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { FunctionDefinitionHandler } from 'serverless'; 2 | 3 | export const humanSize = (size: number) => { 4 | if (size === 0) { 5 | return '0.00 B'; 6 | } 7 | if (size >= Number.MAX_SAFE_INTEGER) { 8 | return 'MSI'; 9 | } 10 | const exponent = Math.floor(Math.log(size) / Math.log(1024)); 11 | const sanitized = (size / 1024 ** exponent).toFixed(2); 12 | 13 | return `${sanitized} ${['B', 'KB', 'MB', 'GB', 'TB'][exponent]}`; 14 | }; 15 | 16 | /** 17 | * Checks if the runtime for the given function is nodejs. 18 | * If the runtime is not set , checks the global runtime. 19 | * @param {FunctionDefinitionHandler} func the function to be checked 20 | * @returns {boolean} true if the function/global runtime is nodejs; false, otherwise 21 | */ 22 | export function isNodeFunction( 23 | func: FunctionDefinitionHandler, 24 | providerRuntime: string | undefined 25 | ): boolean { 26 | const runtime = (func.runtime || providerRuntime) ?? ''; 27 | 28 | return typeof runtime === 'string' && runtime.startsWith('node'); 29 | } 30 | 31 | export function determineFileParts(handlerFile: string) { 32 | const regex = /^(.*)\/([^/]+)$/; 33 | const result = regex.exec(handlerFile); 34 | 35 | const filePath = result?.[1] ?? ''; 36 | const fileName = result?.[2] ?? handlerFile; 37 | return { filePath, fileName }; 38 | } 39 | 40 | export function enabledViaSimpleConfig(field: unknown): field is boolean { 41 | return typeof field === 'boolean' && field === true; 42 | } 43 | 44 | export function enabledViaConfigObject( 45 | field: T 46 | ): field is T { 47 | return ( 48 | typeof field === 'object' && 49 | field !== null && 50 | (field.enable === true || field.enable === undefined) 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 | "namedInputs": { 4 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 5 | "production": [ 6 | "default", 7 | "!{projectRoot}/.eslintrc.json", 8 | "!{projectRoot}/eslint.config.js", 9 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 10 | "!{projectRoot}/tsconfig.spec.json", 11 | "!{projectRoot}/jest.config.[jt]s", 12 | "!{projectRoot}/src/test-setup.[jt]s", 13 | "!{projectRoot}/test-setup.[jt]s" 14 | ], 15 | "sharedGlobals": [] 16 | }, 17 | "nxCloudAccessToken": "M2UwYzE1NjQtNDJlYy00NjI2LWIyZGUtZTBlODA3MjE4YzhjfHJlYWQtd3JpdGU=", 18 | "targetDefaults": { 19 | "@nx/esbuild:esbuild": { 20 | "cache": true, 21 | "dependsOn": ["^build"], 22 | "inputs": ["production", "^production"] 23 | }, 24 | "@nx/js:tsc": { 25 | "cache": true, 26 | "dependsOn": ["^build"], 27 | "inputs": ["production", "^production"] 28 | }, 29 | "e2e-local": { 30 | "cache": true, 31 | "dependsOn": ["^build", "^package"] 32 | } 33 | }, 34 | "release": { 35 | "projects": ["libs/*"], 36 | "version": { 37 | "preVersionCommand": "npx nx run-many -t build", 38 | "conventionalCommits": true 39 | }, 40 | "changelog": { 41 | "workspaceChangelog": { 42 | "createRelease": "github" 43 | } 44 | } 45 | }, 46 | "plugins": [ 47 | { 48 | "plugin": "@nx/eslint/plugin", 49 | "options": { 50 | "targetName": "lint" 51 | } 52 | }, 53 | { 54 | "plugin": "@nx/jest/plugin", 55 | "exclude": ["e2e/**/*"], 56 | "options": { 57 | "targetName": "test" 58 | } 59 | }, 60 | { 61 | "plugin": "@nx/jest/plugin", 62 | "include": ["e2e/**/*"], 63 | "options": { 64 | "targetName": "e2e-local", 65 | "ciTargetName": "e2e-ci", 66 | "disableJestRuntime": false 67 | } 68 | } 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /examples/config/rspack.config.js: -------------------------------------------------------------------------------- 1 | const { rspack } = require('@rspack/core'); 2 | const path = require('node:path'); 3 | const { cwd } = require('node:process'); 4 | 5 | /** @type {import('@rspack/cli').Configuration} */ 6 | const config = (serverless) => { 7 | return { 8 | // mode: 'development', 9 | mode: 'production', 10 | target: 'node', 11 | experiments: { 12 | outputModule: true, 13 | }, 14 | resolve: { 15 | extensions: ['...', '.ts', '.tsx', '.jsx'], 16 | // tsConfig: path.resolve(cwd(), "tsconfig.app.json"), 17 | }, 18 | externals: [ 19 | 'uuid', 20 | function (obj, callback) { 21 | const resource = obj.request; 22 | if ( 23 | /^@aws-sdk\/.*$/.test(resource) || 24 | /^@smithy\/.*$/.test(resource) || 25 | /^isin-validator$/.test(resource) 26 | ) { 27 | return callback(null, 'module ' + resource); 28 | } 29 | callback(); 30 | }, 31 | ], 32 | plugins: [ 33 | new rspack.DefinePlugin({ 34 | 'process.env.NODE_ENV': JSON.stringify(process.env['NODE_ENV']), 35 | }), 36 | new rspack.ProgressPlugin({}), 37 | new rspack.node.NodeTargetPlugin(), 38 | ].filter(Boolean), 39 | output: { 40 | chunkFormat: 'module', 41 | chunkLoading: 'import', 42 | library: { 43 | type: 'module', 44 | }, 45 | }, 46 | module: { 47 | rules: [ 48 | { 49 | test: /\.ts$/, 50 | use: { 51 | loader: 'builtin:swc-loader', 52 | options: { 53 | jsc: { 54 | target: 'es2020', 55 | parser: { 56 | syntax: 'typescript', 57 | }, 58 | }, 59 | }, 60 | }, 61 | }, 62 | ], 63 | }, 64 | optimization: { 65 | moduleIds: 'named', 66 | mangleExports: false, 67 | minimizer: [new rspack.SwcJsMinimizerRspackPlugin()], 68 | }, 69 | }; 70 | }; 71 | module.exports = config; 72 | -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { releaseVersion, releaseChangelog } = require('nx/release'); 3 | const yargs = require('yargs'); 4 | 5 | (async () => { 6 | try { 7 | const options = await yargs 8 | // don't use the default meaning of version in yargs 9 | .version(false) 10 | .option('version', { 11 | type: 'string', 12 | }) 13 | .option('dryRun', { 14 | alias: 'd', 15 | type: 'boolean', 16 | default: true, 17 | }) 18 | .option('verbose', { 19 | type: 'boolean', 20 | default: false, 21 | }) 22 | .parseAsync(); 23 | 24 | if (!options.dryRun) { 25 | if (!process.env.GH_TOKEN && !process.env.GITHUB_TOKEN) { 26 | throw new Error( 27 | `GH_TOKEN or GITHUB_TOKEN environment variable must be set in order to run a real release` 28 | ); 29 | } 30 | } 31 | 32 | console.log(); 33 | console.info(`********* Release Options **********`); 34 | console.info( 35 | `version : ${options.version ?? 'use conventional commits'}` 36 | ); 37 | console.info( 38 | `dryRun : ${options.dryRun} ${options.dryRun ? '😅' : '🚨🚨🚨'}` 39 | ); 40 | console.info(`verbose : ${options.verbose}`); 41 | console.log(); 42 | 43 | const { workspaceVersion, projectsVersionData } = await releaseVersion({ 44 | specifier: options.version, 45 | dryRun: options.dryRun, 46 | verbose: options.verbose, 47 | }); 48 | 49 | // This will create a release on GitHub, which will act as a trigger for the publish.yml workflow 50 | await releaseChangelog({ 51 | version: workspaceVersion, 52 | versionData: projectsVersionData, 53 | dryRun: options.dryRun, 54 | verbose: options.verbose, 55 | }); 56 | 57 | if (!options.dryRun) { 58 | console.log( 59 | 'Check GitHub: https://github.com/kitchenshelf/serverless-rspack/actions/workflows/publish.yml' 60 | ); 61 | } 62 | 63 | process.exit(0); 64 | } catch (error) { 65 | console.error(error); 66 | process.exit(1); 67 | } 68 | })(); 69 | -------------------------------------------------------------------------------- /e2e/esm.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | import path from 'path'; 3 | import { validateLambdaFunction } from './lib/utilities'; 4 | 5 | describe('Lambda Function Deployments', () => { 6 | let cloudformation: any; 7 | let testArtifactPath: string; 8 | const tempAssertDir = path.join('./assert-esm' + Date.now()); 9 | 10 | beforeAll(() => { 11 | testArtifactPath = path.resolve(__dirname, '../examples/esm/.serverless'); 12 | 13 | cloudformation = require(path.join( 14 | testArtifactPath, 15 | 'cloudformation-template-update-stack.json' 16 | )); 17 | }); 18 | 19 | afterEach(async () => { 20 | // Clean extracted files between tests 21 | await fs.emptyDir(tempAssertDir); 22 | }); 23 | 24 | afterAll(() => { 25 | fs.rmSync(tempAssertDir, { recursive: true }); 26 | }); 27 | 28 | test('Top level cloudformation', async () => { 29 | expect(cloudformation.AWSTemplateFormatVersion).toMatchSnapshot(); 30 | }); 31 | 32 | test('Correct zips are outputted', async () => { 33 | const zipFiles = fs 34 | .readdirSync(testArtifactPath) 35 | .filter((file) => file.endsWith('.zip')); 36 | 37 | const expectedZipFiles = [ 38 | 'App1.zip', 39 | 'app2.zip', //Handled and packaged by serverless (python) 40 | 'app3.zip', 41 | 'app4.zip', 42 | 'app5.zip', 43 | ]; 44 | 45 | expect(zipFiles).toEqual(expect.arrayContaining(expectedZipFiles)); 46 | }); 47 | 48 | describe('apps packaged by rspack', () => { 49 | test('App1 Lambda Function', async () => { 50 | await validateLambdaFunction( 51 | 'App1', 52 | cloudformation, 53 | testArtifactPath, 54 | tempAssertDir 55 | ); 56 | }); 57 | 58 | test('app3 Lambda Function', async () => { 59 | await validateLambdaFunction( 60 | 'app3', 61 | cloudformation, 62 | testArtifactPath, 63 | tempAssertDir 64 | ); 65 | }); 66 | 67 | test('app4 Lambda Function', async () => { 68 | await validateLambdaFunction( 69 | 'app4', 70 | cloudformation, 71 | testArtifactPath, 72 | tempAssertDir 73 | ); 74 | }); 75 | 76 | test('app5 Lambda Function', async () => { 77 | await validateLambdaFunction( 78 | 'app5', 79 | cloudformation, 80 | testArtifactPath, 81 | tempAssertDir 82 | ); 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /e2e/config.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | import path from 'path'; 3 | import { validateLambdaFunction } from './lib/utilities'; 4 | 5 | describe('Lambda Function Deployments', () => { 6 | let cloudformation: any; 7 | let testArtifactPath: string; 8 | const tempAssertDir = path.join('./assert-config' + Date.now()); 9 | 10 | beforeAll(() => { 11 | testArtifactPath = path.resolve( 12 | __dirname, 13 | '../examples/config/.serverless' 14 | ); 15 | 16 | cloudformation = require(path.join( 17 | testArtifactPath, 18 | 'cloudformation-template-update-stack.json' 19 | )); 20 | }); 21 | 22 | afterEach(async () => { 23 | // Clean extracted files between tests 24 | await fs.emptyDir(tempAssertDir); 25 | }); 26 | 27 | afterAll(() => { 28 | fs.rmSync(tempAssertDir, { recursive: true }); 29 | }); 30 | 31 | test('Top level cloudformation', async () => { 32 | expect(cloudformation.AWSTemplateFormatVersion).toMatchSnapshot(); 33 | }); 34 | 35 | test('Correct zips are outputted', async () => { 36 | const zipFiles = fs 37 | .readdirSync(testArtifactPath) 38 | .filter((file) => file.endsWith('.zip')); 39 | 40 | const expectedZipFiles = [ 41 | 'App1.zip', 42 | 'app2.zip', //Handled and packaged by serverless (python) 43 | 'app3.zip', 44 | 'app4.zip', 45 | 'app5.zip', 46 | ]; 47 | 48 | expect(zipFiles).toEqual(expect.arrayContaining(expectedZipFiles)); 49 | }); 50 | 51 | describe('apps packaged by rspack', () => { 52 | test('App1 Lambda Function', async () => { 53 | await validateLambdaFunction( 54 | 'App1', 55 | cloudformation, 56 | testArtifactPath, 57 | tempAssertDir 58 | ); 59 | }); 60 | 61 | test('app3 Lambda Function', async () => { 62 | await validateLambdaFunction( 63 | 'app3', 64 | cloudformation, 65 | testArtifactPath, 66 | tempAssertDir 67 | ); 68 | }); 69 | 70 | test('app4 Lambda Function', async () => { 71 | await validateLambdaFunction( 72 | 'app4', 73 | cloudformation, 74 | testArtifactPath, 75 | tempAssertDir 76 | ); 77 | }); 78 | 79 | test('app5 Lambda Function', async () => { 80 | await validateLambdaFunction( 81 | 'app5', 82 | cloudformation, 83 | testArtifactPath, 84 | tempAssertDir 85 | ); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/test/test-utils.ts: -------------------------------------------------------------------------------- 1 | import type Serverless from 'serverless'; 2 | import { Logging } from 'serverless/classes/Plugin.js'; 3 | import type Service from 'serverless/classes/Service'; 4 | 5 | export const logger: Logging = { 6 | log: { 7 | error: jest.fn(), 8 | warning: jest.fn(), 9 | notice: jest.fn(), 10 | info: jest.fn(), 11 | debug: jest.fn(), 12 | verbose: jest.fn(), 13 | success: jest.fn(), 14 | }, 15 | writeText: jest.fn(), 16 | progress: { 17 | get: jest.fn(), 18 | create: jest.fn(), 19 | }, 20 | }; 21 | 22 | export const mockProvider: Service['provider'] = { 23 | name: 'aws', 24 | region: 'eu-west-1', 25 | stage: 'dev', 26 | runtime: 'nodejs20.x', 27 | compiledCloudFormationTemplate: { 28 | Resources: {}, 29 | }, 30 | versionFunctions: true, 31 | }; 32 | 33 | export const functions: Service['functions'] = { 34 | hello1: { 35 | handler: 'hello1.handler', 36 | events: [], 37 | package: { artifact: 'hello1' }, 38 | }, 39 | hello2: { 40 | handler: 'hello2.handler', 41 | events: [], 42 | package: { artifact: 'hello2' }, 43 | }, 44 | }; 45 | 46 | export const packageIndividuallyService: () => Partial = () => ({ 47 | functions: functions, 48 | package: { individually: true }, 49 | provider: mockProvider, 50 | getFunction: jest.fn().mockImplementation((name) => functions[name]), 51 | getAllFunctions: jest.fn().mockReturnValue(Object.keys(functions)), 52 | }); 53 | 54 | export const mockServerlessConfig = ( 55 | serviceOverride?: Partial 56 | ): Serverless => { 57 | const service = { 58 | ...packageIndividuallyService(), 59 | ...serviceOverride, 60 | } as Service; 61 | 62 | const mockCli = { 63 | log: jest.fn(), 64 | }; 65 | 66 | return { 67 | service, 68 | config: { 69 | servicePath: '/workDir', 70 | serviceDir: '/workDir', 71 | }, 72 | configSchemaHandler: { 73 | defineCustomProperties: jest.fn(), 74 | defineFunctionEvent: jest.fn(), 75 | defineFunctionEventProperties: jest.fn(), 76 | defineFunctionProperties: jest.fn(), 77 | defineProvider: jest.fn(), 78 | defineTopLevelProperty: jest.fn(), 79 | }, 80 | cli: mockCli, 81 | utils: { 82 | fileExistsSync: jest.fn(), 83 | } as any, 84 | classes: { 85 | Error: jest.fn() as any, 86 | }, 87 | } as Partial as Serverless; 88 | }; 89 | 90 | export const mockOptions: Serverless.Options = { 91 | region: 'eu-east-1', 92 | stage: 'dev', 93 | }; 94 | -------------------------------------------------------------------------------- /e2e/partial-config.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | import path from 'path'; 3 | import { validateLambdaFunction } from './lib/utilities'; 4 | 5 | describe('Lambda Function Deployments', () => { 6 | let cloudformation: any; 7 | let testArtifactPath: string; 8 | const tempAssertDir = path.join('./assert-partial-config' + Date.now()); 9 | 10 | beforeAll(() => { 11 | testArtifactPath = path.resolve( 12 | __dirname, 13 | '../examples/partial-config/.serverless' 14 | ); 15 | 16 | cloudformation = require(path.join( 17 | testArtifactPath, 18 | 'cloudformation-template-update-stack.json' 19 | )); 20 | }); 21 | 22 | afterEach(async () => { 23 | // Clean extracted files between tests 24 | await fs.emptyDir(tempAssertDir); 25 | }); 26 | 27 | afterAll(() => { 28 | fs.rmSync(tempAssertDir, { recursive: true }); 29 | }); 30 | 31 | test('Top level cloudformation', async () => { 32 | expect(cloudformation.AWSTemplateFormatVersion).toMatchSnapshot(); 33 | }); 34 | 35 | test('Correct zips are outputted', async () => { 36 | const zipFiles = fs 37 | .readdirSync(testArtifactPath) 38 | .filter((file) => file.endsWith('.zip')); 39 | 40 | const expectedZipFiles = [ 41 | 'App1.zip', 42 | 'app2.zip', //Handled and packaged by serverless (python) 43 | 'app3.zip', 44 | 'app4.zip', 45 | 'app5.zip', 46 | ]; 47 | 48 | expect(zipFiles).toEqual(expect.arrayContaining(expectedZipFiles)); 49 | }); 50 | 51 | describe('apps packaged by rspack', () => { 52 | test('App1 Lambda Function', async () => { 53 | await validateLambdaFunction( 54 | 'App1', 55 | cloudformation, 56 | testArtifactPath, 57 | tempAssertDir, 58 | false 59 | ); 60 | }); 61 | 62 | test('app3 Lambda Function', async () => { 63 | await validateLambdaFunction( 64 | 'app3', 65 | cloudformation, 66 | testArtifactPath, 67 | tempAssertDir, 68 | false 69 | ); 70 | }); 71 | 72 | test('app4 Lambda Function', async () => { 73 | await validateLambdaFunction( 74 | 'app4', 75 | cloudformation, 76 | testArtifactPath, 77 | tempAssertDir, 78 | false 79 | ); 80 | }); 81 | 82 | test('app5 Lambda Function', async () => { 83 | await validateLambdaFunction( 84 | 'app5', 85 | cloudformation, 86 | testArtifactPath, 87 | tempAssertDir, 88 | false 89 | ); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/test/hooks/deploy-function/after-package-function.spec.ts: -------------------------------------------------------------------------------- 1 | import { rm } from 'node:fs/promises'; 2 | import { RspackServerlessPlugin } from '../../../lib/serverless-rspack.js'; 3 | import { PluginOptions } from '../../../lib/types.js'; 4 | import { logger, mockOptions, mockServerlessConfig } from '../../test-utils.js'; 5 | 6 | jest.mock('../../../lib/bundle', () => ({ 7 | bundle: jest.fn(), 8 | })); 9 | 10 | jest.mock('node:fs', () => ({ 11 | readdirSync: () => ['hello1.ts', 'hello2.ts'], 12 | })); 13 | 14 | jest.mock('node:fs/promises', () => ({ 15 | rm: jest.fn(), 16 | })); 17 | 18 | afterEach(() => { 19 | jest.resetModules(); 20 | jest.resetAllMocks(); 21 | }); 22 | 23 | describe('after:deploy:function:packageFunction hook', () => { 24 | it('should be defined', () => { 25 | const serverless = mockServerlessConfig(); 26 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 27 | 28 | expect(plugin.hooks['after:deploy:function:packageFunction']).toBeDefined(); 29 | }); 30 | 31 | it('should by default remove the build dir', async () => { 32 | const serverless = mockServerlessConfig(); 33 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 34 | 35 | plugin.pluginOptions = {} as Required; 36 | 37 | await plugin.hooks['after:deploy:function:packageFunction'](); 38 | 39 | expect(rm).toHaveBeenCalledWith('/workDir/.rspack', { 40 | recursive: true, 41 | }); 42 | }); 43 | 44 | it('should remove the build dir when keepOutputDirectory is false', async () => { 45 | const serverless = mockServerlessConfig(); 46 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 47 | 48 | plugin.pluginOptions = { 49 | keepOutputDirectory: false, 50 | } as Required; 51 | 52 | await plugin.hooks['after:deploy:function:packageFunction'](); 53 | 54 | expect(rm).toHaveBeenCalledWith('/workDir/.rspack', { 55 | recursive: true, 56 | }); 57 | }); 58 | 59 | it('keep the build dir when keepOutputDirectory is true', async () => { 60 | const serverless = mockServerlessConfig(); 61 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 62 | 63 | plugin.pluginOptions = { 64 | keepOutputDirectory: true, 65 | } as Required; 66 | 67 | await plugin.hooks['after:deploy:function:packageFunction'](); 68 | expect(rm).not.toHaveBeenCalledWith('/workDir/.rspack', { 69 | recursive: true, 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/test/hooks/package/after-create-deployment-artifacts.spec.ts: -------------------------------------------------------------------------------- 1 | import { rm } from 'node:fs/promises'; 2 | import { RspackServerlessPlugin } from '../../../lib/serverless-rspack.js'; 3 | import { PluginOptions } from '../../../lib/types.js'; 4 | import { logger, mockOptions, mockServerlessConfig } from '../../test-utils.js'; 5 | 6 | jest.mock('../../../lib/bundle', () => ({ 7 | bundle: jest.fn(), 8 | })); 9 | 10 | jest.mock('node:fs', () => ({ 11 | readdirSync: () => ['hello1.ts', 'hello2.ts'], 12 | })); 13 | 14 | jest.mock('node:fs/promises', () => ({ 15 | rm: jest.fn(), 16 | })); 17 | 18 | afterEach(() => { 19 | jest.resetModules(); 20 | jest.resetAllMocks(); 21 | }); 22 | 23 | describe('after:package:createDeploymentArtifacts hook', () => { 24 | it('should be defined', () => { 25 | const serverless = mockServerlessConfig(); 26 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 27 | 28 | expect( 29 | plugin.hooks['after:package:createDeploymentArtifacts'] 30 | ).toBeDefined(); 31 | }); 32 | 33 | it('should by default remove the build dir', async () => { 34 | const serverless = mockServerlessConfig(); 35 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 36 | 37 | plugin.pluginOptions = {} as Required; 38 | 39 | await plugin.hooks['after:package:createDeploymentArtifacts'](); 40 | 41 | expect(rm).toHaveBeenCalledWith('/workDir/.rspack', { 42 | recursive: true, 43 | }); 44 | }); 45 | 46 | it('should remove the build dir when keepOutputDirectory is false', async () => { 47 | const serverless = mockServerlessConfig(); 48 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 49 | 50 | plugin.pluginOptions = { 51 | keepOutputDirectory: false, 52 | } as Required; 53 | 54 | await plugin.hooks['after:package:createDeploymentArtifacts'](); 55 | 56 | expect(rm).toHaveBeenCalledWith('/workDir/.rspack', { 57 | recursive: true, 58 | }); 59 | }); 60 | 61 | it('keep the build dir', async () => { 62 | const serverless = mockServerlessConfig(); 63 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 64 | 65 | plugin.pluginOptions = { 66 | keepOutputDirectory: true, 67 | } as Required; 68 | 69 | await plugin.hooks['after:package:createDeploymentArtifacts'](); 70 | expect(rm).not.toHaveBeenCalledWith('/workDir/.rspack', { 71 | recursive: true, 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/test/helpers.spec.ts: -------------------------------------------------------------------------------- 1 | import type { FunctionDefinitionHandler } from 'serverless'; 2 | import { 3 | determineFileParts, 4 | humanSize, 5 | isNodeFunction, 6 | } from '../lib/helpers.js'; 7 | 8 | describe('humanSize', () => { 9 | it('formats different size ranges correctly', () => { 10 | expect(humanSize(512)).toBe('512.00 B'); 11 | expect(humanSize(1024)).toBe('1.00 KB'); 12 | expect(humanSize(1024 * 1024)).toBe('1.00 MB'); 13 | expect(humanSize(1024 * 1024 * 1024)).toBe('1.00 GB'); 14 | expect(humanSize(1024 * 1024 * 1024 * 1024)).toBe('1.00 TB'); 15 | }); 16 | 17 | it('handles edge cases', () => { 18 | expect(humanSize(0)).toBe('0.00 B'); 19 | expect(humanSize(1)).toBe('1.00 B'); 20 | expect(humanSize(Number.MAX_SAFE_INTEGER)).toBe('MSI'); 21 | }); 22 | }); 23 | 24 | describe('isNodeFunction', () => { 25 | const baseFunc = { 26 | handler: 'handler.js', 27 | } as FunctionDefinitionHandler; 28 | 29 | it('detects nodejs runtime from function', () => { 30 | expect( 31 | isNodeFunction({ ...baseFunc, runtime: 'nodejs18.x' }, undefined) 32 | ).toBe(true); 33 | expect( 34 | isNodeFunction({ ...baseFunc, runtime: 'nodejs16.x' }, undefined) 35 | ).toBe(true); 36 | expect( 37 | isNodeFunction({ ...baseFunc, runtime: 'python3.9' }, undefined) 38 | ).toBe(false); 39 | }); 40 | 41 | it('falls back to provider runtime', () => { 42 | expect(isNodeFunction(baseFunc, 'nodejs18.x')).toBe(true); 43 | expect(isNodeFunction(baseFunc, 'python3.9')).toBe(false); 44 | }); 45 | 46 | it('handles edge cases', () => { 47 | expect(isNodeFunction(baseFunc, undefined)).toBe(false); 48 | expect(isNodeFunction({ ...baseFunc, runtime: '' }, undefined)).toBe(false); 49 | expect(isNodeFunction({ ...baseFunc, runtime: undefined }, undefined)).toBe( 50 | false 51 | ); 52 | }); 53 | }); 54 | 55 | describe('determineFileParts', () => { 56 | it('splits path and filename correctly', () => { 57 | expect(determineFileParts('src/handler.js')).toEqual({ 58 | fileName: 'handler.js', 59 | filePath: 'src', 60 | }); 61 | expect(determineFileParts('deep/nested/path/file.js')).toEqual({ 62 | fileName: 'file.js', 63 | filePath: 'deep/nested/path', 64 | }); 65 | }); 66 | 67 | it('handles files in root directory', () => { 68 | expect(determineFileParts('handler.js')).toEqual({ 69 | fileName: 'handler.js', 70 | filePath: '', 71 | }); 72 | }); 73 | 74 | it('handles edge cases', () => { 75 | expect(determineFileParts('./handler.js')).toEqual({ 76 | fileName: 'handler.js', 77 | filePath: '.', 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/lib/pack.ts: -------------------------------------------------------------------------------- 1 | import archiver from 'archiver'; 2 | import fs from 'node:fs'; 3 | import path from 'node:path'; 4 | import { humanSize } from './helpers.js'; 5 | import type { RspackServerlessPlugin } from './serverless-rspack.js'; 6 | 7 | export async function pack(this: RspackServerlessPlugin) { 8 | if (Object.keys(this.functionEntries).length === 0) { 9 | this.log.verbose('[Pack] No functions to pack'); 10 | return; 11 | } 12 | const pMap = (await import('p-map')).default; 13 | const zipMapper = async (name: string) => { 14 | const loadedFunc = this.serverless.service.getFunction(name); 15 | const zipName = `${name}.zip`; 16 | 17 | const artifactPath = path.join( 18 | this.serviceDirPath, 19 | this.packageOutputFolder, 20 | zipName 21 | ); 22 | try { 23 | this.log.verbose(`[Pack] Compressing ${name} to ${artifactPath}`); 24 | const startZip = Date.now(); 25 | await zipDirectory( 26 | this.serviceDirPath + '/' + this.buildOutputFolder + '/' + name, 27 | artifactPath 28 | ); 29 | const { size } = fs.statSync(artifactPath); 30 | 31 | this.log.verbose( 32 | `[Performance] Pack service ${this.serverless.service.service}: ${ 33 | loadedFunc.name 34 | } - ${humanSize(size)} [${Date.now() - startZip} ms]` 35 | ); 36 | } catch (error) { 37 | throw new this.serverless.classes.Error( 38 | `[Pack] Failed to zip ${name} with: ${error}` 39 | ); 40 | } 41 | loadedFunc.package = { 42 | artifact: artifactPath, 43 | }; 44 | }; 45 | 46 | this.log.verbose( 47 | `[Pack] Packing service ${this.serverless.service.service} with concurrency: [${this.pluginOptions.zipConcurrency}] ` 48 | ); 49 | 50 | await pMap( 51 | Object.entries(this.functionEntries).map((x) => x[0]), 52 | zipMapper, 53 | { 54 | concurrency: this.pluginOptions.zipConcurrency, 55 | } 56 | ); 57 | } 58 | 59 | /** 60 | * From: https://stackoverflow.com/a/51518100 61 | * @param {String} sourceDir: /some/folder/to/compress 62 | * @param {String} outPath: /path/to/created.zip 63 | * @returns {Promise} 64 | */ 65 | function zipDirectory(sourceDir: string, outPath: string) { 66 | const archive = archiver('zip', { zlib: { level: 9 } }); 67 | 68 | const outDir = path.dirname(outPath); 69 | 70 | if (!fs.existsSync(outDir)) { 71 | fs.mkdirSync(outDir, { recursive: true }); 72 | } 73 | const stream = fs.createWriteStream(outPath); 74 | 75 | return new Promise((resolve, reject) => { 76 | archive 77 | .directory(sourceDir, false) 78 | .on('error', (err) => reject(err)) 79 | .pipe(stream); 80 | 81 | stream.on('close', () => resolve('success')); 82 | void archive.finalize(); 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /e2e/complete.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | import path from 'path'; 3 | import { validateLambdaFunction } from './lib/utilities'; 4 | 5 | describe('Lambda Function Deployments', () => { 6 | let cloudformation: any; 7 | let testArtifactPath: string; 8 | const tempAssertDir = path.join('./assert-complete' + Date.now()); 9 | 10 | beforeAll(() => { 11 | testArtifactPath = path.resolve( 12 | __dirname, 13 | '../examples/complete/.serverless' 14 | ); 15 | 16 | cloudformation = require(path.join( 17 | testArtifactPath, 18 | 'cloudformation-template-update-stack.json' 19 | )); 20 | }); 21 | 22 | afterEach(async () => { 23 | // Clean extracted files between tests 24 | await fs.emptyDir(tempAssertDir); 25 | }); 26 | 27 | afterAll(() => { 28 | fs.rmSync(tempAssertDir, { recursive: true }); 29 | }); 30 | 31 | test('Top level cloudformation', async () => { 32 | expect(cloudformation.AWSTemplateFormatVersion).toMatchSnapshot(); 33 | }); 34 | 35 | test('Correct zips are outputted', async () => { 36 | const zipFiles = fs 37 | .readdirSync(testArtifactPath) 38 | .filter((file) => file.endsWith('.zip')); 39 | 40 | const expectedZipFiles = [ 41 | 'App1.zip', 42 | 'app2.zip', //Handled and packaged by serverless (python) 43 | 'app3.zip', 44 | 'app4.zip', 45 | 'app5.zip', 46 | 'app6.zip', // Handled and packaged by serverless (rspack false) 47 | ]; 48 | 49 | expect(zipFiles).toEqual(expect.arrayContaining(expectedZipFiles)); 50 | }); 51 | 52 | describe('apps packaged by rspack', () => { 53 | test('App1 Lambda Function', async () => { 54 | await validateLambdaFunction( 55 | 'App1', 56 | cloudformation, 57 | testArtifactPath, 58 | tempAssertDir 59 | ); 60 | }); 61 | 62 | test('app3 Lambda Function', async () => { 63 | await validateLambdaFunction( 64 | 'app3', 65 | cloudformation, 66 | testArtifactPath, 67 | tempAssertDir 68 | ); 69 | const extractPath = path.join(tempAssertDir, 'app3'); 70 | const nodeModules = fs 71 | .readdirSync(path.join(extractPath, 'node_modules')) 72 | .toString(); 73 | 74 | expect(nodeModules).toEqual(expect.stringContaining('sharp')); 75 | }); 76 | 77 | test('app4 Lambda Function', async () => { 78 | await validateLambdaFunction( 79 | 'app4', 80 | cloudformation, 81 | testArtifactPath, 82 | tempAssertDir 83 | ); 84 | }); 85 | 86 | test('app5 Lambda Function', async () => { 87 | await validateLambdaFunction( 88 | 'app5', 89 | cloudformation, 90 | testArtifactPath, 91 | tempAssertDir 92 | ); 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/test/serverless-rspack.spec.ts: -------------------------------------------------------------------------------- 1 | import { RspackServerlessPlugin } from '../lib/serverless-rspack.js'; 2 | import { logger, mockOptions, mockServerlessConfig } from './test-utils.js'; 3 | 4 | jest.mock('../lib/bundle', () => ({ 5 | bundle: jest.fn(), 6 | })); 7 | jest.mock('../lib/pack', () => ({ 8 | pack: jest.fn(), 9 | })); 10 | 11 | jest.mock('node:fs', () => ({ 12 | readdirSync: () => ['hello1.ts', 'hello2.ts'], 13 | })); 14 | 15 | jest.mock('node:fs/promises', () => ({ 16 | rm: jest.fn(), 17 | })); 18 | 19 | afterEach(() => { 20 | jest.resetModules(); 21 | jest.resetAllMocks(); 22 | }); 23 | 24 | describe('RspackServerlessPlugin', () => { 25 | it('should define serverless framework function properties', () => { 26 | const plugin = new RspackServerlessPlugin( 27 | mockServerlessConfig(), 28 | mockOptions, 29 | logger 30 | ); 31 | 32 | expect( 33 | plugin.serverless.configSchemaHandler.defineFunctionProperties 34 | ).toHaveBeenCalledTimes(1); 35 | expect( 36 | plugin.serverless.configSchemaHandler.defineFunctionProperties 37 | ).toHaveBeenLastCalledWith('aws', { 38 | properties: { 39 | rspack: { 40 | oneOf: [ 41 | { type: 'boolean' }, 42 | { 43 | type: 'object', 44 | properties: { 45 | enable: { type: 'boolean' }, 46 | scripts: { type: 'array', items: { type: 'string' } }, 47 | }, 48 | required: [], 49 | }, 50 | ], 51 | }, 52 | }, 53 | }); 54 | }); 55 | 56 | it('should initialize class fields', () => { 57 | const plugin = new RspackServerlessPlugin( 58 | mockServerlessConfig(), 59 | mockOptions, 60 | logger 61 | ); 62 | 63 | // Workaround as `Received: serializes to the same string` 64 | expect(JSON.stringify(plugin.serverless)).toEqual( 65 | JSON.stringify(mockServerlessConfig()) 66 | ); 67 | expect(plugin.options).toEqual(mockOptions); 68 | expect(plugin.log).toEqual(logger.log); 69 | expect(plugin.serviceDirPath).toEqual( 70 | mockServerlessConfig().config.serviceDir 71 | ); 72 | expect(plugin.packageOutputFolder).toEqual('.serverless'); 73 | expect(plugin.buildOutputFolder).toEqual('.rspack'); 74 | expect(plugin.buildOutputFolderPath).toEqual('/workDir/.rspack'); 75 | expect(plugin.offlineMode).toEqual(false); 76 | }); 77 | 78 | it('should set required hooks', () => { 79 | const plugin = new RspackServerlessPlugin( 80 | mockServerlessConfig(), 81 | mockOptions, 82 | logger 83 | ); 84 | 85 | expect(Object.keys(plugin.hooks)).toEqual([ 86 | 'initialize', 87 | 'before:package:createDeploymentArtifacts', 88 | 'after:package:createDeploymentArtifacts', 89 | 'before:deploy:function:packageFunction', 90 | 'after:deploy:function:packageFunction', 91 | 'before:invoke:local:invoke', 92 | 'before:offline:start:init', 93 | ]); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/test/hooks/package/before-create-deployment-artifacts.spec.ts: -------------------------------------------------------------------------------- 1 | import { bundle } from '../../../lib/bundle.js'; 2 | import { pack } from '../../../lib/pack.js'; 3 | import { scripts } from '../../../lib/scripts.js'; 4 | import { RspackServerlessPlugin } from '../../../lib/serverless-rspack.js'; 5 | import { logger, mockOptions, mockServerlessConfig } from '../../test-utils.js'; 6 | 7 | jest.mock('../../../lib/bundle', () => ({ 8 | bundle: jest.fn(), 9 | })); 10 | 11 | jest.mock('../../../lib/scripts', () => ({ 12 | scripts: jest.fn(), 13 | })); 14 | 15 | jest.mock('../../../lib/pack', () => ({ 16 | pack: jest.fn(), 17 | })); 18 | 19 | jest.mock('node:fs', () => ({ 20 | readdirSync: () => ['hello1.ts', 'hello2.ts'], 21 | })); 22 | 23 | jest.mock('node:fs/promises', () => ({ 24 | rm: jest.fn(), 25 | })); 26 | 27 | afterEach(() => { 28 | jest.resetModules(); 29 | jest.resetAllMocks(); 30 | }); 31 | 32 | describe('before:package:createDeploymentArtifacts hook', () => { 33 | it('should be defined', () => { 34 | const serverless = mockServerlessConfig(); 35 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 36 | 37 | expect( 38 | plugin.hooks['before:package:createDeploymentArtifacts'] 39 | ).toBeDefined(); 40 | }); 41 | 42 | it('should bundle the entries', async () => { 43 | const serverless = mockServerlessConfig(); 44 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 45 | 46 | await plugin.hooks['before:package:createDeploymentArtifacts'](); 47 | 48 | expect(bundle).toHaveBeenCalledTimes(1); 49 | expect(bundle).toHaveBeenCalledWith(plugin.functionEntries); 50 | }); 51 | 52 | it('should run scripts', async () => { 53 | const serverless = mockServerlessConfig(); 54 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 55 | 56 | await plugin.hooks['before:package:createDeploymentArtifacts'](); 57 | 58 | expect(scripts).toHaveBeenCalledTimes(1); 59 | expect(scripts).toHaveBeenCalledWith(); 60 | }); 61 | 62 | it('should pack the entries', async () => { 63 | const serverless = mockServerlessConfig(); 64 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 65 | 66 | await plugin.hooks['before:package:createDeploymentArtifacts'](); 67 | 68 | expect(pack).toHaveBeenCalledTimes(1); 69 | }); 70 | 71 | it('should bundle before scripts', async () => { 72 | const serverless = mockServerlessConfig(); 73 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 74 | 75 | await plugin.hooks['before:package:createDeploymentArtifacts'](); 76 | 77 | expect(jest.mocked(bundle).mock.invocationCallOrder[0]).toBeLessThan( 78 | jest.mocked(scripts).mock.invocationCallOrder[0] 79 | ); 80 | }); 81 | 82 | it('should pack after scripts', async () => { 83 | const serverless = mockServerlessConfig(); 84 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 85 | 86 | await plugin.hooks['before:package:createDeploymentArtifacts'](); 87 | 88 | expect(jest.mocked(scripts).mock.invocationCallOrder[0]).toBeLessThan( 89 | jest.mocked(pack).mock.invocationCallOrder[0] 90 | ); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/test/hooks/invoke-local/before-invoke.spec.ts: -------------------------------------------------------------------------------- 1 | import { bundle } from '../../../lib/bundle.js'; 2 | import { scripts } from '../../../lib/scripts.js'; 3 | import { RspackServerlessPlugin } from '../../../lib/serverless-rspack.js'; 4 | import { 5 | functions, 6 | logger, 7 | mockOptions, 8 | mockServerlessConfig, 9 | } from '../../test-utils.js'; 10 | 11 | jest.mock('../../../lib/bundle', () => ({ 12 | bundle: jest.fn(), 13 | })); 14 | 15 | jest.mock('../../../lib/scripts', () => ({ 16 | scripts: jest.fn(), 17 | })); 18 | 19 | jest.mock('node:fs', () => ({ 20 | readdirSync: () => ['hello1.ts', 'hello2.ts'], 21 | })); 22 | 23 | jest.mock('node:fs/promises', () => ({ 24 | rm: jest.fn(), 25 | })); 26 | 27 | afterEach(() => { 28 | jest.resetModules(); 29 | jest.resetAllMocks(); 30 | }); 31 | 32 | describe('before:invoke:local:invoke hook', () => { 33 | it('should bundle the specified function and update servicePath', async () => { 34 | const serverless = mockServerlessConfig(); 35 | const plugin = new RspackServerlessPlugin( 36 | serverless, 37 | { ...mockOptions, function: Object.keys(functions).at(0) }, 38 | logger 39 | ); 40 | 41 | plugin.functionEntries = { 42 | hello1: { import: './hello1.js', filename: 'hello1.js' }, 43 | }; 44 | 45 | await plugin.hooks['before:invoke:local:invoke'](); 46 | 47 | expect(logger.log.verbose).toHaveBeenCalledWith( 48 | '[sls-rspack] before:invoke:local:invoke' 49 | ); 50 | expect(bundle).toHaveBeenCalledWith({ 51 | hello1: { import: './hello1.js', filename: 'hello1.js' }, 52 | }); 53 | expect(scripts).toHaveBeenCalled(); 54 | expect(jest.mocked(bundle).mock.invocationCallOrder[0]).toBeLessThan( 55 | jest.mocked(scripts).mock.invocationCallOrder[0] 56 | ); 57 | expect(serverless.config.servicePath).toBe('/workDir/.rspack/hello1'); 58 | }); 59 | 60 | it('should throw an error if options are not invoke options', async () => { 61 | const serverless = mockServerlessConfig(); 62 | const plugin = new RspackServerlessPlugin(serverless, {}, logger); 63 | 64 | try { 65 | await plugin.hooks['before:invoke:local:invoke'](); 66 | fail('Expected function to throw an error'); 67 | } catch (error) { 68 | expect(serverless.classes.Error).toHaveBeenCalledTimes(1); 69 | expect(serverless.classes.Error).toHaveBeenCalledWith( 70 | 'This hook only supports invoke options' 71 | ); 72 | } 73 | }); 74 | 75 | it('should throw an error if function entries does not contain the invoke function', async () => { 76 | const serverless = mockServerlessConfig(); 77 | const invokeFunc = Object.keys(functions).at(0); 78 | const plugin = new RspackServerlessPlugin( 79 | serverless, 80 | { ...mockOptions, function: invokeFunc }, 81 | logger 82 | ); 83 | plugin.functionEntries = { 84 | wrongFunc: { import: './wrong.js', filename: 'wrong.js' }, 85 | }; 86 | 87 | try { 88 | await plugin.hooks['before:invoke:local:invoke'](); 89 | fail('Expected function to throw an error'); 90 | } catch (error) { 91 | expect(serverless.classes.Error).toHaveBeenCalledTimes(1); 92 | expect(serverless.classes.Error).toHaveBeenCalledWith( 93 | `Function ${invokeFunc} not found in function entries` 94 | ); 95 | } 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import Serverless from 'serverless'; 2 | import { z } from 'zod'; 3 | 4 | const SourceMapOptions = [ 5 | 'eval', 6 | 'cheap-source-map', 7 | 'cheap-module-source-map', 8 | 'source-map', 9 | 'inline-cheap-source-map', 10 | 'inline-cheap-module-source-map', 11 | 'inline-source-map', 12 | 'inline-nosources-cheap-source-map', 13 | 'inline-nosources-cheap-module-source-map', 14 | 'inline-nosources-source-map', 15 | 'nosources-cheap-source-map', 16 | 'nosources-cheap-module-source-map', 17 | 'nosources-source-map', 18 | 'hidden-nosources-cheap-source-map', 19 | 'hidden-nosources-cheap-module-source-map', 20 | 'hidden-nosources-source-map', 21 | 'hidden-cheap-source-map', 22 | 'hidden-cheap-module-source-map', 23 | 'hidden-source-map', 24 | 'eval-cheap-source-map', 25 | 'eval-cheap-module-source-map', 26 | 'eval-source-map', 27 | 'eval-nosources-cheap-source-map', 28 | 'eval-nosources-cheap-module-source-map', 29 | 'eval-nosources-source-map', 30 | ] as const; 31 | 32 | export const PluginOptionsSchema = z.object({ 33 | keepOutputDirectory: z.boolean().optional().default(false), 34 | zipConcurrency: z.number().optional().default(Infinity), 35 | stats: z.boolean().optional().default(false), 36 | doctor: z 37 | .union([ 38 | z.boolean(), 39 | z 40 | .object({ 41 | enable: z.boolean().optional().default(true), 42 | outputDirectory: z.string().optional().nullable(), 43 | }) 44 | .optional() 45 | .nullable(), 46 | ]) 47 | .optional() 48 | .nullable(), 49 | config: z 50 | .object({ 51 | path: z.string(), 52 | strategy: z.enum(['override', 'combine']).optional().default('override'), 53 | }) 54 | .optional() 55 | .nullable(), 56 | scripts: z.array(z.string()).optional().nullable(), 57 | // [START] Rspack influenced - Ignored if config file is provided with `override` strategy 58 | esm: z.boolean().optional().default(false), 59 | mode: z 60 | .enum(['production', 'development', 'none']) 61 | .optional() 62 | .default('production'), 63 | sourcemap: z.union([z.literal(false), z.enum(SourceMapOptions)]).optional(), 64 | tsConfig: z.string().optional().nullable(), 65 | externals: z.array(z.string()).optional().nullable(), 66 | // [END] Rspack influenced - Ignored if config file is provided with `override` strategy 67 | }); 68 | 69 | export type PluginOptions = z.infer; 70 | 71 | export type RsPackFunctionDefinitionHandler = { 72 | rspack?: RsPackFunctionDefinition | boolean; 73 | } & Serverless.FunctionDefinitionHandler; 74 | 75 | type RsPackFunctionDefinition = { 76 | enable?: boolean; 77 | scripts?: string[]; 78 | }; 79 | 80 | export function isInvokeOptions( 81 | options: Serverless.Options 82 | ): options is Serverless.Options & { function: string } { 83 | return typeof options.function === 'string'; 84 | } 85 | 86 | export function isDeployFunctionOptions( 87 | options: Serverless.Options 88 | ): options is Serverless.Options & { function: string } { 89 | return typeof options.function === 'string'; 90 | } 91 | 92 | export type PluginFunctionEntries = { 93 | [name: string]: { 94 | import: string; 95 | filename: string; 96 | }; 97 | }; 98 | 99 | export type PluginFunctionScripts = { 100 | [name: string]: string[]; 101 | }; 102 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/lib/scripts.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'node:child_process'; 2 | import type { RspackServerlessPlugin } from './serverless-rspack.js'; 3 | 4 | export async function scripts(this: RspackServerlessPlugin) { 5 | runFunctionScripts.call(this); 6 | runGlobalScripts.call(this); 7 | } 8 | 9 | function runFunctionScripts(this: RspackServerlessPlugin) { 10 | const scriptEntries = Object.entries(this.functionScripts); 11 | 12 | if (scriptEntries.length > 0) { 13 | const startScripts = Date.now(); 14 | 15 | for (const [functionName, scripts] of scriptEntries) { 16 | this.log.verbose( 17 | `[Scripts] Running ${scripts.length} scripts for function ${functionName}` 18 | ); 19 | const startFunctionScripts = Date.now(); 20 | 21 | for (const [index, script] of scripts.entries()) { 22 | const startScript = Date.now(); 23 | 24 | try { 25 | execSync(script, { 26 | cwd: 27 | this.serviceDirPath + 28 | `/${this.buildOutputFolder}/${functionName}`, 29 | stdio: this.options.verbose ? 'inherit' : 'ignore', 30 | env: { 31 | ...process.env, 32 | KS_SERVICE_DIR: this.serviceDirPath, 33 | KS_BUILD_OUTPUT_FOLDER: this.buildOutputFolder, 34 | KS_PACKAGE_OUTPUT_FOLDER: this.packageOutputFolder, 35 | KS_FUNCTION_NAME: functionName, 36 | }, 37 | }); 38 | 39 | this.log.verbose( 40 | `[Performance] Script ${index + 1} of ${scripts.length} [${ 41 | Date.now() - startScript 42 | } ms]` 43 | ); 44 | } catch (error) { 45 | throw new this.serverless.classes.Error( 46 | `Failed to execute script: ${script}\nError: ${ 47 | (error as Error).message 48 | }` 49 | ); 50 | } 51 | } 52 | 53 | this.log.verbose( 54 | `[Performance] Scripts total execution time for function ${functionName} [${ 55 | Date.now() - startFunctionScripts 56 | } ms]` 57 | ); 58 | } 59 | 60 | this.log.verbose( 61 | `[Performance] Scripts total execution time for all functions in service ${ 62 | this.serverless.service.service 63 | } [${Date.now() - startScripts} ms]` 64 | ); 65 | } 66 | } 67 | 68 | function runGlobalScripts(this: RspackServerlessPlugin) { 69 | if (this.pluginOptions.scripts && this.pluginOptions.scripts.length > 0) { 70 | this.log.verbose( 71 | `[Scripts] Running ${this.pluginOptions.scripts.length} global scripts` 72 | ); 73 | const startScripts = Date.now(); 74 | 75 | for (const [index, script] of this.pluginOptions.scripts.entries()) { 76 | const startScript = Date.now(); 77 | 78 | try { 79 | execSync(script, { 80 | cwd: this.serviceDirPath, 81 | stdio: this.options.verbose ? 'inherit' : 'ignore', 82 | env: { 83 | ...process.env, 84 | KS_SERVICE_DIR: this.serviceDirPath, 85 | KS_BUILD_OUTPUT_FOLDER: this.buildOutputFolder, 86 | KS_PACKAGE_OUTPUT_FOLDER: this.packageOutputFolder, 87 | }, 88 | }); 89 | 90 | this.log.verbose( 91 | `[Performance] Script ${index + 1} of ${ 92 | this.pluginOptions.scripts.length 93 | } [${Date.now() - startScript} ms]` 94 | ); 95 | } catch (error) { 96 | throw new this.serverless.classes.Error( 97 | `Failed to execute script: ${script}\nError: ${ 98 | (error as Error).message 99 | }` 100 | ); 101 | } 102 | } 103 | 104 | this.log.verbose( 105 | `[Performance] Scripts total execution time for global scripts [${ 106 | Date.now() - startScripts 107 | } ms]` 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0-beta.1 (2024-12-31) 2 | 3 | 4 | ### 🚀 Features 5 | 6 | - **sls-rspack:** support serverless v4 ([102bc1c](https://github.com/kitchenshelf/serverless-rspack/commit/102bc1c)) 7 | - **sls-rspack:** add e2e test suite ([6cc6aeb](https://github.com/kitchenshelf/serverless-rspack/commit/6cc6aeb)) 8 | 9 | ### ❤️ Thank You 10 | 11 | - codingnuclei 12 | 13 | ## 1.0.0-alpha.11 (2024-11-06) 14 | 15 | 16 | ### 🚀 Features 17 | 18 | - **sls-pack:** support serverless-offline ([2041d86](https://github.com/kitchenshelf/serverless-rspack/commit/2041d86)) 19 | - **sls-rspack:** support sls cli deploy function ([20fe405](https://github.com/kitchenshelf/serverless-rspack/commit/20fe405)) 20 | - **sls-rspack:** support async rspack config function ([6a9ebcc](https://github.com/kitchenshelf/serverless-rspack/commit/6a9ebcc)) 21 | - **sls-rspack:** support per function scripts ([8e57b12](https://github.com/kitchenshelf/serverless-rspack/commit/8e57b12)) 22 | - **sls-rspack:** update rspack dep to 1.0.14 ([17bd006](https://github.com/kitchenshelf/serverless-rspack/commit/17bd006)) 23 | 24 | ### ❤️ Thank You 25 | 26 | - codingnuclei 27 | 28 | ## 1.0.0-alpha.10 (2024-10-28) 29 | 30 | 31 | ### 🚀 Features 32 | 33 | - **sls-rspack:** support Rsdoctor ([d44ff2c](https://github.com/kitchenshelf/serverless-rspack/commit/d44ff2c)) 34 | 35 | ### ❤️ Thank You 36 | 37 | - codingnuclei 38 | 39 | ## 1.0.0-alpha.9 (2024-10-28) 40 | 41 | 42 | ### 🚀 Features 43 | 44 | - **sls-rspack:** support user supplied scripts ([22eac3a](https://github.com/kitchenshelf/serverless-rspack/commit/22eac3a)) 45 | 46 | ### ❤️ Thank You 47 | 48 | - codingnuclei 49 | 50 | ## 1.0.0-alpha.8 (2024-10-27) 51 | 52 | 53 | ### 🚀 Features 54 | 55 | - **sls-rspack:** enable invoke local ([1a26470](https://github.com/kitchenshelf/serverless-rspack/commit/1a26470)) 56 | 57 | ### ❤️ Thank You 58 | 59 | - codingnuclei 60 | 61 | ## 1.0.0-alpha.7 (2024-10-26) 62 | 63 | 64 | ### 🚀 Features 65 | 66 | - **sls-rspack:** support partial rspack config fix: formating ([6e26e87](https://github.com/kitchenshelf/serverless-rspack/commit/6e26e87)) 67 | 68 | ### ❤️ Thank You 69 | 70 | - codingnuclei 71 | 72 | ## 1.0.0-alpha.6 (2024-09-17) 73 | 74 | 75 | ### 🚀 Features 76 | 77 | - **sls-rspack:** update rspack dep 1.0.5 ([f96ccd4](https://github.com/kitchenshelf/serverless-rspack/commit/f96ccd4)) 78 | 79 | ### ❤️ Thank You 80 | 81 | - codingnuclei 82 | 83 | ## 1.0.0-alpha.5 (2024-07-08) 84 | 85 | 86 | ### 🚀 Features 87 | 88 | - **sls-rspack:** update rspack dep ([cde2b15](https://github.com/kitchenshelf/serverless-rspack/commit/cde2b15)) 89 | 90 | ### ❤️ Thank You 91 | 92 | - codingnuclei 93 | 94 | ## 1.0.0-alpha.4 (2024-05-18) 95 | 96 | 97 | ### 🩹 Fixes 98 | 99 | - **sls-rspack:** commonjs by default ([c4e9acb](https://github.com/kitchenshelf/serverless-rspack/commit/c4e9acb)) 100 | - **sls-rspack:** externals type set to node-commonjs ([d8c5f19](https://github.com/kitchenshelf/serverless-rspack/commit/d8c5f19)) 101 | 102 | ### ❤️ Thank You 103 | 104 | - codingnuclei 105 | 106 | ## 1.0.0-alpha.3 (2024-05-18) 107 | 108 | 109 | ### 🩹 Fixes 110 | 111 | - **sls-rspack:** prevent mangled exports ([6bea3df](https://github.com/kitchenshelf/serverless-rspack/commit/6bea3df)) 112 | 113 | ### ❤️ Thank You 114 | 115 | - codingnuclei 116 | 117 | ## 1.0.0-alpha.2 (2024-05-13) 118 | 119 | 120 | ### 🚀 Features 121 | 122 | - **sls-rspack:** external rspack config must return a function ([43bb276](https://github.com/kitchenshelf/serverless-rspack/commit/43bb276)) 123 | 124 | ### ❤️ Thank You 125 | 126 | - codingnuclei 127 | 128 | ## 1.0.0-alpha.1 (2024-05-13) 129 | 130 | ### 🚀 Features 131 | 132 | - **serverless-rspack:** add zod validation to user custom config ([a0009b6](https://github.com/kitchenshelf/serverless-rspack/commit/a0009b6)) 133 | - **sls-rspack:** only bundle functions that are node based or have new rspack flag ([f070c1a](https://github.com/kitchenshelf/serverless-rspack/commit/f070c1a)) 134 | - **sls-rspack:** support tsconfigpath ([51e75df](https://github.com/kitchenshelf/serverless-rspack/commit/51e75df)) 135 | - **sls-rspack:** support for externals and tsConfigPath ([41a4c4b](https://github.com/kitchenshelf/serverless-rspack/commit/41a4c4b)) 136 | 137 | ### 🩹 Fixes 138 | 139 | - **serverless-rspack:** add correct approach to throwing errors ([7c629fa](https://github.com/kitchenshelf/serverless-rspack/commit/7c629fa)) 140 | 141 | ### ❤️ Thank You 142 | 143 | - codingnuclei 144 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/test/pack.spec.ts: -------------------------------------------------------------------------------- 1 | import archiver from 'archiver'; 2 | import fs, { WriteStream } from 'node:fs'; 3 | import type Serverless from 'serverless'; 4 | import { pack } from '../lib/pack.js'; 5 | import { RspackServerlessPlugin } from '../lib/serverless-rspack.js'; 6 | import { logger, mockOptions, mockServerlessConfig } from './test-utils.js'; 7 | 8 | jest.mock('archiver'); 9 | jest.mock('node:fs'); 10 | jest.mock('p-map', () => ({ 11 | __esModule: true, 12 | default: jest 13 | .fn() 14 | .mockImplementation((items, fn) => Promise.all(items.map(fn))), 15 | })); 16 | 17 | describe('pack', () => { 18 | let mockArchiver: any; 19 | let serverless: Serverless; 20 | let plugin: RspackServerlessPlugin; 21 | 22 | beforeEach(() => { 23 | serverless = mockServerlessConfig({ service: 'test-service' }); 24 | plugin = createRspackPlugin(plugin, serverless); 25 | 26 | // Setup fs mock 27 | (fs.createWriteStream as jest.Mock).mockReturnValue({ 28 | on: jest 29 | .fn() 30 | .mockImplementation(function (this: WriteStream, event, handler) { 31 | if (event === 'close') { 32 | handler(); 33 | } 34 | return this; 35 | }), 36 | pipe: jest.fn(), 37 | }); 38 | (fs.statSync as jest.Mock).mockReturnValue({ size: 1024 }); 39 | (fs.existsSync as jest.Mock).mockReturnValue(true); 40 | (fs.mkdirSync as jest.Mock).mockReturnValue(undefined); 41 | 42 | // Setup archiver mock 43 | mockArchiver = { 44 | directory: jest.fn().mockReturnThis(), 45 | on: jest.fn().mockReturnThis(), 46 | pipe: jest.fn().mockReturnThis(), 47 | finalize: jest.fn(), 48 | }; 49 | (archiver as unknown as jest.Mock).mockReturnValue(mockArchiver); 50 | }); 51 | 52 | afterEach(() => { 53 | jest.clearAllMocks(); 54 | }); 55 | 56 | it('should return early if functionEntries is empty', async () => { 57 | plugin.functionEntries = {}; 58 | await pack.call(plugin); 59 | 60 | expect(plugin.log.verbose).toHaveBeenCalledWith( 61 | '[Pack] No functions to pack' 62 | ); 63 | }); 64 | 65 | it('should process all functions and create zip files', async () => { 66 | await pack.call(plugin); 67 | 68 | expect(plugin.log.verbose).toHaveBeenCalledWith( 69 | expect.stringContaining('Packing service test-service with concurrency') 70 | ); 71 | expect(plugin.serverless.service.getFunction).toHaveBeenCalledTimes(2); 72 | expect(archiver).toHaveBeenCalledWith('zip', { zlib: { level: 9 } }); 73 | }); 74 | 75 | it('should handle zip creation error', async () => { 76 | const error = new Error('Zip failed'); 77 | mockArchiver.directory.mockImplementation(() => { 78 | throw error; 79 | }); 80 | try { 81 | await pack.call(plugin); 82 | fail('Expected function to throw an error'); 83 | } catch (error) { 84 | expect(plugin.serverless.classes.Error).toHaveBeenCalledTimes(2); 85 | expect(plugin.serverless.classes.Error).toHaveBeenCalledWith( 86 | '[Pack] Failed to zip hello1 with: Error: Zip failed' 87 | ); 88 | expect(plugin.serverless.classes.Error).toHaveBeenCalledWith( 89 | '[Pack] Failed to zip hello2 with: Error: Zip failed' 90 | ); 91 | } 92 | }); 93 | 94 | it('should set artifact path in function configuration', async () => { 95 | const mockFunction = { name: 'hello2' } as any; 96 | serverless.service.getFunction = jest.fn().mockReturnValue(mockFunction); 97 | 98 | await pack.call(plugin); 99 | 100 | expect(mockFunction).toHaveProperty('package.artifact'); 101 | expect(mockFunction.package.artifact).toContain( 102 | '/workDir/.serverless/hello2.zip' 103 | ); 104 | }); 105 | 106 | it("should create output directory if it doesn't exist", async () => { 107 | (fs.existsSync as jest.Mock).mockReturnValue(false); 108 | 109 | await pack.call(plugin); 110 | 111 | expect(fs.mkdirSync).toHaveBeenCalledWith(expect.any(String), { 112 | recursive: true, 113 | }); 114 | }); 115 | 116 | it('should log verbose performance metrics', async () => { 117 | await pack.call(plugin); 118 | 119 | expect(plugin.log.verbose).toHaveBeenCalledWith( 120 | expect.stringContaining('Performance') 121 | ); 122 | expect(plugin.log.verbose).toHaveBeenCalledWith( 123 | expect.stringContaining('1.00 KB') 124 | ); 125 | }); 126 | }); 127 | 128 | function createRspackPlugin( 129 | plugin: RspackServerlessPlugin, 130 | serverless: Serverless 131 | ) { 132 | plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 133 | plugin.pluginOptions = { zipConcurrency: 2 } as any; 134 | plugin.functionEntries = { 135 | hello1: {} as any, 136 | hello2: {} as any, 137 | }; 138 | return plugin; 139 | } 140 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/test/hooks/offline/start-init.spec.ts: -------------------------------------------------------------------------------- 1 | import type Service from 'serverless/classes/Service'; 2 | import { bundle } from '../../../lib/bundle.js'; 3 | import { scripts } from '../../../lib/scripts.js'; 4 | import { RspackServerlessPlugin } from '../../../lib/serverless-rspack.js'; 5 | import { RsPackFunctionDefinitionHandler } from '../../../lib/types.js'; 6 | import { logger, mockOptions, mockServerlessConfig } from '../../test-utils.js'; 7 | 8 | jest.mock('../../../lib/bundle', () => ({ 9 | bundle: jest.fn(), 10 | })); 11 | 12 | jest.mock('../../../lib/scripts', () => ({ 13 | scripts: jest.fn(), 14 | })); 15 | 16 | afterEach(() => { 17 | jest.resetModules(); 18 | jest.resetAllMocks(); 19 | }); 20 | 21 | describe('before:offline:start:init hook', () => { 22 | it('should be defined', () => { 23 | const serverless = mockServerlessConfig(); 24 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 25 | 26 | expect(plugin.hooks['before:offline:start:init']).toBeDefined(); 27 | }); 28 | 29 | it('should set default plugin options', async () => { 30 | const serverless = mockServerlessConfig(); 31 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 32 | 33 | await plugin.hooks['before:offline:start:init'](); 34 | 35 | expect(plugin.pluginOptions.sourcemap).toEqual('source-map'); 36 | }); 37 | 38 | it('should set offline mode', async () => { 39 | const serverless = mockServerlessConfig(); 40 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 41 | 42 | await plugin.hooks['before:offline:start:init'](); 43 | 44 | expect(plugin.offlineMode).toBe(true); 45 | }); 46 | 47 | it('should bundle the entries', async () => { 48 | const serverless = mockServerlessConfig(); 49 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 50 | 51 | await plugin.hooks['before:offline:start:init'](); 52 | 53 | expect(bundle).toHaveBeenCalledTimes(1); 54 | expect(bundle).toHaveBeenCalledWith(plugin.functionEntries); 55 | }); 56 | 57 | it('should run scripts', async () => { 58 | const serverless = mockServerlessConfig(); 59 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 60 | 61 | await plugin.hooks['before:offline:start:init'](); 62 | 63 | expect(scripts).toHaveBeenCalledTimes(1); 64 | expect(scripts).toHaveBeenCalledWith(); 65 | }); 66 | 67 | it('should bundle before scripts', async () => { 68 | const serverless = mockServerlessConfig(); 69 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 70 | 71 | await plugin.hooks['before:offline:start:init'](); 72 | 73 | expect(jest.mocked(bundle).mock.invocationCallOrder[0]).toBeLessThan( 74 | jest.mocked(scripts).mock.invocationCallOrder[0] 75 | ); 76 | }); 77 | 78 | it('should set serverless-offline location when custom.serverless-offline is not defined', async () => { 79 | const serverless = mockServerlessConfig(); 80 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 81 | 82 | await plugin.hooks['before:offline:start:init'](); 83 | 84 | expect(plugin.serverless.service.custom['serverless-offline']).toEqual({ 85 | location: plugin.buildOutputFolderPath, 86 | }); 87 | }); 88 | 89 | it('should update existing serverless-offline config', async () => { 90 | const serverless = mockServerlessConfig(); 91 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 92 | 93 | plugin.serverless.service.custom = { 94 | 'serverless-offline': { 95 | existingProp: true, 96 | }, 97 | }; 98 | 99 | await plugin.hooks['before:offline:start:init'](); 100 | 101 | expect(plugin.serverless.service.custom['serverless-offline']).toEqual({ 102 | existingProp: true, 103 | location: plugin.buildOutputFolderPath, 104 | }); 105 | }); 106 | 107 | it('should update handlers', async () => { 108 | const serverless = mockServerlessConfig(); 109 | const functions: Service['functions'] = { 110 | hello1: { 111 | handler: 'hello1.handler', 112 | events: [], 113 | package: { artifact: 'hello1' }, 114 | }, 115 | hello2: { 116 | handler: 'hello2.handler', 117 | events: [], 118 | package: { artifact: 'hello2' }, 119 | }, 120 | }; 121 | 122 | serverless.service.getFunction = (name) => functions[name]; 123 | serverless.service.getAllFunctions = jest 124 | .fn() 125 | .mockReturnValue(Object.keys(functions)); 126 | serverless.service.functions = { ...functions }; 127 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 128 | 129 | await plugin.hooks['before:offline:start:init'](); 130 | 131 | const functionNames = plugin.serverless.service.getAllFunctions(); 132 | 133 | expect(functionNames).toEqual(['hello1', 'hello2']); 134 | 135 | const handlers = functionNames.map((functionName) => { 136 | const functionDefinitionHandler = plugin.serverless.service.getFunction( 137 | functionName 138 | ) as RsPackFunctionDefinitionHandler; 139 | 140 | return functionDefinitionHandler.handler; 141 | }); 142 | 143 | expect(handlers).toEqual([ 144 | 'hello1/hello1.handler', 145 | 'hello2/hello2.handler', 146 | ]); 147 | }); 148 | }); 149 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/test/hooks/deploy-function/before-package-function.spec.ts: -------------------------------------------------------------------------------- 1 | import { bundle } from '../../../lib/bundle.js'; 2 | import { pack } from '../../../lib/pack.js'; 3 | import { scripts } from '../../../lib/scripts.js'; 4 | import { RspackServerlessPlugin } from '../../../lib/serverless-rspack.js'; 5 | import { logger, mockOptions, mockServerlessConfig } from '../../test-utils.js'; 6 | 7 | jest.mock('../../../lib/bundle', () => ({ 8 | bundle: jest.fn(), 9 | })); 10 | 11 | jest.mock('../../../lib/pack', () => ({ 12 | pack: jest.fn(), 13 | })); 14 | 15 | jest.mock('../../../lib/scripts', () => ({ 16 | scripts: jest.fn(), 17 | })); 18 | 19 | afterEach(() => { 20 | jest.resetModules(); 21 | jest.resetAllMocks(); 22 | }); 23 | 24 | describe('before:deploy:function:packageFunction', () => { 25 | let plugin: RspackServerlessPlugin; 26 | 27 | it('should be defined', () => { 28 | const serverless = mockServerlessConfig(); 29 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 30 | 31 | expect( 32 | plugin.hooks['before:deploy:function:packageFunction'] 33 | ).toBeDefined(); 34 | }); 35 | 36 | it('should throw error if options are invalid', async () => { 37 | const serverless = mockServerlessConfig(); 38 | plugin = new RspackServerlessPlugin(serverless, {}, logger); 39 | 40 | try { 41 | await plugin.hooks['before:deploy:function:packageFunction'](); 42 | fail('Expected function to throw an error'); 43 | } catch (error) { 44 | expect(serverless.classes.Error).toHaveBeenCalledTimes(1); 45 | expect(serverless.classes.Error).toHaveBeenCalledWith( 46 | 'This hook only supports deploy function options' 47 | ); 48 | } 49 | }); 50 | 51 | it('should throw error if function not found in entries', async () => { 52 | const serverless = mockServerlessConfig(); 53 | plugin = new RspackServerlessPlugin( 54 | serverless, 55 | { ...mockOptions, function: 'nonexistentFunc' }, 56 | logger 57 | ); 58 | plugin.functionEntries = {}; 59 | try { 60 | await plugin.hooks['before:deploy:function:packageFunction'](); 61 | fail('Expected function to throw an error'); 62 | } catch (error) { 63 | expect(serverless.classes.Error).toHaveBeenCalledTimes(1); 64 | expect(serverless.classes.Error).toHaveBeenCalledWith( 65 | 'Function nonexistentFunc not found in function entries' 66 | ); 67 | } 68 | }); 69 | 70 | it('should bundle the entries', async () => { 71 | const serverless = mockServerlessConfig(); 72 | const plugin = new RspackServerlessPlugin( 73 | serverless, 74 | { ...mockOptions, function: 'myFunc' }, 75 | logger 76 | ); 77 | 78 | plugin.functionEntries = { 79 | myFunc: { import: 'test/path/entry.ts', filename: 'test/path/entry.ts' }, 80 | }; 81 | 82 | await plugin.hooks['before:deploy:function:packageFunction'](); 83 | 84 | expect(bundle).toHaveBeenCalledTimes(1); 85 | expect(bundle).toHaveBeenCalledWith(plugin.functionEntries); 86 | }); 87 | 88 | it('should run scripts', async () => { 89 | const serverless = mockServerlessConfig(); 90 | const plugin = new RspackServerlessPlugin( 91 | serverless, 92 | { ...mockOptions, function: 'myFunc' }, 93 | logger 94 | ); 95 | 96 | plugin.functionEntries = { 97 | myFunc: { import: 'test/path/entry.ts', filename: 'test/path/entry.ts' }, 98 | }; 99 | 100 | await plugin.hooks['before:deploy:function:packageFunction'](); 101 | 102 | expect(scripts).toHaveBeenCalledTimes(1); 103 | expect(scripts).toHaveBeenCalledWith(); 104 | }); 105 | 106 | it('should pack the entries', async () => { 107 | const serverless = mockServerlessConfig(); 108 | const plugin = new RspackServerlessPlugin( 109 | serverless, 110 | { ...mockOptions, function: 'myFunc' }, 111 | logger 112 | ); 113 | 114 | plugin.functionEntries = { 115 | myFunc: { import: 'test/path/entry.ts', filename: 'test/path/entry.ts' }, 116 | }; 117 | 118 | await plugin.hooks['before:deploy:function:packageFunction'](); 119 | 120 | expect(pack).toHaveBeenCalledTimes(1); 121 | }); 122 | 123 | it('should bundle before scripts', async () => { 124 | const serverless = mockServerlessConfig(); 125 | const plugin = new RspackServerlessPlugin( 126 | serverless, 127 | { ...mockOptions, function: 'myFunc' }, 128 | logger 129 | ); 130 | 131 | plugin.functionEntries = { 132 | myFunc: { import: 'test/path/entry.ts', filename: 'test/path/entry.ts' }, 133 | }; 134 | 135 | await plugin.hooks['before:deploy:function:packageFunction'](); 136 | 137 | expect(jest.mocked(bundle).mock.invocationCallOrder[0]).toBeLessThan( 138 | jest.mocked(scripts).mock.invocationCallOrder[0] 139 | ); 140 | }); 141 | 142 | it('should pack after scripts', async () => { 143 | const serverless = mockServerlessConfig(); 144 | const plugin = new RspackServerlessPlugin( 145 | serverless, 146 | { ...mockOptions, function: 'myFunc' }, 147 | logger 148 | ); 149 | 150 | plugin.functionEntries = { 151 | myFunc: { import: 'test/path/entry.ts', filename: 'test/path/entry.ts' }, 152 | }; 153 | 154 | await plugin.hooks['before:deploy:function:packageFunction'](); 155 | 156 | expect(jest.mocked(scripts).mock.invocationCallOrder[0]).toBeLessThan( 157 | jest.mocked(pack).mock.invocationCallOrder[0] 158 | ); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/test/scripts.spec.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import type Serverless from 'serverless'; 3 | import { scripts } from '../lib/scripts.js'; 4 | import { RspackServerlessPlugin } from '../lib/serverless-rspack.js'; 5 | import { logger, mockOptions, mockServerlessConfig } from './test-utils.js'; 6 | import { PluginOptions } from '../lib/types.js'; 7 | 8 | const ENV_DEFAULTS = { 9 | KS_BUILD_OUTPUT_FOLDER: '.rspack', 10 | KS_PACKAGE_OUTPUT_FOLDER: '.serverless', 11 | KS_SERVICE_DIR: '/workDir', 12 | }; 13 | 14 | describe('scripts', () => { 15 | let serverless: Serverless; 16 | let plugin: RspackServerlessPlugin; 17 | let execSyncMock: jest.Mock; 18 | 19 | beforeEach(() => { 20 | process.env = { 21 | 'test-env': 'test-value', 22 | }; 23 | execSyncMock = jest.fn(); 24 | serverless = mockServerlessConfig({ service: 'test-service' }); 25 | plugin = createRspackPlugin(plugin, serverless); 26 | (execSync as unknown as jest.Mock) = execSyncMock; 27 | }); 28 | 29 | afterEach(() => { 30 | jest.clearAllMocks(); 31 | }); 32 | 33 | describe('runFunctionScripts', () => { 34 | beforeEach(() => { 35 | plugin.functionScripts = { 36 | functionName1: ['script1', 'script2'], 37 | functionName2: ['script3'], 38 | }; 39 | }); 40 | 41 | it('should run function scripts and log performance', async () => { 42 | scripts.call(plugin); 43 | 44 | expect(execSyncMock).toHaveBeenCalledTimes(3); 45 | expect(execSyncMock).toHaveBeenCalledWith('script1', { 46 | cwd: '/workDir/.rspack/functionName1', 47 | env: { 48 | ...process.env, 49 | KS_FUNCTION_NAME: 'functionName1', 50 | ...ENV_DEFAULTS, 51 | }, 52 | stdio: 'ignore', 53 | }); 54 | expect(execSyncMock).toHaveBeenCalledWith('script2', { 55 | cwd: '/workDir/.rspack/functionName1', 56 | env: { 57 | ...process.env, 58 | KS_FUNCTION_NAME: 'functionName1', 59 | ...ENV_DEFAULTS, 60 | }, 61 | stdio: 'ignore', 62 | }); 63 | expect(execSyncMock).toHaveBeenCalledWith('script3', { 64 | cwd: '/workDir/.rspack/functionName2', 65 | env: { 66 | ...process.env, 67 | KS_FUNCTION_NAME: 'functionName2', 68 | ...ENV_DEFAULTS, 69 | }, 70 | stdio: 'ignore', 71 | }); 72 | expect(plugin.log.verbose).toHaveBeenCalledWith( 73 | '[Scripts] Running 2 scripts for function functionName1' 74 | ); 75 | expect(plugin.log.verbose).toHaveBeenCalledWith( 76 | '[Scripts] Running 1 scripts for function functionName2' 77 | ); 78 | }); 79 | 80 | it('should throw an error if script execution fails', async () => { 81 | execSyncMock.mockImplementationOnce(() => { 82 | throw new Error('Script execution failed'); 83 | }); 84 | 85 | try { 86 | await scripts.call(plugin); 87 | fail('Expected function to throw an error'); 88 | } catch (error) { 89 | expect(plugin.serverless.classes.Error).toHaveBeenCalledTimes(1); 90 | expect(plugin.serverless.classes.Error).toHaveBeenCalledWith( 91 | 'Failed to execute script: script1\nError: Script execution failed' 92 | ); 93 | } 94 | }); 95 | 96 | it('should not run scripts if functionScripts is empty', async () => { 97 | plugin.functionScripts = {}; 98 | 99 | await scripts.call(plugin); 100 | 101 | expect(execSyncMock).not.toHaveBeenCalled(); 102 | expect(plugin.log.verbose).not.toHaveBeenCalled(); 103 | }); 104 | }); 105 | 106 | describe('runGlobalScripts', () => { 107 | beforeEach(() => { 108 | plugin.pluginOptions = { 109 | scripts: ['script1', 'script2'], 110 | } as PluginOptions; 111 | }); 112 | 113 | it('should run global scripts and log performance', async () => { 114 | scripts.call(plugin); 115 | 116 | expect(execSyncMock).toHaveBeenCalledTimes(2); 117 | expect(execSyncMock).toHaveBeenCalledWith('script1', { 118 | cwd: '/workDir', 119 | env: { 120 | ...process.env, 121 | ...ENV_DEFAULTS, 122 | }, 123 | stdio: 'ignore', 124 | }); 125 | expect(execSyncMock).toHaveBeenCalledWith('script2', { 126 | cwd: '/workDir', 127 | env: { 128 | ...process.env, 129 | ...ENV_DEFAULTS, 130 | }, 131 | stdio: 'ignore', 132 | }); 133 | expect(plugin.log.verbose).toHaveBeenCalledWith( 134 | '[Scripts] Running 2 global scripts' 135 | ); 136 | }); 137 | 138 | it('should throw an error if script execution fails', async () => { 139 | execSyncMock.mockImplementationOnce(() => { 140 | throw new Error('Script execution failed'); 141 | }); 142 | 143 | try { 144 | await scripts.call(plugin); 145 | fail('Expected function to throw an error'); 146 | } catch (error) { 147 | expect(plugin.serverless.classes.Error).toHaveBeenCalledTimes(1); 148 | expect(plugin.serverless.classes.Error).toHaveBeenCalledWith( 149 | 'Failed to execute script: script1\nError: Script execution failed' 150 | ); 151 | } 152 | }); 153 | 154 | it('should not run scripts if global scripts is empty', async () => { 155 | plugin.pluginOptions.scripts = []; 156 | 157 | await scripts.call(plugin); 158 | 159 | expect(execSyncMock).not.toHaveBeenCalled(); 160 | expect(plugin.log.verbose).not.toHaveBeenCalled(); 161 | }); 162 | }); 163 | }); 164 | 165 | function createRspackPlugin( 166 | plugin: RspackServerlessPlugin, 167 | serverless: Serverless 168 | ) { 169 | plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 170 | plugin.pluginOptions = {} as PluginOptions; 171 | 172 | return plugin; 173 | } 174 | -------------------------------------------------------------------------------- /e2e/__snapshots__/config.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Lambda Function Deployments Top level cloudformation 1`] = `"2010-09-09"`; 4 | 5 | exports[`Lambda Function Deployments apps packaged by rspack App1 Lambda Function 1`] = ` 6 | { 7 | "DependsOn": [ 8 | "App1LogGroup", 9 | ], 10 | "Properties": { 11 | "Code": { 12 | "S3Bucket": { 13 | "Ref": "ServerlessDeploymentBucket", 14 | }, 15 | "S3Key": StringContaining "App1.zip", 16 | }, 17 | "FunctionName": "complete-example-dev-App1", 18 | "Handler": "app1.handler", 19 | "MemorySize": 1024, 20 | "Role": { 21 | "Fn::GetAtt": [ 22 | "IamRoleLambdaExecution", 23 | "Arn", 24 | ], 25 | }, 26 | "Runtime": "provided.al2023", 27 | "Timeout": 6, 28 | }, 29 | "Type": "AWS::Lambda::Function", 30 | } 31 | `; 32 | 33 | exports[`Lambda Function Deployments apps packaged by rspack App1 Lambda Function 2`] = ` 34 | { 35 | "Description": "Current Lambda function version", 36 | "Export": { 37 | "Name": "sls-complete-example-dev-App1LambdaFunctionQualifiedArn", 38 | }, 39 | "Value": { 40 | "Ref": StringContaining "App1LambdaVersion", 41 | }, 42 | } 43 | `; 44 | 45 | exports[`Lambda Function Deployments apps packaged by rspack App1 Lambda Function 3`] = `"import e from"isin-validator";var r={};r.d=(e,a)=>{for(var n in a)r.o(a,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:a[n]})},r.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r);var a={};async function n(r){return{statusCode:200,body:JSON.stringify({message:e(r)?"ISIN is invalid!":"ISIN is fine!",input:r})}}r.d(a,{handler:()=>n});var t=a.handler;export{t as handler};"`; 46 | 47 | exports[`Lambda Function Deployments apps packaged by rspack app3 Lambda Function 1`] = ` 48 | { 49 | "DependsOn": [ 50 | "App3LogGroup", 51 | ], 52 | "Properties": { 53 | "Code": { 54 | "S3Bucket": { 55 | "Ref": "ServerlessDeploymentBucket", 56 | }, 57 | "S3Key": StringContaining "app3.zip", 58 | }, 59 | "FunctionName": "complete-example-dev-app3", 60 | "Handler": "src/App3.handler", 61 | "MemorySize": 1024, 62 | "Role": { 63 | "Fn::GetAtt": [ 64 | "IamRoleLambdaExecution", 65 | "Arn", 66 | ], 67 | }, 68 | "Runtime": "nodejs20.x", 69 | "Timeout": 6, 70 | }, 71 | "Type": "AWS::Lambda::Function", 72 | } 73 | `; 74 | 75 | exports[`Lambda Function Deployments apps packaged by rspack app3 Lambda Function 2`] = ` 76 | { 77 | "Description": "Current Lambda function version", 78 | "Export": { 79 | "Name": "sls-complete-example-dev-App3LambdaFunctionQualifiedArn", 80 | }, 81 | "Value": { 82 | "Ref": StringContaining "App3LambdaVersion", 83 | }, 84 | } 85 | `; 86 | 87 | exports[`Lambda Function Deployments apps packaged by rspack app3 Lambda Function 3`] = `"import e from"isin-validator";var r={};r.d=(e,a)=>{for(var n in a)r.o(a,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:a[n]})},r.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r);var a={};async function n(r){return{statusCode:200,body:JSON.stringify({message:e(r)?"ISIN is invalid!":"ISIN is fine!",input:r})}}r.d(a,{handler:()=>n});var t=a.handler;export{t as handler};"`; 88 | 89 | exports[`Lambda Function Deployments apps packaged by rspack app4 Lambda Function 1`] = ` 90 | { 91 | "DependsOn": [ 92 | "App4LogGroup", 93 | ], 94 | "Properties": { 95 | "Code": { 96 | "S3Bucket": { 97 | "Ref": "ServerlessDeploymentBucket", 98 | }, 99 | "S3Key": StringContaining "app4.zip", 100 | }, 101 | "FunctionName": "complete-example-dev-app4", 102 | "Handler": "src/app4.handler", 103 | "MemorySize": 1024, 104 | "Role": { 105 | "Fn::GetAtt": [ 106 | "IamRoleLambdaExecution", 107 | "Arn", 108 | ], 109 | }, 110 | "Runtime": "nodejs20.x", 111 | "Timeout": 6, 112 | }, 113 | "Type": "AWS::Lambda::Function", 114 | } 115 | `; 116 | 117 | exports[`Lambda Function Deployments apps packaged by rspack app4 Lambda Function 2`] = ` 118 | { 119 | "Description": "Current Lambda function version", 120 | "Export": { 121 | "Name": "sls-complete-example-dev-App4LambdaFunctionQualifiedArn", 122 | }, 123 | "Value": { 124 | "Ref": StringContaining "App4LambdaVersion", 125 | }, 126 | } 127 | `; 128 | 129 | exports[`Lambda Function Deployments apps packaged by rspack app4 Lambda Function 3`] = `"import e from"isin-validator";var r={};r.d=(e,a)=>{for(var n in a)r.o(a,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:a[n]})},r.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r);var a={};async function n(r){return{statusCode:200,body:JSON.stringify({message:e(r)?"ISIN is invalid!":"ISIN is fine!",input:r})}}r.d(a,{handler:()=>n});var t=a.handler;export{t as handler};"`; 130 | 131 | exports[`Lambda Function Deployments apps packaged by rspack app5 Lambda Function 1`] = ` 132 | { 133 | "DependsOn": [ 134 | "App5LogGroup", 135 | ], 136 | "Properties": { 137 | "Code": { 138 | "S3Bucket": { 139 | "Ref": "ServerlessDeploymentBucket", 140 | }, 141 | "S3Key": StringContaining "app5.zip", 142 | }, 143 | "FunctionName": "complete-example-dev-app5", 144 | "Handler": "src/deeply/nested/somewhat/app5.handler", 145 | "MemorySize": 1024, 146 | "Role": { 147 | "Fn::GetAtt": [ 148 | "IamRoleLambdaExecution", 149 | "Arn", 150 | ], 151 | }, 152 | "Runtime": "nodejs20.x", 153 | "Timeout": 6, 154 | }, 155 | "Type": "AWS::Lambda::Function", 156 | } 157 | `; 158 | 159 | exports[`Lambda Function Deployments apps packaged by rspack app5 Lambda Function 2`] = ` 160 | { 161 | "Description": "Current Lambda function version", 162 | "Export": { 163 | "Name": "sls-complete-example-dev-App5LambdaFunctionQualifiedArn", 164 | }, 165 | "Value": { 166 | "Ref": StringContaining "App5LambdaVersion", 167 | }, 168 | } 169 | `; 170 | 171 | exports[`Lambda Function Deployments apps packaged by rspack app5 Lambda Function 3`] = `"import e from"isin-validator";var r={};r.d=(e,a)=>{for(var n in a)r.o(a,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:a[n]})},r.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r);var a={};async function n(r){return{statusCode:200,body:JSON.stringify({message:e(r)?"ISIN is invalid!":"ISIN is fine!",input:r})}}r.d(a,{handler:()=>n});var t=a.handler;export{t as handler};"`; 172 | -------------------------------------------------------------------------------- /e2e/__snapshots__/esm.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Lambda Function Deployments Top level cloudformation 1`] = `"2010-09-09"`; 4 | 5 | exports[`Lambda Function Deployments apps packaged by rspack App1 Lambda Function 1`] = ` 6 | { 7 | "DependsOn": [ 8 | "App1LogGroup", 9 | ], 10 | "Properties": { 11 | "Code": { 12 | "S3Bucket": "serverless-framework-deployments-us-east-1-d7c24357-af77", 13 | "S3Key": StringContaining "App1.zip", 14 | }, 15 | "FunctionName": "esm-example-dev-App1", 16 | "Handler": "app1.handler", 17 | "MemorySize": 1024, 18 | "Role": { 19 | "Fn::GetAtt": [ 20 | "IamRoleLambdaExecution", 21 | "Arn", 22 | ], 23 | }, 24 | "Runtime": "provided.al2023", 25 | "Timeout": 6, 26 | }, 27 | "Type": "AWS::Lambda::Function", 28 | } 29 | `; 30 | 31 | exports[`Lambda Function Deployments apps packaged by rspack App1 Lambda Function 2`] = ` 32 | { 33 | "Description": "Current Lambda function version", 34 | "Export": { 35 | "Name": "sls-esm-example-dev-App1LambdaFunctionQualifiedArn", 36 | }, 37 | "Value": { 38 | "Ref": StringContaining "App1LambdaVersion", 39 | }, 40 | } 41 | `; 42 | 43 | exports[`Lambda Function Deployments apps packaged by rspack App1 Lambda Function 3`] = `"import{createRequire as e}from"node:module";var a={};a.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return a.d(n,{a:n}),n},a.d=(e,n)=>{for(var o in n)a.o(n,o)&&!a.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:n[o]})},a.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a);var n={};a.d(n,{handler:()=>m});let o=e(import.meta.url)("isin-validator");var r=a.n(o);let t=e(import.meta.url)("@aws-sdk/client-dynamodb"),l=e(import.meta.url)("@aws-sdk/lib-dynamodb"),i=new t.DynamoDBClient({}),d=l.DynamoDBDocumentClient.from(i);async function m(e){let a=r()(e),n=new l.GetCommand({TableName:"AngryAnimals",Key:{CommonName:"Shoebill"}});try{let e=await d.send(n);console.log(e)}catch(e){console.log(e)}return{statusCode:200,body:JSON.stringify({message:a?"ISIN is invalid!":"ISIN is fine!",input:e})}}var s=n.handler;export{s as handler};"`; 44 | 45 | exports[`Lambda Function Deployments apps packaged by rspack app3 Lambda Function 1`] = ` 46 | { 47 | "DependsOn": [ 48 | "App3LogGroup", 49 | ], 50 | "Properties": { 51 | "Code": { 52 | "S3Bucket": "serverless-framework-deployments-us-east-1-d7c24357-af77", 53 | "S3Key": StringContaining "app3.zip", 54 | }, 55 | "FunctionName": "esm-example-dev-app3", 56 | "Handler": "src/App3.handler", 57 | "MemorySize": 1024, 58 | "Role": { 59 | "Fn::GetAtt": [ 60 | "IamRoleLambdaExecution", 61 | "Arn", 62 | ], 63 | }, 64 | "Runtime": "nodejs20.x", 65 | "Timeout": 6, 66 | }, 67 | "Type": "AWS::Lambda::Function", 68 | } 69 | `; 70 | 71 | exports[`Lambda Function Deployments apps packaged by rspack app3 Lambda Function 2`] = ` 72 | { 73 | "Description": "Current Lambda function version", 74 | "Export": { 75 | "Name": "sls-esm-example-dev-App3LambdaFunctionQualifiedArn", 76 | }, 77 | "Value": { 78 | "Ref": StringContaining "App3LambdaVersion", 79 | }, 80 | } 81 | `; 82 | 83 | exports[`Lambda Function Deployments apps packaged by rspack app3 Lambda Function 3`] = `"import{createRequire as e}from"node:module";var r={};r.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return r.d(a,{a:a}),a},r.d=(e,a)=>{for(var t in a)r.o(a,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:a[t]})},r.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r);var a={};r.d(a,{handler:()=>o});let t=e(import.meta.url)("isin-validator");var n=r.n(t);async function o(e){return{statusCode:200,body:JSON.stringify({message:n()(e)?"ISIN is invalid!":"ISIN is fine!",input:e})}}var i=a.handler;export{i as handler};"`; 84 | 85 | exports[`Lambda Function Deployments apps packaged by rspack app4 Lambda Function 1`] = ` 86 | { 87 | "DependsOn": [ 88 | "App4LogGroup", 89 | ], 90 | "Properties": { 91 | "Code": { 92 | "S3Bucket": "serverless-framework-deployments-us-east-1-d7c24357-af77", 93 | "S3Key": StringContaining "app4.zip", 94 | }, 95 | "FunctionName": "esm-example-dev-app4", 96 | "Handler": "src/app4.handler", 97 | "MemorySize": 1024, 98 | "Role": { 99 | "Fn::GetAtt": [ 100 | "IamRoleLambdaExecution", 101 | "Arn", 102 | ], 103 | }, 104 | "Runtime": "nodejs20.x", 105 | "Timeout": 6, 106 | }, 107 | "Type": "AWS::Lambda::Function", 108 | } 109 | `; 110 | 111 | exports[`Lambda Function Deployments apps packaged by rspack app4 Lambda Function 2`] = ` 112 | { 113 | "Description": "Current Lambda function version", 114 | "Export": { 115 | "Name": "sls-esm-example-dev-App4LambdaFunctionQualifiedArn", 116 | }, 117 | "Value": { 118 | "Ref": StringContaining "App4LambdaVersion", 119 | }, 120 | } 121 | `; 122 | 123 | exports[`Lambda Function Deployments apps packaged by rspack app4 Lambda Function 3`] = `"import{createRequire as e}from"node:module";var r={};r.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return r.d(a,{a:a}),a},r.d=(e,a)=>{for(var t in a)r.o(a,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:a[t]})},r.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r);var a={};r.d(a,{handler:()=>o});let t=e(import.meta.url)("isin-validator");var n=r.n(t);async function o(e){return{statusCode:200,body:JSON.stringify({message:n()(e)?"ISIN is invalid!":"ISIN is fine!",input:e})}}var i=a.handler;export{i as handler};"`; 124 | 125 | exports[`Lambda Function Deployments apps packaged by rspack app5 Lambda Function 1`] = ` 126 | { 127 | "DependsOn": [ 128 | "App5LogGroup", 129 | ], 130 | "Properties": { 131 | "Code": { 132 | "S3Bucket": "serverless-framework-deployments-us-east-1-d7c24357-af77", 133 | "S3Key": StringContaining "app5.zip", 134 | }, 135 | "FunctionName": "esm-example-dev-app5", 136 | "Handler": "src/deeply/nested/somewhat/app5.handler", 137 | "MemorySize": 1024, 138 | "Role": { 139 | "Fn::GetAtt": [ 140 | "IamRoleLambdaExecution", 141 | "Arn", 142 | ], 143 | }, 144 | "Runtime": "nodejs20.x", 145 | "Timeout": 6, 146 | }, 147 | "Type": "AWS::Lambda::Function", 148 | } 149 | `; 150 | 151 | exports[`Lambda Function Deployments apps packaged by rspack app5 Lambda Function 2`] = ` 152 | { 153 | "Description": "Current Lambda function version", 154 | "Export": { 155 | "Name": "sls-esm-example-dev-App5LambdaFunctionQualifiedArn", 156 | }, 157 | "Value": { 158 | "Ref": StringContaining "App5LambdaVersion", 159 | }, 160 | } 161 | `; 162 | 163 | exports[`Lambda Function Deployments apps packaged by rspack app5 Lambda Function 3`] = `"import{createRequire as e}from"node:module";var r={};r.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return r.d(a,{a:a}),a},r.d=(e,a)=>{for(var t in a)r.o(a,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:a[t]})},r.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r);var a={};r.d(a,{handler:()=>o});let t=e(import.meta.url)("isin-validator");var n=r.n(t);async function o(e){return{statusCode:200,body:JSON.stringify({message:n()(e)?"ISIN is invalid!":"ISIN is fine!",input:e})}}var i=a.handler;export{i as handler};"`; 164 | -------------------------------------------------------------------------------- /e2e/__snapshots__/complete.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Lambda Function Deployments Top level cloudformation 1`] = `"2010-09-09"`; 4 | 5 | exports[`Lambda Function Deployments apps packaged by rspack App1 Lambda Function 1`] = ` 6 | { 7 | "DependsOn": [ 8 | "App1LogGroup", 9 | ], 10 | "Properties": { 11 | "Code": { 12 | "S3Bucket": { 13 | "Ref": "ServerlessDeploymentBucket", 14 | }, 15 | "S3Key": StringContaining "App1.zip", 16 | }, 17 | "FunctionName": "complete-example-dev-App1", 18 | "Handler": "app1.handler", 19 | "MemorySize": 1024, 20 | "Role": { 21 | "Fn::GetAtt": [ 22 | "IamRoleLambdaExecution", 23 | "Arn", 24 | ], 25 | }, 26 | "Runtime": "provided.al2023", 27 | "Timeout": 6, 28 | }, 29 | "Type": "AWS::Lambda::Function", 30 | } 31 | `; 32 | 33 | exports[`Lambda Function Deployments apps packaged by rspack App1 Lambda Function 2`] = ` 34 | { 35 | "Description": "Current Lambda function version", 36 | "Export": { 37 | "Name": "sls-complete-example-dev-App1LambdaFunctionQualifiedArn", 38 | }, 39 | "Value": { 40 | "Ref": StringContaining "App1LambdaVersion", 41 | }, 42 | } 43 | `; 44 | 45 | exports[`Lambda Function Deployments apps packaged by rspack App1 Lambda Function 3`] = ` 46 | "import{createRequire as e}from"node:module";var r={};r.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return r.d(a,{a:a}),a},r.d=(e,a)=>{for(var t in a)r.o(a,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:a[t]})},r.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r);var a={};r.d(a,{handler:()=>o});let t=e(import.meta.url)("isin-validator");var n=r.n(t);async function o(e){return{statusCode:200,body:JSON.stringify({message:n()(e)?"ISIN is invalid!":"ISIN is fine!",input:e})}}var i=a.handler;export{i as handler}; 47 | //# sourceMappingURL=app1.mjs.map" 48 | `; 49 | 50 | exports[`Lambda Function Deployments apps packaged by rspack app3 Lambda Function 1`] = ` 51 | { 52 | "DependsOn": [ 53 | "App3LogGroup", 54 | ], 55 | "Properties": { 56 | "Code": { 57 | "S3Bucket": { 58 | "Ref": "ServerlessDeploymentBucket", 59 | }, 60 | "S3Key": StringContaining "app3.zip", 61 | }, 62 | "FunctionName": "complete-example-dev-app3", 63 | "Handler": "src/App3.handler", 64 | "MemorySize": 1024, 65 | "Role": { 66 | "Fn::GetAtt": [ 67 | "IamRoleLambdaExecution", 68 | "Arn", 69 | ], 70 | }, 71 | "Runtime": "nodejs20.x", 72 | "Timeout": 6, 73 | }, 74 | "Type": "AWS::Lambda::Function", 75 | } 76 | `; 77 | 78 | exports[`Lambda Function Deployments apps packaged by rspack app3 Lambda Function 2`] = ` 79 | { 80 | "Description": "Current Lambda function version", 81 | "Export": { 82 | "Name": "sls-complete-example-dev-App3LambdaFunctionQualifiedArn", 83 | }, 84 | "Value": { 85 | "Ref": StringContaining "App3LambdaVersion", 86 | }, 87 | } 88 | `; 89 | 90 | exports[`Lambda Function Deployments apps packaged by rspack app3 Lambda Function 3`] = ` 91 | "import{fileURLToPath as e}from"node:url";import{dirname as r}from"node:path";import{createRequire as a}from"node:module";var t={};t.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return t.d(r,{a:r}),r},t.d=(e,r)=>{for(var a in r)t.o(r,a)&&!t.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:r[a]})},t.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r);var o={};t.d(o,{handler:()=>f});let i=a(import.meta.url)("isin-validator");var n=t.n(i);let l=a(import.meta.url)("node:fs"),d=a(import.meta.url)("node:path");var m=t.n(d);let p=a(import.meta.url)("sharp");var s=t.n(p),u=r(e(import.meta.url));async function f(e){let r=n()(e.isin),a=m().join(u,"../my-image.jpeg"),t=(0,l.readFileSync)(a),{info:o}=await s()(t).raw().toBuffer({resolveWithObject:!0});return{statusCode:r?200:400,body:JSON.stringify({handler:"App3",message:r?"ISIN is invalid!":"ISIN is fine!",input:e,info:o})}}var h=o.handler;export{h as handler}; 92 | //# sourceMappingURL=App3.mjs.map" 93 | `; 94 | 95 | exports[`Lambda Function Deployments apps packaged by rspack app4 Lambda Function 1`] = ` 96 | { 97 | "DependsOn": [ 98 | "App4LogGroup", 99 | ], 100 | "Properties": { 101 | "Code": { 102 | "S3Bucket": { 103 | "Ref": "ServerlessDeploymentBucket", 104 | }, 105 | "S3Key": StringContaining "app4.zip", 106 | }, 107 | "FunctionName": "complete-example-dev-app4", 108 | "Handler": "src/app4.handler", 109 | "MemorySize": 1024, 110 | "Role": { 111 | "Fn::GetAtt": [ 112 | "IamRoleLambdaExecution", 113 | "Arn", 114 | ], 115 | }, 116 | "Runtime": "nodejs20.x", 117 | "Timeout": 6, 118 | }, 119 | "Type": "AWS::Lambda::Function", 120 | } 121 | `; 122 | 123 | exports[`Lambda Function Deployments apps packaged by rspack app4 Lambda Function 2`] = ` 124 | { 125 | "Description": "Current Lambda function version", 126 | "Export": { 127 | "Name": "sls-complete-example-dev-App4LambdaFunctionQualifiedArn", 128 | }, 129 | "Value": { 130 | "Ref": StringContaining "App4LambdaVersion", 131 | }, 132 | } 133 | `; 134 | 135 | exports[`Lambda Function Deployments apps packaged by rspack app4 Lambda Function 3`] = ` 136 | "import{createRequire as e}from"node:module";var r={};r.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return r.d(a,{a:a}),a},r.d=(e,a)=>{for(var t in a)r.o(a,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:a[t]})},r.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r);var a={};r.d(a,{handler:()=>o});let t=e(import.meta.url)("isin-validator");var n=r.n(t);async function o(e){return{statusCode:200,body:JSON.stringify({message:n()(e)?"ISIN is invalid!":"ISIN is fine!",input:e})}}var i=a.handler;export{i as handler}; 137 | //# sourceMappingURL=app4.mjs.map" 138 | `; 139 | 140 | exports[`Lambda Function Deployments apps packaged by rspack app5 Lambda Function 1`] = ` 141 | { 142 | "DependsOn": [ 143 | "App5LogGroup", 144 | ], 145 | "Properties": { 146 | "Code": { 147 | "S3Bucket": { 148 | "Ref": "ServerlessDeploymentBucket", 149 | }, 150 | "S3Key": StringContaining "app5.zip", 151 | }, 152 | "FunctionName": "complete-example-dev-app5", 153 | "Handler": "src/deeply/nested/somewhat/app5.handler", 154 | "MemorySize": 1024, 155 | "Role": { 156 | "Fn::GetAtt": [ 157 | "IamRoleLambdaExecution", 158 | "Arn", 159 | ], 160 | }, 161 | "Runtime": "nodejs20.x", 162 | "Timeout": 6, 163 | }, 164 | "Type": "AWS::Lambda::Function", 165 | } 166 | `; 167 | 168 | exports[`Lambda Function Deployments apps packaged by rspack app5 Lambda Function 2`] = ` 169 | { 170 | "Description": "Current Lambda function version", 171 | "Export": { 172 | "Name": "sls-complete-example-dev-App5LambdaFunctionQualifiedArn", 173 | }, 174 | "Value": { 175 | "Ref": StringContaining "App5LambdaVersion", 176 | }, 177 | } 178 | `; 179 | 180 | exports[`Lambda Function Deployments apps packaged by rspack app5 Lambda Function 3`] = ` 181 | "import{createRequire as e}from"node:module";var r={};r.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return r.d(a,{a:a}),a},r.d=(e,a)=>{for(var t in a)r.o(a,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:a[t]})},r.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r);var a={};r.d(a,{handler:()=>o});let t=e(import.meta.url)("isin-validator");var n=r.n(t);async function o(e){return{statusCode:200,body:JSON.stringify({message:n()(e)?"ISIN is invalid!":"ISIN is fine!",input:e})}}var i=a.handler;export{i as handler}; 182 | //# sourceMappingURL=app5.mjs.map" 183 | `; 184 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/lib/bundle.ts: -------------------------------------------------------------------------------- 1 | import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin'; 2 | import { 3 | type RspackOptions, 4 | type RspackPluginFunction, 5 | type RspackPluginInstance, 6 | type SwcLoaderOptions, 7 | type WebpackPluginFunction, 8 | type WebpackPluginInstance, 9 | DefinePlugin, 10 | ProgressPlugin, 11 | node, 12 | rspack, 13 | } from '@rspack/core'; 14 | import { writeFileSync } from 'node:fs'; 15 | import path from 'node:path'; 16 | import { cwd } from 'node:process'; 17 | import { mergeWithCustomize } from 'webpack-merge'; 18 | import type { RspackServerlessPlugin } from './serverless-rspack.js'; 19 | import type { PluginOptions } from './types.js'; 20 | 21 | export async function bundle( 22 | this: RspackServerlessPlugin, 23 | entries: RspackOptions['entry'] 24 | ) { 25 | let config: RspackOptions; 26 | 27 | if (this.providedRspackConfig && this.pluginOptions.config?.strategy) { 28 | this.log.verbose( 29 | `[Bundle] Config merge strategy: ${this.pluginOptions.config.strategy}` 30 | ); 31 | if (this.pluginOptions.config.strategy === 'combine') { 32 | const baseConfig = defaultConfig( 33 | entries, 34 | this.pluginOptions, 35 | this.offlineMode, 36 | this.buildOutputFolderPath, 37 | this.log 38 | ); 39 | 40 | const mergedConfig: RspackOptions = mergeWithCustomize({ 41 | customizeArray: mergeArrayUniqueStrategy(this.log), 42 | })([baseConfig, this.providedRspackConfig]); 43 | 44 | config = enforcePluginReadOnlyDefaults( 45 | mergedConfig, 46 | this.buildOutputFolderPath, 47 | entries 48 | ); 49 | } else { 50 | config = enforcePluginReadOnlyDefaults( 51 | this.providedRspackConfig, 52 | this.buildOutputFolderPath, 53 | entries 54 | ); 55 | } 56 | } else { 57 | config = defaultConfig( 58 | entries, 59 | this.pluginOptions, 60 | this.offlineMode, 61 | this.buildOutputFolderPath, 62 | this.log 63 | ); 64 | } 65 | this.log.verbose( 66 | `[Bundle] Bundling with config: ${safelyStringifyConfig(config)}` 67 | ); 68 | const startBundle = Date.now(); 69 | 70 | return new Promise((resolve) => { 71 | rspack(config, (x, y) => { 72 | if (this.pluginOptions.stats) { 73 | const c = y?.toJson(); 74 | try { 75 | writeFileSync( 76 | path.join(this.buildOutputFolderPath, 'stats.json'), 77 | JSON.stringify(c) 78 | ); 79 | } catch (error) { 80 | this.log?.error(`[Bundle] Failed to write stats file: ${error}`); 81 | } 82 | } 83 | this.log.verbose( 84 | `[Performance] Bundle total execution time for service ${ 85 | this.serverless.service.service 86 | } [${Date.now() - startBundle} ms]` 87 | ); 88 | resolve('Success!'); // Yay! Everything went well! 89 | }); 90 | }); 91 | } 92 | 93 | const esmOutput = { 94 | chunkFormat: 'module', 95 | chunkLoading: 'import', 96 | library: { 97 | type: 'module', 98 | }, 99 | environment: { 100 | module: true, 101 | dynamicImport: true, 102 | }, 103 | }; 104 | 105 | const defaultConfig: ( 106 | entries: RspackOptions['entry'], 107 | buildOptions: PluginOptions, 108 | offlineMode: boolean, 109 | workFolderPath: string, 110 | logger: RspackServerlessPlugin['log'] 111 | ) => RspackOptions = ( 112 | entries, 113 | buildOptions, 114 | offlineMode, 115 | workFolderPath, 116 | logger 117 | ) => ({ 118 | mode: buildOptions.mode, 119 | entry: entries, 120 | target: 'node', 121 | experiments: { 122 | outputModule: buildOptions.esm, 123 | }, 124 | devtool: 125 | buildOptions.sourcemap !== undefined ? buildOptions.sourcemap : false, 126 | resolve: { 127 | extensions: ['...', '.ts', '.tsx', '.jsx'], 128 | ...(buildOptions.tsConfig 129 | ? { 130 | tsConfig: path.resolve(cwd(), buildOptions.tsConfig), 131 | } 132 | : {}), 133 | }, 134 | ...(buildOptions.externals?.length && buildOptions.externals?.length > 0 135 | ? { 136 | externals: [ 137 | ({ request }: any, callback: any) => { 138 | const isExternal = buildOptions?.externals?.some((external) => { 139 | return new RegExp(external).test(request); 140 | }); 141 | if (isExternal) { 142 | logger.verbose(`[Bundle] Marking ${request} as external`); 143 | return callback(null, 'node-commonjs ' + request); 144 | } 145 | callback(); 146 | }, 147 | ], 148 | } 149 | : {}), 150 | plugins: [ 151 | new DefinePlugin({ 152 | 'process.env.NODE_ENV': JSON.stringify(process.env['NODE_ENV']), 153 | }), 154 | new ProgressPlugin({}), 155 | new node.NodeTargetPlugin(), 156 | createDoctorPlugin(buildOptions), 157 | ].filter(Boolean), 158 | module: { 159 | rules: [ 160 | { 161 | test: /\.ts$/, 162 | use: { 163 | loader: 'builtin:swc-loader', 164 | options: { 165 | jsc: { 166 | target: 'es2020', 167 | parser: { 168 | syntax: 'typescript', 169 | }, 170 | }, 171 | } satisfies SwcLoaderOptions, 172 | }, 173 | }, 174 | ], 175 | }, 176 | optimization: { 177 | mangleExports: false, 178 | }, 179 | output: { 180 | path: workFolderPath, 181 | library: { type: 'commonjs2' }, 182 | ...(buildOptions.esm ? esmOutput : {}), 183 | ...(offlineMode 184 | ? { devtoolModuleFilenameTemplate: '[absolute-resource-path]' } 185 | : {}), 186 | }, 187 | }); 188 | 189 | function mergeArrayUniqueStrategy(logger: RspackServerlessPlugin['log']) { 190 | return (base: unknown, provided: unknown, key: string) => { 191 | if (key === 'plugins' && isPlugins(base) && isPlugins(provided)) { 192 | const plugins = [...provided]; 193 | base.forEach((basePlugin) => { 194 | const matchedPlugin = provided.find( 195 | (providedPlugin) => basePlugin.name === providedPlugin.name 196 | ); 197 | if (matchedPlugin) { 198 | logger.warning( 199 | `[Bundle] You have provided your own ${matchedPlugin.name}. This will override the default one provided by @kitchenshelf/serverless-rspack.` 200 | ); 201 | } else { 202 | plugins.push(basePlugin); 203 | } 204 | }); 205 | 206 | return plugins; 207 | } 208 | // Fall back to default merging 209 | return undefined; 210 | }; 211 | } 212 | 213 | const enforcePluginReadOnlyDefaults: ( 214 | config: RspackOptions, 215 | buildOutputFolderPath: string, 216 | entries: RspackOptions['entry'] 217 | ) => RspackOptions = (config, buildOutputFolderPath, entries) => { 218 | return { 219 | ...config, 220 | entry: entries, 221 | optimization: { 222 | ...config.optimization, 223 | mangleExports: false, 224 | }, 225 | output: { 226 | ...config.output, 227 | path: buildOutputFolderPath, 228 | }, 229 | }; 230 | }; 231 | 232 | function isPlugins( 233 | a: unknown 234 | ): a is ( 235 | | RspackPluginInstance 236 | | RspackPluginFunction 237 | | WebpackPluginInstance 238 | | WebpackPluginFunction 239 | )[] { 240 | return Array.isArray(a); 241 | } 242 | 243 | function createDoctorPlugin(buildOptions: PluginOptions) { 244 | const isEnabled = 245 | enabledViaSimpleConfig(buildOptions.doctor) || 246 | enabledViaConfigObject(buildOptions.doctor); 247 | 248 | return isEnabled 249 | ? new RsdoctorRspackPlugin({ 250 | disableClientServer: true, 251 | mode: 'brief', 252 | output: { reportDir: getReportDir(buildOptions.doctor) }, 253 | experiments: { enableNativePlugin: true }, 254 | }) 255 | : null; 256 | } 257 | 258 | function enabledViaSimpleConfig( 259 | doctor: PluginOptions['doctor'] 260 | ): doctor is boolean { 261 | return typeof doctor === 'boolean' && doctor === true; 262 | } 263 | 264 | function enabledViaConfigObject( 265 | doctor: PluginOptions['doctor'] 266 | ): doctor is Exclude { 267 | return ( 268 | typeof doctor === 'object' && doctor !== null && doctor.enable === true 269 | ); 270 | } 271 | 272 | function getReportDir(doctor: PluginOptions['doctor']) { 273 | if (enabledViaConfigObject(doctor) && doctor.outputDirectory) { 274 | return doctor.outputDirectory; 275 | } 276 | return undefined; 277 | } 278 | 279 | function safelyStringifyConfig(config: RspackOptions) { 280 | return JSON.stringify(config, (key, value) => { 281 | if (key === 'plugins') { 282 | return value.map((plugin: any) => plugin.constructor.name); 283 | } 284 | return value; 285 | }); 286 | } 287 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/lib/serverless-rspack.ts: -------------------------------------------------------------------------------- 1 | import { RspackOptions } from '@rspack/core'; 2 | import { assert } from 'node:console'; 3 | import { readdirSync } from 'node:fs'; 4 | import { rm } from 'node:fs/promises'; 5 | import path from 'node:path'; 6 | import type Serverless from 'serverless'; 7 | import type ServerlessPlugin from 'serverless/classes/Plugin'; 8 | import { bundle } from './bundle.js'; 9 | import { SERVERLESS_FOLDER, WORK_FOLDER } from './constants.js'; 10 | import { 11 | determineFileParts, 12 | enabledViaConfigObject, 13 | enabledViaSimpleConfig, 14 | isNodeFunction, 15 | } from './helpers.js'; 16 | import { AfterDeployFunctionPackageFunction } from './hooks/deploy-function/after-package-function.js'; 17 | import { BeforeDeployFunctionPackageFunction } from './hooks/deploy-function/before-package-function.js'; 18 | import { Initialize } from './hooks/initialize.js'; 19 | import { BeforeInvokeLocalInvoke } from './hooks/invoke-local/before-invoke.js'; 20 | import { BeforeOfflineStartInit } from './hooks/offline/start-init.js'; 21 | import { AfterPackageCreateDeploymentArtifacts } from './hooks/package/after-create-deployment-artifacts.js'; 22 | import { BeforePackageCreateDeploymentArtifacts } from './hooks/package/before-create-deployment-artifacts.js'; 23 | import { pack } from './pack.js'; 24 | import { scripts } from './scripts.js'; 25 | import { 26 | PluginFunctionEntries, 27 | PluginFunctionScripts, 28 | PluginOptions, 29 | PluginOptionsSchema, 30 | RsPackFunctionDefinitionHandler, 31 | } from './types.js'; 32 | 33 | export class RspackServerlessPlugin implements ServerlessPlugin { 34 | serviceDirPath: string; 35 | buildOutputFolder: string; 36 | buildOutputFolderPath: string; 37 | packageOutputFolder: string; 38 | 39 | log: ServerlessPlugin.Logging['log']; 40 | serverless: Serverless; 41 | options: Serverless.Options; 42 | hooks: ServerlessPlugin.Hooks; 43 | 44 | providedRspackConfig: RspackOptions | undefined; 45 | pluginOptions!: PluginOptions; 46 | functionEntries: PluginFunctionEntries = {}; 47 | functionScripts: PluginFunctionScripts = {}; 48 | offlineMode = false; 49 | 50 | timings = new Map(); 51 | 52 | protected bundle = bundle.bind(this); 53 | protected pack = pack.bind(this); 54 | protected scripts = scripts.bind(this); 55 | 56 | constructor( 57 | serverless: Serverless, 58 | options: Serverless.Options, 59 | logging: ServerlessPlugin.Logging 60 | ) { 61 | assert(logging, 'Please use serverless V4'); 62 | 63 | serverless.configSchemaHandler.defineFunctionProperties('aws', { 64 | properties: { 65 | rspack: { 66 | oneOf: [ 67 | { type: 'boolean' }, 68 | { 69 | type: 'object', 70 | properties: { 71 | enable: { type: 'boolean' }, 72 | scripts: { type: 'array', items: { type: 'string' } }, 73 | }, 74 | required: [], 75 | }, 76 | ], 77 | }, 78 | }, 79 | }); 80 | 81 | this.serverless = serverless; 82 | this.options = options; 83 | this.log = logging.log; 84 | this.serviceDirPath = this.serverless.config.serviceDir; 85 | this.packageOutputFolder = SERVERLESS_FOLDER; 86 | this.buildOutputFolder = WORK_FOLDER; 87 | this.buildOutputFolderPath = path.join( 88 | this.serviceDirPath, 89 | this.buildOutputFolder 90 | ); 91 | 92 | this.hooks = { 93 | initialize: Initialize.bind(this), 94 | 'before:package:createDeploymentArtifacts': 95 | BeforePackageCreateDeploymentArtifacts.bind(this), 96 | 'after:package:createDeploymentArtifacts': 97 | AfterPackageCreateDeploymentArtifacts.bind(this), 98 | 'before:deploy:function:packageFunction': 99 | BeforeDeployFunctionPackageFunction.bind(this), 100 | 'after:deploy:function:packageFunction': 101 | AfterDeployFunctionPackageFunction.bind(this), 102 | 'before:invoke:local:invoke': BeforeInvokeLocalInvoke.bind(this), 103 | 'before:offline:start:init': BeforeOfflineStartInit.bind(this), 104 | }; 105 | } 106 | 107 | protected buildFunctionEntries(functions: string[]) { 108 | this.log.verbose( 109 | `[sls-rspack] Building function entries for: ${functions}` 110 | ); 111 | let entries: PluginFunctionEntries = {}; 112 | 113 | functions.forEach((functionName) => { 114 | const functionDefinitionHandler = this.serverless.service.getFunction( 115 | functionName 116 | ) as RsPackFunctionDefinitionHandler; 117 | if ( 118 | this.isEnabledViaRspack(functionDefinitionHandler) || 119 | this.isEnabledNodeFunction(functionDefinitionHandler) 120 | ) { 121 | // TODO: support container images 122 | const entry = this.getEntryForFunction( 123 | functionName, 124 | functionDefinitionHandler as Serverless.FunctionDefinitionHandler 125 | ); 126 | entries = { 127 | ...entries, 128 | ...entry, 129 | }; 130 | } 131 | }); 132 | return entries; 133 | } 134 | 135 | protected buildFunctionScripts(functions: string[]) { 136 | this.log.verbose( 137 | `[sls-rspack] Building function scripts for: ${functions}` 138 | ); 139 | const scripts: PluginFunctionScripts = {}; 140 | 141 | functions.forEach((functionName) => { 142 | const functionDefinitionHandler = this.serverless.service.getFunction( 143 | functionName 144 | ) as RsPackFunctionDefinitionHandler; 145 | 146 | if ( 147 | functionDefinitionHandler.rspack && 148 | !enabledViaSimpleConfig(functionDefinitionHandler.rspack) && 149 | functionDefinitionHandler.rspack.enable !== false && 150 | functionDefinitionHandler.rspack.scripts 151 | ) { 152 | this.log.verbose( 153 | `[sls-rspack] Found ${functionDefinitionHandler.rspack.scripts.length} scripts for function ${functionName}` 154 | ); 155 | scripts[functionName] = functionDefinitionHandler.rspack.scripts; 156 | } 157 | }); 158 | return scripts; 159 | } 160 | 161 | protected async cleanup(): Promise { 162 | if (!this.pluginOptions.keepOutputDirectory) { 163 | await rm(path.join(this.buildOutputFolderPath), { recursive: true }); 164 | } 165 | } 166 | 167 | protected getPluginOptions() { 168 | return PluginOptionsSchema.parse( 169 | this.serverless.service.custom?.['rspack'] ?? {} 170 | ); 171 | } 172 | 173 | private isEnabledNodeFunction( 174 | functionDefinitionHandler: RsPackFunctionDefinitionHandler 175 | ): boolean | undefined { 176 | return ( 177 | isNodeFunction( 178 | functionDefinitionHandler, 179 | this.serverless.service.provider.runtime 180 | ) && !this.isDisabledViaRspack(functionDefinitionHandler) 181 | ); 182 | } 183 | 184 | private isDisabledViaRspack( 185 | functionDefinitionHandler: RsPackFunctionDefinitionHandler 186 | ) { 187 | return ( 188 | functionDefinitionHandler.rspack !== undefined && 189 | !enabledViaSimpleConfig(functionDefinitionHandler.rspack) && 190 | !enabledViaConfigObject( 191 | functionDefinitionHandler.rspack 192 | ) 193 | ); 194 | } 195 | 196 | private isEnabledViaRspack( 197 | functionDefinitionHandler: RsPackFunctionDefinitionHandler 198 | ) { 199 | return ( 200 | functionDefinitionHandler.rspack && 201 | (enabledViaSimpleConfig(functionDefinitionHandler.rspack) || 202 | enabledViaConfigObject( 203 | functionDefinitionHandler.rspack 204 | )) 205 | ); 206 | } 207 | 208 | private getEntryForFunction( 209 | name: string, 210 | serverlessFunction: Serverless.FunctionDefinitionHandler 211 | ) { 212 | const handler = serverlessFunction.handler; 213 | this.log.verbose( 214 | `[sls-rspack] Processing function ${name} with provided handler ${handler}` 215 | ); 216 | 217 | const handlerFile = this.getHandlerFile(handler); 218 | 219 | const { filePath, fileName } = determineFileParts(handlerFile); 220 | const safeFilePath = filePath ? '/' + filePath + '/' : '/'; 221 | 222 | const files = readdirSync(`./${filePath}`); 223 | 224 | const file = files.find((file) => { 225 | return file.startsWith(fileName); 226 | }); 227 | 228 | if (!file) { 229 | throw new this.serverless.classes.Error( 230 | `Unable to find file [${fileName}] in path: [./${filePath}]` 231 | ); 232 | } 233 | const ext = path.extname(file); 234 | 235 | this.log.verbose( 236 | `[sls-rspack] Determined: filePath: [${safeFilePath}] - fileName: [${fileName}] - ext: [${ext}]` 237 | ); 238 | const outputExtension = this.isESM() ? 'mjs' : 'js'; 239 | 240 | return { 241 | [name]: { 242 | import: `./${handlerFile}${ext}`, 243 | filename: `[name]${safeFilePath}${fileName}.${outputExtension}`, 244 | }, 245 | }; 246 | } 247 | 248 | private getHandlerFile(handler: string) { 249 | // Check if handler is a well-formed path based handler. 250 | const handlerEntry = /(.*)\..*?$/.exec(handler); 251 | if (handlerEntry) { 252 | return handlerEntry[1]; 253 | } 254 | throw new this.serverless.classes.Error(`malformed handler: ${handler}`); 255 | } 256 | 257 | private isESM() { 258 | return ( 259 | this.providedRspackConfig?.experiments?.outputModule || 260 | this.pluginOptions.esm 261 | ); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/test/bundle.spec.ts: -------------------------------------------------------------------------------- 1 | import { rspack, RspackOptions } from '@rspack/core'; 2 | import { writeFileSync } from 'node:fs'; 3 | import path from 'path'; 4 | import Serverless from 'serverless'; 5 | import { bundle } from '../lib/bundle.js'; 6 | import { RspackServerlessPlugin } from '../lib/serverless-rspack.js'; 7 | import { PluginOptions } from '../lib/types.js'; 8 | import { logger, mockOptions, mockServerlessConfig } from './test-utils.js'; 9 | 10 | jest.mock('@rspack/core', () => { 11 | const mockRspack = jest.fn((config, callback) => 12 | callback(null, { toJson: jest.fn().mockImplementation(() => ({})) }) 13 | ); 14 | return { 15 | rspack: mockRspack, 16 | DefinePlugin: jest 17 | .fn() 18 | .mockImplementation(() => ({ name: 'DefinePlugin' })), 19 | ProgressPlugin: jest 20 | .fn() 21 | .mockImplementation(() => ({ name: 'ProgressPlugin' })), 22 | node: { 23 | NodeTargetPlugin: jest 24 | .fn() 25 | .mockImplementation(() => ({ name: 'NodeTargetPlugin' })), 26 | }, 27 | }; 28 | }); 29 | 30 | jest.mock('@rsdoctor/rspack-plugin', () => ({ 31 | RsdoctorRspackPlugin: jest.fn().mockImplementation(() => ({ 32 | name: 'RsdoctorRspackPlugin', 33 | apply: jest.fn(), 34 | })), 35 | })); 36 | 37 | jest.mock('node:fs', () => ({ 38 | writeFileSync: jest.fn(), 39 | })); 40 | 41 | jest.mock('node:process', () => ({ 42 | cwd: jest.fn().mockReturnValue('/Users/test/dir'), 43 | })); 44 | 45 | describe('bundle', () => { 46 | let entries: RspackOptions['entry']; 47 | let serverless: Serverless; 48 | let plugin: RspackServerlessPlugin; 49 | 50 | beforeEach(() => { 51 | serverless = mockServerlessConfig({ service: 'test-service' }); 52 | plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 53 | plugin.pluginOptions = {} as PluginOptions; 54 | 55 | entries = { 56 | main: './src/index.ts', 57 | }; 58 | }); 59 | 60 | afterEach(() => { 61 | jest.clearAllMocks(); 62 | }); 63 | 64 | it('should bundle with default config', async () => { 65 | await bundle.call(plugin, entries); 66 | 67 | expect(plugin.log.verbose).not.toHaveBeenCalledWith( 68 | expect.stringContaining('[Bundle] Config merge strategy:') 69 | ); 70 | expect(plugin.log.verbose).toHaveBeenCalledWith( 71 | expect.stringContaining('[Bundle] Bundling with config:') 72 | ); 73 | expect(rspack).toHaveBeenCalledWith(defaultConfig, expect.any(Function)); 74 | }); 75 | 76 | describe('with plugin options', () => { 77 | it('should bundle with default config extended with pluginOption `mode`', async () => { 78 | plugin.pluginOptions = { 79 | mode: 'development', 80 | } as PluginOptions; 81 | 82 | const expectedDefaultConfig = { 83 | ...defaultConfig, 84 | mode: 'development', 85 | }; 86 | 87 | await bundle.call(plugin, entries); 88 | 89 | expect(plugin.log.verbose).not.toHaveBeenCalledWith( 90 | expect.stringContaining('[Bundle] Config merge strategy:') 91 | ); 92 | expect(plugin.log.verbose).toHaveBeenCalledWith( 93 | expect.stringContaining('[Bundle] Bundling with config:') 94 | ); 95 | expect(rspack).toHaveBeenCalledWith( 96 | expectedDefaultConfig, 97 | expect.any(Function) 98 | ); 99 | }); 100 | 101 | it('should bundle with default config extended with pluginOption `esm`', async () => { 102 | plugin.pluginOptions = { 103 | esm: true, 104 | } as PluginOptions; 105 | 106 | const expectedDefaultConfig = { 107 | ...defaultConfig, 108 | mode: undefined, 109 | experiments: { 110 | outputModule: true, 111 | }, 112 | output: { 113 | ...defaultConfig.output, 114 | environment: { 115 | dynamicImport: true, 116 | module: true, 117 | }, 118 | chunkFormat: 'module', 119 | chunkLoading: 'import', 120 | library: { 121 | type: 'module', 122 | }, 123 | }, 124 | }; 125 | await bundle.call(plugin, entries); 126 | 127 | expect(plugin.log.verbose).not.toHaveBeenCalledWith( 128 | expect.stringContaining('[Bundle] Config merge strategy:') 129 | ); 130 | expect(plugin.log.verbose).toHaveBeenCalledWith( 131 | expect.stringContaining('[Bundle] Bundling with config:') 132 | ); 133 | expect(rspack).toHaveBeenCalledWith( 134 | expectedDefaultConfig, 135 | expect.any(Function) 136 | ); 137 | }); 138 | 139 | it('should bundle with default config extended with pluginOption `sourcemap`', async () => { 140 | plugin.pluginOptions = { 141 | sourcemap: 'cheap-source-map', 142 | } as PluginOptions; 143 | 144 | const expectedDefaultConfig = { 145 | ...defaultConfig, 146 | devtool: 'cheap-source-map', 147 | }; 148 | await bundle.call(plugin, entries); 149 | 150 | expect(plugin.log.verbose).not.toHaveBeenCalledWith( 151 | expect.stringContaining('[Bundle] Config merge strategy:') 152 | ); 153 | expect(plugin.log.verbose).toHaveBeenCalledWith( 154 | expect.stringContaining('[Bundle] Bundling with config:') 155 | ); 156 | expect(rspack).toHaveBeenCalledWith( 157 | expectedDefaultConfig, 158 | expect.any(Function) 159 | ); 160 | }); 161 | 162 | it('should bundle with default config extended with pluginOption `externals`', async () => { 163 | plugin.pluginOptions = { 164 | externals: ['^@aws-sdk/.*$'], 165 | } as PluginOptions; 166 | 167 | const expectedDefaultConfig = { 168 | ...defaultConfig, 169 | externals: [expect.any(Function)], 170 | }; 171 | await bundle.call(plugin, entries); 172 | 173 | expect(plugin.log.verbose).not.toHaveBeenCalledWith( 174 | expect.stringContaining('[Bundle] Config merge strategy:') 175 | ); 176 | expect(plugin.log.verbose).toHaveBeenCalledWith( 177 | expect.stringContaining('[Bundle] Bundling with config:') 178 | ); 179 | expect(rspack).toHaveBeenCalledWith( 180 | expectedDefaultConfig, 181 | expect.any(Function) 182 | ); 183 | }); 184 | 185 | it('should bundle with default config extended with pluginOption `doctor`', async () => { 186 | plugin.pluginOptions = { 187 | doctor: true, 188 | } as PluginOptions; 189 | 190 | const expectedDefaultConfig = { 191 | ...defaultConfig, 192 | plugins: [ 193 | ...defaultConfig.plugins, 194 | { apply: expect.any(Function), name: 'RsdoctorRspackPlugin' }, 195 | ], 196 | }; 197 | await bundle.call(plugin, entries); 198 | 199 | expect(plugin.log.verbose).not.toHaveBeenCalledWith( 200 | expect.stringContaining('[Bundle] Config merge strategy:') 201 | ); 202 | expect(plugin.log.verbose).toHaveBeenCalledWith( 203 | expect.stringContaining('[Bundle] Bundling with config:') 204 | ); 205 | expect(rspack).toHaveBeenCalledWith( 206 | expectedDefaultConfig, 207 | expect.any(Function) 208 | ); 209 | }); 210 | 211 | it('should bundle with default config extended with pluginOption `tsConfig`', async () => { 212 | plugin.pluginOptions = { 213 | tsConfig: './test/tsconfig.json', 214 | } as PluginOptions; 215 | 216 | const expectedDefaultConfig = { 217 | ...defaultConfig, 218 | resolve: { 219 | ...defaultConfig.resolve, 220 | tsConfig: '/Users/test/dir/test/tsconfig.json', 221 | }, 222 | }; 223 | await bundle.call(plugin, entries); 224 | 225 | expect(plugin.log.verbose).not.toHaveBeenCalledWith( 226 | expect.stringContaining('[Bundle] Config merge strategy:') 227 | ); 228 | expect(plugin.log.verbose).toHaveBeenCalledWith( 229 | expect.stringContaining('[Bundle] Bundling with config:') 230 | ); 231 | expect(rspack).toHaveBeenCalledWith( 232 | expectedDefaultConfig, 233 | expect.any(Function) 234 | ); 235 | }); 236 | }); 237 | 238 | describe('with provided config', () => { 239 | it('should merge provided config with default config with combine strategy', async () => { 240 | plugin.providedRspackConfig = { 241 | mode: 'production', 242 | }; 243 | plugin.pluginOptions.config = { path: './rspack', strategy: 'combine' }; 244 | 245 | const expectedDefaultConfig = { 246 | ...defaultConfig, 247 | ...plugin.providedRspackConfig, 248 | }; 249 | await bundle.call(plugin, entries); 250 | 251 | expect(plugin.log.verbose).toHaveBeenCalledWith( 252 | expect.stringContaining('[Bundle] Config merge strategy: combine') 253 | ); 254 | expect(rspack).toHaveBeenCalledWith( 255 | expectedDefaultConfig, 256 | expect.any(Function) 257 | ); 258 | }); 259 | 260 | it('should override default config with provided config with override strategy', async () => { 261 | plugin.providedRspackConfig = { 262 | mode: 'production', 263 | }; 264 | plugin.pluginOptions.config = { path: './rspack', strategy: 'override' }; 265 | 266 | const expectedDefaultConfig = { 267 | mode: 'production', 268 | ...enforcedDefaultConfig, 269 | }; 270 | await bundle.call(plugin, entries); 271 | 272 | expect(plugin.log.verbose).toHaveBeenCalledWith( 273 | expect.stringContaining('[Bundle] Config merge strategy: override') 274 | ); 275 | expect(rspack).toHaveBeenCalledWith( 276 | expectedDefaultConfig, 277 | expect.any(Function) 278 | ); 279 | }); 280 | }); 281 | 282 | it('should resolve with success message', async () => { 283 | const result = await bundle.call(plugin, entries); 284 | expect(plugin.log.verbose).toHaveBeenCalledWith( 285 | expect.stringContaining( 286 | '[Performance] Bundle total execution time for service' 287 | ) 288 | ); 289 | expect(result).toBe('Success!'); 290 | }); 291 | 292 | it('should write stats file if stats option is enabled', async () => { 293 | plugin.pluginOptions.stats = true; 294 | 295 | await bundle.call(plugin, entries); 296 | 297 | expect(writeFileSync).toHaveBeenCalledWith( 298 | path.join(plugin.buildOutputFolderPath, 'stats.json'), 299 | expect.any(String) 300 | ); 301 | }); 302 | 303 | it('should log error if writing stats file fails', async () => { 304 | plugin.pluginOptions.stats = true; 305 | (writeFileSync as jest.Mock).mockImplementationOnce(() => { 306 | throw new Error('Failed to write'); 307 | }); 308 | 309 | await bundle.call(plugin, entries); 310 | 311 | expect(plugin.log.error).toHaveBeenCalledWith( 312 | '[Bundle] Failed to write stats file: Error: Failed to write' 313 | ); 314 | }); 315 | }); 316 | 317 | const defaultConfig = { 318 | devtool: false, 319 | entry: { 320 | main: './src/index.ts', 321 | }, 322 | experiments: { 323 | outputModule: undefined, 324 | }, 325 | mode: undefined, 326 | module: { 327 | rules: [ 328 | { 329 | test: /\.ts$/, 330 | use: { 331 | loader: 'builtin:swc-loader', 332 | options: { 333 | jsc: { 334 | parser: { 335 | syntax: 'typescript', 336 | }, 337 | target: 'es2020', 338 | }, 339 | }, 340 | }, 341 | }, 342 | ], 343 | }, 344 | optimization: { 345 | mangleExports: false, 346 | }, 347 | output: { 348 | library: { 349 | type: 'commonjs2', 350 | }, 351 | path: '/workDir/.rspack', 352 | }, 353 | plugins: [ 354 | { 355 | name: 'DefinePlugin', 356 | }, 357 | { 358 | name: 'ProgressPlugin', 359 | }, 360 | { 361 | name: 'NodeTargetPlugin', 362 | }, 363 | ], 364 | resolve: { 365 | extensions: ['...', '.ts', '.tsx', '.jsx'], 366 | }, 367 | target: 'node', 368 | }; 369 | 370 | const enforcedDefaultConfig = { 371 | entry: { 372 | main: './src/index.ts', 373 | }, 374 | optimization: { 375 | mangleExports: false, 376 | }, 377 | output: { 378 | path: '/workDir/.rspack', 379 | }, 380 | }; 381 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚡ serverless-rspack 2 | 3 | [Serverless Framework](https://www.serverless.com) plugin for zero-config JavaScript and TypeScript code bundling using the high performance Rust-based JavaScript bundler [`rspack`](https://rspack.dev/guide/start/introduction) 4 | 5 | [![Serverless][ico-serverless]][link-serverless] 6 | [![Build Status][ico-build]][link-build] 7 | [![NPM][ico-npm]][link-npm] 8 | 9 | Look for the plugin under the [/libs](/libs/serverless-rspack/) directory. 10 | 11 | Example serverless projects are under the [/examples](/examples) directory. 12 | 13 | For Developers - [DEVELOPER.MD](./docs/DEVELOPER.md) 14 | 15 | 16 | ## Features 17 | 18 | - From zero to hero: configuration possibilities range from zero-config to fully customizable 19 | - Build and runtime performance at its core 20 | - Supports `sls package`, `sls deploy`, `sls deploy function` 21 | - Integrates with [`Serverless Invoke Local`](https://www.serverless.com/framework/docs/providers/aws/cli-reference/invoke-local) & [`serverless-offline`](https://github.com/dherault/serverless-offline) 22 | 23 | ## Table of Contents 24 | 25 | - [Install](#install) 26 | - [Serverless V4](#serverless-v4-requirement) 27 | - [Plugin Options](#plugin-options) 28 | - [Examples](#examples) 29 | - [Options](#options) 30 | - [Read-only default Rspack Options](#read-only-default-rspack-options) 31 | - [Supported Runtimes](#supported-runtimes) 32 | - [Advanced Configuration](#advanced-configuration) 33 | - [Config File](#config-file) 34 | - [Config File Merge Strategies](#config-file-merge-strategies) 35 | - [External Dependencies](#external-dependencies) 36 | - [Scripts](#scripts) 37 | - [Function Scripts](#function-scripts) 38 | - [Global Scripts](#global-scripts) 39 | - [Doctor](#doctor) 40 | - [Integrations](#integrations) 41 | - [Serverless Offline](#serverless-offline) 42 | - [Known Issues](#known-issues) 43 | 44 | 45 | ## Install 46 | 47 | ```sh 48 | # install `serverless-rspack` 49 | yarn add --dev @kitchenshelf/serverless-rspack 50 | # or 51 | npm install -D @kitchenshelf/serverless-rspack 52 | # or 53 | pnpm install -D @kitchenshelf/serverless-rspack 54 | ``` 55 | 56 | Add the following plugin to your `serverless.yml`: 57 | 58 | ```yaml 59 | plugins: 60 | - @kitchenshelf/serverless-rspack 61 | ``` 62 | 63 | ### Serverless v4 requirement 64 | 65 | If you are using Serverless v4 you must disable the default builtin ESBuild support in your `serverless.yml` 66 | 67 | ```yml 68 | build: 69 | esbuild: false 70 | ``` 71 | 72 | ## Plugin Options 73 | 74 | By default, no plugin options is required, but you can override the reasonable defaults via the `custom.rspack` section in the `serverless.yml` file. 75 | 76 | ```yml 77 | custom: 78 | rspack: 79 | mode: 'production' 80 | esm: true 81 | ``` 82 | 83 | ### Examples 84 | 85 | See [example folder](../../examples) for example plugin option configuration. 86 | 87 | ### Options 88 | 89 | | Option | Description | Default | 90 | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------ | 91 | | `zipConcurrency` | The number of concurrent zip operations to run at once. eg. `8`. _NOTE_: This can be memory intensive and could produce slower builds. | `Infinity` | 92 | | `keepOutputDirectory` | Keeps the `.rspack` output folder. Useful for debugging. | `false` | 93 | | `stats` | Generate packaging information that can be used to analyze module dependencies and optimize compilation speed. | `false` | 94 | | [`config`](#config-file) | rspack config options. | `undefined` | 95 | | `config.path` | Relative rspack config path. | `undefined` | 96 | | [`config.strategy`](#config-file-merge-strategies) | Strategy to use when a rspack config is provided. | `override` | 97 | | `esm` | Output format will be ESM (experimental). | `false` | 98 | | `mode` | Used to set the build mode of Rspack to enable the default optimization strategy (https://www.rspack.dev/config/mode). | `production` | 99 | | `tsConfig` | Relative path to your tsconfig. | `undefined` | 100 | | `sourcemap` | Configure rspack [sourcemaps](https://rspack.dev/config/devtool). | `false` | 101 | | [`externals`](#external-dependencies) | Provides a way of excluding dependencies from the output bundles. | `undefined` | 102 | | [`scripts`](#scripts) | Array of scripts to execute after your code has been bundled by rspack. | `undefined` | 103 | | [`doctor`](#doctor) | Enable the `Rsdoctor` plugin. | `undefined` | 104 | 105 | #### Read-only default Rspack Options 106 | 107 | The following `rspack` options are automatically set and **cannot** be overwritten. 108 | 109 | | Option | Notes | 110 | | ------------- | ---------------------------------------------------------------------------------------------------------------- | 111 | | `entry` | Handler entries are determined by the plugin | 112 | | `output.path` | The plugin needs to have full control over where bundles are output to, so it can correctly create zip artifacts | 113 | 114 | #### Function Options 115 | 116 | | Option | Description | Default | 117 | | -------- | --------------------------------------------------------------------------------------------- | ----------- | 118 | | `rspack` | Set this property on a function definition to force the handler to be processed by the plugin | `undefined` | 119 | 120 | 121 | ## Supported Runtimes 122 | 123 | This plugin will automatically process any function that has a runtime that starts with `node` i.e `nodejs20.x` 124 | 125 | ### Non-Node functions 126 | 127 | If you wish to use this plugin alongside non Node functions like Python or functions with images, this plugin will automatically ignore any function which does not match the supported runtimes. 128 | 129 | If you wish to force a function to be process set `rspack: true` on a function definition. This is handy when using custom provided node runtimes i.e ` runtime: 'provided.al2023'` 130 | 131 | ⚠️ **Note: this will only work if your custom runtime and function are written in JavaScript/Typescript. 132 | Make sure you know what you are doing when this option is set to `true`** 133 | 134 | 135 | ## Advanced Configuration 136 | 137 | ### Config file 138 | 139 | Rspack configuration can be defined by a config file. 140 | 141 | ```yml 142 | custom: 143 | rspack: 144 | config: 145 | path: './rspack.config.js' 146 | ``` 147 | 148 | ```js 149 | // rspack.config.js 150 | module.exports = (serverless) => { 151 | externals: ['lodash'], 152 | // etc 153 | }; 154 | ``` 155 | 156 | You can also return an async function. 157 | 158 | ```js 159 | module.exports = async (serverless) => { 160 | const getExternals = new Promise((resolve, reject) => { 161 | setTimeout(() => { 162 | resolve(['lodash']); 163 | }, 250); 164 | }); 165 | 166 | const externals = await getExternals; 167 | 168 | return { 169 | externals: externals, 170 | // etc 171 | }; 172 | }; 173 | ``` 174 | 175 | #### Config file merge strategies 176 | 177 | You can change how the plugin uses a provided config via the `strategy` option: 178 | 179 | ```yml 180 | custom: 181 | rspack: 182 | config: 183 | path: './rspack.config.js' 184 | strategy: combine 185 | ``` 186 | 187 | 1. `override`: ***Default*** - Enables power users to provided their own complete Rspack configuration: `rspack.config.js` -> [`PluginReadOnlyDefaults`](#read-only-default-rspack-options) 188 | 2. `combine`: Enables providing partial configuration. Merges all configuration together: `PluginDefaults` -> `PluginOptions` -> `rspack.config.js` -> [`PluginReadOnlyDefaults`](#read-only-default-rspack-options). 189 | 190 | ⚠️ **Note: Pay attention to the order in which configuration is combined. Each time the right take precedence. ** 191 | 192 | ### External Dependencies 193 | 194 | By providing a regex you can mark packages as `external` and they will be excluded from the output bundles. 195 | 196 | ```yml 197 | custom: 198 | rspack: 199 | externals: 200 | - "^@aws-sdk\/.*$" 201 | - "^@smithy\/.*$" 202 | - '^isin-validator$' 203 | ``` 204 | 205 | ### Scripts 206 | 207 | Run custom shell commands after your code has been bundled by rspack. This is useful for modifying the output of the build before it is packaged. 208 | 209 | There are two types of scripts: 210 | 211 | 1. **Function**: Executed once per defining function after the bundle step. 212 | 2. **Global**: Executed once before the package step. 213 | 214 | **Order**: `bundle` -> `function scripts` -> `global scripts` -> `package` 215 | 216 | ⚠️ **Note: Scripts run sequentially and will fail the build if any errors occur in any of the scripts.** 217 | 218 | The following environment variables are available to all your scripts: 219 | 220 | - `process.env`: All system environment variables. 221 | - `KS_SERVICE_DIR`: The absolute path to the service directory (e.g. `/Users/user/code/my-service`). 222 | - `KS_BUILD_OUTPUT_FOLDER`: The name of the build output folder (e.g. `.rspack`). 223 | - `KS_PACKAGE_OUTPUT_FOLDER`: The name of the package output folder (e.g. `.serverless`). 224 | 225 | #### Function Scripts 226 | 227 | Scripts are executed from the function directory in the output folder i.e `.rspack/`. 228 | 229 | ##### Usage 230 | 231 | ```yml 232 | functions: 233 | app3: 234 | handler: src/App3.handler 235 | runtime: nodejs20.x 236 | rspack: 237 | enable: true 238 | scripts: 239 | - 'echo "First function script"' 240 | - 'npx npm init -y && npm install --force --os=linux --cpu=x64 --include=optional sharp @img/sharp-linux-x64' 241 | - 'cp $KS_SERVICE_DIR/src/my-image.jpeg ./' 242 | - 'echo "Last function script"' 243 | ``` 244 | 245 | The following extra environment variables are available to your function scripts: 246 | 247 | - `KS_FUNCTION_NAME`: The name of the function being processed. 248 | 249 | #### Global Scripts 250 | 251 | Scripts are executed from the root of the service directory. 252 | 253 | ##### Usage 254 | 255 | ```yml 256 | custom: 257 | rspack: 258 | externals: ['^@aws-sdk/.*$'], 259 | scripts: 260 | - 'echo "First global script"' 261 | - 'echo "Last global script"' 262 | ``` 263 | 264 | ### Doctor 265 | 266 | [Rsdoctor](https://rsdoctor.dev/guide/start/intro) is a one-stop tool for diagnosing and analyzing the build process and build artifacts. 267 | 268 | The serverless-rspack plugin will automatically enable the `Rsdoctor` plugin when the `doctor` option is provided. 269 | 270 | ```yml 271 | custom: 272 | rspack: 273 | doctor: true 274 | ``` 275 | 276 | You can also provide an `outputDirectory` to specify where the report should be saved. By default, the report will be saved in the `.rspack` folder. 277 | 278 | ```yml 279 | custom: 280 | rspack: 281 | doctor: 282 | enable: true 283 | outputDirectory: ./doctor-report 284 | ``` 285 | 286 | ⚠️ **Note: Rsdoctor is configured to run in [`brief`](https://rsdoctor.dev/guide/start/cicd#enabling-brief-mode) mode. If you want to use another mode, you can register `RsdoctorRspackPlugin` manually using the [rspack config option](#config-file).** 287 | 288 | ## Integrations 289 | 290 | ### Serverless Offline 291 | 292 | The plugin has first class support for [serverless-offline](https://github.com/dherault/serverless-offline). 293 | 294 | Add the plugins to your `serverless.yml` file and make sure that `serverless-rspack` 295 | precedes `serverless-offline` as the order is important: 296 | 297 | ```yaml 298 | plugins: ... 299 | - serverless-rspack 300 | ... 301 | - serverless-offline 302 | ... 303 | ``` 304 | 305 | Run `serverless offline start` to start the Lambda/API simulation. 306 | 307 | ⚠️ **Note: The plugin will automatically set sourcemap to `source-map` when running in offline mode and change the `devtoolModuleFilenameTemplate` to `[absolute-resource-path]`.** 308 | 309 | ⚠️ **Note: If you are using a custom rspack config, then the serverless plugin passed to your config function will have `offlineMode` set to true.** 310 | 311 | 312 | ## Known Issues 313 | 314 | - Invoke Local does not work with ESM enabled when using serverless V3: [ISSUE-11308](https://github.com/serverless/serverless/issues/11308#issuecomment-1719297694) 315 | 316 | 317 | --- 318 | 319 | --- 320 | 321 | --- 322 | 323 | Inspired by [serverless-plugin-typescript](https://github.com/prisma-labs/serverless-plugin-typescript), [serverless-webpack](https://github.com/serverless-heaven/serverless-webpack) and [serverless-esbuild](https://github.com/floydspace/serverless-esbuild) 324 | 325 | [ico-serverless]: http://public.serverless.com/badges/v3.svg 326 | [ico-npm]: https://img.shields.io/npm/v/@kitchenshelf/serverless-rspack.svg 327 | [ico-build]: https://github.com/kitchenshelf/serverless-rspack/actions/workflows/ci.yml/badge.svg 328 | [link-serverless]: https://www.serverless.com/ 329 | [link-npm]: https://www.npmjs.com/package/@kitchenshelf/serverless-rspack 330 | [link-build]: https://github.com/kitchenshelf/serverless-rspack/actions/workflows/ci.yml 331 | -------------------------------------------------------------------------------- /libs/serverless-rspack/src/test/hooks/initialize.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { RspackServerlessPlugin } from '../../lib/serverless-rspack.js'; 3 | import { PluginOptions } from '../../lib/types.js'; 4 | import { logger, mockOptions, mockServerlessConfig } from '../test-utils.js'; 5 | 6 | jest.mock('node:fs', () => ({ 7 | readdirSync: () => [ 8 | 'hello1.ts', 9 | 'hello2.ts', 10 | 'hello3.ts', 11 | 'hello4.ts', 12 | 'hello5.ts', 13 | 'hello6.ts', 14 | 'hello7.ts', 15 | 'hello8.ts', 16 | 'hello9.ts', 17 | 'hello10.ts', 18 | 'hello11.ts', 19 | ], 20 | })); 21 | 22 | afterEach(() => { 23 | jest.resetModules(); 24 | jest.resetAllMocks(); 25 | }); 26 | 27 | describe('initialize hook', () => { 28 | it('should set default plugin options', async () => { 29 | const expectedDefaultRspackPluginOptions = { 30 | esm: false, 31 | mode: 'production', 32 | stats: false, 33 | keepOutputDirectory: false, 34 | zipConcurrency: Infinity, 35 | }; 36 | 37 | const serverless = mockServerlessConfig(); 38 | 39 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 40 | 41 | await plugin.hooks['initialize'](); 42 | 43 | expect(plugin.pluginOptions).toEqual(expectedDefaultRspackPluginOptions); 44 | }); 45 | 46 | it('should set user defined plugin options', async () => { 47 | const userRspackPluginOptions: PluginOptions = { 48 | esm: false, 49 | mode: 'development', 50 | stats: true, 51 | doctor: true, 52 | keepOutputDirectory: true, 53 | zipConcurrency: 8, 54 | externals: ['test', 'test2'], 55 | tsConfig: './app.tsconfig.json', 56 | }; 57 | 58 | const serverless = mockServerlessConfig({ 59 | custom: { 60 | rspack: userRspackPluginOptions, 61 | }, 62 | }); 63 | 64 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 65 | 66 | await plugin.hooks['initialize'](); 67 | 68 | expect(plugin.pluginOptions).toEqual({ 69 | ...userRspackPluginOptions, 70 | }); 71 | }); 72 | 73 | it('should load a rspack config from file if `custom.rspack.config.path` is a string', async () => { 74 | const loadedConfig = () => ({ 75 | mode: 'development', 76 | }); 77 | 78 | jest.doMock( 79 | path.join('testServicePath', './rspack.config.js'), 80 | () => loadedConfig, 81 | { virtual: true } 82 | ); 83 | 84 | const serverless = mockServerlessConfig({ 85 | custom: { 86 | rspack: { 87 | config: { path: './rspack.config.js' }, 88 | }, 89 | }, 90 | }); 91 | serverless.config.serviceDir = 'testServicePath'; 92 | serverless.utils.fileExistsSync = jest.fn().mockReturnValue(true); 93 | 94 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 95 | 96 | await plugin.hooks['initialize'](); 97 | 98 | expect(plugin.providedRspackConfig).toEqual(loadedConfig()); 99 | }); 100 | 101 | it('should error if `custom.rspack.config.path` does not exist', async () => { 102 | const serverless = mockServerlessConfig({ 103 | custom: { 104 | rspack: { 105 | config: { path: './rspack.config.js' }, 106 | }, 107 | }, 108 | }); 109 | 110 | serverless.utils.fileExistsSync = jest.fn().mockReturnValue(false); 111 | 112 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 113 | try { 114 | await plugin.hooks['initialize'](); 115 | fail('Expected function to throw an error'); 116 | } catch (error) { 117 | expect(serverless.classes.Error).toHaveBeenCalledTimes(1); 118 | expect(serverless.classes.Error).toHaveBeenCalledWith( 119 | 'Rspack config does not exist at path: /workDir/rspack.config.js' 120 | ); 121 | } 122 | }); 123 | 124 | it('should error if `custom.rspack.config.path` does not return a function', async () => { 125 | const loadedConfig = { 126 | mode: 'development', 127 | }; 128 | 129 | jest.doMock( 130 | path.join('testServicePath', './rspack.config.js'), 131 | () => loadedConfig, 132 | { virtual: true } 133 | ); 134 | const serverless = mockServerlessConfig({ 135 | custom: { 136 | rspack: { 137 | config: { path: './rspack.config.js' }, 138 | }, 139 | }, 140 | }); 141 | serverless.config.serviceDir = 'testServicePath'; 142 | serverless.utils.fileExistsSync = jest.fn().mockReturnValue(true); 143 | 144 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 145 | 146 | try { 147 | await plugin.hooks['initialize'](); 148 | fail('Expected function to throw an error'); 149 | } catch (error) { 150 | expect(serverless.classes.Error).toHaveBeenCalledTimes(1); 151 | expect(serverless.classes.Error).toHaveBeenCalledWith( 152 | 'Config located at testServicePath/rspack.config.js does not return a function. See for reference: https://github.com/kitchenshelf/serverless-rspack/blob/main/README.md#config-file' 153 | ); 154 | } 155 | }); 156 | 157 | it('should error when no functions entries are created', async () => { 158 | const serverless = mockServerlessConfig(); 159 | serverless.service.getAllFunctions = jest.fn().mockReturnValue([]); 160 | 161 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 162 | 163 | try { 164 | await plugin.hooks['initialize'](); 165 | fail('Expected function to throw an error'); 166 | } catch (error) { 167 | expect(serverless.classes.Error).toHaveBeenCalledTimes(1); 168 | expect(serverless.classes.Error).toHaveBeenCalledWith( 169 | 'No functions detected in service - you can remove this plugin from your service' 170 | ); 171 | } 172 | }); 173 | 174 | it('should only process node functions that are not disabled via rspack', async () => { 175 | const functions = { 176 | hello1: { 177 | handler: 'hello1.handler', 178 | events: [], 179 | package: { artifact: 'hello1' }, 180 | rspack: { 181 | enable: false, 182 | }, 183 | }, 184 | hello2: { 185 | handler: 'hello2.handler', 186 | events: [], 187 | package: { artifact: 'hello2' }, 188 | rspack: { 189 | enable: true, 190 | }, 191 | }, 192 | hello3: { 193 | handler: 'hello3.handler', 194 | events: [], 195 | package: { artifact: 'hello3' }, 196 | rspack: false, 197 | }, 198 | hello4: { 199 | handler: 'hello4.handler', 200 | runtime: 'python3.10', 201 | events: [], 202 | package: { artifact: 'hello4' }, 203 | }, 204 | hello5: { 205 | handler: 'hello5.handler', 206 | events: [], 207 | package: { artifact: 'hello5' }, 208 | }, 209 | hello6: { 210 | handler: 'hello6.handler', 211 | events: [], 212 | package: { artifact: 'hello6' }, 213 | rspack: true, 214 | }, 215 | hello7: { 216 | handler: 'hello7.handler', 217 | events: [], 218 | package: { artifact: 'hello7' }, 219 | rspack: false, 220 | }, 221 | hello8: { 222 | handler: 'hello8.handler', 223 | runtime: 'nodejs20.x', 224 | events: [], 225 | package: { artifact: 'hello8' }, 226 | rspack: true, 227 | }, 228 | hello9: { 229 | handler: 'hello9.handler', 230 | runtime: 'nodejs20.x', 231 | events: [], 232 | package: { artifact: 'hello9' }, 233 | rspack: false, 234 | }, 235 | hello10: { 236 | handler: 'hello10.handler', 237 | runtime: 'nodejs20.x', 238 | events: [], 239 | package: { artifact: 'hello10' }, 240 | rspack: { enable: true }, 241 | }, 242 | hello11: { 243 | handler: 'hello11.handler', 244 | runtime: 'nodejs20.x', 245 | events: [], 246 | package: { artifact: 'hello11' }, 247 | rspack: { enable: false }, 248 | }, 249 | }; 250 | const serverless = mockServerlessConfig({ 251 | functions, 252 | getAllFunctions: jest.fn().mockReturnValue(Object.keys(functions)), 253 | getFunction: (name: string) => (functions as any)[name], 254 | }); 255 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 256 | 257 | await plugin.hooks['initialize'](); 258 | 259 | expect(plugin.functionEntries).toEqual({ 260 | hello2: { 261 | filename: '[name]/hello2.js', 262 | import: './hello2.ts', 263 | }, 264 | hello5: { 265 | filename: '[name]/hello5.js', 266 | import: './hello5.ts', 267 | }, 268 | hello6: { 269 | filename: '[name]/hello6.js', 270 | import: './hello6.ts', 271 | }, 272 | hello8: { 273 | filename: '[name]/hello8.js', 274 | import: './hello8.ts', 275 | }, 276 | hello10: { 277 | filename: '[name]/hello10.js', 278 | import: './hello10.ts', 279 | }, 280 | }); 281 | }); 282 | 283 | it('should process none node functions when rspack is enabled', async () => { 284 | const functions = { 285 | hello1: { 286 | handler: 'hello1.handler', 287 | runtime: 'custom', 288 | events: [], 289 | package: { artifact: 'hello1' }, 290 | rspack: false, 291 | }, 292 | hello2: { 293 | handler: 'hello2.handler', 294 | runtime: 'custom', 295 | events: [], 296 | package: { artifact: 'hello2' }, 297 | rspack: true, 298 | }, 299 | hello3: { 300 | handler: 'hello3.handler', 301 | runtime: 'custom', 302 | events: [], 303 | package: { artifact: 'hello3' }, 304 | rspack: { 305 | enable: false, 306 | }, 307 | }, 308 | hello4: { 309 | handler: 'hello4.handler', 310 | runtime: 'custom', 311 | events: [], 312 | package: { artifact: 'hello4' }, 313 | rspack: { 314 | enable: true, 315 | }, 316 | }, 317 | hello5: { 318 | handler: 'hello5.handler', 319 | runtime: 'custom', 320 | events: [], 321 | package: { artifact: 'hello5' }, 322 | rspack: { 323 | scripts: ['echo "hello5"'], 324 | }, 325 | }, 326 | }; 327 | const serverless = mockServerlessConfig({ 328 | functions, 329 | getAllFunctions: jest.fn().mockReturnValue(Object.keys(functions)), 330 | getFunction: (name: string) => (functions as any)[name], 331 | }); 332 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 333 | 334 | await plugin.hooks['initialize'](); 335 | 336 | expect(plugin.functionEntries).toEqual({ 337 | hello2: { 338 | filename: '[name]/hello2.js', 339 | import: './hello2.ts', 340 | }, 341 | hello4: { 342 | filename: '[name]/hello4.js', 343 | import: './hello4.ts', 344 | }, 345 | hello5: { 346 | filename: '[name]/hello5.js', 347 | import: './hello5.ts', 348 | }, 349 | }); 350 | }); 351 | 352 | it('should create cjs entries by default', async () => { 353 | const serverless = mockServerlessConfig(); 354 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 355 | 356 | await plugin.hooks['initialize'](); 357 | 358 | expect(plugin.functionEntries).toEqual({ 359 | hello1: { 360 | filename: '[name]/hello1.js', 361 | import: './hello1.ts', 362 | }, 363 | hello2: { 364 | filename: '[name]/hello2.js', 365 | import: './hello2.ts', 366 | }, 367 | }); 368 | }); 369 | 370 | it('should create mjs entries when providedRspackConfig experiments outputModule', async () => { 371 | const loadedConfig = () => ({ 372 | experiments: { 373 | outputModule: true, 374 | }, 375 | }); 376 | 377 | jest.doMock( 378 | path.join('testServicePath2', './rspack.config.js'), 379 | () => loadedConfig, 380 | { virtual: true } 381 | ); 382 | const serverless = mockServerlessConfig({ 383 | custom: { 384 | rspack: { 385 | esm: false, 386 | config: { path: './rspack.config.js' }, 387 | }, 388 | }, 389 | }); 390 | serverless.config.serviceDir = 'testServicePath2'; 391 | serverless.utils.fileExistsSync = jest.fn().mockReturnValue(true); 392 | 393 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 394 | 395 | await plugin.hooks['initialize'](); 396 | 397 | expect(plugin.functionEntries).toEqual({ 398 | hello1: { 399 | filename: '[name]/hello1.mjs', 400 | import: './hello1.ts', 401 | }, 402 | hello2: { 403 | filename: '[name]/hello2.mjs', 404 | import: './hello2.ts', 405 | }, 406 | }); 407 | }); 408 | 409 | it('should create mjs entries when plugin option esm is provided', async () => { 410 | const serverless = mockServerlessConfig({ 411 | custom: { 412 | rspack: { esm: true }, 413 | experiments: {}, 414 | }, 415 | }); 416 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 417 | 418 | await plugin.hooks['initialize'](); 419 | 420 | expect(plugin.functionEntries).toEqual({ 421 | hello1: { 422 | filename: '[name]/hello1.mjs', 423 | import: './hello1.ts', 424 | }, 425 | hello2: { 426 | filename: '[name]/hello2.mjs', 427 | import: './hello2.ts', 428 | }, 429 | }); 430 | }); 431 | 432 | it('should error if file in handler does not exist', async () => { 433 | const functions = { 434 | hello3: { 435 | handler: 'src/hello3.handler', 436 | events: [], 437 | package: { artifact: 'hello2' }, 438 | }, 439 | }; 440 | const serverless = mockServerlessConfig({ 441 | custom: { rspack: { esm: false } }, 442 | functions, 443 | getAllFunctions: jest.fn().mockReturnValue(Object.keys(functions)), 444 | getFunction: (name: string) => (functions as any)[name], 445 | }); 446 | 447 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 448 | 449 | try { 450 | await plugin.hooks['initialize'](); 451 | } catch (error) { 452 | expect(plugin.serverless.classes.Error).toHaveBeenLastCalledWith( 453 | 'Unable to find file [hello3] in path: [./src]' 454 | ); 455 | } 456 | }); 457 | 458 | it('should error if handler is malformed', async () => { 459 | const functions = { 460 | hello3: { 461 | handler: 'src/hello2-handler', 462 | events: [], 463 | package: { artifact: 'hello2' }, 464 | }, 465 | }; 466 | const serverless = mockServerlessConfig({ 467 | custom: { rspack: { esm: false } }, 468 | functions, 469 | getAllFunctions: jest.fn().mockReturnValue(Object.keys(functions)), 470 | getFunction: (name: string) => (functions as any)[name], 471 | }); 472 | 473 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 474 | 475 | try { 476 | await plugin.hooks['initialize'](); 477 | fail('Expected function to throw an error'); 478 | } catch (error) { 479 | expect(plugin.serverless.classes.Error).toHaveBeenLastCalledWith( 480 | 'malformed handler: src/hello2-handler' 481 | ); 482 | } 483 | }); 484 | 485 | it('should add script to functionScripts when rspack.scripts is provided and rspack is enabled', async () => { 486 | const functions = { 487 | hello1: { 488 | handler: 'hello1.handler', 489 | events: [], 490 | package: { artifact: 'hello1' }, 491 | rspack: { 492 | enable: true, 493 | scripts: [ 494 | 'echo "First function script"', 495 | 'echo "Last function script"', 496 | ], 497 | }, 498 | }, 499 | hello2: { 500 | handler: 'hello2.handler', 501 | events: [], 502 | package: { artifact: 'hello2' }, 503 | rspack: { 504 | enable: true, 505 | }, 506 | }, 507 | hello3: { 508 | handler: 'hello3.handler', 509 | events: [], 510 | package: { artifact: 'hello3' }, 511 | rspack: true, 512 | }, 513 | hello4: { 514 | handler: 'hello4.handler', 515 | events: [], 516 | package: { artifact: 'hello4' }, 517 | }, 518 | hello5: { 519 | handler: 'hello5.handler', 520 | events: [], 521 | package: { artifact: 'hello5' }, 522 | rspack: { enable: false, scripts: ['echo "hello5"'] }, 523 | }, 524 | hello6: { 525 | handler: 'hello6.handler', 526 | events: [], 527 | package: { artifact: 'hello6' }, 528 | rspack: { scripts: ['echo "hello6"'] }, 529 | }, 530 | }; 531 | const serverless = mockServerlessConfig({ 532 | functions, 533 | getAllFunctions: jest.fn().mockReturnValue(Object.keys(functions)), 534 | getFunction: (name: string) => (functions as any)[name], 535 | }); 536 | const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger); 537 | 538 | await plugin.hooks['initialize'](); 539 | 540 | expect(plugin.functionScripts).toEqual({ 541 | hello1: ['echo "First function script"', 'echo "Last function script"'], 542 | hello6: ['echo "hello6"'], 543 | }); 544 | }); 545 | }); 546 | --------------------------------------------------------------------------------