Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add serverless.ts support #7755

Merged
merged 11 commits into from
Jun 1, 2020
2 changes: 2 additions & 0 deletions lib/classes/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ class Utils {
servicePath = process.cwd();
} else if (fileExistsSync(path.join(process.cwd(), 'serverless.js'))) {
servicePath = process.cwd();
} else if (fileExistsSync(path.join(process.cwd(), 'serverless.ts'))) {
servicePath = process.cwd();
}

return servicePath;
Expand Down
12 changes: 12 additions & 0 deletions lib/classes/Utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,18 @@ describe('Utils', () => {
expect(servicePath).to.not.equal(null);
});

it('should detect if the CWD is a service directory when using Serverless .ts files', () => {
const tmpDirPath = getTmpDirPath();
const tmpFilePath = path.join(tmpDirPath, 'serverless.ts');

serverless.utils.writeFileSync(tmpFilePath, 'foo');
process.chdir(tmpDirPath);

const servicePath = serverless.utils.findServicePath();

expect(servicePath).to.not.equal(null);
});

it('should detect if the CWD is not a service directory', () => {
// just use the root of the tmpdir because findServicePath will
// also check parent directories (and may find matching tmp dirs
Expand Down
58 changes: 48 additions & 10 deletions lib/utils/getServerlessConfigFile.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
const _ = require('lodash');
const BbPromise = require('bluebird');
const path = require('path');
const resolveModulePath = require('ncjsm/resolve');
const spawn = require('child-process-ext/spawn');
const fileExists = require('./fs/fileExists');
const readFile = require('./fs/readFile');
const ServerlessError = require('../classes/Error').ServerlessError;

const getConfigFilePath = (servicePath, options = {}) => {
if (options.config) {
Expand All @@ -18,12 +21,14 @@ const getConfigFilePath = (servicePath, options = {}) => {
const ymlPath = path.join(servicePath, 'serverless.yml');
const yamlPath = path.join(servicePath, 'serverless.yaml');
const jsPath = path.join(servicePath, 'serverless.js');
const tsPath = path.join(servicePath, 'serverless.ts');

return BbPromise.props({
json: fileExists(jsonPath),
yml: fileExists(ymlPath),
yaml: fileExists(yamlPath),
js: fileExists(jsPath),
ts: fileExists(tsPath),
}).then(exists => {
if (exists.yaml) {
return yamlPath;
Expand All @@ -33,6 +38,8 @@ const getConfigFilePath = (servicePath, options = {}) => {
return jsonPath;
} else if (exists.js) {
return jsPath;
} else if (exists.ts) {
return tsPath;
}

return null;
Expand All @@ -46,28 +53,59 @@ const getServerlessConfigFilePath = serverless => {
);
};

const handleJsConfigFile = jsConfigFile =>
const resolveTsNode = serviceDir => {
const resolveModuleRealPath = (...args) =>
resolveModulePath(...args).then(({ realPath }) => realPath);

const ifNotFoundContinueWith = cb => error => {
if (error.code !== 'MODULE_NOT_FOUND') throw error;
return cb();
};

const resolveAsServerlessPeerDependency = () => resolveModuleRealPath(__dirname, 'ts-node');
const resolveAsServiceDependency = () => resolveModuleRealPath(serviceDir, 'ts-node');
const resolveAsGlobalInstallation = () =>
spawn('npm', ['root', '-g']).then(({ stdoutBuffer }) =>
require.resolve(`${String(stdoutBuffer).trim()}/ts-node`)
);
const throwTsNodeError = () => {
throw new ServerlessError(
'Ensure "ts-node" dependency when working with TypeScript configuration files',
'TS_NODE_NOT_FOUND'
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For better processing (e.g. in tests), it'll be nice to add code to this error (it's accepted as second argument), e.g. 'TS_NODE_NOT_FOUND'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done and used in test :)

};

return resolveAsServerlessPeerDependency()
.catch(ifNotFoundContinueWith(resolveAsServiceDependency))
.catch(ifNotFoundContinueWith(resolveAsGlobalInstallation))
.catch(ifNotFoundContinueWith(throwTsNodeError));
};

const handleJsOrTsConfigFile = configFile =>
BbPromise.try(() => {
// use require to load serverless.js
// eslint-disable-next-line global-require
const configExport = require(jsConfigFile);
// In case of a promise result, first resolve it.
return configExport;
if (configFile.endsWith('.ts')) {
return resolveTsNode(path.dirname(configFile)).then(tsNodePath => {
require(tsNodePath).register();
return require(configFile);
});
}
return require(configFile);
}).then(config => {
if (_.isPlainObject(config)) {
return config;
}
throw new Error('serverless.js must export plain object');
throw new Error(`${path.basename(configFile)} must export plain object`);
});

const getServerlessConfigFile = _.memoize(
serverless =>
getServerlessConfigFilePath(serverless).then(configFilePath => {
if (configFilePath !== null) {
const isJSConfigFile = _.last(_.split(configFilePath, '.')) === 'js';
const fileExtension = path.extname(configFilePath);
const isJSOrTsConfigFile = fileExtension === '.js' || fileExtension === '.ts';

if (isJSConfigFile) {
return handleJsConfigFile(configFilePath);
if (isJSOrTsConfigFile) {
return handleJsOrTsConfigFile(configFilePath);
}

return readFile(configFilePath).then(result => result || {});
Expand Down
40 changes: 40 additions & 0 deletions lib/utils/getServerlessConfigFile.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

const path = require('path');
const chai = require('chai');
const sinon = require('sinon');
const writeFileSync = require('./fs/writeFileSync');
const serverlessConfigFileUtils = require('./getServerlessConfigFile');
const { getTmpDirPath } = require('../../tests/utils/fs');
const fixtures = require('../../tests/fixtures');
const runServerless = require('../../tests/utils/run-serverless');

const getServerlessConfigFile = serverlessConfigFileUtils.getServerlessConfigFile;

chai.use(require('chai-as-promised'));
chai.use(require('sinon-chai'));

const expect = chai.expect;

Expand Down Expand Up @@ -168,4 +172,40 @@ describe('#getServerlessConfigFile()', () => {
}
);
});

describe('serverless.ts', () => {
const tsNodePath = 'node_modules/ts-node.js';

after(() => fixtures.cleanup({ extraPaths: [tsNodePath] }));

afterEach(() => {
sinon.restore();
});

it('should throw TS_NODE_NOT_FOUND error when ts-node is not found', () => {
return runServerless({
cwd: fixtures.map.serverlessTs,
cliArgs: ['package'],
shouldStubSpawn: true,
}).catch(error => {
return expect(error.code).to.equal('TS_NODE_NOT_FOUND');
});
});

it('should package serverless.ts files when ts-node is found', () => {
const tsNodeFullPath = `${fixtures.map.serverlessTs}/${tsNodePath}`;

writeFileSync(tsNodeFullPath, 'module.exports.register = () => null;');
const registerSpy = sinon.spy(require(tsNodeFullPath), 'register');

return runServerless({
cwd: fixtures.map.serverlessTs,
cliArgs: ['package'],
}).then(serverless => {
expect(registerSpy).to.have.callCount(1);
expect(serverless.service.service).to.equal('serverless-ts-service');
return expect(serverless.service.provider.name).to.equal('aws');
});
});
});
});
8 changes: 8 additions & 0 deletions tests/fixtures/serverlessTs/serverless.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict';

module.exports = {
service: 'serverless-ts-service',
provider: {
name: 'aws'
}
};