Skip to content

Commit 6e1a0d8

Browse files
authored
Merge pull request #30 from zirkelc/main
feat: Support ESM and Typescript configs
2 parents 0dc5518 + 81e2202 commit 6e1a0d8

File tree

4 files changed

+38
-122
lines changed

4 files changed

+38
-122
lines changed

docs/guides/intro.md

-2
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,6 @@ const serverlessConfiguration: Serverless = {
124124
module.exports = serverlessConfiguration;
125125
```
126126

127-
Note: when deploying using a `serverless.ts` file, `ts-node` needs to be installed separately as a dev dependency.
128-
129127
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.
130128

131129
### Plugins

lib/configuration/read.js

+16-78
Original file line numberDiff line numberDiff line change
@@ -5,63 +5,9 @@ const isPlainObject = require('type/plain-object/is');
55
const path = require('path');
66
const fsp = require('fs').promises;
77
const yaml = require('js-yaml');
8-
const getRequire = require('../utils/get-require');
9-
const spawn = require('child-process-ext/spawn');
108
const cloudformationSchema = require('@serverless/utils/cloudformation-schema');
119
const ServerlessError = require('../serverless-error');
1210

13-
const resolveTsNode = async (serviceDir) => {
14-
// 1. If installed aside of a Framework, use it
15-
try {
16-
return getRequire(__dirname).resolve('ts-node');
17-
} catch (slsDepError) {
18-
if (slsDepError.code !== 'MODULE_NOT_FOUND') {
19-
throw new ServerlessError(
20-
`Cannot resolve "ts-node" due to: ${slsDepError.message}`,
21-
'TS_NODE_RESOLUTION_ERROR'
22-
);
23-
}
24-
25-
// 2. If installed in a service, use it
26-
try {
27-
return getRequire(serviceDir).resolve('ts-node');
28-
} catch (serviceDepError) {
29-
if (serviceDepError.code !== 'MODULE_NOT_FOUND') {
30-
throw new ServerlessError(
31-
`Cannot resolve "ts-node" due to: ${serviceDepError.message}`,
32-
'TS_NODE_IN_SERVICE_RESOLUTION_ERROR'
33-
);
34-
}
35-
36-
// 3. If installed globally, use it
37-
const { stdoutBuffer } = await (async () => {
38-
try {
39-
return await spawn('npm', ['root', '-g']);
40-
} catch (error) {
41-
if (error.code !== 'ENOENT') {
42-
throw new ServerlessError(
43-
`Cannot resolve "ts-node" due to unexpected "npm" error: ${error.message}`,
44-
'TS_NODE_NPM_RESOLUTION_ERROR'
45-
);
46-
}
47-
throw new ServerlessError('"ts-node" not found', 'TS_NODE_NOT_FOUND');
48-
}
49-
})();
50-
try {
51-
return require.resolve(`${String(stdoutBuffer).trim()}/ts-node`);
52-
} catch (globalDepError) {
53-
if (globalDepError.code !== 'MODULE_NOT_FOUND') {
54-
throw new ServerlessError(
55-
`Cannot resolve "ts-node" due to: ${globalDepError.message}`,
56-
'TS_NODE_NPM_GLOBAL_RESOLUTION_ERROR'
57-
);
58-
}
59-
throw new ServerlessError('"ts-node" not found', 'TS_NODE_NOT_FOUND');
60-
}
61-
}
62-
}
63-
};
64-
6511
const readConfigurationFile = async (configurationPath) => {
6612
try {
6713
return await fsp.readFile(configurationPath, 'utf8');
@@ -107,30 +53,22 @@ const parseConfigurationFile = async (configurationPath) => {
10753
);
10854
}
10955
}
110-
case '.ts': {
111-
if (!process[Symbol.for('ts-node.register.instance')]) {
112-
const tsNodePath = await (async () => {
113-
try {
114-
return await resolveTsNode(path.dirname(configurationPath));
115-
} catch (error) {
116-
throw new ServerlessError(
117-
`Cannot parse "${path.basename(
118-
configurationPath
119-
)}": Resolution of "ts-node" failed with: ${error.message}`,
120-
'CONFIGURATION_RESOLUTION_ERROR'
121-
);
122-
}
123-
})();
124-
try {
125-
require(tsNodePath).register();
126-
} catch (error) {
127-
throw new ServerlessError(
128-
`Cannot parse "${path.basename(
129-
configurationPath
130-
)}": Register of "ts-node" failed with: ${error.message}`,
131-
'CONFIGURATION_RESOLUTION_ERROR'
132-
);
133-
}
56+
case '.ts':
57+
case '.mts': {
58+
try {
59+
const { createJiti } = require('jiti');
60+
const jiti = createJiti(null, { interopDefault: true });
61+
62+
const content = await jiti.import(configurationPath, { default: true });
63+
64+
return content;
65+
} catch (error) {
66+
throw new ServerlessError(
67+
`Cannot parse "${path.basename(configurationPath)}": Initialization error: ${
68+
error && error.stack ? error.stack : error
69+
}`,
70+
'CONFIGURATION_INITIALIZATION_ERROR'
71+
);
13472
}
13573
}
13674
// fallthrough

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"graceful-fs": "^4.2.11",
5353
"https-proxy-agent": "^5.0.1",
5454
"is-docker": "^2.2.1",
55+
"jiti": "^2.4.2",
5556
"js-yaml": "^4.1.0",
5657
"json-cycle": "^1.5.0",
5758
"json-refs": "^3.0.15",

