diff --git a/docs/guides/intro.md b/docs/guides/intro.md index 00a326f93..b9a15eafd 100644 --- a/docs/guides/intro.md +++ b/docs/guides/intro.md @@ -124,8 +124,6 @@ const serverlessConfiguration: Serverless = { module.exports = serverlessConfiguration; ``` -Note: when deploying using a `serverless.ts` file, `ts-node` needs to be installed separately as a dev dependency. - For the sake of simplicity, most examples in the documentation refer to the `serverless.yml` format. However, all functionalities work with the other available service file formats. ### Plugins diff --git a/lib/configuration/read.js b/lib/configuration/read.js index 2d8edb456..e57bd830b 100644 --- a/lib/configuration/read.js +++ b/lib/configuration/read.js @@ -5,63 +5,9 @@ const isPlainObject = require('type/plain-object/is'); const path = require('path'); const fsp = require('fs').promises; const yaml = require('js-yaml'); -const getRequire = require('../utils/get-require'); -const spawn = require('child-process-ext/spawn'); const cloudformationSchema = require('@serverless/utils/cloudformation-schema'); const ServerlessError = require('../serverless-error'); -const resolveTsNode = async (serviceDir) => { - // 1. If installed aside of a Framework, use it - try { - return getRequire(__dirname).resolve('ts-node'); - } catch (slsDepError) { - if (slsDepError.code !== 'MODULE_NOT_FOUND') { - throw new ServerlessError( - `Cannot resolve "ts-node" due to: ${slsDepError.message}`, - 'TS_NODE_RESOLUTION_ERROR' - ); - } - - // 2. If installed in a service, use it - try { - return getRequire(serviceDir).resolve('ts-node'); - } catch (serviceDepError) { - if (serviceDepError.code !== 'MODULE_NOT_FOUND') { - throw new ServerlessError( - `Cannot resolve "ts-node" due to: ${serviceDepError.message}`, - 'TS_NODE_IN_SERVICE_RESOLUTION_ERROR' - ); - } - - // 3. If installed globally, use it - const { stdoutBuffer } = await (async () => { - try { - return await spawn('npm', ['root', '-g']); - } catch (error) { - if (error.code !== 'ENOENT') { - throw new ServerlessError( - `Cannot resolve "ts-node" due to unexpected "npm" error: ${error.message}`, - 'TS_NODE_NPM_RESOLUTION_ERROR' - ); - } - throw new ServerlessError('"ts-node" not found', 'TS_NODE_NOT_FOUND'); - } - })(); - try { - return require.resolve(`${String(stdoutBuffer).trim()}/ts-node`); - } catch (globalDepError) { - if (globalDepError.code !== 'MODULE_NOT_FOUND') { - throw new ServerlessError( - `Cannot resolve "ts-node" due to: ${globalDepError.message}`, - 'TS_NODE_NPM_GLOBAL_RESOLUTION_ERROR' - ); - } - throw new ServerlessError('"ts-node" not found', 'TS_NODE_NOT_FOUND'); - } - } - } -}; - const readConfigurationFile = async (configurationPath) => { try { return await fsp.readFile(configurationPath, 'utf8'); @@ -107,30 +53,22 @@ const parseConfigurationFile = async (configurationPath) => { ); } } - case '.ts': { - if (!process[Symbol.for('ts-node.register.instance')]) { - const tsNodePath = await (async () => { - try { - return await resolveTsNode(path.dirname(configurationPath)); - } catch (error) { - throw new ServerlessError( - `Cannot parse "${path.basename( - configurationPath - )}": Resolution of "ts-node" failed with: ${error.message}`, - 'CONFIGURATION_RESOLUTION_ERROR' - ); - } - })(); - try { - require(tsNodePath).register(); - } catch (error) { - throw new ServerlessError( - `Cannot parse "${path.basename( - configurationPath - )}": Register of "ts-node" failed with: ${error.message}`, - 'CONFIGURATION_RESOLUTION_ERROR' - ); - } + case '.ts': + case '.mts': { + try { + const { createJiti } = require('jiti'); + const jiti = createJiti(null, { interopDefault: true }); + + const content = await jiti.import(configurationPath, { default: true }); + + return content; + } catch (error) { + throw new ServerlessError( + `Cannot parse "${path.basename(configurationPath)}": Initialization error: ${ + error && error.stack ? error.stack : error + }`, + 'CONFIGURATION_INITIALIZATION_ERROR' + ); } } // fallthrough diff --git a/package.json b/package.json index d2e656dc7..18dfc5efd 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "graceful-fs": "^4.2.11", "https-proxy-agent": "^5.0.1", "is-docker": "^2.2.1", + "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json-cycle": "^1.5.0", "json-refs": "^3.0.15", diff --git a/test/unit/lib/configuration/read.test.js b/test/unit/lib/configuration/read.test.js index 313404499..383ac5877 100644 --- a/test/unit/lib/configuration/read.test.js +++ b/test/unit/lib/configuration/read.test.js @@ -7,7 +7,6 @@ const { expect } = chai; const fsp = require('fs').promises; const fse = require('fs-extra'); -const proxyquire = require('proxyquire'); const readConfiguration = require('../../../../lib/configuration/read'); describe('test/unit/lib/configuration/read.test.js', () => { @@ -58,7 +57,7 @@ describe('test/unit/lib/configuration/read.test.js', () => { expect(await readConfiguration(configurationPath)).to.deep.equal(configuration); }); - it('should read "serverless.js"', async () => { + it('should read "serverless.js" as CJS', async () => { configurationPath = 'serverless.js'; const configuration = { service: 'test-js', @@ -68,23 +67,7 @@ describe('test/unit/lib/configuration/read.test.js', () => { expect(await readConfiguration(configurationPath)).to.deep.equal(configuration); }); - it('should read "serverless.ts"', async () => { - await fse.ensureDir('node_modules'); - try { - await fsp.writeFile('node_modules/ts-node.js', 'module.exports.register = () => null;'); - configurationPath = 'serverless.ts'; - const configuration = { - service: 'test-ts', - provider: { name: 'aws' }, - }; - await fsp.writeFile(configurationPath, `module.exports = ${JSON.stringify(configuration)}`); - expect(await readConfiguration(configurationPath)).to.deep.equal(configuration); - } finally { - await fse.remove('node_modules'); - } - }); - - it('should read "serverless.cjs"', async () => { + it('should read "serverless.cjs" as CJS', async () => { configurationPath = 'serverless.cjs'; const configuration = { service: 'test-js', @@ -94,7 +77,7 @@ describe('test/unit/lib/configuration/read.test.js', () => { expect(await readConfiguration(configurationPath)).to.deep.equal(configuration); }); - it('should read "serverless.mjs"', async () => { + it('should read "serverless.mjs" as ESM', async () => { configurationPath = 'serverless.mjs'; const configuration = { service: 'test-js', @@ -104,10 +87,9 @@ describe('test/unit/lib/configuration/read.test.js', () => { expect(await readConfiguration(configurationPath)).to.deep.equal(configuration); }); - it('should register ts-node only if it is not already registered', async () => { + it('should read "serverless.ts" as CJS', async () => { + await fse.ensureDir('node_modules'); try { - expect(process[Symbol.for('ts-node.register.instance')]).to.be.undefined; - process[Symbol.for('ts-node.register.instance')] = 'foo'; configurationPath = 'serverless.ts'; const configuration = { service: 'test-ts', @@ -116,7 +98,22 @@ describe('test/unit/lib/configuration/read.test.js', () => { await fsp.writeFile(configurationPath, `module.exports = ${JSON.stringify(configuration)}`); expect(await readConfiguration(configurationPath)).to.deep.equal(configuration); } finally { - delete process[Symbol.for('ts-node.register.instance')]; + await fse.remove('node_modules'); + } + }); + + it('should read "serverless.ts" as ESM', async () => { + await fse.ensureDir('node_modules'); + try { + configurationPath = 'serverless.ts'; + const configuration = { + service: 'test-ts', + provider: { name: 'aws' }, + }; + await fsp.writeFile(configurationPath, `export default ${JSON.stringify(configuration)}`); + expect(await readConfiguration(configurationPath)).to.deep.equal(configuration); + } finally { + await fse.remove('node_modules'); } }); @@ -179,24 +176,6 @@ describe('test/unit/lib/configuration/read.test.js', () => { ); }); - it('should reject TS configuration if "ts-node" is not found', async () => { - // Test against different service dir, to not fall into cached `require.resolve` value - configurationPath = 'other/serverless-errored.ts'; - const configuration = { - service: 'test-ts', - provider: { name: 'aws' }, - }; - await fse.ensureFile(configurationPath); - await fsp.writeFile(configurationPath, `module.exports = ${JSON.stringify(configuration)}`); - await expect( - proxyquire('../../../../lib/configuration/read', { - 'child-process-ext/spawn': async () => { - throw Object.assign(new Error('Not found'), { code: 'ENOENT' }); - }, - })(configurationPath) - ).to.eventually.be.rejected.and.have.property('code', 'CONFIGURATION_RESOLUTION_ERROR'); - }); - it('should reject non object configuration', async () => { configurationPath = 'serverless.json'; await fsp.writeFile(configurationPath, JSON.stringify([]));