├── .codeclimate.yml
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github
├── ISSUE_TEMPLATE
│ └── bug_report.md
└── workflows
│ ├── build.yml
│ └── stale.yml
├── .gitignore
├── .istanbul.yml
├── LICENSE
├── README.md
├── index.js
├── package-lock.json
├── package.json
├── renovate.json
└── test
├── .eslintrc
├── index.test.js
└── scripts
├── .eslintrc
├── check-is-support-colors.js
└── test-with-color.js
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 |
3 | checks:
4 | method-lines:
5 | enabled: true
6 | config:
7 | threshold: 50
8 |
9 | engines:
10 | duplication:
11 | enabled: true
12 | config:
13 | languages:
14 | javascript:
15 | mass_threshold: 50
16 |
17 | eslint:
18 | enabled: true
19 | channel: "eslint-5"
20 | checks:
21 | import/no-unresolved:
22 | enabled: false
23 | fixme:
24 | enabled: true
25 | ratings:
26 | paths:
27 | - "**.js"
28 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org/
2 | root = true
3 |
4 | [*.js]
5 | charset = utf-8
6 | end_of_line = lf
7 | insert_final_newline = true
8 | indent_style = space
9 | indent_size = 2
10 | trim_trailing_whitespace = true
11 | curly_bracket_next_line = false
12 | max_line_length = 120
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/*{.,-}min.js
2 | coverage
3 | tmp*
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | ---
2 | root: true
3 |
4 | extends: airbnb-base
5 |
6 | parserOptions:
7 | ecmaVersion: 2019
8 |
9 | rules:
10 | class-methods-use-this: off
11 | no-console: off
12 | strict: off
13 | max-len: [error, {code: 120, tabWidth: 2, ignoreUrls: true}]
14 | indent: [error, 2, {"FunctionDeclaration": {"parameters": "first"}}]
15 | no-underscore-dangle: off
16 | arrow-parens: [error, "as-needed"]
17 | max-classes-per-file: [error, 2 ]
18 |
19 | env:
20 | es6: true
21 | node: true
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior, please include all the related info to this issue, include:
15 | 1. OS, [e.g. MacOSX ]
16 | 2. Serverless version, [e.g. 1.67.3 ]
17 | 2. Related settings in serverless.yml
18 | 3. Hook script
19 | 4. Command output
20 | 5. Command exit code if related (execute echo $? immediately after the command)
21 |
22 | **Expected behavior**
23 | A clear and concise description of what you expected to happen.
24 |
25 | **Additional context**
26 | Add any other context about the problem here.
27 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches: [ master, '*' ]
6 | pull_request:
7 | branches: [ master, '*' ]
8 |
9 | env:
10 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
11 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
12 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
13 |
14 | jobs:
15 | Test:
16 | name: Unit Test
17 | runs-on: ubuntu-latest
18 | strategy:
19 | matrix:
20 | node-version: [18]
21 |
22 | steps:
23 | - uses: actions/checkout@v4
24 | - name: Use Node.js ${{ matrix.node-version }}
25 | uses: actions/setup-node@v4
26 | with:
27 | node-version: ${{ matrix.node-version }}
28 | - run: npm ci
29 | - run: curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
30 | - run: chmod +x ./cc-test-reporter && ./cc-test-reporter before-build
31 | - run: npm test
32 | env:
33 | CI: Github_Action
34 | TRAVIS: 1
35 | - run: ./cc-test-reporter after-build -t lcov || true
36 |
37 | Release:
38 | name: Publish to npm
39 | runs-on: ubuntu-latest
40 | needs: Test
41 | if: github.ref == 'refs/heads/master'
42 | strategy:
43 | matrix:
44 | node-version: [20]
45 |
46 | steps:
47 | - uses: actions/checkout@v4
48 | - name: Use Node.js ${{ matrix.node-version }}
49 | uses: actions/setup-node@v4
50 | with:
51 | node-version: ${{ matrix.node-version }}
52 | - run: npm ci
53 | - run: npm run semantic-release
54 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: Stale
2 |
3 | # yamllint disable-line rule:truthy
4 | on:
5 | schedule:
6 | - cron: "0 8 * * *"
7 | workflow_dispatch:
8 |
9 | jobs:
10 | stale:
11 | runs-on: ubuntu-latest
12 | steps:
13 | # The 90 day stale policy
14 | # Used for:
15 | # - Issues & PRs
16 | # - No PRs marked as no-stale
17 | # - No issues marked as no-stale or help-wanted
18 | - name: 90 days stale issues & PRs policy
19 | uses: actions/stale@v9.0.0
20 | with:
21 | repo-token: ${{ secrets.GITHUB_TOKEN }}
22 | days-before-stale: 90
23 | days-before-close: 20
24 | operations-per-run: 100
25 | remove-stale-when-updated: true
26 | stale-issue-label: "stale"
27 | exempt-issue-labels: "no-stale,help-wanted"
28 | stale-issue-message: >
29 | This issue has now been marked as stale because there hasn't been any activity on this issue recently.
30 | It will be closed if no further activity occurs. Thank you for your contributions.
31 |
32 | stale-pr-label: "stale"
33 | exempt-pr-labels: "no-stale"
34 | stale-pr-message: >
35 | There hasn't been any activity on this pull request recently. This
36 | pull request has been automatically marked as stale because of that
37 | and will be closed if no further activity occurs within 20 days.
38 |
39 | Thank you for your contributions.
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
3 | coverage
4 | .nyc_output
5 |
--------------------------------------------------------------------------------
/.istanbul.yml:
--------------------------------------------------------------------------------
1 | instrumentation:
2 | excludes: ['example/**', 'tmp-*']
3 | include-all-sources: true
4 | reporting:
5 | reports:
6 | - lcov
7 | - text
8 | report-config:
9 | text:
10 | file: test-coverage.txt
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Wei Xu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://badge.fury.io/js/serverless-scriptable-plugin)
2 | 
3 | [](https://github.com/weixu365/serverless-scriptable-plugin/actions)
4 | [](https://codeclimate.com/github/weixu365/serverless-scriptable-plugin/coverage)
5 | [](https://codeclimate.com/github/weixu365/serverless-scriptable-plugin)
6 | [](https://codeclimate.com/github/weixu365/serverless-scriptable-plugin)
7 | [](https://www.npmjs.com/package/serverless-scriptable-plugin)
8 |
9 | ## What's the plugins for?
10 | This plugin allows you to write scripts to customize Serverless behavior for Serverless 1.x and upper
11 |
12 | It also supports running node.js scripts in any build stage.
13 |
14 | Features:
15 | - Run any command or nodejs scripts in any stage of serverless lifecycle
16 | - Add custom commands to serverless, e.g. `npx serverless YOUR-COMMAND` [Example](#add-a-custom-command)
17 |
18 | ## Quick Start
19 | 1. Install
20 | ```bash
21 | npm install --save-dev serverless-scriptable-plugin
22 | ```
23 | 2. Add to Serverless config
24 | ```yaml
25 | plugins:
26 | - serverless-scriptable-plugin
27 |
28 | custom:
29 | scriptable:
30 | # add custom hooks
31 | hooks:
32 | before:package:createDeploymentArtifacts: npm run build
33 |
34 | # or custom commands
35 | commands:
36 | migrate: echo Running migration
37 | ```
38 |
39 | ## Upgrade from <=1.1.0
40 |
41 | This `serverless-scriptable-plugin` now supports event hooks and custom commands. Here's an example of upgrade to the latest schema. The previous config schema still works for backward compatibility.
42 |
43 | Example that using the previous schema:
44 |
45 | ```yaml
46 | plugins:
47 | - serverless-scriptable-plugin
48 |
49 | custom:
50 | scriptHooks:
51 | before:package:createDeploymentArtifacts: npm run build
52 | ```
53 |
54 | Changed to:
55 | ```yaml
56 | plugins:
57 | - serverless-scriptable-plugin
58 |
59 | custom:
60 | scriptable:
61 | hooks:
62 | before:package:createDeploymentArtifacts: npm run build
63 | ```
64 |
65 | ## Examples
66 | 1. ### Customize package behavior
67 |
68 | The following config is using babel for transcompilation and packaging only the required folders: dist and node_modules without aws-sdk
69 |
70 | ```yml
71 | plugins:
72 | - serverless-scriptable-plugin
73 |
74 | custom:
75 | scriptable:
76 | hooks:
77 | before:package:createDeploymentArtifacts: npm run build
78 |
79 | package:
80 | exclude:
81 | - '**/**'
82 | - '!dist/**'
83 | - '!node_modules/**'
84 | - node_modules/aws-sdk/**
85 | ```
86 |
87 | 1. ### Add a custom command
88 | ```yaml
89 | plugins:
90 | - serverless-scriptable-plugin
91 |
92 | custom:
93 | scriptable:
94 | hooks:
95 | before:migrate:command: echo before migrating
96 | after:migrate:command: echo after migrating
97 | commands:
98 | migrate: echo Running migration
99 | ```
100 |
101 | Then you could run this command by:
102 | ```bash
103 | $ npx serverless migrate
104 | Running command: echo before migrating
105 | before migrating
106 | Running command: echo Running migrating
107 | Running migrating
108 | Running command: echo after migrating
109 | after migrating
110 | ```
111 |
112 | 1. ### Deploy python function
113 | ```yml
114 | plugins:
115 | - serverless-scriptable-plugin
116 |
117 | custom:
118 | scriptable:
119 | hooks:
120 | before:package:createDeploymentArtifacts: ./package.sh
121 |
122 | # serverless will use the specified package that generated by `./package.sh`
123 | package:
124 | artifact: .serverless/package.zip
125 | ```
126 |
127 | and package.sh script file to package the zip file (https://docs.aws.amazon.com/lambda/latest/dg/python-package.html)
128 |
129 | ```bash
130 | PACKAGE_FILE=.serverless/package.zip
131 | rm -f $PACKAGE_FILE && rm -rf output && mkdir -p output
132 | pip install -r requirements.txt --target output/libs
133 | # You can use the following command to install if you are using pipenv
134 | # pipenv requirements > output/requirements.txt && pip install -r output/requirements.txt --target output/libs
135 | (cd output/libs && zip -r ../../$PACKAGE_FILE . -x '*__pycache__*')
136 | (zip -r $PACKAGE_FILE your-src-folder -x '*__pycache__*')
137 | ```
138 |
139 | Serverless would then deploy the zip file you built to aws lambda.
140 |
141 | 1. ### Run any command as a hook script
142 |
143 | It's possible to run any command as the hook script, e.g. use the following command to zip the required folders
144 |
145 | ```yml
146 | plugins:
147 | - serverless-scriptable-plugin
148 |
149 | custom:
150 | scriptable:
151 | hooks:
152 | before:package:createDeploymentArtifacts: zip -q -r .serverless/package.zip src node_modules
153 |
154 | service: service-name
155 | package:
156 | artifact: .serverless/package.zip
157 | ```
158 |
159 | 1. ### Dynamically change resources
160 |
161 | Create CloudWatch Log subscription filter for all Lambda function Log groups, e.g. subscribe to a Kinesis stream
162 |
163 | ```yml
164 | plugins:
165 | - serverless-scriptable-plugin
166 |
167 | custom:
168 | scriptable:
169 | hooks:
170 | after:package:compileEvents: build/serverless/add-log-subscriptions.js
171 |
172 | provider:
173 | logSubscriptionDestinationArn: 'arn:aws:logs:ap-southeast-2:{account-id}:destination:'
174 | ```
175 |
176 | and in build/serverless/add-log-subscriptions.js file:
177 |
178 | ```js
179 | const resources = serverless.service.provider.compiledCloudFormationTemplate.Resources;
180 | const logSubscriptionDestinationArn = serverless.service.provider.logSubscriptionDestinationArn;
181 |
182 | Object.keys(resources)
183 | .filter(name => resources[name].Type === 'AWS::Logs::LogGroup')
184 | .forEach(logGroupName => resources[`${logGroupName}Subscription`] = {
185 | Type: "AWS::Logs::SubscriptionFilter",
186 | Properties: {
187 | DestinationArn: logSubscriptionDestinationArn,
188 | FilterPattern: ".",
189 | LogGroupName: { "Ref": logGroupName }
190 | }
191 | }
192 | );
193 | ```
194 |
195 | 1. ### Run multiple commands
196 |
197 | It's possible to run multiple commands for the same serverless event, e.g. Add CloudWatch log subscription and dynamodb auto scaling support
198 |
199 | ```yml
200 | plugins:
201 | - serverless-scriptable-plugin
202 |
203 | custom:
204 | scriptable:
205 | hooks:
206 | after:package:createDeploymentArtifacts:
207 | - build/serverless/add-log-subscriptions.js
208 | - build/serverless/add-dynamodb-auto-scaling.js
209 |
210 | service: service-name
211 | package:
212 | artifact: .serverless/package.zip
213 | ```
214 |
215 |
216 | 1. ### Suppress console output
217 | You could control what to show during running commands, in case there are sensitive info in command or console output.
218 |
219 | ```yml
220 | custom:
221 | scriptable:
222 | showStdoutOutput: false # Default true. true: output stderr to console, false: output nothing
223 | showStderrOutput: false # Default true. true: output stderr to console, false: output nothing
224 | showCommands: false # Default true. true: show the command before execute, false: do not show commands
225 |
226 | hooks:
227 | ...
228 | commands:
229 | ...
230 | ```
231 |
232 | ## Hooks
233 | The serverless lifecycle hooks are different to providers, here's a reference of AWS hooks:
234 | https://gist.github.com/HyperBrain/50d38027a8f57778d5b0f135d80ea406#file-lifecycle-cheat-sheet-md
235 |
236 | ## Change Log
237 | - Version 0.8.0 and above
238 | - Check details at https://github.com/weixu365/serverless-scriptable-plugin/releases
239 |
240 | - Version 0.7.1
241 | - [Feature] Fix vulnerability warning by remove unnecessary dev dependencies
242 | - Version 0.7.0
243 | - [Feature] Return promise object to let serverless to wait until script is finished
244 | - Version 0.6.0
245 | - [Feature] Supported execute multiple script/command for the same serverless event
246 | - Version 0.5.0
247 | - [Feature] Supported serverless variables in script/command
248 | - [Improvement] Integrated with codeclimate for code analysis and test coverage
249 | - Version 0.4.0
250 | - [Feature] Supported colored output in script/command
251 | - [Improvement] Integrated with travis for CI
252 | - Version 0.3.0
253 | - [Feature] Supported to execute any command for serverless event
254 | - Version 0.2.0
255 | - [Feature] Supported to execute javascript file for serverless event
256 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const vm = require('vm');
4 | const fs = require('fs');
5 | const Module = require('module');
6 | const path = require('path');
7 | const Bluebird = require('bluebird');
8 | const { execSync } = require('child_process');
9 |
10 | // Error without stack trace
11 | class SimpleError extends Error {
12 | constructor(msg) {
13 | super(msg);
14 | this.stack = null;
15 | }
16 | }
17 |
18 | class Scriptable {
19 | constructor(serverless, options) {
20 | this.serverless = serverless;
21 | this.options = options;
22 | this.hooks = {};
23 | this.commands = {};
24 |
25 | this.stdin = process.stdin;
26 | this.stdout = process.stdout;
27 | this.stderr = process.stderr;
28 | this.showCommands = true;
29 |
30 | const scriptable = this.getMergedConfig();
31 |
32 | if (this.isFalse(scriptable.showCommands)) {
33 | this.showCommands = false;
34 | }
35 |
36 | if (this.isFalse(scriptable.showStdoutOutput)) {
37 | console.log('Not showing command output because showStdoutOutput is false');
38 | this.stdout = 'ignore';
39 | }
40 |
41 | if (this.isFalse(scriptable.showStderrOutput)) {
42 | console.log('Not showing command error output because showStderrOutput is false');
43 | this.stderr = 'ignore';
44 | }
45 |
46 | this.setupHooks(scriptable.hooks);
47 | this.setupCustomCommands(scriptable.commands);
48 | }
49 |
50 | getMergedConfig() {
51 | const legacyScriptHooks = this.getScripts('scriptHooks') || {};
52 | const scriptable = this.getScripts('scriptable') || {};
53 |
54 | const hooks = { ...legacyScriptHooks, ...scriptable.hooks };
55 | delete hooks.showCommands;
56 | delete hooks.showStdoutOutput;
57 | delete hooks.showStderrOutput;
58 |
59 | return {
60 | showCommands: this.first(scriptable.showCommands, legacyScriptHooks.showCommands),
61 | showStdoutOutput: this.first(scriptable.showStdoutOutput, legacyScriptHooks.showStdoutOutput),
62 | showStderrOutput: this.first(scriptable.showStderrOutput, legacyScriptHooks.showStderrOutput),
63 | hooks,
64 | commands: scriptable.commands || {},
65 | };
66 | }
67 |
68 | setupHooks(hooks) {
69 | // Hooks are run at serverless lifecycle events.
70 | Object.keys(hooks).forEach(event => {
71 | this.hooks[event] = this.runScript(hooks[event]);
72 | }, this);
73 | }
74 |
75 | setupCustomCommands(commands) {
76 | // Custom Serverless commands would run by `npx serverless `
77 | Object.keys(commands).forEach(name => {
78 | this.hooks[`${name}:command`] = this.runScript(commands[name]);
79 |
80 | this.commands[name] = {
81 | usage: `Run ${commands[name]}`,
82 | lifecycleEvents: ['command'],
83 | };
84 | }, this);
85 | }
86 |
87 | isFalse(val) {
88 | return val != null && !val;
89 | }
90 |
91 | first(...vals) {
92 | return vals.find(val => typeof val !== 'undefined');
93 | }
94 |
95 | getScripts(namespace) {
96 | const { custom } = this.serverless.service;
97 | return custom && custom[namespace];
98 | }
99 |
100 | runScript(eventScript) {
101 | return () => {
102 | const scripts = Array.isArray(eventScript) ? eventScript : [eventScript];
103 |
104 | return Bluebird.each(scripts, script => {
105 | if (fs.existsSync(script) && path.extname(script) === '.js') {
106 | return this.runJavascriptFile(script);
107 | }
108 |
109 | return this.runCommand(script);
110 | });
111 | };
112 | }
113 |
114 | runCommand(script) {
115 | if (this.showCommands) {
116 | console.log(`Running command: ${script}`);
117 | }
118 |
119 | try {
120 | return execSync(script, { stdio: [this.stdin, this.stdout, this.stderr] });
121 | } catch (err) {
122 | throw new SimpleError(`Failed to run command: ${script}`);
123 | }
124 | }
125 |
126 | runJavascriptFile(scriptFile) {
127 | if (this.showCommands) {
128 | console.log(`Running javascript file: ${scriptFile}`);
129 | }
130 |
131 | const buildModule = () => {
132 | const m = new Module(scriptFile, module.parent);
133 | m.exports = exports;
134 | m.filename = scriptFile;
135 | m.paths = Module._nodeModulePaths(path.dirname(scriptFile)).concat(module.paths);
136 |
137 | return m;
138 | };
139 |
140 | const globalProperties = Object.fromEntries(
141 | Object.getOwnPropertyNames(global).map(
142 | key => [key, global[key]],
143 | ),
144 | );
145 | delete globalProperties.globalThis;
146 | delete globalProperties.global;
147 |
148 | const sandbox = {
149 | ...globalProperties,
150 | module: buildModule(),
151 | require: id => sandbox.module.require(id),
152 | serverless: this.serverless,
153 | options: this.options,
154 | __filename: scriptFile,
155 | __dirname: path.dirname(fs.realpathSync(scriptFile)),
156 | exports: Object(),
157 | };
158 |
159 | // See: https://github.com/nodejs/node/blob/7c452845b8d44287f5db96a7f19e7d395e1899ab/lib/internal/modules/cjs/helpers.js#L14
160 | sandbox.require.resolve = req => Module._resolveFilename(req, sandbox.module);
161 |
162 | const scriptCode = fs.readFileSync(scriptFile);
163 | const script = vm.createScript(scriptCode, scriptFile);
164 | const context = vm.createContext(sandbox);
165 |
166 | return script.runInContext(context);
167 | }
168 | }
169 |
170 | module.exports = Scriptable;
171 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "serverless-scriptable-plugin",
3 | "version": "0.0.0-development",
4 | "description": "Add scripts(nodejs) support to serverless 1.x",
5 | "main": "index.js",
6 | "scripts": {
7 | "check-style": "eslint -f stylish .",
8 | "test": "nyc --reporter=lcov mocha && nyc check-coverage --statements 100 --functions 100 --branches 100 --lines 100",
9 | "semantic-release": "semantic-release"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/weixu365/serverless-scriptable-plugin.git"
14 | },
15 | "keywords": [
16 | "serverless",
17 | "script",
18 | "shell",
19 | "command",
20 | "cli",
21 | "javascript",
22 | "nodejs",
23 | "hook",
24 | "customize",
25 | "plugin"
26 | ],
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/weixu365/serverless-scriptable-plugin/issues"
30 | },
31 | "homepage": "https://github.com/weixu365/serverless-scriptable-plugin#readme",
32 | "devDependencies": {
33 | "chai": "4.4.1",
34 | "color-support": "1.1.3",
35 | "commitizen": "4.3.0",
36 | "cz-conventional-changelog": "3.3.0",
37 | "eslint": "8.56.0",
38 | "eslint-config-airbnb-base": "15.0.0",
39 | "eslint-plugin-import": "2.29.1",
40 | "mocha": "10.3.0",
41 | "nyc": "15.1.0",
42 | "semantic-release": "23.0.2",
43 | "tmp": "0.2.1"
44 | },
45 | "dependencies": {
46 | "bluebird": "3.7.2"
47 | },
48 | "overrides": {
49 | "minimist": "1.2.8"
50 | },
51 | "config": {
52 | "commitizen": {
53 | "path": "./node_modules/cz-conventional-changelog"
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base",
4 | "group:all"
5 | ],
6 | "recreateClosed": true,
7 | "packageRules": [
8 | {
9 | "matchDepTypes": ["devDependencies"],
10 | "automerge": true,
11 | "automergeType": "pr",
12 | "platformAutomerge": true
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | ---
2 | rules:
3 | no-use-before-define: off
4 | no-template-curly-in-string: off
5 | import/no-extraneous-dependencies: [error, { devDependencies: true }]
6 |
7 | env:
8 | mocha: true
--------------------------------------------------------------------------------
/test/index.test.js:
--------------------------------------------------------------------------------
1 | const chai = require('chai');
2 | const fs = require('fs');
3 | const path = require('path');
4 | const tmp = require('tmp');
5 | const Bluebird = require('bluebird');
6 | const Scriptable = require('../index');
7 |
8 | const { expect } = chai;
9 | chai.config.truncateThreshold = 0;
10 |
11 | describe('ScriptablePluginTest', () => {
12 | it('should run command', () => {
13 | const randomString = `current time ${new Date().getTime()}`;
14 | const scriptable = new Scriptable(serviceWithScripts({ test: `echo ${randomString}` }));
15 |
16 | scriptable.stdout = tmp.fileSync({ prefix: 'stdout-' });
17 | scriptable.stderr = tmp.fileSync({ prefix: 'stderr-' });
18 |
19 | return runScript(scriptable, 'test')
20 | .then(() => {
21 | console.log('checking file', scriptable.stdout.name);
22 | expect(fs.readFileSync(scriptable.stdout.name, { encoding: 'utf-8' })).string(randomString);
23 | expect(fs.readFileSync(scriptable.stderr.name, { encoding: 'utf-8' })).equal('');
24 | });
25 | });
26 |
27 | it('should able to run multiple commands', () => {
28 | const randomString = `current time ${new Date().getTime()}`;
29 | const randomString2 = `current time 2 ${new Date().getTime()}`;
30 | const scriptable = new Scriptable(serviceWithScripts({
31 | test: [
32 | `echo ${randomString}`,
33 | `echo ${randomString2}`,
34 | ],
35 | }));
36 |
37 | scriptable.stdout = tmp.fileSync({ prefix: 'stdout-' });
38 | scriptable.stderr = tmp.fileSync({ prefix: 'stderr-' });
39 |
40 | return runScript(scriptable, 'test')
41 | .then(() => {
42 | const consoleOutput = fs.readFileSync(scriptable.stdout.name, { encoding: 'utf-8' });
43 | expect(consoleOutput).string(`${randomString}\n${randomString2}`);
44 | expect(fs.readFileSync(scriptable.stderr.name, { encoding: 'utf-8' })).equal('');
45 | });
46 | });
47 |
48 | it('should able to suppress outputs', () => {
49 | const scriptable = new Scriptable(serviceWithScripts({
50 | test: [
51 | 'bash -c "echo this should not be visible on console"',
52 | ],
53 | showCommands: false,
54 | showStdoutOutput: false,
55 | showStderrOutput: false,
56 | }));
57 |
58 | return runScript(scriptable, 'test')
59 | .then(() => {
60 | console.log('Should has no any output');
61 | });
62 | });
63 |
64 | it('Manual check: should able to print all outputs', () => {
65 | const randomString = `current time ${new Date().getTime()}`;
66 | const randomString2 = `current time 2 ${new Date().getTime()}`;
67 | const scriptable = new Scriptable(serviceWithScripts({
68 | test: [
69 | `echo ${randomString}`,
70 | `echo ${randomString2}`,
71 | ],
72 | }));
73 |
74 | return runScript(scriptable, 'test')
75 | .then(() => {
76 | console.log('Done');
77 | });
78 | });
79 |
80 | it('should support color in child process', () => {
81 | const serverless = serviceWithScripts({ test: 'test/scripts/check-is-support-colors.js' });
82 | const scriptable = new Scriptable(serverless);
83 |
84 | process.env.CI = 'Github_Action';
85 | process.env.TRAVIS = 1;
86 | return runScript(scriptable, 'test')
87 | .then(() => expect(serverless.supportColorLevel).greaterThan(0));
88 | });
89 |
90 | it('should print error message when failed to run command', () => {
91 | const scriptable = new Scriptable(serviceWithScripts({ test: 'not-exists' }));
92 | scriptable.stdout = tmp.fileSync({ prefix: 'stdout-' });
93 | scriptable.stderr = tmp.fileSync({ prefix: 'stderr-' });
94 |
95 | return runScript(scriptable, 'test')
96 | .then(() => expect(false).equal(true, 'Should throw exception when command not exists'))
97 | .catch(() => {
98 | expect(fs.readFileSync(scriptable.stderr.name, { encoding: 'utf-8' })).string('/bin/sh');
99 | expect(fs.readFileSync(scriptable.stderr.name, { encoding: 'utf-8' })).string('not-exists:');
100 | expect(fs.readFileSync(scriptable.stderr.name, { encoding: 'utf-8' })).string('not found');
101 | expect(fs.readFileSync(scriptable.stdout.name, { encoding: 'utf-8' })).equal('');
102 | });
103 | });
104 |
105 | it('should run javascript', () => {
106 | const scriptFile = tmp.fileSync({ postfix: '.js' });
107 | fs.writeFileSync(scriptFile.name, 'serverless.service.artifact = "test.zip";');
108 |
109 | const serverless = serviceWithScripts({ test: scriptFile.name });
110 | const scriptable = new Scriptable(serverless);
111 |
112 | return runScript(scriptable, 'test')
113 | .then(() => expect(serverless.service.artifact).equal('test.zip'));
114 | });
115 |
116 | it('should run javascript in quiet mode', () => {
117 | const scriptFile = tmp.fileSync({ postfix: '.js' });
118 | fs.writeFileSync(scriptFile.name, 'serverless.service.artifact = "test.zip";');
119 |
120 | const serverless = serviceWithScripts({
121 | test: scriptFile.name,
122 | showCommands: false,
123 | showStdoutOutput: false,
124 | showStderrOutput: false,
125 | });
126 | const scriptable = new Scriptable(serverless);
127 |
128 | return runScript(scriptable, 'test')
129 | .then(() => expect(serverless.service.artifact).equal('test.zip'));
130 | });
131 |
132 | it('should able to import modules in javascript', () => {
133 | const scriptModuleFile = tmp.fileSync({ postfix: '.js' });
134 | const moduleName = path.basename(scriptModuleFile.name);
135 | fs.writeFileSync(scriptModuleFile.name, 'module.exports = { test: () => "hello" }');
136 |
137 | const scriptFile = tmp.fileSync({ postfix: '.js' });
138 | fs.writeFileSync(scriptFile.name, `
139 | const m = require('./${moduleName}');
140 | const path = require('path');
141 | const modulePath = require.resolve('./${moduleName}');
142 | serverless.service.artifact = m.test() + path.basename(modulePath);
143 | `);
144 |
145 | const serverless = serviceWithScripts({ test: scriptFile.name });
146 | const scriptable = new Scriptable(serverless);
147 |
148 | return runScript(scriptable, 'test')
149 | .then(() => expect(serverless.service.artifact).equal(`hello${moduleName}`));
150 | });
151 |
152 | it('should able to use default classes from node in javascript', () => {
153 | const scriptFile = tmp.fileSync({ postfix: '.js' });
154 | fs.writeFileSync(scriptFile.name, 'new URL("http://localhost"); serverless.service.artifact = "test.zip";');
155 |
156 | const serverless = serviceWithScripts({ test: scriptFile.name });
157 | const scriptable = new Scriptable(serverless);
158 |
159 | return runScript(scriptable, 'test')
160 | .then(() => expect(serverless.service.artifact).equal('test.zip'));
161 | });
162 |
163 | it('should able to use exports object in javascript', () => {
164 | const scriptFile = tmp.fileSync({ postfix: '.js' });
165 | fs.writeFileSync(
166 | scriptFile.name,
167 | `Object.defineProperty(exports, "__esModule", { value: true });
168 | serverless.service.artifact = "test.zip";`,
169 | );
170 |
171 | const serverless = serviceWithScripts({ test: scriptFile.name });
172 | const scriptable = new Scriptable(serverless);
173 |
174 | return runScript(scriptable, 'test')
175 | .then(() => expect(serverless.service.artifact).equal('test.zip'));
176 | });
177 |
178 | it('should wait for async method to be finished', () => {
179 | const scriptFile = tmp.fileSync({ postfix: '.js' });
180 | const script = 'require("bluebird").delay(100).then(() => serverless.service.artifact = "test.zip")';
181 | fs.writeFileSync(scriptFile.name, script);
182 |
183 | const serverless = serviceWithScripts({ test: scriptFile.name });
184 | const scriptable = new Scriptable(serverless);
185 |
186 | return Bluebird.resolve(runScript(scriptable, 'test'))
187 | .then(() => expect(serverless.service.artifact).equal('test.zip'));
188 | });
189 |
190 | it('should run multiple javascript files', () => {
191 | const scriptFile = tmp.fileSync({ postfix: '.js' });
192 | fs.writeFileSync(scriptFile.name, 'serverless.service.artifact = "test.zip";');
193 |
194 | const scriptFile2 = tmp.fileSync({ postfix: '.js' });
195 | fs.writeFileSync(scriptFile2.name, 'serverless.service.provider = "AWS";');
196 |
197 | const serverless = serviceWithScripts({ test: [scriptFile.name, scriptFile2.name] });
198 | const scriptable = new Scriptable(serverless);
199 |
200 | return runScript(scriptable, 'test')
201 | .then(() => {
202 | expect(serverless.service.artifact).equal('test.zip');
203 | expect(serverless.service.provider).equal('AWS');
204 | });
205 | });
206 |
207 | it('should run any executable file', () => {
208 | const randomString = `current time ${new Date().getTime()}`;
209 |
210 | const scriptFile = tmp.fileSync({ postfix: '.sh' });
211 | fs.chmodSync(scriptFile.name, '755');
212 | fs.writeFileSync(scriptFile.name, `echo ${randomString}`);
213 | fs.closeSync(scriptFile.fd);
214 |
215 | const serverless = serviceWithScripts({ test: scriptFile.name });
216 | const scriptable = new Scriptable(serverless);
217 |
218 | scriptable.stdout = tmp.fileSync({ prefix: 'stdout-' });
219 | scriptable.stderr = tmp.fileSync({ prefix: 'stderr-' });
220 |
221 | return runScript(scriptable, 'test')
222 | .then(() => {
223 | expect(fs.readFileSync(scriptable.stdout.name, { encoding: 'utf-8' })).string(randomString);
224 | expect(fs.readFileSync(scriptable.stderr.name, { encoding: 'utf-8' })).equal('');
225 | })
226 | .catch(() => {
227 | const stdout = fs.readFileSync(scriptable.stdout.name, { encoding: 'utf-8' });
228 | const stderr = fs.readFileSync(scriptable.stderr.name, { encoding: 'utf-8' });
229 |
230 | expect(true).equals(false, `stdout: ${stdout}\n stderr: ${stderr}`);
231 | });
232 | });
233 |
234 | it('should suppress stack track when failed to run executable file', () => {
235 | const serverless = serviceWithScripts({ test: 'non-exist-file' });
236 | const scriptable = new Scriptable(serverless);
237 |
238 | scriptable.stdout = tmp.fileSync({ prefix: 'stdout-' });
239 | scriptable.stderr = tmp.fileSync({ prefix: 'stderr-' });
240 |
241 | return runScript(scriptable, 'test')
242 | .then(() => { throw new Error('Should throw exception'); })
243 | .catch(err => {
244 | chai.expect(err.stack).equals(null);
245 | expect(err.message).equals('Failed to run command: non-exist-file');
246 | });
247 | });
248 |
249 | it('should support serverless variables when run javascript', () => {
250 | const scriptFile = tmp.fileSync({ postfix: '.js' });
251 | fs.writeFileSync(scriptFile.name, 'serverless.service.artifact = "test.zip";');
252 |
253 | const serverless = serviceWithCustom({
254 | scriptName: scriptFile.name,
255 | scriptHooks: { test: scriptFile.name },
256 | });
257 |
258 | const scriptable = new Scriptable(serverless);
259 |
260 | return runScript(scriptable, 'test')
261 | .then(() => expect(serverless.service.artifact).equal('test.zip'));
262 | });
263 |
264 | it('should able to load scriptHooks', () => {
265 | const scriptFile = tmp.fileSync({ postfix: '.js' });
266 | fs.writeFileSync(scriptFile.name, 'serverless.service.artifact = "test.zip";');
267 |
268 | const serverless = serviceWithCustom({
269 | scriptName: scriptFile.name,
270 | scriptHooks: {
271 | showCommands: false,
272 | showStdoutOutput: false,
273 | showStderrOutput: false,
274 | test: 'echo legacy-script',
275 | },
276 | });
277 |
278 | const scriptable = new Scriptable(serverless);
279 |
280 | expect(scriptable.showCommands).equal(false);
281 | expect(scriptable.stdout).equal('ignore');
282 | expect(scriptable.stderr).equal('ignore');
283 |
284 | scriptable.stdout = tmp.fileSync({ prefix: 'stdout-' });
285 | scriptable.stderr = tmp.fileSync({ prefix: 'stderr-' });
286 |
287 | return runScript(scriptable, 'test')
288 | .then(() => {
289 | expect(fs.readFileSync(scriptable.stdout.name, { encoding: 'utf-8' })).string('legacy-script');
290 | expect(fs.readFileSync(scriptable.stderr.name, { encoding: 'utf-8' })).equal('');
291 | });
292 | });
293 |
294 | it('should able to load scriptable configs', () => {
295 | const scriptFile = tmp.fileSync({ postfix: '.js' });
296 | fs.writeFileSync(scriptFile.name, 'serverless.service.artifact = "test.zip";');
297 |
298 | const serverless = serviceWithCustom({
299 | scriptName: scriptFile.name,
300 | scriptable: {
301 | showCommands: true,
302 | showStdoutOutput: true,
303 | showStderrOutput: true,
304 | hooks: {
305 | test: 'echo scriptable-script',
306 | },
307 | commands: {
308 | customCommand: 'echo custom command',
309 | },
310 | },
311 | });
312 |
313 | const scriptable = new Scriptable(serverless);
314 |
315 | expect(scriptable.showCommands).equal(true);
316 | expect(scriptable.stdout).equal(process.stdout);
317 | expect(scriptable.stderr).equal(process.stderr);
318 |
319 | scriptable.stdout = tmp.fileSync({ prefix: 'stdout-' });
320 | scriptable.stderr = tmp.fileSync({ prefix: 'stderr-' });
321 |
322 | return runScript(scriptable, 'test')
323 | .then(() => {
324 | expect(fs.readFileSync(scriptable.stdout.name, { encoding: 'utf-8' })).string('scriptable-script');
325 | expect(fs.readFileSync(scriptable.stderr.name, { encoding: 'utf-8' })).equal('');
326 | });
327 | });
328 |
329 | it('should support scriptHooks and scriptable configs', () => {
330 | const scriptFile = tmp.fileSync({ postfix: '.js' });
331 | fs.writeFileSync(scriptFile.name, 'serverless.service.artifact = "test.zip";');
332 |
333 | const serverless = serviceWithCustom({
334 | scriptName: scriptFile.name,
335 | scriptHooks: {
336 | showCommands: false,
337 | showStdoutOutput: false,
338 | showStderrOutput: false,
339 | test: 'echo legacy-script',
340 | },
341 | scriptable: {
342 | showCommands: true,
343 | showStdoutOutput: true,
344 | showStderrOutput: true,
345 | hooks: {
346 | test: 'echo scriptable-script',
347 | },
348 | commands: {
349 | customCommand: 'echo custom command',
350 | },
351 | },
352 | });
353 |
354 | const scriptable = new Scriptable(serverless);
355 |
356 | expect(scriptable.showCommands).equal(true);
357 | expect(scriptable.stdout).equal(process.stdout);
358 | expect(scriptable.stderr).equal(process.stderr);
359 |
360 | scriptable.stdout = tmp.fileSync({ prefix: 'stdout-' });
361 | scriptable.stderr = tmp.fileSync({ prefix: 'stderr-' });
362 |
363 | return runScript(scriptable, 'test')
364 | .then(() => {
365 | expect(fs.readFileSync(scriptable.stdout.name, { encoding: 'utf-8' })).string('scriptable-script');
366 | expect(fs.readFileSync(scriptable.stderr.name, { encoding: 'utf-8' })).equal('');
367 | });
368 | });
369 |
370 | it('should skip hook registration when no hook scripts', () => {
371 | const serverless = { service: {} };
372 | const scriptable = new Scriptable(serverless);
373 |
374 | expect(scriptable.hooks).deep.equal({});
375 | });
376 |
377 | it('should able to check boolean configs', () => {
378 | const serverless = { service: {} };
379 | const scriptable = new Scriptable(serverless);
380 |
381 | expect(scriptable.isFalse(undefined)).equal(false);
382 | expect(scriptable.isFalse(null)).equal(false);
383 | expect(scriptable.isFalse(true)).equal(false);
384 | expect(scriptable.isFalse('0')).equal(false);
385 | expect(scriptable.isFalse(1)).equal(false);
386 |
387 | expect(scriptable.isFalse(false)).equal(true);
388 | expect(scriptable.isFalse(0)).equal(true);
389 | });
390 |
391 | it('should able to get the first non-undefined value', () => {
392 | const serverless = { service: {} };
393 | const scriptable = new Scriptable(serverless);
394 |
395 | expect(scriptable.first(undefined, false)).equal(false);
396 | expect(scriptable.first(null, false)).equal(null);
397 | expect(scriptable.first(true, false)).equal(true);
398 | expect(scriptable.first(false, false)).equal(false);
399 | expect(scriptable.first(0, false)).equal(0);
400 | expect(scriptable.first(1, false)).equal(1);
401 | });
402 |
403 | it('manual check: should run command with color', () => {
404 | const scriptable = new Scriptable(serviceWithScripts({ test: 'node test/scripts/test-with-color.js' }));
405 | return runScript(scriptable, 'test');
406 | }).timeout(5000);
407 |
408 | it('manual check: should run js with color', () => {
409 | const scriptable = new Scriptable(serviceWithScripts({ test: 'test/scripts/test-with-color.js' }));
410 |
411 | return runScript(scriptable, 'test');
412 | });
413 |
414 | function runScript(scriptable, event) {
415 | return Bluebird.resolve(scriptable.hooks[event]());
416 | }
417 |
418 | function serviceWithScripts(scriptHooks) {
419 | return serviceWithCustom({ scriptHooks });
420 | }
421 |
422 | function serviceWithCustom(custom) {
423 | return { service: { custom } };
424 | }
425 | });
426 |
--------------------------------------------------------------------------------
/test/scripts/.eslintrc:
--------------------------------------------------------------------------------
1 | ---
2 | rules:
3 | no-undef: off
--------------------------------------------------------------------------------
/test/scripts/check-is-support-colors.js:
--------------------------------------------------------------------------------
1 | const colorSupport = require('color-support');
2 |
3 | const supportsColor = colorSupport({ ignoreTTY: true });
4 |
5 | console.log(`check if support colors in current process? ${JSON.stringify(supportsColor)}`);
6 |
7 | if (typeof serverless !== 'undefined' && serverless !== null) {
8 | serverless.supportColorLevel = supportsColor.level;
9 |
10 | if (serverless.supportColorLevel > 0) {
11 | console.log('Support colors in current process');
12 | } else {
13 | console.error('FAILED: DO NOT SUPPORT colors');
14 | }
15 | } else {
16 | console.error('IGNORED: not running under serverless');
17 | }
18 |
--------------------------------------------------------------------------------
/test/scripts/test-with-color.js:
--------------------------------------------------------------------------------
1 | const colorSupport = require('color-support');
2 |
3 | const supportsColor = colorSupport({ ignoreTTY: true });
4 |
5 | if (!supportsColor) {
6 | console.log('color is not supported');
7 | } else if (supportsColor.has16m) {
8 | console.log('\x1b[38;2;102;194;255msupport 16m colors\x1b[0m');
9 | } else if (supportsColor.has256) {
10 | console.log('\x1b[38;5;119msupport 256 colors\x1b[0m');
11 | } else if (supportsColor.hasBasic) {
12 | console.log('\x1b[31msupport basic colors\x1b[0m');
13 | } else {
14 | console.log('should not reach here, but colors are not supported');
15 | }
16 |
--------------------------------------------------------------------------------