diff --git a/src/lib/functions/server.mjs b/src/lib/functions/server.mjs index 1e79d3bb65d..9fedd0ddf29 100644 --- a/src/lib/functions/server.mjs +++ b/src/lib/functions/server.mjs @@ -219,6 +219,20 @@ const getFunctionsServer = (options) => { return app } +/** + * + * @param {object} options + * @param {import('../../commands/base-command.mjs').default} options.command + * @param {*} options.capabilities + * @param {*} options.config + * @param {boolean} options.debug + * @param {*} options.loadDistFunctions + * @param {*} options.settings + * @param {*} options.site + * @param {string} options.siteUrl + * @param {*} options.timeouts + * @returns + */ export const startFunctionsServer = async (options) => { const { capabilities, command, config, debug, loadDistFunctions, settings, site, siteUrl, timeouts } = options const internalFunctionsDir = await getInternalFunctionsDir({ base: site.root }) @@ -260,15 +274,22 @@ export const startFunctionsServer = async (options) => { const server = await getFunctionsServer(Object.assign(options, { functionsRegistry })) - await startWebServer({ server, settings }) + await startWebServer({ server, settings, debug }) } -const startWebServer = async ({ server, settings }) => { - await new Promise((resolve) => { - server.listen(settings.functionsPort, (err) => { +/** + * + * @param {object} config + * @param {boolean} config.debug + * @param {ReturnType>} config.server + * @param {*} config.settings + */ +const startWebServer = async ({ debug, server, settings }) => { + await new Promise((/** @type {(resolve: void) => void} */ resolve) => { + server.listen(settings.functionsPort, (/** @type {unknown} */ err) => { if (err) { errorExit(`${NETLIFYDEVERR} Unable to start functions server: ${err}`) - } else { + } else if (debug) { log(`${NETLIFYDEVLOG} Functions server is listening on ${settings.functionsPort}`) } resolve() diff --git a/tests/integration/600.framework-detection.test.cjs b/tests/integration/600.framework-detection.test.cjs index 69c5e9b5e53..c0def572de7 100644 --- a/tests/integration/600.framework-detection.test.cjs +++ b/tests/integration/600.framework-detection.test.cjs @@ -1,13 +1,12 @@ // eslint-disable-next-line ava/use-test const avaTest = require('ava') const { isCI } = require('ci-info') -// const execa = require('execa') +const execa = require('execa') -// const cliPath = require('./utils/cli-path.cjs') -// const { getExecaOptions, withDevServer } = require('./utils/dev-server.cjs') -const { withDevServer } = require('./utils/dev-server.cjs') +const cliPath = require('./utils/cli-path.cjs') +const { getExecaOptions, withDevServer } = require('./utils/dev-server.cjs') const got = require('./utils/got.cjs') -// const { DOWN, answerWithValue, handleQuestions } = require('./utils/handle-questions.cjs') +const { DOWN, answerWithValue, handleQuestions } = require('./utils/handle-questions.cjs') const { withSiteBuilder } = require('./utils/site-builder.cjs') const { normalize } = require('./utils/snapshots.cjs') @@ -223,147 +222,147 @@ test(`should print specific error when command doesn't exist`, async (t) => { }) }) -// test.skip('should prompt when multiple frameworks are detected', async (t) => { -// await withSiteBuilder('site-with-multiple-frameworks', async (builder) => { -// await builder -// .withPackageJson({ -// packageJson: { -// dependencies: { 'react-scripts': '1.0.0', gatsby: '^3.0.0' }, -// scripts: { start: 'react-scripts start', develop: 'gatsby develop' }, -// }, -// }) -// .withContentFile({ path: 'gatsby-config.js', content: '' }) -// .buildAsync() - -// // a failure is expected since this is not a true framework project -// const error = await t.throwsAsync(async () => { -// const childProcess = execa(cliPath, ['dev', '--offline'], getExecaOptions({ cwd: builder.directory })) - -// handleQuestions(childProcess, [ -// { -// question: 'Multiple possible start commands found', -// answer: answerWithValue(DOWN), -// }, -// ]) - -// await childProcess -// }) -// t.snapshot(normalize(error.stdout, { duration: true, filePath: true })) -// }) -// }) - -// test.skip('should not run framework detection if command and targetPort are configured', async (t) => { -// await withSiteBuilder('site-with-hugo-config', async (builder) => { -// await builder.withContentFile({ path: 'config.toml', content: '' }).buildAsync() - -// // a failure is expected since the command exits early -// const error = await t.throwsAsync(() => -// withDevServer( -// { cwd: builder.directory, args: ['--command', 'echo hello', '--target-port', '3000'] }, -// () => {}, -// true, -// ), -// ) -// t.snapshot(normalize(error.stdout, { duration: true, filePath: true })) -// }) -// }) - -// test.skip('should filter frameworks with no dev command', async (t) => { -// await withSiteBuilder('site-with-gulp', async (builder) => { -// await builder -// .withContentFile({ -// path: 'index.html', -// content, -// }) -// .withPackageJson({ -// packageJson: { dependencies: { gulp: '1.0.0' } }, -// }) -// .buildAsync() - -// await withDevServer({ cwd: builder.directory }, async ({ output, url }) => { -// const response = await got(url).text() -// t.is(response, content) - -// t.snapshot(normalize(output, { duration: true, filePath: true })) -// }) -// }) -// }) - -// test.skip('should start static service for frameworks without port, forced framework', async (t) => { -// await withSiteBuilder('site-with-remix', async (builder) => { -// await builder.withNetlifyToml({ config: { dev: { framework: 'remix' } } }).buildAsync() - -// // a failure is expected since this is not a true remix project -// const error = await t.throwsAsync(() => withDevServer({ cwd: builder.directory }, () => {}, true)) -// t.true(error.stdout.includes(`Failed running command: remix watch. Please verify 'remix' exists`)) -// }) -// }) - -// test.skip('should start static service for frameworks without port, detected framework', async (t) => { -// await withSiteBuilder('site-with-remix', async (builder) => { -// await builder -// .withPackageJson({ -// packageJson: { -// dependencies: { remix: '^1.0.0', '@remix-run/netlify': '^1.0.0' }, -// scripts: {}, -// }, -// }) -// .withContentFile({ path: 'remix.config.js', content: '' }) -// .buildAsync() - -// // a failure is expected since this is not a true remix project -// const error = await t.throwsAsync(() => withDevServer({ cwd: builder.directory }, () => {}, true)) -// t.true(error.stdout.includes(`Failed running command: remix watch. Please verify 'remix' exists`)) -// }) -// }) - -// test.skip('should run and serve a production build when using the `serve` command', async (t) => { -// await withSiteBuilder('site-with-framework', async (builder) => { -// await builder -// .withNetlifyToml({ -// config: { -// build: { publish: 'public' }, -// context: { -// dev: { environment: { CONTEXT_CHECK: 'DEV' } }, -// production: { environment: { CONTEXT_CHECK: 'PRODUCTION' } }, -// }, -// functions: { directory: 'functions' }, -// plugins: [{ package: './plugins/frameworker' }], -// }, -// }) -// .withBuildPlugin({ -// name: 'frameworker', -// plugin: { -// onPreBuild: async ({ netlifyConfig }) => { -// // eslint-disable-next-line n/global-require -// const { mkdir, writeFile } = require('fs').promises - -// const generatedFunctionsDir = 'new_functions' -// netlifyConfig.functions.directory = generatedFunctionsDir - -// netlifyConfig.redirects.push({ -// from: '/hello', -// to: '/.netlify/functions/hello', -// }) - -// await mkdir(generatedFunctionsDir) -// await writeFile( -// `${generatedFunctionsDir}/hello.js`, -// `const { CONTEXT_CHECK, NETLIFY_DEV } = process.env; exports.handler = async () => ({ statusCode: 200, body: JSON.stringify({ CONTEXT_CHECK, NETLIFY_DEV }) })`, -// ) -// }, -// }, -// }) -// .buildAsync() - -// await withDevServer( -// { cwd: builder.directory, context: null, debug: true, serve: true }, -// async ({ output, url }) => { -// const response = await got(`${url}/hello`).json() -// t.deepEqual(response, { CONTEXT_CHECK: 'PRODUCTION' }) - -// t.snapshot(normalize(output, { duration: true, filePath: true })) -// }, -// ) -// }) -// }) +test('should prompt when multiple frameworks are detected', async (t) => { + await withSiteBuilder('site-with-multiple-frameworks', async (builder) => { + await builder + .withPackageJson({ + packageJson: { + dependencies: { 'react-scripts': '1.0.0', gatsby: '^3.0.0' }, + scripts: { start: 'react-scripts start', develop: 'gatsby develop' }, + }, + }) + .withContentFile({ path: 'gatsby-config.js', content: '' }) + .buildAsync() + + // a failure is expected since this is not a true framework project + const error = await t.throwsAsync(async () => { + const childProcess = execa(cliPath, ['dev', '--offline'], getExecaOptions({ cwd: builder.directory })) + + handleQuestions(childProcess, [ + { + question: 'Multiple possible dev commands found', + answer: answerWithValue(DOWN), + }, + ]) + + await childProcess + }) + t.snapshot(normalize(error.stdout, { duration: true, filePath: true })) + }) +}) + +test('should not run framework detection if command and targetPort are configured', async (t) => { + await withSiteBuilder('site-with-hugo-config', async (builder) => { + await builder.withContentFile({ path: 'config.toml', content: '' }).buildAsync() + + // a failure is expected since the command exits early + const error = await t.throwsAsync(() => + withDevServer( + { cwd: builder.directory, args: ['--command', 'echo hello', '--target-port', '3000'] }, + () => {}, + true, + ), + ) + t.snapshot(normalize(error.stdout, { duration: true, filePath: true })) + }) +}) + +test('should filter frameworks with no dev command', async (t) => { + await withSiteBuilder('site-with-gulp', async (builder) => { + await builder + .withContentFile({ + path: 'index.html', + content, + }) + .withPackageJson({ + packageJson: { dependencies: { gulp: '1.0.0' } }, + }) + .buildAsync() + + await withDevServer({ cwd: builder.directory }, async ({ output, url }) => { + const response = await got(url).text() + t.is(response, content) + + t.snapshot(normalize(output, { duration: true, filePath: true })) + }) + }) +}) + +test('should start static service for frameworks without port, forced framework', async (t) => { + await withSiteBuilder('site-with-remix', async (builder) => { + await builder.withNetlifyToml({ config: { dev: { framework: 'remix' } } }).buildAsync() + + // a failure is expected since this is not a true remix project + const error = await t.throwsAsync(() => withDevServer({ cwd: builder.directory }, () => {}, true)) + t.true(error.stdout.includes(`Failed running command: remix watch. Please verify 'remix' exists`)) + }) +}) + +test('should start static service for frameworks without port, detected framework', async (t) => { + await withSiteBuilder('site-with-remix', async (builder) => { + await builder + .withPackageJson({ + packageJson: { + dependencies: { remix: '^1.0.0', '@remix-run/netlify': '^1.0.0' }, + scripts: {}, + }, + }) + .withContentFile({ path: 'remix.config.js', content: '' }) + .buildAsync() + + // a failure is expected since this is not a true remix project + const error = await t.throwsAsync(() => withDevServer({ cwd: builder.directory }, () => {}, true)) + t.true(error.stdout.includes(`Failed running command: remix watch. Please verify 'remix' exists`)) + }) +}) + +test('should run and serve a production build when using the `serve` command', async (t) => { + await withSiteBuilder('site-with-framework', async (builder) => { + await builder + .withNetlifyToml({ + config: { + build: { publish: 'public' }, + context: { + dev: { environment: { CONTEXT_CHECK: 'DEV' } }, + production: { environment: { CONTEXT_CHECK: 'PRODUCTION' } }, + }, + functions: { directory: 'functions' }, + plugins: [{ package: './plugins/frameworker' }], + }, + }) + .withBuildPlugin({ + name: 'frameworker', + plugin: { + onPreBuild: async ({ netlifyConfig }) => { + // eslint-disable-next-line n/global-require + const { mkdir, writeFile } = require('fs').promises + + const generatedFunctionsDir = 'new_functions' + netlifyConfig.functions.directory = generatedFunctionsDir + + netlifyConfig.redirects.push({ + from: '/hello', + to: '/.netlify/functions/hello', + }) + + await mkdir(generatedFunctionsDir) + await writeFile( + `${generatedFunctionsDir}/hello.js`, + `const { CONTEXT_CHECK, NETLIFY_DEV } = process.env; exports.handler = async () => ({ statusCode: 200, body: JSON.stringify({ CONTEXT_CHECK, NETLIFY_DEV }) })`, + ) + }, + }, + }) + .buildAsync() + + await withDevServer( + { cwd: builder.directory, context: null, debug: true, serve: true }, + async ({ output, url }) => { + const response = await got(`${url}/hello`).json() + t.deepEqual(response, { CONTEXT_CHECK: 'PRODUCTION' }) + + t.snapshot(normalize(output, { duration: true, filePath: true })) + }, + ) + }) +}) diff --git a/tests/integration/snapshots/600.framework-detection.test.cjs.md b/tests/integration/snapshots/600.framework-detection.test.cjs.md index dfc01619b3c..0ca0ed1b5f2 100644 --- a/tests/integration/snapshots/600.framework-detection.test.cjs.md +++ b/tests/integration/snapshots/600.framework-detection.test.cjs.md @@ -177,12 +177,21 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 `◈ Netlify Dev ◈␊ - ? Multiple possible start commands found (Use arrow keys or type to search)␊ + ? Multiple possible dev commands found (Use arrow keys or type to search)␊ > [Gatsby] 'npm run develop' ␊ - [Create React App] 'npm run start' ? Multiple possible start commands found ␊ + [Create React App] 'npm run start' ? Multiple possible dev commands found ␊ [Gatsby] 'npm run develop' ␊ - > [Create React App] 'npm run start' ? Multiple possible start commands found Create React App-npm run start␊ - Add 'framework = "create-react-app"' to the [dev] section of your netlify.toml to avoid this selection prompt next time␊ + > [Create React App] 'npm run start' ? Multiple possible dev commands found Create React App-npm run start␊ + ␊ + Update your netlify.toml to avoid this selection prompt next time:␊ + ␊ + [build]␊ + command = "react-scripts build"␊ + publish = "build"␊ + ␊ + [dev]␊ + command = "npm run start"␊ + ␊ ◈ Setting up local development server␊ ◈ Starting Netlify Dev with Create React App␊ ␊ @@ -224,20 +233,6 @@ Generated by [AVA](https://avajs.dev). │ │␊ └──────────────────────────────────────────────────┘` -## should pass framework-info env to framework sub process - -> Snapshot 1 - - `◈ Netlify Dev ◈␊ - ◈ Setting up local development server␊ - ◈ Starting Netlify Dev with Nuxt 3␊ - ␊ - > dev␊ - > node -p process.env.NODE_VERSION␊ - ␊ - 14␊ - ◈ "npm run dev" exited with code *. Shutting down Netlify Dev server` - ## should run and serve a production build when using the `serve` command > Snapshot 1 @@ -256,6 +251,7 @@ Generated by [AVA](https://avajs.dev). ​␊ > Flags␊ configPath:/file/path␊ + edgeFunctionsBootstrapURL: https://64c888887e9cbb8888821df3--edge.netlify.com/bootstrap/index-combined.ts␊ offline: true␊ outputConfigPath:/file/path␊ ​␊ @@ -299,7 +295,6 @@ Generated by [AVA](https://avajs.dev). (Netlify Build completed in Xms)␊ ␊ ◈ Static server listening to 88888␊ - ◈ Functions server is listening on 88888␊ ◈ Loaded function hello␊ ␊ ┌──────────────────────────────────────────────────┐␊ diff --git a/tests/integration/snapshots/600.framework-detection.test.cjs.snap b/tests/integration/snapshots/600.framework-detection.test.cjs.snap index 13c7d9eebef..e4f1bc25578 100644 Binary files a/tests/integration/snapshots/600.framework-detection.test.cjs.snap and b/tests/integration/snapshots/600.framework-detection.test.cjs.snap differ