Skip to content

Commit

Permalink
feat: add no-dynamic-createelement rule (#8656)
Browse files Browse the repository at this point in the history
**Related Issue:** #8651

## Summary

This adds a custom ESLint rule to ensure Stencil bundles and
auto-defines child components created via `document.createElement`.
  • Loading branch information
jcfranco authored Jan 26, 2024
1 parent e6d792b commit c7e9444
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 0 deletions.
6 changes: 6 additions & 0 deletions packages/calcite-components/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,10 @@ module.exports = {
ignorePrivate: true,
},
},
overrides: [{
files: ["**/*.e2e.ts", "src/tests/**/*"],
rules: {
"@esri/calcite-components/no-dynamic-createelement": "off",
}
}]
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# no-dynamic-createelement

Helps prevent usage of dynamic element tags used with `createElement`. This ensures the component is properly bundled and auto-defined in Stencil's `components` output target.

## Config

No config is needed

## Usage

```json
{ "@esri/calcite-components/no-dynamic-createelement": "error" }
```
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default {
rules: {
"@esri/calcite-components/ban-props-on-host": 2,
"@esri/calcite-components/enforce-ref-last-prop": 2,
"@esri/calcite-components/no-dynamic-createelement": 2,
"@esri/calcite-components/require-event-emitter-type": 2,
"@esri/calcite-components/strict-boolean-attributes": 2,
},
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin-calcite-components/src/rules/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import banEvents from "./ban-events";
import banPropsOnHost from "./ban-props-on-host";
import enforceRefLastProp from "./enforce-ref-last-prop";
import noDynamicCreateelement from "./no-dynamic-createelement";
import requireEventEmitterType from "./require-event-emitter-type";
import strictBooleanAttributes from "./strict-boolean-attributes";

export default {
"ban-events": banEvents,
"ban-props-on-host": banPropsOnHost,
"enforce-ref-last-prop": enforceRefLastProp,
"no-dynamic-createelement": noDynamicCreateelement,
"require-event-emitter-type": requireEventEmitterType,
"strict-boolean-attributes": strictBooleanAttributes,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Rule } from "eslint";

function isCreateElement(node) {
return (
node?.callee?.type === "MemberExpression" &&
node?.callee?.object?.name === "document" &&
node?.callee?.property?.name === "createElement" &&
node.arguments.length >= 1
);
}

function isStaticValue(arg) {
return arg.type === "Literal" || (arg.type === "TemplateLiteral" && arg.expressions.length === 0);
}

const rule: Rule.RuleModule = {
meta: {
docs: {
description:
"This ensures supporting components created with `document.createElement()` are auto-defined in Stencil's `components` output target.",
recommended: true,
},
fixable: "code",
schema: [],
type: "problem",
},

create(context) {
return {
CallExpression(node) {
if (!node.arguments[0] || isStaticValue(node.arguments[0])) {
return;
}

if (isCreateElement(node)) {
return context.report({
node,
message: "Calls to document.createElement() should use string literals",
});
}
},
};
},
};

export default rule;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// @ts-nocheck
@Component({ tag: "sample-tag" })
export class SampleTag {
@Prop()
type: "one" | "two" = "one";

connectedCallback() {
const child =
this.type === "one"
? document.createElement("my-component-1")
: document.createElement("my-component-2");
this.el.append(child);
this.internalEl = child;
}

disconnectedCallback() {
this.internalEl.remove();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import rule from "../../../../src/rules/no-dynamic-createelement";
import { ruleTester } from "stencil-eslint-core";
import * as path from "path";
import * as fs from "fs";

const projectPath = path.resolve(__dirname, "../../../tsconfig.json");

describe("no-dynamic-createelement rule", () => {
const files = {
good: path.resolve(__dirname, "no-dynamic-createelement.good.tsx"),
wrong: path.resolve(__dirname, "no-dynamic-createelement.wrong.tsx"),
};
ruleTester(projectPath).run("no-dynamic-createelement", rule, {
valid: [
{
code: fs.readFileSync(files.good, "utf8"),
filename: files.good,
},
],

invalid: [
{
code: fs.readFileSync(files.wrong, "utf8"),
filename: files.wrong,
errors: 1,
},
],
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// @ts-nocheck
@Component({ tag: "sample-tag" })
export class SampleTag {
@Prop()
type: "one" | "two" = "one";

connectedCallback() {
const child = document.createElement(this.type === "one" ? "my-component-1" : "my-component-2");
this.el.append(child);
this.internalEl = child;
}

disconnectedCallback() {
this.internalEl.remove();
}
}

0 comments on commit c7e9444

Please sign in to comment.