Repository: eslint/eslint
Pull request #15894 (2022-05-19 18:18:53)

chore: Add deploy workflow for docs site
<!--
    Thank you for contributing!

    ESLint adheres to the [JS Foundation Code of Conduct](https://eslint.org/conduct).
-->

#### Prerequisites checklist

- [x] I have read the [contributing guidelines](https://github.com/eslint/eslint/blob/HEAD/CONTRIBUTING.md).

#### What is the purpose of this pull request? (put an "X" next to an item)

<!--
    The following template is intentionally not a markdown checkbox list for the reasons
    explained in https://github.com/eslint/eslint/pull/12848#issuecomment-580302888
-->

[ ] Documentation update
[ ] Bug fix ([template](https://raw.githubusercontent.com/eslint/eslint/HEAD/templates/bug-report.md))
[ ] New rule ([template](https://raw.githubusercontent.com/eslint/eslint/HEAD/templates/rule-proposal.md))
[ ] Changes an existing rule ([template](https://raw.githubusercontent.com/eslint/eslint/HEAD/templates/rule-change-proposal.md))
[ ] Add autofix to a rule
[ ] Add a CLI option
[ ] Add something to the core
[x] Other, please explain:

<!--
    If the item you've checked above has a template, please paste the template questions below and answer them. (If this pull request is addressing an issue, you can just paste a link to the issue here instead.)
-->

<!--
    Please ensure your pull request is ready:

    - Read the pull request guide (https://eslint.org/docs/developer-guide/contributing/pull-requests)
    - Include tests for this change
    - Update documentation for this change (if appropriate)
-->

<!--
    The following is required for all pull requests:
-->

#### What changes did you make? (Give an overview)

I created a workflow that will trigger a docs site deploy whenever there is a release.

Because the docs site currently deploys every time a pull request is merged, the site will always be ahead of the last release (not desirable). I switched off automatic publishing so merges won't automatically be deployed, but that means we need a way to trigger a deploy. So, this workflow will trigger the deploy for each release, making it work the same as our current release process.

#### Is there anything you'd like reviewers to focus on?

Does this approach make sense?

<!-- markdownlint-disable-file MD004 -->


netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *docs-eslint* processing.


|  Name | Link |
|---------------------------------|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | e929351aed51608fcbc54b05114f4ce8492d3d91 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/docs-eslint/deploys/62868a0e5223fa000af00a4a |
mdjermanovic: > Does this approach make sense?

Absolutely. Maybe we could also trigger a playground deploy in the same way?

> I switched off automatic publishing

It would be useful to keep the on-commit publishing until we switch to the new site (I'm assuming it won't be tomorrow). Some things cannot be noticed in the preview, in particular possible problems with proxying or with missing `/docs` prefix in URLs.
nzakas: I've turned off automatic publishing for now so we can test if this workflow works correctly with tomorrow's release. We can turn it on after that if we think it's necessary.
mdjermanovic: > > Does this approach make sense?
> 
> Absolutely. Maybe we could also trigger a playground deploy in the same way?

I thought this just triggers a build on Netlify, which then pulls the files from GitHub and builds the site. Didn't realize that this builds the site locally and sends the files to Netlify. Did you consider [Build hooks](https://docs.netlify.com/configure-builds/build-hooks/)?

Patch:
diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml
new file mode 100644
index 000000000..d1c79835f
--- /dev/null
+++ b/.github/workflows/docs-deploy.yml
@@ -0,0 +1,25 @@
+name: 'Docs Deploy'
+
+on:
+  release:
+    types: ['published']
+
+jobs:
+  deploy:
+    name: 'Deploy'
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-node@v3
+        with:
+          node-version: '16.x'
+      - working-directory: docs
+        run: |
+          npm install
+          npm run build
+          npx netlify deploy --dir=_site --message='Docs deploy ${{ github.event.release.tag_name }}' 
+        env:
+          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
+          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
+          NETLIFY_DEPLOY_TO_PROD: true
diff --git a/docs/.eleventy.js b/docs/.eleventy.js
index d1138eff3..7b4c4f1a0 100644
--- a/docs/.eleventy.js
+++ b/docs/.eleventy.js
@@ -25,6 +25,15 @@ const {
 module.exports = function(eleventyConfig) {
     let now = new Date();
 
+    /*
+     * The site is loaded from /docs on eslint.org and so we need to adjust
+     * the path prefix so URLs are evaluated correctly.
+     *
+     * The path prefix is turned off for deploy previews so we can properly
+     * see changes before deployed.
+    */
+    const pathPrefix = process.env.CONTEXT === "deploy-preview" ? "" : "/docs";
+
     /*****************************************************************************************
      *  Filters
      * ***************************************************************************************/
@@ -180,7 +189,7 @@ module.exports = function(eleventyConfig) {
 
         rules.forEach(function(rule) {
             let list_item = `<li class="related-rules__list__item">
-                <a href="/rules/${rule}">
+                <a href="${pathPrefix}/rules/${rule}">
                     <span>${rule}</span>
                     <svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false">
                         <path d="M5 12H19M19 12L12 5M19 12L12 19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -343,14 +352,7 @@ module.exports = function(eleventyConfig) {
     return {
         passthroughFileCopy: true,
 
-        /*
-         * The site is loaded from /docs on eslint.org and so we need to adjust
-         * the path prefix so URLs are evaluated correctly.
-         * 
-         * The path prefix is turned off for deploy previews so we can properly
-         * see changes before deployed.
-         */
-        pathPrefix: process.env.CONTEXT === "deploy-preview" ? "" : "/docs",
+        pathPrefix,
 
         markdownTemplateEngine: 'njk',
         dataTemplateEngine: 'njk',
diff --git a/docs/package.json b/docs/package.json
index da9319743..f09a9c187 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -30,6 +30,7 @@
         "imagemin-cli": "^7.0.0",
         "markdown-it": "^12.2.0",
         "markdown-it-anchor": "^8.1.2",
+        "netlify-cli": "^10.3.1",
         "node-sass": "^6.0.1",
         "npm-run-all": "^4.1.5",
         "rimraf": "^3.0.2",
diff --git a/docs/src/rules/no-control-regex.md b/docs/src/rules/no-control-regex.md
index 57e85e470..d7bdff87f 100644
--- a/docs/src/rules/no-control-regex.md
+++ b/docs/src/rules/no-control-regex.md
@@ -22,6 +22,7 @@ The following elements of regular expression patterns are considered possible er
 
 * Hexadecimal character escapes from `\x00` to `\x1F`.
 * Unicode character escapes from `\u0000` to `\u001F`.
+* Unicode code point escapes from `\u{0}` to `\u{1F}`.
 * Unescaped raw characters from U+0000 to U+001F.
 
 Control escapes such as `\t` and `\n` are allowed by this rule.
@@ -35,8 +36,9 @@ var pattern1 = /\x00/;
 var pattern2 = /\x0C/;
 var pattern3 = /\x1F/;
 var pattern4 = /\u000C/;
-var pattern5 = new RegExp("\x0C"); // raw U+000C character in the pattern
-var pattern6 = new RegExp("\\x0C"); // \x0C pattern
+var pattern5 = /\u{C}/u;
+var pattern6 = new RegExp("\x0C"); // raw U+000C character in the pattern
+var pattern7 = new RegExp("\\x0C"); // \x0C pattern
 ```
 
 Examples of **correct** code for this rule:
@@ -46,11 +48,12 @@ Examples of **correct** code for this rule:
 
 var pattern1 = /\x20/;
 var pattern2 = /\u0020/;
-var pattern3 = /\t/;
-var pattern4 = /\n/;
-var pattern5 = new RegExp("\x20");
-var pattern6 = new RegExp("\\t");
-var pattern7 = new RegExp("\\n");
+var pattern3 = /\u{20}/u;
+var pattern4 = /\t/;
+var pattern5 = /\n/;
+var pattern6 = new RegExp("\x20");
+var pattern7 = new RegExp("\\t");
+var pattern8 = new RegExp("\\n");
 ```
 
 ## Known Limitations
diff --git a/lib/rules/no-control-regex.js b/lib/rules/no-control-regex.js
index 04f3449fb..00278f5c1 100644
--- a/lib/rules/no-control-regex.js
+++ b/lib/rules/no-control-regex.js
@@ -30,10 +30,12 @@ const collector = new (class {
         }
     }
 
-    collectControlChars(regexpStr) {
+    collectControlChars(regexpStr, flags) {
+        const uFlag = typeof flags === "string" && flags.includes("u");
+
         try {
             this._source = regexpStr;
-            this._validator.validatePattern(regexpStr); // Call onCharacter hook
+            this._validator.validatePattern(regexpStr, void 0, void 0, uFlag); // Call onCharacter hook
         } catch {
 
             // Ignore syntax errors in RegExp.
@@ -68,13 +70,15 @@ module.exports = {
 
         /**
          * Get the regex expression
-         * @param {ASTNode} node node to evaluate
-         * @returns {RegExp|null} Regex if found else null
+         * @param {ASTNode} node `Literal` node to evaluate
+         * @returns {{ pattern: string, flags: string | null } | null} Regex if found (the given node is either a regex literal
+         * or a string literal that is the pattern argument of a RegExp constructor call). Otherwise `null`. If flags cannot be determined,
+         * the `flags` property will be `null`.
          * @private
          */
-        function getRegExpPattern(node) {
+        function getRegExp(node) {
             if (node.regex) {
-                return node.regex.pattern;
+                return node.regex;
             }
             if (typeof node.value === "string" &&
                 (node.parent.type === "NewExpression" || node.parent.type === "CallExpression") &&
@@ -82,7 +86,15 @@ module.exports = {
                 node.parent.callee.name === "RegExp" &&
                 node.parent.arguments[0] === node
             ) {
-                return node.value;
+                const pattern = node.value;
+                const flags =
+                    node.parent.arguments.length > 1 &&
+                    node.parent.arguments[1].type === "Literal" &&
+                    typeof node.parent.arguments[1].value === "string"
+                        ? node.parent.arguments[1].value
+                        : null;
+
+                return { pattern, flags };
             }
 
             return null;
@@ -90,10 +102,11 @@ module.exports = {
 
         return {
             Literal(node) {
-                const pattern = getRegExpPattern(node);
+                const regExp = getRegExp(node);
 
-                if (pattern) {
-                    const controlCharacters = collector.collectControlChars(pattern);
+                if (regExp) {
+                    const { pattern, flags } = regExp;
+                    const controlCharacters = collector.collectControlChars(pattern, flags);
 
                     if (controlCharacters.length > 0) {
                         context.report({


Test patch:
diff --git a/tests/lib/rules/no-control-regex.js b/tests/lib/rules/no-control-regex.js
index 059e50dc4..14abfbce4 100644
--- a/tests/lib/rules/no-control-regex.js
+++ b/tests/lib/rules/no-control-regex.js
@@ -26,7 +26,14 @@ ruleTester.run("no-control-regex", rule, {
         "var regex = RegExp('x1f')",
         "new RegExp('[')",
         "RegExp('[')",
-        "new (function foo(){})('\\x1f')"
+        "new (function foo(){})('\\x1f')",
+        { code: String.raw`/\u{20}/u`, parserOptions: { ecmaVersion: 2015 } },
+        String.raw`/\u{1F}/`,
+        String.raw`/\u{1F}/g`,
+        String.raw`new RegExp("\\u{20}", "u")`,
+        String.raw`new RegExp("\\u{1F}")`,
+        String.raw`new RegExp("\\u{1F}", "g")`,
+        String.raw`new RegExp("\\u{1F}", flags)` // when flags are unknown, this rule assumes there's no `u` flag
     ],
     invalid: [
         { code: String.raw`var regex = /\x1f/`, errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }] },
@@ -46,6 +53,38 @@ ruleTester.run("no-control-regex", rule, {
             code: String.raw`var regex = /(?<\u{1d49c}>.)\x1f/`,
             parserOptions: { ecmaVersion: 2020 },
             errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }]
+        },
+        {
+            code: String.raw`new RegExp("\\u001F", flags)`,
+            errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }]
+        },
+        {
+            code: String.raw`/\u{1111}*\x1F/u`,
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }]
+        },
+        {
+            code: String.raw`new RegExp("\\u{1111}*\\x1F", "u")`,
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }]
+        },
+        {
+            code: String.raw`/\u{1F}/u`,
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }]
+        },
+        {
+            code: String.raw`/\u{1F}/gui`,
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }]
+        },
+        {
+            code: String.raw`new RegExp("\\u{1F}", "u")`,
+            errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }]
+        },
+        {
+            code: String.raw`new RegExp("\\u{1F}", "gui")`,
+            errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }]
         }
     ]
 });