Skip to content

Commit

Permalink
Netlify Dev: Add framework = "#custom" option (#843)
Browse files Browse the repository at this point in the history
* Add framework = "#custom" option

* Error if framework not #custom but command and targetPort

* Grammar

* Improve static server settings

* Throw more errors ‼️

* Grammar

* Improve static server log

* devConfig: Get publish directory from build as well

* Dev: Pass site.root to serverSettings

* Fix tests

* Add new test for no config

* Move tests out of src

* Dev test: Always cleanup

* isStatic: Fix a loop mistake 🤦🏽‍♂️

* Dev: Fix force behavior on redirects

* Add tests for create-react-app

* Fix tests

* Fix a bug in framework name

* Apply settings from framework

* Add test for create-react-app framework option

* GH Actions: Install cra dependencies

* Use yarn for cra

* Build tests: Fix bin path

* Tests: Use random ports

* Test: Add framework to netlify.toml

* Random port util

* Whitespace

* Change famework properties to match file names

* Detect Server: Fix command for npm

* Fix tests

* Detect Server: Better error log

* Improve docs

* Move npm command logic up
  • Loading branch information
RaeesBhatti committed May 4, 2020
1 parent 78cc13f commit 23b8dae
Show file tree
Hide file tree
Showing 60 changed files with 11,479 additions and 86 deletions.
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: cd tests/site-cra && yarn && cd ../..
- run: npm test
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
10 changes: 6 additions & 4 deletions docs/netlify-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,10 @@ Netlify Dev is meant to work with zero config for the majority of users, by usin
# sample netlify.toml
[build]
command = "yarn run build"
functions = "functions" # netlify dev uses this to know where to scaffold and serve your functions
functions = "functions" # netlify dev uses this directory to scaffold and serve your functions
publish = "dist"
# note: each of these fields are OPTIONAL
# note: each of these fields are OPTIONAL, with an exception that when you're specifying "command" and "port", you must specify framework = "#custom"
[dev]
command = "yarn start" # Command to start your dev server
port = 8888 # Port that the dev server will be accessible on
Expand All @@ -129,6 +129,7 @@ Netlify Dev will attempt to detect the SSG or build command that you are using,
# sample dev block in the toml
# note: each of these fields are OPTIONAL and should only be used if you need an override
[dev]
framework = "#custom"
command = "yarn start" # Command to start your dev server
port = 8888 # Port that the dev server will be accessible on
publish = "dist" # If you use a _redirect file, provide the path to your static content folder
Expand All @@ -145,8 +146,9 @@ against your project.

The `framework` option should be one of the available
[project types which Netlify Dev can detect](https://github.com/netlify/cli/tree/master/src/detectors)
or `#auto` (default) to test all available detectors or `#staic` for a static
file server.
or `#auto` (default) to test all available detectors, `#static` for a static
file server or `#custom` to use `command` option to run an app server and
`targetPort` option to connect to it.

## Explanation of ports in Netlify Dev

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@
},
"ava": {
"files": [
"src/**/*.test.js"
"src/**/*.test.js",
"tests/*.test.js"
],
"cache": true,
"concurrency": 5,
Expand Down
15 changes: 7 additions & 8 deletions src/commands/dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ function addonUrl(addonUrls, req) {
async function isStatic(pathname, publicFolder) {
const alternatives = alternativePathsFor(pathname).map(p => path.resolve(publicFolder, p.substr(1)))

for (const p in alternatives) {
for (const i in alternatives) {
const p = alternatives[i]
try {
const pathStats = await stat(p)
if (pathStats.isFile()) return true
Expand Down Expand Up @@ -282,10 +283,7 @@ async function serveRedirect(req, res, proxy, match, options) {
return render404(options.publicFolder)
}

if (
match.force ||
((!(await isStatic(reqUrl.pathname, options.publicFolder)) || options.framework) && match.status !== 404)
) {
if (match.force || (!(await isStatic(reqUrl.pathname, options.publicFolder) || options.framework) && match.status !== 404)) {
const dest = new url.URL(match.to, `${reqUrl.protocol}//${reqUrl.host}`)
if (isRedirect(match)) {
res.writeHead(match.status, {
Expand All @@ -311,7 +309,7 @@ async function serveRedirect(req, res, proxy, match, options) {
const destURL = dest.pathname + (urlParams.toString() && '?' + urlParams.toString())

let status
if (isInternal(destURL) || !options.framework) {
if (match.force || isInternal(destURL) || !options.framework) {
req.url = destURL
status = match.status
console.log(`${NETLIFYDEVLOG} Rewrote URL to `, req.url)
Expand Down Expand Up @@ -397,8 +395,9 @@ class DevCommand extends Command {
const devConfig = {
framework: '#auto',
...(config.build.functions && { functions: config.build.functions }),
...(config.build.publish && { publish: config.build.publish }),
...config.dev,
...flags
...flags,
}
let addonUrls = {}

Expand All @@ -420,7 +419,7 @@ class DevCommand extends Command {
Object.entries(vars).forEach(([key, val]) => (process.env[key] = val))
}

let settings = await serverSettings(devConfig, flags, this.log)
let settings = await serverSettings(devConfig, flags, site.root, this.log)

await startDevServer(settings, this.log)

Expand Down
2 changes: 1 addition & 1 deletion src/detectors/angular.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module.exports = function() {
}

return {
framework: '@angular/cli',
framework: 'angular',
command: getYarnOrNPMCommand(),
port: 8888,
proxyPort: 4200,
Expand Down
2 changes: 1 addition & 1 deletion src/detectors/ember.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module.exports = function() {
}

return {
framework: 'ember-cli',
framework: 'ember',
command: getYarnOrNPMCommand(),
port: 8888,
proxyPort: 4200,
Expand Down
2 changes: 1 addition & 1 deletion src/detectors/next.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ module.exports = function() {
possibleArgsArrs.push(['next'])
}
return {
framework: 'next.js',
framework: 'next',
command: getYarnOrNPMCommand(),
port: 8888,
proxyPort: 3000,
Expand Down
2 changes: 1 addition & 1 deletion src/detectors/quasar.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module.exports = function() {
}

return {
framework: 'quasar-cli',
framework: 'quasar',
command: getYarnOrNPMCommand(),
port: 8888,
proxyPort: 8081,
Expand Down
2 changes: 1 addition & 1 deletion src/detectors/vue.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ module.exports = function() {
}

return {
framework: 'vue-cli',
framework: 'vue',
command: getYarnOrNPMCommand(),
port: 8888,
proxyPort: 8080,
Expand Down
5 changes: 0 additions & 5 deletions src/tests/utils/cliPath.js

This file was deleted.

106 changes: 67 additions & 39 deletions src/utils/detect-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@ const inquirer = require('inquirer')
const fuzzy = require('fuzzy')
const fs = require('fs')

module.exports.serverSettings = async (devConfig, flags, log) => {
module.exports.serverSettings = async (devConfig, flags, projectDir, log) => {
let settings = { env: { ...process.env } }
const detectorsFiles = fs.readdirSync(path.join(__dirname, '..', 'detectors')).filter(x => x.endsWith('.js')) // only accept .js detector files

if (typeof devConfig.framework !== 'string') throw new Error('Invalid "framework" option provided in config')

if (devConfig.framework === '#auto') {
if (flags.dir) {
settings = await getStaticServerSettings(settings, flags, projectDir, log)
if (['command','targetPort'].some(p => devConfig.hasOwnProperty(p))) {
throw new Error('"command" or "targetPort" options cannot be used in conjunction with "dir" flag')
}
} else if (devConfig.framework === '#auto' && !(devConfig.command && devConfig.targetPort)) {
let settingsArr = []
const detectors = detectorsFiles.map(det => {
try {
Expand Down Expand Up @@ -48,15 +53,23 @@ module.exports.serverSettings = async (devConfig, flags, log) => {
})
settings = chosenSetting // finally! we have a selected option

console.log(
`Add \`framework = "${chosenSetting.framework}"\` to [dev] section of your netlify.toml to avoid this selection prompt next time`
)
log(`Add \`framework = "${chosenSetting.framework}"\` to [dev] section of your netlify.toml to avoid this selection prompt next time`)
}
} else if (devConfig.framework === '#custom' || (devConfig.command && devConfig.targetPort)) {
settings.framework = '#custom'
if (devConfig.framework && !['command', 'targetPort'].every(p => devConfig.hasOwnProperty(p))) {
throw new Error('"command" and "targetPort" properties are required when "framework" is set to "#custom"')
}
if (devConfig.framework !== '#custom' && devConfig.command && devConfig.targetPort) {
throw new Error('"framework" option must be set to "#custom" when specifying both "command" and "targetPort" options')
}
} else if (devConfig.framework === '#static') {
settings.framework = devConfig.framework
// Do nothing
} else {
const detectorName = detectorsFiles.find(dt => `${dt}.js` === devConfig.framework)
if (!detectorName) throw new Error('Unsupported value provided for "framework" option in config')
const detectorName = detectorsFiles.find(dt => dt === `${devConfig.framework}.js`)
if (!detectorName) throw new Error('Unsupported value provided for "framework" option in config. Please use "#custom"' +
` if you're using a framework not intrinsically supported by Netlify Dev. E.g. with "command" and "targetPort" options.` +
` Or use one of following values: ${detectorsFiles.map(f => `"${path.parse(f).name}"`).join(', ')}`)

const detector = loadDetector(detectorName)
const detectorResult = detector()
Expand All @@ -65,49 +78,29 @@ module.exports.serverSettings = async (devConfig, flags, log) => {
`Specified "framework" detector "${devConfig.framework}" did not pass requirements for your project`
)

settings = detectorResult
settings.args = chooseDefaultArgs(detectorResult.possibleArgsArrs)
}

if (settings.command === 'npm' && !['start', 'run'].includes(settings.args[0])) {
settings.args.unshift('run')
}
if (devConfig.command) {
settings.command = assignLoudly(devConfig.command.split(/\s/)[0], settings.command || null, tellUser('command')) // if settings.command is empty, its bc no settings matched
let devConfigArgs = devConfig.command.split(/\s/).slice(1)
settings.args = assignLoudly(devConfigArgs, settings.command || null, tellUser('command')) // if settings.command is empty, its bc no settings matched
}
settings.dist = devConfig.publish || settings.dist // dont loudassign if they dont need it

if (flags.dir || devConfig.framework === '#static' || (!settings.framework && !settings.proxyPort)) {
let dist = settings.dist
if (flags.dir) {
log(`${NETLIFYDEVWARN} Using simple static server because --dir flag was specified`)
} else if (devConfig.framework === '#static') {
log(`${NETLIFYDEVWARN} Using simple static server because "framework" option was set to "#static" in config`)
} else {
log(`${NETLIFYDEVWARN} No app server detected, using simple static server`)
}
if (!dist) {
log(`${NETLIFYDEVLOG} Using current working directory`)
log(`${NETLIFYDEVWARN} Unable to determine public folder to serve files from.`)
log(`${NETLIFYDEVWARN} Setup a netlify.toml file with a [dev] section to specify your dev server settings.`)
log(`${NETLIFYDEVWARN} See docs at: https://cli.netlify.com/netlify-dev#project-detection`)
log(`${NETLIFYDEVWARN} Using current working directory for now...`)
dist = process.cwd()
}
settings = {
env: { ...process.env },
port: 8888,
proxyPort: await getPort({ port: 3999 }),
dist,
...(settings.command ? { command: settings.command, args: settings.args } : { noCmd: true })
}
settings.args = assignLoudly(devConfig.command.split(/\s/).slice(1), [], tellUser('command')) // if settings.command is empty, its bc no settings matched
}
settings.dist = flags.dir || devConfig.publish || settings.dist // dont loudassign if they dont need it

settings.port = devConfig.port || settings.port
if (devConfig.targetPort) {
if (devConfig.targetPort === devConfig.port) {
throw new Error(
'"port" and "targetPort" options cannot have same values. Please consult the documentation for more details: https://cli.netlify.com/netlify-dev#netlifytoml-dev-block'
)
}

if (!settings.command) throw new Error('No "command" specified or detected. The "command" option is required to use "targetPort" option.')
if (flags.dir) throw new Error('"targetPort" option cannot be used in conjunction with "dir" flag which is used to run a static server.')

settings.proxyPort = devConfig.targetPort
settings.urlRegexp = devConfig.urlRegexp || new RegExp(`(http:https://)([^:]+:)${devConfig.targetPort}(/)?`, 'g')
} else if (devConfig.port && devConfig.port === settings.proxyPort) {
Expand All @@ -116,7 +109,17 @@ module.exports.serverSettings = async (devConfig, flags, log) => {
)
}

const port = await getPort({ port: settings.port })
if (!settings.command && !settings.framework && !settings.noCmd) {
settings = await getStaticServerSettings(settings, flags, projectDir, log)
}

if (!settings.proxyPort) throw new Error('No "targetPort" option specified or detected.')

settings.port = devConfig.port || settings.port
if (devConfig.port && devConfig.port === settings.proxyPort) {
throw new Error('The "port" option you specified conflicts with the port of your application. Please use a different value for "port"')
}
const port = await getPort({ port: settings.port || 8888 })
if (port !== settings.port && devConfig.port) {
throw new Error(`Could not acquire required "port": ${settings.port}`)
}
Expand All @@ -129,6 +132,31 @@ module.exports.serverSettings = async (devConfig, flags, log) => {
return settings
}

async function getStaticServerSettings(settings, flags, projectDir, log) {
let dist = settings.dist
if (flags.dir) {
log(`${NETLIFYDEVWARN} Using simple static server because --dir flag was specified`)
dist = flags.dir
} else {
log(`${NETLIFYDEVWARN} No app server detected and no "command" specified`)
}
if (!dist) {
log(`${NETLIFYDEVLOG} Using current working directory`)
log(`${NETLIFYDEVWARN} Unable to determine public folder to serve files from`)
log(`${NETLIFYDEVWARN} Setup a netlify.toml file with a [dev] section to specify your dev server settings.`)
log(`${NETLIFYDEVWARN} See docs at: https://cli.netlify.com/netlify-dev#project-detection`)
dist = process.cwd()
}
log(`${NETLIFYDEVWARN} Running static server from "${path.relative(path.dirname(projectDir), dist)}"`)
return {
env: { ...process.env },
noCmd: true,
port: 8888,
proxyPort: await getPort({ port: 3999 }),
dist,
}
}

const tellUser = settingsField => dV =>
// eslint-disable-next-line no-console
console.log(
Expand Down
Loading

0 comments on commit 23b8dae

Please sign in to comment.