Skip to content
This repository was archived by the owner on Sep 12, 2019. It is now read-only.

Commit f749d38

Browse files
authored
Merge pull request #96 from netlify/cra-detector-update
generalize dependency and config file matching to a common utility and add UI for multiple matching detectors and scripts
2 parents c9be7ca + a1f2aec commit f749d38

File tree

11 files changed

+290
-167
lines changed

11 files changed

+290
-167
lines changed

src/commands/dev/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,8 @@ class DevCommand extends Command {
182182
}
183183
process.env.NETLIFY_DEV = "true";
184184

185-
let settings = serverSettings(config.dev);
185+
let settings = await serverSettings(config.dev);
186+
186187
if (!(settings && settings.command)) {
187188
this.log(
188189
`${NETLIFYDEV} No dev server detected, using simple static server`

src/detect-server.js

Lines changed: 104 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,130 @@
11
const path = require("path");
22
const chalk = require("chalk");
33
const NETLIFYDEV = `[${chalk.cyan("Netlify Dev")}]`;
4-
5-
const detectors = require("fs")
4+
const inquirer = require("inquirer");
5+
const fs = require("fs");
6+
const detectors = fs
67
.readdirSync(path.join(__dirname, "detectors"))
78
.filter(x => x.endsWith(".js")) // only accept .js detector files
89
.map(det => require(path.join(__dirname, `detectors/${det}`)));
910

10-
module.exports.serverSettings = devConfig => {
11-
let settings = null;
11+
module.exports.serverSettings = async devConfig => {
12+
let settingsArr = [],
13+
settings = null;
1214
for (const i in detectors) {
13-
settings = detectors[i]();
14-
if (settings) {
15-
break;
15+
const detectorResult = detectors[i]();
16+
if (detectorResult) settingsArr.push(detectorResult);
17+
}
18+
if (settingsArr.length === 1) {
19+
// vast majority of projects will only have one matching detector
20+
settings = settingsArr[0];
21+
settings.args = settings.possibleArgsArrs[0]; // just pick the first one
22+
if (!settings.args) {
23+
console.error(
24+
"empty args assigned, this is an internal Netlify Dev bug, please report your settings and scripts so we can improve"
25+
);
26+
const { scripts } = JSON.parse(
27+
fs.readFileSync("package.json", { encoding: "utf8" })
28+
);
29+
process.exit(1);
30+
}
31+
} else if (settingsArr.length > 1) {
32+
/** multiple matching detectors, make the user choose */
33+
// lazy loading on purpose
34+
inquirer.registerPrompt(
35+
"autocomplete",
36+
require("inquirer-autocomplete-prompt")
37+
);
38+
const fuzzy = require("fuzzy");
39+
const scriptInquirerOptions = formatSettingsArrForInquirer(settingsArr);
40+
const { chosenSetting } = await inquirer.prompt({
41+
name: "chosenSetting",
42+
message: `Multiple possible start commands found`,
43+
type: "autocomplete",
44+
source: async function(_, input) {
45+
if (!input || input === "") {
46+
return scriptInquirerOptions;
47+
}
48+
// only show filtered results
49+
return filterSettings(scriptInquirerOptions, input);
50+
}
51+
});
52+
settings = chosenSetting; // finally! we have a selected option
53+
// TODO: offer to save this setting to netlify.toml so you dont keep doing this
54+
55+
/** utiltities for the inquirer section above */
56+
function filterSettings(scriptInquirerOptions, input) {
57+
const filteredSettings = fuzzy.filter(
58+
input,
59+
scriptInquirerOptions.map(x => x.name)
60+
);
61+
const filteredSettingNames = filteredSettings.map(x =>
62+
input ? x.string : x
63+
);
64+
return scriptInquirerOptions.filter(t =>
65+
filteredSettingNames.includes(t.name)
66+
);
67+
}
68+
69+
/** utiltities for the inquirer section above */
70+
function formatSettingsArrForInquirer(settingsArr) {
71+
let ans = [];
72+
settingsArr.forEach(setting => {
73+
setting.possibleArgsArrs.forEach(args => {
74+
ans.push({
75+
name: `[${chalk.yellow(setting.type)}] ${
76+
setting.command
77+
} ${args.join(" ")}`,
78+
value: { ...setting, args },
79+
short: setting.type + "-" + args.join(" ")
80+
});
81+
});
82+
});
83+
return ans;
1684
}
1785
}
1886

87+
/** everything below assumes we have settled on one detector */
88+
1989
if (devConfig) {
2090
settings = settings || {};
2191
if (devConfig.command) {
22-
assignLoudly(settings, "command", devConfig.command.split(/\s/)[0]);
23-
assignLoudly(settings, "args", devConfig.command.split(/\s/).slice(1));
92+
settings.command = assignLoudly(
93+
devConfig.command,
94+
settings.command.split(/\s/)[0]
95+
);
96+
settings.args = assignLoudly(
97+
devConfig.command,
98+
settings.command.split(/\s/).slice(1)
99+
);
24100
}
25101
if (devConfig.port) {
26-
assignLoudly(settings, "proxyPort", devConfig["port"]);
27-
assignLoudly(
28-
settings,
29-
"urlRegexp",
102+
settings.proxyPort = assignLoudly(devConfig.port, settings.proxyPort);
103+
const regexp =
30104
devConfig.urlRegexp ||
31-
new RegExp(`(http://)([^:]+:)${devConfig.port}(/)?`, "g")
32-
);
105+
new RegExp(`(http://)([^:]+:)${devConfig.port}(/)?`, "g");
106+
settings.urlRegexp = assignLoudly(settings.urlRegexp);
33107
}
34-
assignLoudly(settings, "dist", devConfig["publish"]);
108+
settings.dist = assignLoudly(devConfig.publish, settings.dist);
35109
}
36-
37110
return settings;
38111
};
39112

40-
// mutates the settings field, but tell the user if it does
41-
function assignLoudly(settings, settingsField, newValue) {
42-
if (settings[settingsField] !== newValue) {
43-
// silent if command is exactly same
113+
// if first arg is undefined, use default, but tell user about it in case it is unintentional
114+
function assignLoudly(
115+
optionalValue,
116+
defaultValue,
117+
tellUser = dV =>
44118
console.log(
45119
`${NETLIFYDEV} Overriding ${settingsField} with setting derived from netlify.toml [dev] block: `,
46-
newValue
47-
);
48-
settings[settingsField] === newValue;
120+
dV
121+
)
122+
) {
123+
if (defaultValue === undefined) throw new Error("must have a defaultValue");
124+
if (defaultValue !== optionalValue && optionalValue === undefined) {
125+
tellUser(defaultValue);
126+
return defaultValue;
127+
} else {
128+
return optionalValue;
49129
}
50130
}

src/detectors/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
port: Number, // e.g. 8888
1212
proxyPort: Number, // e.g. 3000
1313
env: Object, // env variables, see examples
14-
args: String, // e.g 'run develop', so that the combined command is 'npm run develop'
14+
possibleArgsArrs: [[String]], // e.g [['run develop]], so that the combined command is 'npm run develop', but we allow for multiple
1515
urlRegexp: RegExp, // see examples
16-
dist: String // e.g. 'dist' or 'build'
16+
dist: String, // e.g. 'dist' or 'build'
1717
}
1818
```
1919

src/detectors/cra.js

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,38 @@
1-
const { existsSync, readFileSync } = require("fs");
1+
const {
2+
hasRequiredDeps,
3+
hasRequiredFiles,
4+
getYarnOrNPMCommand,
5+
scanScripts
6+
} = require("./utils/jsdetect");
27

8+
/**
9+
* detection logic - artificial intelligence!
10+
* */
311
module.exports = function() {
4-
if (!existsSync("package.json")) {
5-
return false;
6-
}
12+
// REQUIRED FILES
13+
if (!hasRequiredFiles(["package.json"])) return false;
14+
// REQUIRED DEPS
15+
if (!hasRequiredDeps(["react-scripts"])) return false;
716

8-
const packageSettings = JSON.parse(
9-
readFileSync("package.json", { encoding: "utf8" })
10-
);
11-
const { dependencies, scripts } = packageSettings;
12-
if (!(dependencies && dependencies["react-scripts"])) {
13-
return false;
14-
}
17+
/** everything below now assumes that we are within create-react-app */
1518

16-
const npmCommand =
17-
scripts &&
18-
((scripts.start && "start") ||
19-
(scripts.serve && "serve") ||
20-
(scripts.run && "run"));
19+
const possibleArgsArrs = scanScripts({
20+
preferredScriptsArr: ["start", "serve", "run"],
21+
preferredCommand: "react-scripts start"
22+
});
2123

22-
if (!npmCommand) {
23-
console.error("Couldn't determine the script to run. Use the -c flag.");
24-
process.exit(1);
24+
if (!possibleArgsArrs.length) {
25+
// ofer to run it when the user doesnt have any scripts setup! 🤯
26+
possibleArgsArrs.push(["react-scripts", "start"]);
2527
}
2628

27-
const yarnExists = existsSync("yarn.lock");
2829
return {
2930
type: "create-react-app",
30-
command: yarnExists ? "yarn" : "npm",
31-
port: 8888,
32-
proxyPort: 3000,
31+
command: getYarnOrNPMCommand(),
32+
port: 8888, // the port that the Netlify Dev User will use
33+
proxyPort: 3000, // the port that create-react-app normally outputs
3334
env: { ...process.env, BROWSER: "none", PORT: 3000 },
34-
args:
35-
yarnExists || npmCommand != "start" ? ["run", npmCommand] : [npmCommand],
35+
possibleArgsArrs,
3636
urlRegexp: new RegExp(`(http://)([^:]+:)${3000}(/)?`, "g"),
3737
dist: "dist"
3838
};

src/detectors/eleventy.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1-
const { existsSync } = require("fs");
1+
const {
2+
hasRequiredDeps,
3+
hasRequiredFiles,
4+
scanScripts
5+
} = require("./utils/jsdetect");
26

37
module.exports = function() {
4-
if (!existsSync(".eleventy.js")) {
5-
return false;
6-
}
8+
// REQUIRED FILES
9+
if (!hasRequiredFiles(["package.json", ".eleventy.js"])) return false;
10+
// commented this out because we're not sure if we want to require it
11+
// // REQUIRED DEPS
12+
// if (!hasRequiredDeps(["@11y/eleventy"])) return false;
713

814
return {
915
type: "eleventy",
1016
port: 8888,
1117
proxyPort: 8080,
1218
env: { ...process.env },
1319
command: "npx",
14-
args: ["eleventy", "--serve", "--watch"],
20+
possibleArgsArrs: [["eleventy", "--serve", "--watch"]],
1521
urlRegexp: new RegExp(`(http://)([^:]+:)${8080}(/)?`, "g"),
1622
dist: "_site"
1723
};

src/detectors/gatsby.js

Lines changed: 21 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,33 @@
1-
const { existsSync, readFileSync } = require("fs");
2-
1+
const {
2+
hasRequiredDeps,
3+
hasRequiredFiles,
4+
getYarnOrNPMCommand,
5+
scanScripts
6+
} = require("./utils/jsdetect");
37
module.exports = function() {
4-
if (!existsSync("gatsby-config.js") || !existsSync("package.json")) {
5-
return false;
6-
}
8+
// REQUIRED FILES
9+
if (!hasRequiredFiles(["package.json", "gatsby-config.js"])) return false;
10+
// REQUIRED DEPS
11+
if (!hasRequiredDeps(["gatsby"])) return false;
712

8-
const packageSettings = JSON.parse(
9-
readFileSync("package.json", { encoding: "utf8" })
10-
);
11-
const { dependencies, scripts } = packageSettings;
12-
if (!(dependencies && dependencies["gatsby"])) {
13-
return false;
14-
}
13+
/** everything below now assumes that we are within gatsby */
1514

16-
const npmCommand =
17-
scripts &&
18-
((scripts.start && "start") ||
19-
(scripts.develop && "develop") ||
20-
(scripts.dev && "dev"));
21-
if (!npmCommand) {
22-
if (!scripts) {
23-
console.error(
24-
"Couldn't determine the package.json script to run for this Gatsby project. Use the --command flag."
25-
);
26-
process.exit(1);
27-
}
28-
// search all the scripts for something that starts with 'gatsby develop'
29-
Object.entries(scripts).forEach(([k, v]) => {
30-
if (v.startsWith("gatsby develop")) {
31-
npmCommand = k;
32-
}
33-
});
34-
if (!npmCommand) {
35-
console.error(
36-
"Couldn't determine the package.json script to run for this Gatsby project. Use the --command flag."
37-
);
38-
process.exit(1);
39-
} else {
40-
console.log("using npm script starting with gatsby develop: ", k);
41-
}
42-
}
15+
const possibleArgsArrs = scanScripts({
16+
preferredScriptsArr: ["start", "develop", "dev"],
17+
preferredCommand: "gatsby develop"
18+
});
4319

44-
const yarnExists = existsSync("yarn.lock");
20+
if (!possibleArgsArrs.length) {
21+
// ofer to run it when the user doesnt have any scripts setup! 🤯
22+
possibleArgsArrs.push(["gatsby", "develop"]);
23+
}
4524
return {
4625
type: "gatsby",
47-
command: yarnExists ? "yarn" : "npm",
26+
command: getYarnOrNPMCommand(),
4827
port: 8888,
4928
proxyPort: 8000,
5029
env: { ...process.env },
51-
args:
52-
yarnExists || npmCommand != "start" ? ["run", npmCommand] : [npmCommand],
30+
possibleArgsArrs,
5331
urlRegexp: new RegExp(`(http://)([^:]+:)${8000}(/)?`, "g"),
5432
dist: "public"
5533
};

src/detectors/hugo.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ module.exports = function() {
1111
proxyPort: 1313,
1212
env: { ...process.env },
1313
command: "hugo",
14-
args: ["server", "-w"],
14+
possibleArgsArrs: [["server", "-w"]],
1515
urlRegexp: new RegExp(`(http://)([^:]+:)${1313}(/)?`, "g"),
1616
dist: "public"
1717
};

src/detectors/jekyll.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ module.exports = function() {
1111
proxyPort: 4000,
1212
env: { ...process.env },
1313
command: "bundle",
14-
args: ["exec", "jekyll", "serve", "-w", "-l"],
14+
possibleArgsArrs: [["exec", "jekyll", "serve", "-w", "-l"]],
1515
urlRegexp: new RegExp(`(http://)([^:]+:)${4000}(/)?`, "g"),
1616
dist: "_site"
1717
};

0 commit comments

Comments
 (0)