diff --git a/tests/integration/600.framework-detection.test.cjs b/tests/integration/600.framework-detection.test.cjs deleted file mode 100644 index 1cd20213b2b..00000000000 --- a/tests/integration/600.framework-detection.test.cjs +++ /dev/null @@ -1,365 +0,0 @@ -// eslint-disable-next-line ava/use-test -const avaTest = require('ava') -const { isCI } = require('ci-info') -const execa = require('execa') - -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 { withSiteBuilder } = require('./utils/site-builder.cjs') -const { normalize } = require('./utils/snapshots.cjs') - -const content = 'Hello World!' - -const test = isCI ? avaTest.serial.bind(avaTest) : avaTest - -test('should default to process.cwd() and static server', async (t) => { - await withSiteBuilder('site-with-index-file', async (builder) => { - await builder - .withContentFile({ - path: 'index.html', - content, - }) - .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 use static server when --dir flag is passed', async (t) => { - await withSiteBuilder('site-with-index-file', async (builder) => { - await builder - .withContentFile({ - path: 'public/index.html', - content, - }) - .buildAsync() - - await withDevServer({ cwd: builder.directory, args: ['--dir', 'public'] }, async ({ output, url }) => { - const response = await got(url).text() - t.is(response, content) - - t.snapshot(normalize(output, { duration: true, filePath: true })) - }) - }) -}) - -test('should use static server when framework is set to #static', async (t) => { - await withSiteBuilder('site-with-index-file', async (builder) => { - await builder - .withContentFile({ - path: 'index.html', - content, - }) - .withNetlifyToml({ config: { dev: { framework: '#static' } } }) - .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 log the command if using static server and `command` is configured', async (t) => { - await withSiteBuilder('site-with-index-file', async (builder) => { - await builder - .withContentFile({ - path: 'public/index.html', - content, - }) - .buildAsync() - - await withDevServer( - { cwd: builder.directory, args: ['--dir', 'public', '--command', 'npm run start'] }, - async ({ output, url }) => { - const response = await got(url).text() - t.is(response, content) - - t.snapshot(normalize(output, { duration: true, filePath: true })) - }, - ) - }) -}) - -test('should warn if using static server and `targetPort` is configured', async (t) => { - await withSiteBuilder('site-with-index-file', async (builder) => { - await builder - .withContentFile({ - path: 'public/index.html', - content, - }) - .buildAsync() - - await withDevServer( - { cwd: builder.directory, args: ['--dir', 'public', '--target-port', '3000'] }, - async ({ output, url }) => { - const response = await got(url).text() - t.is(response, content) - - t.snapshot(normalize(output, { duration: true, filePath: true })) - }, - ) - }) -}) - -test('should run `command` when both `command` and `targetPort` are configured', async (t) => { - await withSiteBuilder('empty-site', async (builder) => { - await builder.withNetlifyToml({ config: { build: { publish: 'public' } } }).buildAsync() - - // a failure is expected since we use `echo hello` instead of starting a server - 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 force a specific framework when configured', async (t) => { - await withSiteBuilder('site-with-mocked-cra', async (builder) => { - await builder.withNetlifyToml({ config: { dev: { framework: 'create-react-app' } } }).buildAsync() - - // a failure is expected since this is not a true create-react-app project - const error = await t.throwsAsync(() => withDevServer({ cwd: builder.directory }, () => {}, true)) - t.snapshot(normalize(error.stdout, { duration: true, filePath: true })) - }) -}) - -test('should throw when forcing a non supported framework', async (t) => { - await withSiteBuilder('site-with-unknown-framework', async (builder) => { - await builder.withNetlifyToml({ config: { dev: { framework: 'to-infinity-and-beyond-js' } } }).buildAsync() - - const error = await t.throwsAsync(() => withDevServer({ cwd: builder.directory }, () => {}, true)) - t.snapshot(normalize(error.stdout, { duration: true, filePath: true })) - }) -}) - -test('should detect a known framework', async (t) => { - await withSiteBuilder('site-with-cra', async (builder) => { - await builder - .withPackageJson({ - packageJson: { dependencies: { 'react-scripts': '1.0.0' }, scripts: { start: 'react-scripts start' } }, - }) - .buildAsync() - - // a failure is expected since this is not a true create-react-app project - const error = await t.throwsAsync(() => withDevServer({ cwd: builder.directory }, () => {}, true)) - t.snapshot(normalize(error.stdout, { duration: true, filePath: true })) - }) -}) - -test('should throw if framework=#custom but command is missing', async (t) => { - await withSiteBuilder('site-with-framework-and-no-command', async (builder) => { - await builder.withNetlifyToml({ config: { dev: { framework: '#custom' } } }).buildAsync() - - const error = await t.throwsAsync(() => - withDevServer({ cwd: builder.directory, args: ['--target-port', '3000'] }, () => {}, true), - ) - t.snapshot(normalize(error.stdout, { duration: true, filePath: true })) - }) -}) - -test('should throw if framework=#custom but targetPort is missing', async (t) => { - await withSiteBuilder('site-with-framework-and-no-command', async (builder) => { - await builder.withNetlifyToml({ config: { dev: { framework: '#custom' } } }).buildAsync() - - const error = await t.throwsAsync(() => - withDevServer({ cwd: builder.directory, args: ['--command', 'echo hello'] }, () => {}, true), - ) - t.snapshot(normalize(error.stdout, { duration: true, filePath: true })) - }) -}) - -test('should start custom command if framework=#custom, command and targetPort are configured', async (t) => { - await withSiteBuilder('site-with-custom-framework', async (builder) => { - await builder.withNetlifyToml({ config: { dev: { framework: '#custom', publish: 'public' } } }).buildAsync() - - 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 print specific error when command doesn't exist`, async (t) => { - await withSiteBuilder('site-with-custom-framework', async (builder) => { - await builder.buildAsync() - - const error = await t.throwsAsync(() => - withDevServer( - { - cwd: builder.directory, - args: [ - '--command', - 'oops-i-did-it-again forgot-to-use-a-valid-command', - '--target-port', - '3000', - '--framework', - '#custom', - ], - }, - () => {}, - true, - ), - ) - t.snapshot(normalize(error.stdout, { 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, 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/frameworks/__snapshots__/framework-detection.test.mjs.snap b/tests/integration/frameworks/__snapshots__/framework-detection.test.mjs.snap new file mode 100644 index 00000000000..dd2cf1872f6 --- /dev/null +++ b/tests/integration/frameworks/__snapshots__/framework-detection.test.mjs.snap @@ -0,0 +1,376 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`frameworks/framework-detection > should default to process.cwd() and static server 1`] = ` +"◈ Netlify Dev ◈ +◈ No app server detected. Using simple static server +◈ Unable to determine public folder to serve files from. Using current working directory +◈ Setup a netlify.toml file with a [dev] section to specify your dev server settings. +◈ See docs at: https://cli.netlify.com/netlify-dev#project-detection +◈ Running static server from \\"site-with-index-file\\" +◈ Setting up local development server + +◈ Static server listening to 88888 + + ┌──────────────────────────────────────────────────┐ + │ │ + │ ◈ Server now ready on http://localhost:88888 │ + │ │ + └──────────────────────────────────────────────────┘" +`; + +exports[`frameworks/framework-detection > should detect a known framework 1`] = ` +"◈ Netlify Dev ◈ +◈ Setting up local development server +◈ Starting Netlify Dev with Create React App + +> start +> react-scripts start + +◈ Command failed with exit code *: npm run start. Shutting down Netlify Dev server" +`; + +exports[`frameworks/framework-detection > should filter frameworks with no dev command 1`] = ` +"◈ Netlify Dev ◈ +◈ No app server detected. Using simple static server +◈ Unable to determine public folder to serve files from. Using current working directory +◈ Setup a netlify.toml file with a [dev] section to specify your dev server settings. +◈ See docs at: https://cli.netlify.com/netlify-dev#project-detection +◈ Running static server from \\"site-with-gulp\\" +◈ Setting up local development server + +◈ Static server listening to 88888 + + ┌──────────────────────────────────────────────────┐ + │ │ + │ ◈ Server now ready on http://localhost:88888 │ + │ │ + └──────────────────────────────────────────────────┘" +`; + +exports[`frameworks/framework-detection > should force a specific framework when configured 1`] = ` +"◈ Netlify Dev ◈ +◈ Setting up local development server +◈ Starting Netlify Dev with Create React App +◈ Failed running command: react-scripts start. Please verify 'react-scripts' exists" +`; + +exports[`frameworks/framework-detection > should log the command if using static server and \`command\` is configured 1`] = ` +"◈ Netlify Dev ◈ +◈ Using simple static server because '--dir' flag was specified +◈ Running static server from \\"site-with-index-file/public\\" +◈ Setting up local development server + +◈ Static server listening to 88888 + + ┌──────────────────────────────────────────────────┐ + │ │ + │ ◈ Server now ready on http://localhost:88888 │ + │ │ + └──────────────────────────────────────────────────┘" +`; + +exports[`frameworks/framework-detection > should not run framework detection if command and targetPort are configured 1`] = ` +"◈ Netlify Dev ◈ +◈ Unable to determine public folder to serve files from. Using current working directory +◈ Setup a netlify.toml file with a [dev] section to specify your dev server settings. +◈ See docs at: https://cli.netlify.com/netlify-dev#project-detection +◈ Setting up local development server +◈ Starting Netlify Dev with custom config +hello +◈ \\"echo hello\\" exited with code *. Shutting down Netlify Dev server" +`; + +exports[`frameworks/framework-detection > should pass framework-info env to framework sub process 1`] = ` +"◈ Netlify Dev ◈ +◈ Setting up local development server +◈ Starting Netlify Dev with Nuxt 3 + +> dev +> node -p process.env.NODE_VERSION + +18 +◈ \\"npm run dev\\" exited with code *. Shutting down Netlify Dev server" +`; + +exports[`frameworks/framework-detection > should print specific error when command doesn't exist 1`] = ` +"◈ Netlify Dev ◈ +◈ Unable to determine public folder to serve files from. Using current working directory +◈ Setup a netlify.toml file with a [dev] section to specify your dev server settings. +◈ See docs at: https://cli.netlify.com/netlify-dev#project-detection +◈ Setting up local development server +◈ Starting Netlify Dev with #custom +◈ Failed running command: oops-i-did-it-again forgot-to-use-a-valid-command. Please verify 'oops-i-did-it-again' exists" +`; + +exports[`frameworks/framework-detection > should prompt when multiple frameworks are detected 1`] = ` +"◈ Netlify Dev ◈ +? Multiple possible dev commands found (Use arrow keys or type to search) +> [Gatsby] 'npm run develop' + [Create React App] 'npm run start' ? Multiple possible dev commands found + [Gatsby] 'npm run develop' +> [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 + +> start +> react-scripts start + +◈ Command failed with exit code *: npm run start. Shutting down Netlify Dev server" +`; + +exports[`frameworks/framework-detection > should run \`command\` when both \`command\` and \`targetPort\` are configured 1`] = ` +"◈ Netlify Dev ◈ +◈ Setting up local development server +◈ Starting Netlify Dev with custom config +hello +◈ \\"echo hello\\" exited with code *. Shutting down Netlify Dev server" +`; + +exports[`frameworks/framework-detection > should run and serve a production build when using the \`serve\` command 1`] = ` +"[project.ts]: detectFrameworks +[project.ts]: detectBuildSystem +[project.ts]: detectWorkspaces +[project.ts]: detectPackageManager +[project.ts]: detectFrameworksInPath - undefined +◈ Injected netlify.toml file env var: CONTEXT_CHECK +◈ Using simple static server because '[dev.framework]' was set to '#static' +◈ Running static server from \\"site-with-framework/public\\" +◈ Building site for production +◈ Changes will not be hot-reloaded, so if you need to rebuild your site you must exit and run 'netlify serve' again +​ +Netlify Build +──────────────────────────────────────────────────────────────── +​ +> Version + @netlify/build 0.0.0 +​ +> Flags + configPath:/file/path + cwd:/file/path + debug: true + edgeFunctionsBootstrapURL: https://650bfd807b21ed888883e25c--edge.netlify.com/bootstrap/index-combined.ts + mode: cli + offline: true + outputConfigPath:/file/path + saveConfig: true +​ +> Current directory + /file/path +​ +> Config file + /file/path +​ +> Resolved config + build: + environment: + - CONTEXT_CHECK + publish:/file/path + publishOrigin: config + functionsDirectory:/file/path + plugins: + - inputs: {} + origin: config + package:/file/path +​ +> Context + production +​ +> Loading plugins + -/file/path from netlify.toml +​ +/file/path (onPreBuild event) +──────────────────────────────────────────────────────────────── +​ +Netlify configuration property \\"functions.directory\\" value changed to 'new_functions'. +Netlify configuration property \\"redirects\\" value changed to [ { from: /file/path', to: /file/path' } ]. +​ +> Updated config + build: + environment: + - CONTEXT_CHECK + publish:/file/path + publishOrigin: config + functionsDirectory:/file/path + plugins: + - inputs: {} + origin: config + package:/file/path + redirects: + - from:/file/path + to:/file/path + redirectsOrigin: inline +​ +/file/path onPreBuild completed in Xms) +Build step duration:/file/path onPreBuild completed in Xms +​ +Functions bundling +──────────────────────────────────────────────────────────────── +​ +Packaging Functions from new_functions directory: + - hello.js +​ +​ +(Functions bundling completed in Xms) +Build step duration: Functions bundling completed in Xms +​ +Save deploy artifacts +──────────────────────────────────────────────────────────────── +​ +​ +> Uploaded config + [build] + publish = \\"public\\" +​ + [context] +​ + [context.dev] +​ + [context.dev.environment] + CONTEXT_CHECK = \\"DEV\\" +​ + [context.production] +​ + [context.production.environment] + CONTEXT_CHECK = \\"PRODUCTION\\" +​ + [functions] + directory = \\"functions\\" +​ + [[plugins]] + package = /file/path\\" +​ +> Uploaded headers + No headers +​ +> Uploaded redirects + No redirects +​ +​ +(Save deploy artifacts completed in Xms) +Build step duration: Save deploy artifacts completed in Xms +​ +Netlify Build Complete +──────────────────────────────────────────────────────────────── +​ +(Netlify Build completed in Xms) +Build step duration: Netlify Build completed in Xms + +◈ Static server listening to 88888 +◈ Extracted function hello from/file/path +◈ Functions server is listening on 88888 +getBinaryVersion failed Error: Command failed with ENOENT: deno --version +spawn deno ENOENT + at ChildProcess._handle.onexit (node:internal/child_process:283:19) + at onErrorNT (node:internal/child_process:476:16) + at process.processTicksAndRejections (node:internal/process/task_queues:82:21) { + errno: -2, + code: 'ENOENT', + syscall: 'spawn deno', + path: 'deno', + spawnargs: [ '--version' ], + originalMessage: 'spawn deno ENOENT', + shortMessage: 'Command failed with ENOENT: deno --version/nspawn deno ENOENT', + command: 'deno --version', + escapedCommand: 'deno --version', + exitCode: undefined, + signal: undefined, + signalDescription: undefined, + stdout: '', + stderr: '', + failed: true, + timedOut: false, + isCanceled: false, + killed: false +} +No globalVersion or semver not satisfied. globalVersion: undefined, versionRange: ^1.32.5 +Using cached Deno CLI from/file/path + + ┌──────────────────────────────────────────────────┐ + │ │ + │ ◈ Server now ready on http://localhost:88888 │ + │ │ + └──────────────────────────────────────────────────┘" +`; + +exports[`frameworks/framework-detection > should start custom command if framework=#custom, command and targetPort are configured 1`] = ` +"◈ Netlify Dev ◈ +◈ Setting up local development server +◈ Starting Netlify Dev with #custom +hello +◈ \\"echo hello\\" exited with code *. Shutting down Netlify Dev server" +`; + +exports[`frameworks/framework-detection > should throw if framework=#custom but command is missing 1`] = ` +"◈ Netlify Dev ◈ +◈ 'command' and 'targetPort' properties are required when 'framework' is set to '#custom'" +`; + +exports[`frameworks/framework-detection > should throw if framework=#custom but targetPort is missing 1`] = ` +"◈ Netlify Dev ◈ +◈ 'command' and 'targetPort' properties are required when 'framework' is set to '#custom'" +`; + +exports[`frameworks/framework-detection > should throw when forcing a non supported framework 1`] = ` +"◈ Netlify Dev ◈ +◈ Invalid framework \\"to-infinity-and-beyond-js\\". It should be one of: *" +`; + +exports[`frameworks/framework-detection > should use static server when --dir flag is passed 1`] = ` +"◈ Netlify Dev ◈ +◈ Using simple static server because '--dir' flag was specified +◈ Running static server from \\"site-with-index-file/public\\" +◈ Setting up local development server + +◈ Static server listening to 88888 + + ┌──────────────────────────────────────────────────┐ + │ │ + │ ◈ Server now ready on http://localhost:88888 │ + │ │ + └──────────────────────────────────────────────────┘" +`; + +exports[`frameworks/framework-detection > should use static server when framework is set to #static 1`] = ` +"◈ Netlify Dev ◈ +◈ Using simple static server because '[dev.framework]' was set to '#static' +◈ Unable to determine public folder to serve files from. Using current working directory +◈ Setup a netlify.toml file with a [dev] section to specify your dev server settings. +◈ See docs at: https://cli.netlify.com/netlify-dev#project-detection +◈ Running static server from \\"site-with-index-file\\" +◈ Setting up local development server + +◈ Static server listening to 88888 + + ┌──────────────────────────────────────────────────┐ + │ │ + │ ◈ Server now ready on http://localhost:88888 │ + │ │ + └──────────────────────────────────────────────────┘" +`; + +exports[`frameworks/framework-detection > should warn if using static server and \`targetPort\` is configured 1`] = ` +"◈ Netlify Dev ◈ +◈ Using simple static server because '--dir' flag was specified +◈ Ignoring 'targetPort' setting since using a simple static server. +◈ Use --staticServerPort or [dev.staticServerPort] to configure the static server port +◈ Running static server from \\"site-with-index-file/public\\" +◈ Setting up local development server + +◈ Static server listening to 88888 + + ┌──────────────────────────────────────────────────┐ + │ │ + │ ◈ Server now ready on http://localhost:88888 │ + │ │ + └──────────────────────────────────────────────────┘" +`; diff --git a/tests/integration/frameworks/framework-detection.test.mjs b/tests/integration/frameworks/framework-detection.test.mjs new file mode 100644 index 00000000000..4ca2b812ed8 --- /dev/null +++ b/tests/integration/frameworks/framework-detection.test.mjs @@ -0,0 +1,383 @@ +import execa from 'execa' +import fetch from 'node-fetch' +import { describe, test } from 'vitest' + +import cliPath from '../utils/cli-path.cjs' +import { getExecaOptions, withDevServer } from '../utils/dev-server.cjs' +import { DOWN, answerWithValue, handleQuestions } from '../utils/handle-questions.cjs' +import { withSiteBuilder } from '../utils/site-builder.cjs' +import { normalize } from '../utils/snapshots.cjs' + +const content = 'Hello World!' + +describe.concurrent('frameworks/framework-detection', () => { + test('should default to process.cwd() and static server', async (t) => { + await withSiteBuilder('site-with-index-file', async (builder) => { + await builder + .withContentFile({ + path: 'index.html', + content, + }) + .buildAsync() + + await withDevServer({ cwd: builder.directory }, async ({ output, url }) => { + const response = await fetch(url).then((res) => res.text()) + t.expect(response).toEqual(content) + + t.expect(normalize(output, { duration: true, filePath: true })).toMatchSnapshot() + }) + }) + }) + + test('should use static server when --dir flag is passed', async (t) => { + await withSiteBuilder('site-with-index-file', async (builder) => { + await builder + .withContentFile({ + path: 'public/index.html', + content, + }) + .buildAsync() + + await withDevServer({ cwd: builder.directory, args: ['--dir', 'public'] }, async ({ output, url }) => { + const response = await fetch(url).then((res) => res.text()) + t.expect(response).toEqual(content) + + t.expect(normalize(output, { duration: true, filePath: true })).toMatchSnapshot() + }) + }) + }) + + test('should use static server when framework is set to #static', async (t) => { + await withSiteBuilder('site-with-index-file', async (builder) => { + await builder + .withContentFile({ + path: 'index.html', + content, + }) + .withNetlifyToml({ config: { dev: { framework: '#static' } } }) + .buildAsync() + + await withDevServer({ cwd: builder.directory }, async ({ output, url }) => { + const response = await fetch(url).then((res) => res.text()) + t.expect(response).toEqual(content) + + t.expect(normalize(output, { duration: true, filePath: true })).toMatchSnapshot() + }) + }) + }) + + test('should log the command if using static server and `command` is configured', async (t) => { + await withSiteBuilder('site-with-index-file', async (builder) => { + await builder + .withContentFile({ + path: 'public/index.html', + content, + }) + .buildAsync() + + await withDevServer( + { cwd: builder.directory, args: ['--dir', 'public', '--command', 'npm run start'] }, + async ({ output, url }) => { + const response = await fetch(url).then((res) => res.text()) + t.expect(response).toEqual(content) + + t.expect(normalize(output, { duration: true, filePath: true })).toMatchSnapshot() + }, + ) + }) + }) + + test('should warn if using static server and `targetPort` is configured', async (t) => { + await withSiteBuilder('site-with-index-file', async (builder) => { + await builder + .withContentFile({ + path: 'public/index.html', + content, + }) + .buildAsync() + + await withDevServer( + { cwd: builder.directory, args: ['--dir', 'public', '--target-port', '3000'] }, + async ({ output, url }) => { + const response = await fetch(url).then((res) => res.text()) + t.expect(response).toEqual(content) + + t.expect(normalize(output, { duration: true, filePath: true })).toMatchSnapshot() + }, + ) + }) + }) + + test('should run `command` when both `command` and `targetPort` are configured', async (t) => { + await withSiteBuilder('empty-site', async (builder) => { + await builder.withNetlifyToml({ config: { build: { publish: 'public' } } }).buildAsync() + + // a failure is expected since we use `echo hello` instead of starting a server + const error = await withDevServer( + { cwd: builder.directory, args: ['--command', 'echo hello', '--target-port', '3000'] }, + () => {}, + true, + ).catch((error_) => error_) + + t.expect(normalize(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + }) + }) + + test('should force a specific framework when configured', async (t) => { + await withSiteBuilder('site-with-mocked-cra', async (builder) => { + await builder.withNetlifyToml({ config: { dev: { framework: 'create-react-app' } } }).buildAsync() + + // a failure is expected since this is not a true create-react-app project + const error = await withDevServer({ cwd: builder.directory }, () => {}, true).catch((error_) => error_) + t.expect(normalize(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + }) + }) + + test('should throw when forcing a non supported framework', async (t) => { + await withSiteBuilder('site-with-unknown-framework', async (builder) => { + await builder.withNetlifyToml({ config: { dev: { framework: 'to-infinity-and-beyond-js' } } }).buildAsync() + + const error = await withDevServer({ cwd: builder.directory }, () => {}, true).catch((error_) => error_) + t.expect(normalize(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + }) + }) + + test('should detect a known framework', async (t) => { + await withSiteBuilder('site-with-cra', async (builder) => { + await builder + .withPackageJson({ + packageJson: { dependencies: { 'react-scripts': '1.0.0' }, scripts: { start: 'react-scripts start' } }, + }) + .buildAsync() + + // a failure is expected since this is not a true create-react-app project + const error = await withDevServer({ cwd: builder.directory }, () => {}, true).catch((error_) => error_) + t.expect(normalize(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + }) + }) + + test('should throw if framework=#custom but command is missing', async (t) => { + await withSiteBuilder('site-with-framework-and-no-command', async (builder) => { + await builder.withNetlifyToml({ config: { dev: { framework: '#custom' } } }).buildAsync() + + const error = await withDevServer( + { cwd: builder.directory, args: ['--target-port', '3000'] }, + () => {}, + true, + ).catch((error_) => error_) + t.expect(normalize(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + }) + }) + + test('should throw if framework=#custom but targetPort is missing', async (t) => { + await withSiteBuilder('site-with-framework-and-no-command', async (builder) => { + await builder.withNetlifyToml({ config: { dev: { framework: '#custom' } } }).buildAsync() + + const error = await withDevServer( + { cwd: builder.directory, args: ['--command', 'echo hello'] }, + () => {}, + true, + ).catch((error_) => error_) + t.expect(normalize(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + }) + }) + + test('should start custom command if framework=#custom, command and targetPort are configured', async (t) => { + await withSiteBuilder('site-with-custom-framework', async (builder) => { + await builder.withNetlifyToml({ config: { dev: { framework: '#custom', publish: 'public' } } }).buildAsync() + + const error = await withDevServer( + { cwd: builder.directory, args: ['--command', 'echo hello', '--target-port', '3000'] }, + () => {}, + true, + ).catch((error_) => error_) + t.expect(normalize(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + }) + }) + + test(`should print specific error when command doesn't exist`, async (t) => { + await withSiteBuilder('site-with-custom-framework', async (builder) => { + await builder.buildAsync() + + const error = await withDevServer( + { + cwd: builder.directory, + args: [ + '--command', + 'oops-i-did-it-again forgot-to-use-a-valid-command', + '--target-port', + '3000', + '--framework', + '#custom', + ], + }, + () => {}, + true, + ).catch((error_) => error_) + + t.expect(normalize(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + }) + }) + + 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 asyncErrorBlock = async () => { + const childProcess = execa(cliPath, ['dev', '--offline'], getExecaOptions({ cwd: builder.directory })) + + handleQuestions(childProcess, [ + { + question: 'Multiple possible dev commands found', + answer: answerWithValue(DOWN), + }, + ]) + + await childProcess + } + const error = await asyncErrorBlock().catch((error_) => error_) + t.expect(normalize(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + }) + }) + + 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 withDevServer( + { cwd: builder.directory, args: ['--command', 'echo hello', '--target-port', '3000'] }, + () => {}, + true, + ).catch((error_) => error_) + + t.expect(normalize(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + }) + }) + + 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 fetch(url).then((res) => res.text()) + t.expect(response).toEqual(content) + + t.expect(normalize(output, { duration: true, filePath: true })).toMatchSnapshot() + }) + }) + }) + + test('should pass framework-info env to framework sub process', async (t) => { + await withSiteBuilder('site-with-gatsby', async (builder) => { + await builder + .withPackageJson({ + packageJson: { + dependencies: { nuxt3: '^2.0.0' }, + scripts: { dev: 'node -p process.env.NODE_VERSION' }, + }, + }) + .buildAsync() + + // a failure is expected since this is not a true Gatsby project + const error = await withDevServer({ cwd: builder.directory }, () => {}, true).catch((error_) => error_) + t.expect(normalize(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + }) + }) + + 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 withDevServer({ cwd: builder.directory }, () => {}, true).catch((error_) => error_) + t.expect(error.stdout.includes(`Failed running command: remix watch. Please verify 'remix' exists`)).toBe(true) + }) + }) + + 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 withDevServer({ cwd: builder.directory }, () => {}, true).catch((error_) => error_) + t.expect(error.stdout.includes(`Failed running command: remix watch. Please verify 'remix' exists`)).toBe(true) + }) + }) + + 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, no-undef + 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 fetch(`${url}/hello`).then((res) => res.json()) + t.expect(response).toStrictEqual({ CONTEXT_CHECK: 'PRODUCTION' }) + + t.expect(normalize(output, { duration: true, filePath: true })).toMatchSnapshot() + }, + ) + }) + }) +}) diff --git a/tests/integration/snapshots/600.framework-detection.test.cjs.md b/tests/integration/snapshots/600.framework-detection.test.cjs.md deleted file mode 100644 index f93f27bdc77..00000000000 --- a/tests/integration/snapshots/600.framework-detection.test.cjs.md +++ /dev/null @@ -1,302 +0,0 @@ -# Snapshot report for `tests/integration/600.framework-detection.test.cjs` - -The actual snapshot is saved in `600.framework-detection.test.cjs.snap`. - -Generated by [AVA](https://avajs.dev). - -## should default to process.cwd() and static server - -> Snapshot 1 - - `◈ Netlify Dev ◈␊ - ◈ No app server detected. Using simple static server␊ - ◈ Unable to determine public folder to serve files from. Using current working directory␊ - ◈ Setup a netlify.toml file with a [dev] section to specify your dev server settings.␊ - ◈ See docs at: https://cli.netlify.com/netlify-dev#project-detection␊ - ◈ Running static server from "site-with-index-file"␊ - ◈ Setting up local development server␊ - ␊ - ◈ Static server listening to 88888␊ - ␊ - ┌──────────────────────────────────────────────────┐␊ - │ │␊ - │ ◈ Server now ready on http://localhost:88888 │␊ - │ │␊ - └──────────────────────────────────────────────────┘` - -## should use static server when --dir flag is passed - -> Snapshot 1 - - `◈ Netlify Dev ◈␊ - ◈ Using simple static server because '--dir' flag was specified␊ - ◈ Running static server from "site-with-index-file/public"␊ - ◈ Setting up local development server␊ - ␊ - ◈ Static server listening to 88888␊ - ␊ - ┌──────────────────────────────────────────────────┐␊ - │ │␊ - │ ◈ Server now ready on http://localhost:88888 │␊ - │ │␊ - └──────────────────────────────────────────────────┘` - -## should use static server when framework is set to #static - -> Snapshot 1 - - `◈ Netlify Dev ◈␊ - ◈ Using simple static server because '[dev.framework]' was set to '#static'␊ - ◈ Unable to determine public folder to serve files from. Using current working directory␊ - ◈ Setup a netlify.toml file with a [dev] section to specify your dev server settings.␊ - ◈ See docs at: https://cli.netlify.com/netlify-dev#project-detection␊ - ◈ Running static server from "site-with-index-file"␊ - ◈ Setting up local development server␊ - ␊ - ◈ Static server listening to 88888␊ - ␊ - ┌──────────────────────────────────────────────────┐␊ - │ │␊ - │ ◈ Server now ready on http://localhost:88888 │␊ - │ │␊ - └──────────────────────────────────────────────────┘` - -## should log the command if using static server and `command` is configured - -> Snapshot 1 - - `◈ Netlify Dev ◈␊ - ◈ Using simple static server because '--dir' flag was specified␊ - ◈ Running static server from "site-with-index-file/public"␊ - ◈ Setting up local development server␊ - ␊ - ◈ Static server listening to 88888␊ - ␊ - ┌──────────────────────────────────────────────────┐␊ - │ │␊ - │ ◈ Server now ready on http://localhost:88888 │␊ - │ │␊ - └──────────────────────────────────────────────────┘` - -## should warn if using static server and `targetPort` is configured - -> Snapshot 1 - - `◈ Netlify Dev ◈␊ - ◈ Using simple static server because '--dir' flag was specified␊ - ◈ Ignoring 'targetPort' setting since using a simple static server.␊ - ◈ Use --staticServerPort or [dev.staticServerPort] to configure the static server port␊ - ◈ Running static server from "site-with-index-file/public"␊ - ◈ Setting up local development server␊ - ␊ - ◈ Static server listening to 88888␊ - ␊ - ┌──────────────────────────────────────────────────┐␊ - │ │␊ - │ ◈ Server now ready on http://localhost:88888 │␊ - │ │␊ - └──────────────────────────────────────────────────┘` - -## should run `command` when both `command` and `targetPort` are configured - -> Snapshot 1 - - `◈ Netlify Dev ◈␊ - ◈ Setting up local development server␊ - ◈ Starting Netlify Dev with custom config␊ - hello␊ - ◈ "echo hello" exited with code *. Shutting down Netlify Dev server` - -## should force a specific framework when configured - -> Snapshot 1 - - `◈ Netlify Dev ◈␊ - ◈ Setting up local development server␊ - ◈ Starting Netlify Dev with Create React App␊ - ◈ Failed running command: react-scripts start. Please verify 'react-scripts' exists` - -## should throw when forcing a non supported framework - -> Snapshot 1 - - `◈ Netlify Dev ◈␊ - ◈ Invalid framework "to-infinity-and-beyond-js". It should be one of: *` - -## should detect a known framework - -> Snapshot 1 - - `◈ Netlify Dev ◈␊ - ◈ Setting up local development server␊ - ◈ Starting Netlify Dev with Create React App␊ - ␊ - > start␊ - > react-scripts start␊ - ␊ - ◈ Command failed with exit code *: npm run start. Shutting down Netlify Dev server` - -## should throw if framework=#custom but command is missing - -> Snapshot 1 - - `◈ Netlify Dev ◈␊ - ◈ 'command' and 'targetPort' properties are required when 'framework' is set to '#custom'` - -## should throw if framework=#custom but targetPort is missing - -> Snapshot 1 - - `◈ Netlify Dev ◈␊ - ◈ 'command' and 'targetPort' properties are required when 'framework' is set to '#custom'` - -## should start custom command if framework=#custom, command and targetPort are configured - -> Snapshot 1 - - `◈ Netlify Dev ◈␊ - ◈ Setting up local development server␊ - ◈ Starting Netlify Dev with #custom␊ - hello␊ - ◈ "echo hello" exited with code *. Shutting down Netlify Dev server` - -## should print specific error when command doesn't exist - -> Snapshot 1 - - `◈ Netlify Dev ◈␊ - ◈ Unable to determine public folder to serve files from. Using current working directory␊ - ◈ Setup a netlify.toml file with a [dev] section to specify your dev server settings.␊ - ◈ See docs at: https://cli.netlify.com/netlify-dev#project-detection␊ - ◈ Setting up local development server␊ - ◈ Starting Netlify Dev with #custom␊ - ◈ Failed running command: oops-i-did-it-again forgot-to-use-a-valid-command. Please verify 'oops-i-did-it-again' exists` - -## should prompt when multiple frameworks are detected - -> Snapshot 1 - - `◈ Netlify Dev ◈␊ - ? Multiple possible dev commands found (Use arrow keys or type to search)␊ - > [Gatsby] 'npm run develop' ␊ - [Create React App] 'npm run start' ? Multiple possible dev commands found ␊ - [Gatsby] 'npm run develop' ␊ - > [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␊ - ␊ - > start␊ - > react-scripts start␊ - ␊ - ◈ Command failed with exit code *: npm run start. Shutting down Netlify Dev server` - -## should not run framework detection if command and targetPort are configured - -> Snapshot 1 - - `◈ Netlify Dev ◈␊ - ◈ Unable to determine public folder to serve files from. Using current working directory␊ - ◈ Setup a netlify.toml file with a [dev] section to specify your dev server settings.␊ - ◈ See docs at: https://cli.netlify.com/netlify-dev#project-detection␊ - ◈ Setting up local development server␊ - ◈ Starting Netlify Dev with custom config␊ - hello␊ - ◈ "echo hello" exited with code *. Shutting down Netlify Dev server` - -## should filter frameworks with no dev command - -> Snapshot 1 - - `◈ Netlify Dev ◈␊ - ◈ No app server detected. Using simple static server␊ - ◈ Unable to determine public folder to serve files from. Using current working directory␊ - ◈ Setup a netlify.toml file with a [dev] section to specify your dev server settings.␊ - ◈ See docs at: https://cli.netlify.com/netlify-dev#project-detection␊ - ◈ Running static server from "site-with-gulp"␊ - ◈ Setting up local development server␊ - ␊ - ◈ Static server listening to 88888␊ - ␊ - ┌──────────────────────────────────────────────────┐␊ - │ │␊ - │ ◈ Server now ready on http://localhost:88888 │␊ - │ │␊ - └──────────────────────────────────────────────────┘` - -## should run and serve a production build when using the `serve` command - -> Snapshot 1 - - `◈ Injected netlify.toml file env var: CONTEXT_CHECK␊ - ◈ Using simple static server because '[dev.framework]' was set to '#static'␊ - ◈ Running static server from "site-with-framework/public"␊ - ◈ Building site for production␊ - ◈ Changes will not be hot-reloaded, so if you need to rebuild your site you must exit and run 'netlify serve' again␊ - ​␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - > Version␊ - @netlify/build 0.0.0␊ - ​␊ - > Flags␊ - configPath:/file/path␊ - offline: true␊ - outputConfigPath:/file/path␊ - ​␊ - > Current directory␊ - /file/path␊ - ​␊ - > Config file␊ - /file/path␊ - ​␊ - > Context␊ - production␊ - ​␊ - > Loading plugins␊ - -/file/path from netlify.toml␊ - ​␊ - /file/path (onPreBuild event) ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - Netlify configuration property "redirects" value changed to [ { from: /file/path', to: /file/path' } ].␊ - ​␊ - /file/path onPreBuild completed in Xms)␊ - ​␊ - Functions bundling ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - Packaging Functions from new_functions directory:␊ - - hello.js␊ - ​␊ - ​␊ - (Functions bundling completed in Xms)␊ - ​␊ - Save deploy artifacts ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - ​␊ - (Save deploy artifacts completed in Xms)␊ - ​␊ - Netlify Build Complete ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - (Netlify Build completed in Xms)␊ - ␊ - ◈ Static server listening to 88888␊ - ␊ - ┌──────────────────────────────────────────────────┐␊ - │ │␊ - │ ◈ Server now ready on http://localhost:88888 │␊ - │ │␊ - └──────────────────────────────────────────────────┘` diff --git a/tests/integration/snapshots/600.framework-detection.test.cjs.snap b/tests/integration/snapshots/600.framework-detection.test.cjs.snap deleted file mode 100644 index d6ceebca537..00000000000 Binary files a/tests/integration/snapshots/600.framework-detection.test.cjs.snap and /dev/null differ