test/unit/lib/configuration/read.test.js

+21-42
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ const { expect } = chai;
77

88
const fsp = require('fs').promises;
99
const fse = require('fs-extra');
10-
const proxyquire = require('proxyquire');
1110
const readConfiguration = require('../../../../lib/configuration/read');
1211

1312
describe('test/unit/lib/configuration/read.test.js', () => {
@@ -58,7 +57,7 @@ describe('test/unit/lib/configuration/read.test.js', () => {
5857
expect(await readConfiguration(configurationPath)).to.deep.equal(configuration);
5958
});
6059

61-
it('should read "serverless.js"', async () => {
60+
it('should read "serverless.js" as CJS', async () => {
6261
configurationPath = 'serverless.js';
6362
const configuration = {
6463
service: 'test-js',
@@ -68,23 +67,7 @@ describe('test/unit/lib/configuration/read.test.js', () => {
6867
expect(await readConfiguration(configurationPath)).to.deep.equal(configuration);
6968
});
7069

71-
it('should read "serverless.ts"', async () => {
72-
await fse.ensureDir('node_modules');
73-
try {
74-
await fsp.writeFile('node_modules/ts-node.js', 'module.exports.register = () => null;');
75-
configurationPath = 'serverless.ts';
76-
const configuration = {
77-
service: 'test-ts',
78-
provider: { name: 'aws' },
79-
};
80-
await fsp.writeFile(configurationPath, `module.exports = ${JSON.stringify(configuration)}`);
81-
expect(await readConfiguration(configurationPath)).to.deep.equal(configuration);
82-
} finally {
83-
await fse.remove('node_modules');
84-
}
85-
});
86-
87-
it('should read "serverless.cjs"', async () => {
70+
it('should read "serverless.cjs" as CJS', async () => {
8871
configurationPath = 'serverless.cjs';
8972
const configuration = {
9073
service: 'test-js',
@@ -94,7 +77,7 @@ describe('test/unit/lib/configuration/read.test.js', () => {
9477
expect(await readConfiguration(configurationPath)).to.deep.equal(configuration);
9578
});
9679

97-
it('should read "serverless.mjs"', async () => {
80+
it('should read "serverless.mjs" as ESM', async () => {
9881
configurationPath = 'serverless.mjs';
9982
const configuration = {
10083
service: 'test-js',
@@ -104,10 +87,9 @@ describe('test/unit/lib/configuration/read.test.js', () => {
10487
expect(await readConfiguration(configurationPath)).to.deep.equal(configuration);
10588
});
10689

107-
it('should register ts-node only if it is not already registered', async () => {
90+
it('should read "serverless.ts" as CJS', async () => {
91+
await fse.ensureDir('node_modules');
10892
try {
109-
expect(process[Symbol.for('ts-node.register.instance')]).to.be.undefined;
110-
process[Symbol.for('ts-node.register.instance')] = 'foo';
11193
configurationPath = 'serverless.ts';
11294
const configuration = {
11395
service: 'test-ts',
@@ -116,7 +98,22 @@ describe('test/unit/lib/configuration/read.test.js', () => {
11698
await fsp.writeFile(configurationPath, `module.exports = ${JSON.stringify(configuration)}`);
11799
expect(await readConfiguration(configurationPath)).to.deep.equal(configuration);
118100
} finally {
119-
delete process[Symbol.for('ts-node.register.instance')];
101+
await fse.remove('node_modules');
102+
}
103+
});
104+
105+
it('should read "serverless.ts" as ESM', async () => {
106+
await fse.ensureDir('node_modules');
107+
try {
108+
configurationPath = 'serverless.ts';
109+
const configuration = {
110+
service: 'test-ts',
111+
provider: { name: 'aws' },
112+
};
113+
await fsp.writeFile(configurationPath, `export default ${JSON.stringify(configuration)}`);
114+
expect(await readConfiguration(configurationPath)).to.deep.equal(configuration);
115+
} finally {
116+
await fse.remove('node_modules');
120117
}
121118
});
122119

@@ -179,24 +176,6 @@ describe('test/unit/lib/configuration/read.test.js', () => {
179176
);
180177
});
181178

182-
it('should reject TS configuration if "ts-node" is not found', async () => {
183-
// Test against different service dir, to not fall into cached `require.resolve` value
184-
configurationPath = 'other/serverless-errored.ts';
185-
const configuration = {
186-
service: 'test-ts',
187-
provider: { name: 'aws' },
188-
};
189-
await fse.ensureFile(configurationPath);
190-
await fsp.writeFile(configurationPath, `module.exports = ${JSON.stringify(configuration)}`);
191-
await expect(
192-
proxyquire('../../../../lib/configuration/read', {
193-
'child-process-ext/spawn': async () => {
194-
throw Object.assign(new Error('Not found'), { code: 'ENOENT' });
195-
},
196-
})(configurationPath)
197-
).to.eventually.be.rejected.and.have.property('code', 'CONFIGURATION_RESOLUTION_ERROR');
198-
});
199-
200179
it('should reject non object configuration', async () => {
201180
configurationPath = 'serverless.json';
202181
await fsp.writeFile(configurationPath, JSON.stringify([]));

0 commit comments

Comments
 (0)