Skip to content

Commit 38a7278

Browse files
author
Lukas Holzer
committed
feat: automatic create tsconfig for editor support on functions
Fixes https://github.com/netlify/pod-dev-foundations/issues/595
1 parent d33fb0c commit 38a7278

File tree

5 files changed

+110
-6
lines changed

5 files changed

+110
-6
lines changed

package-lock.json

Lines changed: 7 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
"from2-array": "0.0.4",
125125
"fuzzy": "0.1.3",
126126
"get-port": "5.1.1",
127+
"get-tsconfig": "^4.7.2",
127128
"gh-release-fetch": "4.0.3",
128129
"git-repo-info": "2.1.1",
129130
"gitconfiglocal": "2.1.0",

src/commands/dev/dev.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import process from 'process'
44
import { Option } from 'commander'
55

66
import { promptEditorHelper } from '../../lib/edge-functions/editor-helper.mjs'
7+
import { checkTsconfigForV2Api } from '../../lib/functions/check-tsconfig-for-v2-api.mjs'
78
import { startFunctionsServer } from '../../lib/functions/server.mjs'
89
import { printBanner } from '../../utils/banner.mjs'
910
import {
@@ -161,6 +162,8 @@ const dev = async (options, command) => {
161162
},
162163
})
163164

165+
await checkTsconfigForV2Api({ functionsDir: settings.functions })
166+
164167
const functionsRegistry = await startFunctionsServer({
165168
api,
166169
command,

src/commands/functions/functions-create.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import fetch from 'node-fetch'
1616
import ora from 'ora'
1717

1818
import { fileExistsAsync } from '../../lib/fs.mjs'
19+
import { checkTsconfigForV2Api } from '../../lib/functions/check-tsconfig-for-v2-api.mjs'
1920
import { getAddons, getCurrentAddon, getSiteData } from '../../utils/addons/prepare.mjs'
2021
import { NETLIFYDEVERR, NETLIFYDEVLOG, NETLIFYDEVWARN, chalk, error, log } from '../../utils/command-helpers.mjs'
2122
import { getDotEnvVariables, injectEnvVariables } from '../../utils/dev.mjs'
@@ -715,6 +716,10 @@ const functionsCreate = async (name, options, command) => {
715716
const functionsDir =
716717
functionType === 'edge' ? await ensureEdgeFuncDirExists(command) : await ensureFunctionDirExists(command)
717718

719+
if (functionType === 'serverless') {
720+
await checkTsconfigForV2Api({ functionsDir })
721+
}
722+
718723
/* either download from URL or scaffold from template */
719724
const mainFunc = options.url ? downloadFromURL : scaffoldFromTemplate
720725
await mainFunc(command, options, name, functionsDir, functionType)
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// @ts-check
2+
import { existsSync } from 'fs'
3+
import { writeFile } from 'fs/promises'
4+
import { join } from 'path'
5+
6+
import { parseTsconfig } from 'get-tsconfig'
7+
import terminalLink from 'terminal-link'
8+
9+
import { NETLIFYDEVLOG, chalk, log } from '../../utils/command-helpers.mjs'
10+
11+
/**
12+
* The `tsconfig.json` we are going to write to the functions directory.
13+
* We use a template string instead of JSON.stringify to be able to add comments to the JSON.
14+
* Comments inside the JSON are accepted by TypeScript and tsconfig.
15+
*/
16+
const TSCONFIG_TMPL = `{
17+
// "extends": "../tsconfig.json", /** If you want to share configuration enable the extends property (like strict: true) */
18+
"compilerOptions": {
19+
"noEmit": true, /** This tsconfig.json is only used for type checking and editor support */
20+
"target": "ESNext", /** We allow the latest JavaScript feature set to be available as a target */
21+
"module": "NodeNext", /** This tells TypeScript that we are using ECMAScript modules */
22+
"moduleResolution": "NodeNext", /** This tells TypeScript that we use ECMAScript modules in Node.js and we have to import from files and specify an extension of the file */
23+
"allowImportingTsExtensions": true /** This allows using .ts file extension instead of the standard .js extension. We allow this for better compatibility with Deno Edge Functions */
24+
}
25+
}`
26+
27+
/** @type {Set<`${Lowercase<import('get-tsconfig').TsConfigJson.CompilerOptions.Module>}`>} */
28+
const BLOCKLIST_MODULE_VALUES = new Set(['commonjs', 'amd', 'es2015', 'none', 'system', 'umd'])
29+
30+
/** @type {Set<`${Lowercase<import('get-tsconfig').TsConfigJson.CompilerOptions.Target>}`>} */
31+
const BLOCKLIST_TARGET_VALUES = new Set(['es2015', 'es3', 'es5', 'es2016'])
32+
33+
/**
34+
* Function that is responsible for validating the typescript configuration for serverless functions.
35+
* It validates the `tsconfig.json` settings and if they don't comply it will throw an error.
36+
* @param {object} config
37+
* @param {string|undefined} config.functionsDir An absolute path to the functions directory
38+
*/
39+
export async function checkTsconfigForV2Api(config) {
40+
console.log('checkTsconfigForV2Api', config)
41+
// if no functionsDir is specified or the dir does not exist just return
42+
if (!config.functionsDir || !existsSync(config.functionsDir)) {
43+
return
44+
}
45+
46+
const tsconfig = join(config.functionsDir, 'tsconfig.json')
47+
48+
if (existsSync(tsconfig)) {
49+
const { compilerOptions } = parseTsconfig(tsconfig)
50+
const moduleFormat = compilerOptions?.module?.toLowerCase()
51+
const moduleResolution =
52+
/** @type {Lowercase<import('get-tsconfig').TsConfigJson.CompilerOptions.ModuleResolution>} */ (
53+
compilerOptions?.moduleResolution?.toLowerCase()
54+
)
55+
const target = compilerOptions?.target?.toLowerCase()
56+
57+
if (!moduleFormat || /** @type {Set<unknown>} */ (BLOCKLIST_MODULE_VALUES).has(moduleFormat)) {
58+
throw new Error(`typescript Module format ${chalk.dim(compilerOptions?.module)} not supported!`)
59+
}
60+
61+
if (!target || /** @type {Set<unknown>} */ (BLOCKLIST_TARGET_VALUES).has(target)) {
62+
throw new Error(
63+
`typescript target ${chalk.dim(
64+
compilerOptions?.module,
65+
)} for serverless functions is not supported! Serverless functions must be written as ECMASCript Module!`,
66+
)
67+
}
68+
69+
if (moduleResolution !== 'nodenext' && moduleResolution !== 'node16') {
70+
throw new Error(
71+
`TypeScript module resolution for serverless functions must be either ${chalk.dim('Node16')} or ${chalk.dim(
72+
'NodeNext',
73+
)} inside the ${chalk.dim('tsconfig.json')}!`,
74+
)
75+
}
76+
return
77+
}
78+
79+
await writeFile(tsconfig, TSCONFIG_TMPL, 'utf-8')
80+
81+
log(`${NETLIFYDEVLOG} Successfully created a ${chalk.dim('tsconfig.json')} file in your functions folder!`)
82+
log(
83+
`${NETLIFYDEVLOG} This is important as serverless functions must be written as ${terminalLink(
84+
'ECMAScript Module',
85+
'https://nodejs.org/api/esm.html',
86+
)}.`,
87+
)
88+
log(
89+
`${NETLIFYDEVLOG} For more information check out our ${terminalLink(
90+
'documentation',
91+
'https://docs.netlify.com/functions/create/?fn-language=ts',
92+
)}.`,
93+
)
94+
}

0 commit comments

Comments
 (0)