Skip to content

Commit

Permalink
feat(sls-rspack): support sls cli deploy function
Browse files Browse the repository at this point in the history
Closes #11
  • Loading branch information
codingnuclei committed Nov 1, 2024
1 parent e19a6ad commit 20fe405
Show file tree
Hide file tree
Showing 11 changed files with 303 additions and 9 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ testem.log
.DS_Store
Thumbs.db

# env
.env*
!.env.example

.nx/cache
.env.local
examples/complete/package-lock.json
2 changes: 2 additions & 0 deletions docs/DEVELOPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ To try the plugin in another local repo then
4. `npx nx release publish` to publish the plugin to the local registry
5. Follow the [Install Instructions](../README.md#install) as normal in your other local repository, replacing the version with the one you just published.
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.

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).
4 changes: 3 additions & 1 deletion examples/complete/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
"name": "@serverless-rspack/complete",
"version": "0.0.0",
"scripts": {
"package": "sls package --verbose"
"package": "sls package --verbose",
"deploy": "sls deploy --verbose",
"deploy:func:app3": "sls deploy function -f app3 --verbose"
},
"dependencies": {
"isin-validator": "^1.1.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { RspackServerlessPlugin } from '../../serverless-rspack.js';

export async function AfterDeployFunctionPackageFunction(
this: RspackServerlessPlugin
) {
this.log.verbose('[sls-rspack] after:deploy:function:packageFunction');
await this.cleanup();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { RspackServerlessPlugin } from '../../serverless-rspack.js';
import { isDeployFunctionOptions } from '../../types.js';

export async function BeforeDeployFunctionPackageFunction(
this: RspackServerlessPlugin
) {
this.log.verbose('[sls-rspack] before:deploy:function:packageFunction');

if (!isDeployFunctionOptions(this.options)) {
throw new this.serverless.classes.Error(
'This hook only supports deploy function options'
);
}

const deployFunc = this.options.function;

if (!(deployFunc in this.functionEntries)) {
throw new this.serverless.classes.Error(
`Function ${deployFunc} not found in function entries`
);
}

const entry = this.functionEntries[deployFunc];

await this.bundle({
[deployFunc]: entry,
});

this.functionEntries = {};
this.functionEntries[deployFunc] = entry;

await this.scripts();
await this.pack();
}
6 changes: 6 additions & 0 deletions libs/serverless-rspack/src/lib/pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ export async function pack(this: RspackServerlessPlugin) {
*/
function zipDirectory(sourceDir: string, outPath: string) {
const archive = archiver('zip', { zlib: { level: 9 } });

const outDir = path.dirname(outPath);

if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir, { recursive: true });
}
const stream = fs.createWriteStream(outPath);

return new Promise((resolve, reject) => {
Expand Down
13 changes: 6 additions & 7 deletions libs/serverless-rspack/src/lib/serverless-rspack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type ServerlessPlugin from 'serverless/classes/Plugin';
import { bundle } from './bundle.js';
import { SERVERLESS_FOLDER, WORK_FOLDER } from './constants.js';
import { determineFileParts, isNodeFunction } from './helpers.js';
import { AfterDeployFunctionPackageFunction } from './hooks/deploy-function/after-package-function.js';
import { BeforeDeployFunctionPackageFunction } from './hooks/deploy-function/before-package-function.js';
import { Initialize } from './hooks/initialize.js';
import { BeforeInvokeLocalInvoke } from './hooks/invoke-local/before-invoke.js';
import { AfterPackageCreateDeploymentArtifacts } from './hooks/package/after-create-deployment-artifacts.js';
Expand All @@ -20,7 +22,6 @@ import {
PluginOptionsSchema,
RsPackFunctionDefinitionHandler,
} from './types.js';

export class RspackServerlessPlugin implements ServerlessPlugin {
serviceDirPath: string;
buildOutputFolder: string;
Expand Down Expand Up @@ -72,12 +73,10 @@ export class RspackServerlessPlugin implements ServerlessPlugin {
BeforePackageCreateDeploymentArtifacts.bind(this),
'after:package:createDeploymentArtifacts':
AfterPackageCreateDeploymentArtifacts.bind(this),
// 'before:deploy:function:packageFunction': () => {
// this.log.verbose('after:deploy:function:packageFunction');
// },
// 'after:deploy:function:packageFunction': () => {
// this.log.verbose('after:deploy:function:packageFunction');
// },
'before:deploy:function:packageFunction':
BeforeDeployFunctionPackageFunction.bind(this),
'after:deploy:function:packageFunction':
AfterDeployFunctionPackageFunction.bind(this),
'before:invoke:local:invoke': BeforeInvokeLocalInvoke.bind(this),
};
}
Expand Down
6 changes: 6 additions & 0 deletions libs/serverless-rspack/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ export function isInvokeOptions(
return typeof options.function === 'string';
}

export function isDeployFunctionOptions(
options: Serverless.Options
): options is Serverless.Options & { function: string } {
return typeof options.function === 'string';
}

export type PluginFunctionEntries = {
[name: string]: {
import: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { rm } from 'node:fs/promises';
import { RspackServerlessPlugin } from '../../../lib/serverless-rspack.js';
import { PluginOptions } from '../../../lib/types.js';
import { logger, mockOptions, mockServerlessConfig } from '../../test-utils.js';

jest.mock('../../../lib/bundle', () => ({
bundle: jest.fn(),
}));

jest.mock('node:fs', () => ({
readdirSync: () => ['hello1.ts', 'hello2.ts'],
}));

jest.mock('node:fs/promises', () => ({
rm: jest.fn(),
}));

afterEach(() => {
jest.resetModules();
jest.resetAllMocks();
});

describe('after:deploy:function:packageFunction hook', () => {
it('should be defined', () => {
const serverless = mockServerlessConfig();
const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger);

expect(plugin.hooks['after:deploy:function:packageFunction']).toBeDefined();
});

it('should by default remove the build dir', async () => {
const serverless = mockServerlessConfig();
const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger);

plugin.pluginOptions = {} as Required<PluginOptions>;

await plugin.hooks['after:deploy:function:packageFunction']();

expect(rm).toHaveBeenCalledWith('/workDir/.rspack', {
recursive: true,
});
});

it('should remove the build dir when keepOutputDirectory is false', async () => {
const serverless = mockServerlessConfig();
const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger);

plugin.pluginOptions = {
keepOutputDirectory: false,
} as Required<PluginOptions>;

await plugin.hooks['after:deploy:function:packageFunction']();

expect(rm).toHaveBeenCalledWith('/workDir/.rspack', {
recursive: true,
});
});

it('keep the build dir when keepOutputDirectory is true', async () => {
const serverless = mockServerlessConfig();
const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger);

plugin.pluginOptions = {
keepOutputDirectory: true,
} as Required<PluginOptions>;

await plugin.hooks['after:deploy:function:packageFunction']();
expect(rm).not.toHaveBeenCalledWith('/workDir/.rspack', {
recursive: true,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { bundle } from '../../../lib/bundle.js';
import { pack } from '../../../lib/pack.js';
import { scripts } from '../../../lib/scripts.js';
import { RspackServerlessPlugin } from '../../../lib/serverless-rspack.js';
import { logger, mockOptions, mockServerlessConfig } from '../../test-utils.js';

jest.mock('../../../lib/bundle', () => ({
bundle: jest.fn(),
}));

jest.mock('../../../lib/pack', () => ({
pack: jest.fn(),
}));

jest.mock('../../../lib/scripts', () => ({
scripts: jest.fn(),
}));

afterEach(() => {
jest.resetModules();
jest.resetAllMocks();
});

describe('before:deploy:function:packageFunction', () => {
let plugin: RspackServerlessPlugin;

it('should be defined', () => {
const serverless = mockServerlessConfig();
const plugin = new RspackServerlessPlugin(serverless, mockOptions, logger);

expect(
plugin.hooks['before:deploy:function:packageFunction']
).toBeDefined();
});

it('should throw error if options are invalid', async () => {
const serverless = mockServerlessConfig();
plugin = new RspackServerlessPlugin(serverless, {}, logger);

try {
await plugin.hooks['before:deploy:function:packageFunction']();
fail('Expected function to throw an error');
} catch (error) {
expect(serverless.classes.Error).toHaveBeenCalledTimes(1);
expect(serverless.classes.Error).toHaveBeenCalledWith(
'This hook only supports deploy function options'
);
}
});

it('should throw error if function not found in entries', async () => {
const serverless = mockServerlessConfig();
plugin = new RspackServerlessPlugin(
serverless,
{ ...mockOptions, function: 'nonexistentFunc' },
logger
);
plugin.functionEntries = {};
try {
await plugin.hooks['before:deploy:function:packageFunction']();
fail('Expected function to throw an error');
} catch (error) {
expect(serverless.classes.Error).toHaveBeenCalledTimes(1);
expect(serverless.classes.Error).toHaveBeenCalledWith(
'Function nonexistentFunc not found in function entries'
);
}
});

it('should bundle the entries', async () => {
const serverless = mockServerlessConfig();
const plugin = new RspackServerlessPlugin(
serverless,
{ ...mockOptions, function: 'myFunc' },
logger
);

plugin.functionEntries = {
myFunc: { import: 'test/path/entry.ts', filename: 'test/path/entry.ts' },
};

await plugin.hooks['before:deploy:function:packageFunction']();

expect(bundle).toHaveBeenCalledTimes(1);
expect(bundle).toHaveBeenCalledWith(plugin.functionEntries);
});

it('should run scripts', async () => {
const serverless = mockServerlessConfig();
const plugin = new RspackServerlessPlugin(
serverless,
{ ...mockOptions, function: 'myFunc' },
logger
);

plugin.functionEntries = {
myFunc: { import: 'test/path/entry.ts', filename: 'test/path/entry.ts' },
};

await plugin.hooks['before:deploy:function:packageFunction']();

expect(scripts).toHaveBeenCalledTimes(1);
expect(scripts).toHaveBeenCalledWith();
});

it('should pack the entries', async () => {
const serverless = mockServerlessConfig();
const plugin = new RspackServerlessPlugin(
serverless,
{ ...mockOptions, function: 'myFunc' },
logger
);

plugin.functionEntries = {
myFunc: { import: 'test/path/entry.ts', filename: 'test/path/entry.ts' },
};

await plugin.hooks['before:deploy:function:packageFunction']();

expect(pack).toHaveBeenCalledTimes(1);
});

it('should bundle before scripts', async () => {
const serverless = mockServerlessConfig();
const plugin = new RspackServerlessPlugin(
serverless,
{ ...mockOptions, function: 'myFunc' },
logger
);

plugin.functionEntries = {
myFunc: { import: 'test/path/entry.ts', filename: 'test/path/entry.ts' },
};

await plugin.hooks['before:deploy:function:packageFunction']();

expect(jest.mocked(bundle).mock.invocationCallOrder[0]).toBeLessThan(
jest.mocked(scripts).mock.invocationCallOrder[0]
);
});

it('should pack after scripts', async () => {
const serverless = mockServerlessConfig();
const plugin = new RspackServerlessPlugin(
serverless,
{ ...mockOptions, function: 'myFunc' },
logger
);

plugin.functionEntries = {
myFunc: { import: 'test/path/entry.ts', filename: 'test/path/entry.ts' },
};

await plugin.hooks['before:deploy:function:packageFunction']();

expect(jest.mocked(scripts).mock.invocationCallOrder[0]).toBeLessThan(
jest.mocked(pack).mock.invocationCallOrder[0]
);
});
});
2 changes: 2 additions & 0 deletions libs/serverless-rspack/src/test/serverless-rspack.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ describe('RspackServerlessPlugin', () => {
'initialize',
'before:package:createDeploymentArtifacts',
'after:package:createDeploymentArtifacts',
'before:deploy:function:packageFunction',
'after:deploy:function:packageFunction',
'before:invoke:local:invoke',
]);
});
Expand Down

0 comments on commit 20fe405

Please sign in to comment.