2 * @fileoverview Build file
7 /* eslint no-use-before-define: "off", no-console: "off" */
10 //------------------------------------------------------------------------------
12 //------------------------------------------------------------------------------
14 require("shelljs/make");
16 const lodash
= require("lodash"),
17 checker
= require("npm-license"),
18 ReleaseOps
= require("eslint-release"),
19 dateformat
= require("dateformat"),
21 glob
= require("glob"),
22 markdownlint
= require("markdownlint"),
24 path
= require("path"),
25 semver
= require("semver"),
27 loadPerf
= require("load-perf"),
28 yaml
= require("js-yaml"),
29 { CLIEngine
} = require("./lib/cli-engine"),
30 builtinRules
= require("./lib/rules/index");
32 const { cat
, cd
, cp
, echo
, exec
, exit
, find
, ls
, mkdir
, pwd
, rm
, test
} = require("shelljs");
34 //------------------------------------------------------------------------------
36 //------------------------------------------------------------------------------
39 * A little bit fuzzy. My computer has a first CPU speed of 3392 and the perf test
40 * always completes in < 3800ms. However, Travis is less predictable due to
41 * multiple different VM types. So I'm fudging this for now in the hopes that it
42 * at least provides some sort of useful signal.
44 const PERF_MULTIPLIER
= 13e6
;
46 const OPEN_SOURCE_LICENSES
= [
47 /MIT/u, /BSD/u, /Apache/u, /ISC/u, /WTF/u, /Public Domain/u, /LGPL/u
50 //------------------------------------------------------------------------------
52 //------------------------------------------------------------------------------
54 const NODE
= "node ", // intentional extra space
55 NODE_MODULES
= "./node_modules/",
57 DEBUG_DIR
= "./debug/",
59 DOCS_DIR
= "../website/docs",
60 SITE_DIR
= "../website/",
61 PERF_TMP_DIR
= path
.join(TEMP_DIR
, "eslint", "performance"),
63 // Utilities - intentional extra space at the end of each string
64 MOCHA
= `${NODE_MODULES}mocha/bin/_mocha `,
65 ESLINT
= `${NODE} bin/eslint.js --report-unused-disable-directives `,
68 RULE_FILES
= glob
.sync("lib/rules/*.js").filter(filePath
=> path
.basename(filePath
) !== "index.js"),
69 JSON_FILES
= find("conf/").filter(fileType("json")),
70 MARKDOWN_FILES_ARRAY
= find("docs/").concat(ls(".")).filter(fileType("md")),
71 TEST_FILES
= "\"tests/{bin,lib,tools}/**/*.js\"",
72 PERF_ESLINTRC
= path
.join(PERF_TMP_DIR
, "eslintrc.yml"),
73 PERF_MULTIFILES_TARGET_DIR
= path
.join(PERF_TMP_DIR
, "eslint"),
74 PERF_MULTIFILES_TARGETS
= `"${PERF_MULTIFILES_TARGET_DIR + path.sep}{lib,tests${path.sep}lib}${path.sep}**${path.sep}*.js"`,
77 MOCHA_TIMEOUT
= 10000;
79 //------------------------------------------------------------------------------
81 //------------------------------------------------------------------------------
84 * Simple JSON file validation that relies on ES JSON parser.
85 * @param {string} filePath Path to JSON.
86 * @throws Error If file contents is invalid JSON.
87 * @returns {undefined}
89 function validateJsonFile(filePath
) {
90 const contents
= fs
.readFileSync(filePath
, "utf8");
96 * Generates a function that matches files with a particular extension.
97 * @param {string} extension The file extension (i.e. "js")
98 * @returns {Function} The function to pass into a filter method.
101 function fileType(extension
) {
102 return function(filename
) {
103 return filename
.slice(filename
.lastIndexOf(".") + 1) === extension
;
108 * Executes a command and returns the output instead of printing it to stdout.
109 * @param {string} cmd The command string to execute.
110 * @returns {string} The result of the executed command.
112 function execSilent(cmd
) {
113 return exec(cmd
, { silent
: true }).stdout
;
117 * Generates a release blog post for eslint.org
118 * @param {Object} releaseInfo The release metadata.
119 * @param {string} [prereleaseMajorVersion] If this is a prerelease, the next major version after this prerelease
123 function generateBlogPost(releaseInfo
, prereleaseMajorVersion
) {
124 const ruleList
= RULE_FILES
126 // Strip the .js extension
127 .map(ruleFileName
=> path
.basename(ruleFileName
, ".js"))
130 * Sort by length descending. This ensures that rule names which are substrings of other rule names are not
131 * matched incorrectly. For example, the string "no-undefined" should get matched with the `no-undefined` rule,
132 * instead of getting matched with the `no-undef` rule followed by the string "ined".
134 .sort((ruleA
, ruleB
) => ruleB
.length
- ruleA
.length
);
136 const renderContext
= Object
.assign({ prereleaseMajorVersion
, ruleList
}, releaseInfo
);
138 const output
= ejs
.render(cat("./templates/blogpost.md.ejs"), renderContext
),
140 month
= now
.getMonth() + 1,
142 filename
= `../website/_posts/${now.getFullYear()}-${
143 month < 10 ? `0${month}
` : month}-${
144 day < 10 ? `0${day}
` : day}-eslint-v${
145 releaseInfo.version}-released.md`;
151 * Generates a doc page with formatter result examples
152 * @param {Object} formatterInfo Linting results from each formatter
153 * @param {string} [prereleaseVersion] The version used for a prerelease. This
154 * changes where the output is stored.
157 function generateFormatterExamples(formatterInfo
, prereleaseVersion
) {
158 const output
= ejs
.render(cat("./templates/formatter-examples.md.ejs"), formatterInfo
);
159 let filename
= "../website/docs/user-guide/formatters/index.md",
160 htmlFilename
= "../website/docs/user-guide/formatters/html-formatter-example.html";
162 if (prereleaseVersion
) {
163 filename
= filename
.replace("/docs", `/docs/${prereleaseVersion}`);
164 htmlFilename
= htmlFilename
.replace("/docs", `/docs/${prereleaseVersion}`);
165 if (!test("-d", path
.dirname(filename
))) {
166 mkdir(path
.dirname(filename
));
171 formatterInfo
.formatterResults
.html
.result
.to(htmlFilename
);
175 * Generate a doc page that lists all of the rules and links to them
178 function generateRuleIndexPage() {
179 const outputFile
= "../website/_data/rules.yml",
180 categoryList
= "conf/category-list.json",
181 categoriesData
= JSON
.parse(cat(path
.resolve(categoryList
)));
184 .map(filename
=> [filename
, path
.basename(filename
, ".js")])
185 .sort((a
, b
) => a
[1].localeCompare(b
[1]))
187 const filename
= pair
[0];
188 const basename
= pair
[1];
189 const rule
= require(path
.resolve(filename
));
191 if (rule
.meta
.deprecated
) {
192 categoriesData
.deprecated
.rules
.push({
194 replacedBy
: rule
.meta
.replacedBy
|| []
199 description
: rule
.meta
.docs
.description
,
200 recommended
: rule
.meta
.docs
.recommended
|| false,
201 fixable
: !!rule
.meta
.fixable
203 category
= lodash
.find(categoriesData
.categories
, { name
: rule
.meta
.docs
.category
});
205 if (!category
.rules
) {
209 category
.rules
.push(output
);
213 const output
= yaml
.safeDump(categoriesData
, { sortKeys
: true });
215 output
.to(outputFile
);
219 * Creates a git commit and tag in an adjacent `website` repository, without pushing it to
220 * the remote. This assumes that the repository has already been modified somehow (e.g. by adding a blogpost).
221 * @param {string} [tag] The string to tag the commit with
224 function commitSiteToGit(tag
) {
225 const currentDir
= pwd();
228 exec("git add -A .");
229 exec(`git commit -m "Autogenerated new docs and demo at ${dateformat(new Date())}"`);
232 exec(`git tag ${tag}`);
235 exec("git fetch origin && git rebase origin/master");
240 * Publishes the changes in an adjacent `website` repository to the remote. The
241 * site should already have local commits (e.g. from running `commitSiteToGit`).
244 function publishSite() {
245 const currentDir
= pwd();
248 exec("git push origin master --tags");
253 * Updates the changelog, bumps the version number in package.json, creates a local git commit and tag,
254 * and generates the site in an adjacent `website` folder.
257 function generateRelease() {
258 ReleaseOps
.generateRelease();
259 const releaseInfo
= JSON
.parse(cat(".eslint-release-info.json"));
261 echo("Generating site");
263 generateBlogPost(releaseInfo
);
264 commitSiteToGit(`v${releaseInfo.version}`);
268 * Updates the changelog, bumps the version number in package.json, creates a local git commit and tag,
269 * and generates the site in an adjacent `website` folder.
270 * @param {string} prereleaseId The prerelease identifier (alpha, beta, etc.)
273 function generatePrerelease(prereleaseId
) {
274 ReleaseOps
.generateRelease(prereleaseId
);
275 const releaseInfo
= JSON
.parse(cat(".eslint-release-info.json"));
276 const nextMajor
= semver
.inc(releaseInfo
.version
, "major");
278 echo("Generating site");
280 // always write docs into the next major directory (so 2.0.0-alpha.0 writes to 2.0.0)
281 target
.gensite(nextMajor
);
284 * Premajor release should have identical "next major version".
285 * Preminor and prepatch release will not.
286 * 5.0.0-alpha.0 --> next major = 5, current major = 5
287 * 4.4.0-alpha.0 --> next major = 5, current major = 4
288 * 4.0.1-alpha.0 --> next major = 5, current major = 4
290 if (semver
.major(releaseInfo
.version
) === semver
.major(nextMajor
)) {
293 * This prerelease is for a major release (not preminor/prepatch).
294 * Blog post generation logic needs to be aware of this (as well as
295 * know what the next major version is actually supposed to be).
297 generateBlogPost(releaseInfo
, nextMajor
);
299 generateBlogPost(releaseInfo
);
302 commitSiteToGit(`v${releaseInfo.version}`);
306 * Publishes a generated release to npm and GitHub, and pushes changes to the adjacent `website` repo
310 function publishRelease() {
311 ReleaseOps
.publishRelease();
316 * Splits a command result to separate lines.
317 * @param {string} result The command result string.
318 * @returns {Array} The separated lines.
320 function splitCommandResultToLines(result
) {
321 return result
.trim().split("\n");
325 * Gets the first commit sha of the given file.
326 * @param {string} filePath The file path which should be checked.
327 * @returns {string} The commit sha.
329 function getFirstCommitOfFile(filePath
) {
330 let commits
= execSilent(`git rev-list HEAD -- ${filePath}`);
332 commits
= splitCommandResultToLines(commits
);
333 return commits
[commits
.length
- 1].trim();
337 * Gets the tag name where a given file was introduced first.
338 * @param {string} filePath The file path to check.
339 * @returns {string} The tag name.
341 function getFirstVersionOfFile(filePath
) {
342 const firstCommit
= getFirstCommitOfFile(filePath
);
343 let tags
= execSilent(`git tag --contains ${firstCommit}`);
345 tags
= splitCommandResultToLines(tags
);
346 return tags
.reduce((list
, version
) => {
347 const validatedVersion
= semver
.valid(version
.trim());
349 if (validatedVersion
) {
350 list
.push(validatedVersion
);
353 }, []).sort(semver
.compare
)[0];
357 * Gets the commit that deleted a file.
358 * @param {string} filePath The path to the deleted file.
359 * @returns {string} The commit sha.
361 function getCommitDeletingFile(filePath
) {
362 const commits
= execSilent(`git rev-list HEAD -- ${filePath}`);
364 return splitCommandResultToLines(commits
)[0];
368 * Gets the first version number where a given file is no longer present.
369 * @param {string} filePath The path to the deleted file.
370 * @returns {string} The version number.
372 function getFirstVersionOfDeletion(filePath
) {
373 const deletionCommit
= getCommitDeletingFile(filePath
),
374 tags
= execSilent(`git tag --contains ${deletionCommit}`);
376 return splitCommandResultToLines(tags
)
377 .map(version
=> semver
.valid(version
.trim()))
378 .filter(version
=> version
)
379 .sort(semver
.compare
)[0];
383 * Lints Markdown files.
384 * @param {Array} files Array of file names to lint.
385 * @returns {Object} exec-style exit code object.
388 function lintMarkdown(files
) {
389 const config
= yaml
.safeLoad(fs
.readFileSync(path
.join(__dirname
, "./.markdownlint.yml"), "utf8")),
390 result
= markdownlint
.sync({
395 resultString
= result
.toString(),
396 returnCode
= resultString
? 1 : 0;
399 console
.error(resultString
);
401 return { code
: returnCode
};
405 * Gets linting results from every formatter, based on a hard-coded snippet and config
406 * @returns {Object} Output from each formatter
408 function getFormatterResults() {
409 const stripAnsi
= require("strip-ansi");
411 const formatterFiles
= fs
.readdirSync("./lib/cli-engine/formatters/"),
413 "no-else-return": "warn",
415 "space-unary-ops": "error",
416 semi
: ["warn", "always"],
417 "consistent-return": "error"
419 cli
= new CLIEngine({
421 baseConfig
: { extends: "eslint:recommended" },
425 "function addOne(i) {",
433 rawMessages
= cli
.executeOnText(codeString
, "fullOfProblems.js", true),
434 rulesMap
= cli
.getRules(),
437 Object
.keys(rules
).forEach(ruleId
=> {
438 rulesMeta
[ruleId
] = rulesMap
.get(ruleId
).meta
;
441 return formatterFiles
.reduce((data
, filename
) => {
442 const fileExt
= path
.extname(filename
),
443 name
= path
.basename(filename
, fileExt
);
445 if (fileExt
=== ".js") {
446 const formattedOutput
= cli
.getFormatter(name
)(
451 data
.formatterResults
[name
] = {
452 result
: stripAnsi(formattedOutput
)
456 }, { formatterResults
: {} });
460 * Gets a path to an executable in node_modules/.bin
461 * @param {string} command The executable name
462 * @returns {string} The executable path
464 function getBinFile(command
) {
465 return path
.join("node_modules", ".bin", command
);
468 //------------------------------------------------------------------------------
470 //------------------------------------------------------------------------------
472 target
.all = function() {
476 target
.lint = function([fix
= false] = []) {
480 echo("Validating JavaScript files");
481 lastReturn
= exec(`${ESLINT}${fix ? "--fix" : ""} .`);
482 if (lastReturn
.code
!== 0) {
486 echo("Validating JSON Files");
487 lodash
.forEach(JSON_FILES
, validateJsonFile
);
489 echo("Validating Markdown Files");
490 lastReturn
= lintMarkdown(MARKDOWN_FILES_ARRAY
);
491 if (lastReturn
.code
!== 0) {
500 target
.fuzz = function({ amount
= 1000, fuzzBrokenAutofixes
= false } = {}) {
501 const fuzzerRunner
= require("./tools/fuzzer-runner");
502 const fuzzResults
= fuzzerRunner
.run({ amount
, fuzzBrokenAutofixes
});
504 if (fuzzResults
.length
) {
506 const uniqueStackTraceCount
= new Set(fuzzResults
.map(result
=> result
.error
)).size
;
508 echo(`The fuzzer reported ${fuzzResults.length} error${fuzzResults.length === 1 ? "" : "s"} with a total of ${uniqueStackTraceCount} unique stack trace${uniqueStackTraceCount === 1 ? "" : "s"}.`);
510 const formattedResults
= JSON
.stringify({ results
: fuzzResults
}, null, 4);
512 if (process
.env
.CI
) {
513 echo("More details can be found below.");
514 echo(formattedResults
);
516 if (!test("-d", DEBUG_DIR
)) {
523 // To avoid overwriting any existing fuzzer log files, append a numeric suffix to the end of the filename.
525 fuzzLogPath
= path
.join(DEBUG_DIR
, `fuzzer-log-${fileSuffix}.json`);
527 } while (test("-f", fuzzLogPath
));
529 formattedResults
.to(fuzzLogPath
);
531 // TODO: (not-an-aardvark) Create a better way to isolate and test individual fuzzer errors from the log file
532 echo(`More details can be found in ${fuzzLogPath}.`);
539 target
.mocha
= () => {
543 echo("Running unit tests");
545 lastReturn
= exec(`${getBinFile("nyc")} -- ${MOCHA} -R progress -t ${MOCHA_TIMEOUT} -c ${TEST_FILES}`);
546 if (lastReturn
.code
!== 0) {
550 lastReturn
= exec(`${getBinFile("nyc")} check-coverage --statement 99 --branch 98 --function 99 --lines 99`);
551 if (lastReturn
.code
!== 0) {
560 target
.karma
= () => {
561 echo("Running unit tests on browsers");
563 target
.webpack("production");
565 const lastReturn
= exec(`${getBinFile("karma")} start karma.conf.js`);
567 if (lastReturn
.code
!== 0) {
572 target
.test = function() {
574 target
.checkRuleFiles();
577 target
.fuzz({ amount
: 150, fuzzBrokenAutofixes
: false });
578 target
.checkLicenses();
581 target
.docs = function() {
582 echo("Generating documentation");
583 exec(`${getBinFile("jsdoc")} -d jsdoc lib`);
584 echo("Documentation has been output to /jsdoc");
587 target
.gensite = function(prereleaseVersion
) {
588 echo("Generating eslint.org");
593 "/maintainer-guide/",
599 if (prereleaseVersion
) {
600 docFiles
= docFiles
.map(docFile
=> `/${prereleaseVersion}${docFile}`);
603 // 1. create temp and build directory
604 echo("> Creating a temporary directory (Step 1)");
605 if (!test("-d", TEMP_DIR
)) {
609 // 2. remove old files from the site
610 echo("> Removing old files (Step 2)");
611 docFiles
.forEach(filePath
=> {
612 const fullPath
= path
.join(DOCS_DIR
, filePath
),
613 htmlFullPath
= fullPath
.replace(".md", ".html");
615 if (test("-f", fullPath
)) {
619 if (filePath
.indexOf(".md") >= 0 && test("-f", htmlFullPath
)) {
620 rm("-rf", htmlFullPath
);
625 // 3. Copy docs folder to a temporary directory
626 echo("> Copying the docs folder (Step 3)");
627 cp("-rf", "docs/*", TEMP_DIR
);
629 let versions
= test("-f", "./versions.json") ? JSON
.parse(cat("./versions.json")) : {};
631 if (!versions
.added
) {
638 const rules
= require(".").linter
.getRules();
640 const RECOMMENDED_TEXT
= "\n\n(recommended) The `\"extends\": \"eslint:recommended\"` property in a configuration file enables this rule.";
641 const FIXABLE_TEXT
= "\n\n(fixable) The `--fix` option on the [command line](../user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.";
643 // 4. Loop through all files in temporary directory
644 process
.stdout
.write("> Updating files (Steps 4-9): 0/... - ...\r");
645 const tempFiles
= find(TEMP_DIR
);
646 const length
= tempFiles
.length
;
648 tempFiles
.forEach((filename
, i
) => {
649 if (test("-f", filename
) && path
.extname(filename
) === ".md") {
651 const rulesUrl
= "https://github.com/eslint/eslint/tree/master/lib/rules/",
652 docsUrl
= "https://github.com/eslint/eslint/tree/master/docs/rules/",
653 baseName
= path
.basename(filename
),
654 sourceBaseName
= `${path.basename(filename, ".md")}.js`,
655 sourcePath
= path
.join("lib/rules", sourceBaseName
),
656 ruleName
= path
.basename(filename
, ".md"),
657 filePath
= path
.join("docs", path
.relative("tmp", filename
));
658 let text
= cat(filename
),
662 process
.stdout
.write(`> Updating files (Steps 4-9): ${i}/${length} - ${filePath + " ".repeat(30)}\r`);
664 // 5. Prepend page title and layout variables at the top of rules
665 if (path
.dirname(filename
).indexOf("rules") >= 0) {
667 // Find out if the rule requires a special docs portion (e.g. if it is recommended and/or fixable)
668 const rule
= rules
.get(ruleName
);
669 const isRecommended
= rule
&& rule
.meta
.docs
.recommended
;
670 const isFixable
= rule
&& rule
.meta
.fixable
;
672 // Incorporate the special portion into the documentation content
673 const textSplit
= text
.split("\n");
674 const ruleHeading
= textSplit
[0];
675 const ruleDocsContent
= textSplit
.slice(1).join("\n");
677 text
= `${ruleHeading}${isRecommended ? RECOMMENDED_TEXT : ""}${isFixable ? FIXABLE_TEXT : ""}\n${ruleDocsContent}`;
678 title
= `${ruleName} - Rules`;
680 if (rule
&& rule
.meta
) {
681 ruleType
= `rule_type: ${rule.meta.type}`;
685 // extract the title from the file itself
686 title
= text
.match(/#([^#].+)\n/u);
688 title
= title
[1].trim();
690 title
= "Documentation";
698 `edit_link: https://github.com/eslint/eslint/edit/master/${filePath}`,
701 "<!-- Note: No pull requests accepted for this file. See README.md in the root directory for details. -->",
706 // 6. Remove .md extension for relative links and change README to empty string
707 text
= text
.replace(/\((?!https?:\/\/)([^)]*?)\.md(.*?)\)/gu, "($1$2)").replace("README.html", "");
709 // 7. Check if there's a trailing white line at the end of the file, if there isn't one, add it
710 if (!/\n$/u.test(text
)) {
714 // 8. Append first version of ESLint rule was added at.
715 if (filename
.indexOf("rules/") !== -1) {
716 if (!versions
.added
[baseName
]) {
717 versions
.added
[baseName
] = getFirstVersionOfFile(sourcePath
);
719 const added
= versions
.added
[baseName
];
721 if (!versions
.removed
[baseName
] && !test("-f", sourcePath
)) {
722 versions
.removed
[baseName
] = getFirstVersionOfDeletion(sourcePath
);
724 const removed
= versions
.removed
[baseName
];
726 text
+= "\n## Version\n\n";
728 ? `This rule was introduced in ESLint ${added} and removed in ${removed}.\n`
729 : `This rule was introduced in ESLint ${added}.\n`;
731 text
+= "\n## Resources\n\n";
733 text
+= `* [Rule source](${rulesUrl}${sourceBaseName})\n`;
735 text
+= `* [Documentation source](${docsUrl}${baseName})\n`;
738 // 9. Update content of the file with changes
739 text
.to(filename
.replace("README.md", "index.md"));
742 JSON
.stringify(versions
).to("./versions.json");
743 echo(`> Updating files (Steps 4-9)${" ".repeat(50)}`);
745 // 10. Copy temporary directory to site's docs folder
746 echo("> Copying the temporary directory the site (Step 10)");
747 let outputDir
= DOCS_DIR
;
749 if (prereleaseVersion
) {
750 outputDir
+= `/${prereleaseVersion}`;
751 if (!test("-d", outputDir
)) {
755 cp("-rf", `${TEMP_DIR}*`, outputDir
);
757 // 11. Generate rule listing page
758 echo("> Generating the rule listing (Step 11)");
759 generateRuleIndexPage();
761 // 12. Delete temporary directory
762 echo("> Removing the temporary directory (Step 12)");
765 // 13. Create Example Formatter Output Page
766 echo("> Creating the formatter examples (Step 14)");
767 generateFormatterExamples(getFormatterResults(), prereleaseVersion
);
769 echo("Done generating eslint.org");
772 target
.webpack = function(mode
= "none") {
773 exec(`${getBinFile("webpack")} --mode=${mode} --output-path=${BUILD_DIR}`);
776 target
.checkRuleFiles = function() {
778 echo("Validating rules");
780 const ruleTypes
= require("./tools/rule-types.json");
783 RULE_FILES
.forEach(filename
=> {
784 const basename
= path
.basename(filename
, ".js");
785 const docFilename
= `docs/rules/${basename}.md`;
788 * Check if basename is present in rule-types.json file.
789 * @returns {boolean} true if present
792 function isInRuleTypes() {
793 return Object
.prototype.hasOwnProperty
.call(ruleTypes
, basename
);
797 * Check if id is present in title
798 * @param {string} id id to check for
799 * @returns {boolean} true if present
802 function hasIdInTitle(id
) {
803 const docText
= cat(docFilename
);
804 const idOldAtEndOfTitleRegExp
= new RegExp(`^# (.*?) \\(${id}\\)`, "u"); // original format
805 const idNewAtBeginningOfTitleRegExp
= new RegExp(`^# ${id}: `, "u"); // new format is same as rules index
807 * 1. Added support for new format.
808 * 2. Will remove support for old format after all docs files have new format.
809 * 3. Will remove this check when the main heading is automatically generated from rule metadata.
812 return idNewAtBeginningOfTitleRegExp
.test(docText
) || idOldAtEndOfTitleRegExp
.test(docText
);
816 if (!test("-f", docFilename
)) {
817 console
.error("Missing documentation for rule %s", basename
);
821 // check for proper doc format
822 if (!hasIdInTitle(basename
)) {
823 console
.error("Missing id in the doc page's title of rule %s", basename
);
828 // check for recommended configuration
829 if (!isInRuleTypes()) {
830 console
.error("Missing setting for %s in tools/rule-types.json", basename
);
834 // check parity between rules index file and rules directory
835 const ruleIdsInIndex
= require("./lib/rules/index");
836 const ruleDef
= ruleIdsInIndex
.get(basename
);
839 console
.error(`Missing rule from index (./lib/rules/index.js): ${basename}. If you just added a new rule then add an entry for it in this file.`);
843 // check eslint:recommended
844 const recommended
= require("./conf/eslint-recommended");
847 if (ruleDef
.meta
.docs
.recommended
) {
848 if (recommended
.rules
[basename
] !== "error") {
849 console
.error(`Missing rule from eslint:recommended (./conf/eslint-recommended.js): ${basename}. If you just made a rule recommended then add an entry for it in this file.`);
853 if (basename
in recommended
.rules
) {
854 console
.error(`Extra rule in eslint:recommended (./conf/eslint-recommended.js): ${basename}. If you just added a rule then don't add an entry for it in this file.`);
861 if (!test("-f", `tests/lib/rules/${basename}.js`)) {
862 console
.error("Missing tests for rule %s", basename
);
874 target
.checkLicenses = function() {
877 * Check if a dependency is eligible to be used by us
878 * @param {Object} dependency dependency to check
879 * @returns {boolean} true if we have permission
882 function isPermissible(dependency
) {
883 const licenses
= dependency
.licenses
;
885 if (Array
.isArray(licenses
)) {
886 return licenses
.some(license
=> isPermissible({
887 name
: dependency
.name
,
892 return OPEN_SOURCE_LICENSES
.some(license
=> license
.test(licenses
));
895 echo("Validating licenses");
900 const impermissible
= Object
.keys(deps
).map(dependency
=> ({
902 licenses
: deps
[dependency
].licenses
903 })).filter(dependency
=> !isPermissible(dependency
));
905 if (impermissible
.length
) {
906 impermissible
.forEach(dependency
=> {
908 "%s license for %s is impermissible.",
919 * Downloads a repository which has many js files to test performance with multi files.
920 * Here, it's eslint@1.10.3 (450 files)
921 * @param {Function} cb A callback function.
924 function downloadMultifilesTestTarget(cb
) {
925 if (test("-d", PERF_MULTIFILES_TARGET_DIR
)) {
926 process
.nextTick(cb
);
928 mkdir("-p", PERF_MULTIFILES_TARGET_DIR
);
929 echo("Downloading the repository of multi-files performance test target.");
930 exec(`git clone -b v1.10.3 --depth 1 https://github.com/eslint/eslint.git "${PERF_MULTIFILES_TARGET_DIR}"`, { silent
: true }, cb
);
935 * Creates a config file to use performance tests.
936 * This config is turning all core rules on.
939 function createConfigForPerformanceTest() {
948 for (const [ruleId
] of builtinRules
) {
949 content
.push(` ${ruleId}: 1`);
952 content
.join("\n").to(PERF_ESLINTRC
);
956 * Calculates the time for each run for performance
957 * @param {string} cmd cmd
958 * @param {int} runs Total number of runs to do
959 * @param {int} runNumber Current run number
960 * @param {int[]} results Collection results from each run
961 * @param {Function} cb Function to call when everything is done
962 * @returns {int[]} calls the cb with all the results
965 function time(cmd
, runs
, runNumber
, results
, cb
) {
966 const start
= process
.hrtime();
968 exec(cmd
, { maxBuffer
: 64 * 1024 * 1024, silent
: true }, (code
, stdout
, stderr
) => {
969 const diff
= process
.hrtime(start
),
970 actual
= (diff
[0] * 1e3
+ diff
[1] / 1e6
); // ms
973 echo(` Performance Run #${runNumber} failed.`);
975 echo(`STDOUT:\n${stdout}\n\n`);
979 echo(`STDERR:\n${stderr}\n\n`);
984 results
.push(actual
);
985 echo(` Performance Run #${runNumber}: %dms`, actual
);
987 return time(cmd
, runs
- 1, runNumber
+ 1, results
, cb
);
996 * Run a performance test.
997 * @param {string} title A title.
998 * @param {string} targets Test targets.
999 * @param {number} multiplier A multiplier for limitation.
1000 * @param {Function} cb A callback function.
1003 function runPerformanceTest(title
, targets
, multiplier
, cb
) {
1004 const cpuSpeed
= os
.cpus()[0].speed
,
1005 max
= multiplier
/ cpuSpeed
,
1006 cmd
= `${ESLINT}--config "${PERF_ESLINTRC}" --no-eslintrc --no-ignore ${targets}`;
1010 echo(" CPU Speed is %d with multiplier %d", cpuSpeed
, multiplier
);
1012 time(cmd
, 5, 1, [], results
=> {
1013 if (!results
|| results
.length
=== 0) { // No results? Something is wrong.
1014 throw new Error("Performance test failed.");
1017 results
.sort((a
, b
) => a
- b
);
1019 const median
= results
[~~(results
.length
/ 2)];
1023 echo(" Performance budget exceeded: %dms (limit: %dms)", median
, max
);
1025 echo(" Performance budget ok: %dms (limit: %dms)", median
, max
);
1033 * Run the load performance for eslint
1037 function loadPerformance() {
1043 for (let cnt
= 0; cnt
< 5; cnt
++) {
1044 const loadPerfData
= loadPerf({
1045 checkDependencies
: false
1048 echo(` Load performance Run #${cnt + 1}: %dms`, loadPerfData
.loadTime
);
1049 results
.push(loadPerfData
.loadTime
);
1052 results
.sort((a
, b
) => a
- b
);
1053 const median
= results
[~~(results
.length
/ 2)];
1056 echo(" Load Performance median: %dms", median
);
1060 target
.perf = function() {
1061 downloadMultifilesTestTarget(() => {
1062 createConfigForPerformanceTest();
1068 "tests/performance/jshint.js",
1072 // Count test target files.
1073 const count
= glob
.sync(
1074 process
.platform
=== "win32"
1075 ? PERF_MULTIFILES_TARGETS
.slice(2).replace(/\\/gu, "/")
1076 : PERF_MULTIFILES_TARGETS
1080 `Multi Files (${count} files):`,
1081 PERF_MULTIFILES_TARGETS,
1082 3 * PERF_MULTIPLIER,
1090 target.generateRelease = generateRelease;
1091 target.generatePrerelease = ([prereleaseType]) => generatePrerelease(prereleaseType);
1092 target.publishRelease = publishRelease;