@prairielearn/eslint-plugin 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @prairielearn/eslint-plugin
2
2
 
3
+ ## 4.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9717de9: Add a new rule `@prairielearn/html-no-duplicate-id`. It's a variant of `@html-eslint/no-duplicate-id` that ignores tags whose names start with `pl-`, since the `id` attribute on PrairieLearn elements (e.g. `<pl-sketch-tool id="fd">`) is an element-scoped identifier consumed by the parent element, not a DOM id. It also ignores `id` values that contain mustache template parameters.
8
+
9
+ ### Patch Changes
10
+
11
+ - 3927197: Update dependencies
12
+
3
13
  ## 4.2.0
4
14
 
5
15
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ declare const _default: {
2
2
  rules: {
3
3
  'aws-client-mandatory-config': import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingConfig", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
4
4
  'aws-client-shared-config': import("@typescript-eslint/utils/ts-eslint").RuleModule<"improperConfig" | "unknownConfig", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
5
+ 'html-no-duplicate-id': import("@typescript-eslint/utils/ts-eslint").RuleModule<"duplicateId", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
5
6
  'jsx-no-dollar-interpolation': import("@typescript-eslint/utils/ts-eslint").RuleModule<"dollarInterpolationNotAllowed", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
6
7
  'no-current-target-in-callback': import("@typescript-eslint/utils/ts-eslint").RuleModule<"noCurrentTargetInCallback", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
7
8
  'no-hydrate-reslocals': import("@typescript-eslint/utils/ts-eslint").RuleModule<"forbiddenProp" | "forbiddenSpread", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"","sourcesContent":["import awsClientMandatoryConfig from './rules/aws-client-mandatory-config.js';\nimport awsClientSharedConfig from './rules/aws-client-shared-config.js';\nimport jsxNoDollarInterpolation from './rules/jsx-no-dollar-interpolation.js';\nimport noCurrentTargetInCallback from './rules/no-current-target-in-callback.js';\nimport noHydrateResLocals from './rules/no-hydrate-reslocals.js';\nimport noUnusedSqlBlocks from './rules/no-unused-sql-blocks.js';\nimport requireTrpcPermissionMiddleware from './rules/require-trpc-permission-middleware.js';\nimport safeDbTypes from './rules/safe-db-types.js';\n\nconst rules = {\n 'aws-client-mandatory-config': awsClientMandatoryConfig,\n 'aws-client-shared-config': awsClientSharedConfig,\n 'jsx-no-dollar-interpolation': jsxNoDollarInterpolation,\n 'no-current-target-in-callback': noCurrentTargetInCallback,\n 'no-hydrate-reslocals': noHydrateResLocals,\n 'no-unused-sql-blocks': noUnusedSqlBlocks,\n 'require-trpc-permission-middleware': requireTrpcPermissionMiddleware,\n 'safe-db-types': safeDbTypes,\n};\n\nexport default { rules };\n"]}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"","sourcesContent":["import awsClientMandatoryConfig from './rules/aws-client-mandatory-config.js';\nimport awsClientSharedConfig from './rules/aws-client-shared-config.js';\nimport htmlNoDuplicateId from './rules/html-no-duplicate-id.js';\nimport jsxNoDollarInterpolation from './rules/jsx-no-dollar-interpolation.js';\nimport noCurrentTargetInCallback from './rules/no-current-target-in-callback.js';\nimport noHydrateResLocals from './rules/no-hydrate-reslocals.js';\nimport noUnusedSqlBlocks from './rules/no-unused-sql-blocks.js';\nimport requireTrpcPermissionMiddleware from './rules/require-trpc-permission-middleware.js';\nimport safeDbTypes from './rules/safe-db-types.js';\n\nconst rules = {\n 'aws-client-mandatory-config': awsClientMandatoryConfig,\n 'aws-client-shared-config': awsClientSharedConfig,\n 'html-no-duplicate-id': htmlNoDuplicateId,\n 'jsx-no-dollar-interpolation': jsxNoDollarInterpolation,\n 'no-current-target-in-callback': noCurrentTargetInCallback,\n 'no-hydrate-reslocals': noHydrateResLocals,\n 'no-unused-sql-blocks': noUnusedSqlBlocks,\n 'require-trpc-permission-middleware': requireTrpcPermissionMiddleware,\n 'safe-db-types': safeDbTypes,\n};\n\nexport default { rules };\n"]}
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import awsClientMandatoryConfig from './rules/aws-client-mandatory-config.js';
2
2
  import awsClientSharedConfig from './rules/aws-client-shared-config.js';
3
+ import htmlNoDuplicateId from './rules/html-no-duplicate-id.js';
3
4
  import jsxNoDollarInterpolation from './rules/jsx-no-dollar-interpolation.js';
4
5
  import noCurrentTargetInCallback from './rules/no-current-target-in-callback.js';
5
6
  import noHydrateResLocals from './rules/no-hydrate-reslocals.js';
@@ -9,6 +10,7 @@ import safeDbTypes from './rules/safe-db-types.js';
9
10
  const rules = {
10
11
  'aws-client-mandatory-config': awsClientMandatoryConfig,
11
12
  'aws-client-shared-config': awsClientSharedConfig,
13
+ 'html-no-duplicate-id': htmlNoDuplicateId,
12
14
  'jsx-no-dollar-interpolation': jsxNoDollarInterpolation,
13
15
  'no-current-target-in-callback': noCurrentTargetInCallback,
14
16
  'no-hydrate-reslocals': noHydrateResLocals,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,wBAAwB,MAAM,wCAAwC,CAAC;AAC9E,OAAO,qBAAqB,MAAM,qCAAqC,CAAC;AACxE,OAAO,wBAAwB,MAAM,wCAAwC,CAAC;AAC9E,OAAO,yBAAyB,MAAM,0CAA0C,CAAC;AACjF,OAAO,kBAAkB,MAAM,iCAAiC,CAAC;AACjE,OAAO,iBAAiB,MAAM,iCAAiC,CAAC;AAChE,OAAO,+BAA+B,MAAM,+CAA+C,CAAC;AAC5F,OAAO,WAAW,MAAM,0BAA0B,CAAC;AAEnD,MAAM,KAAK,GAAG;IACZ,6BAA6B,EAAE,wBAAwB;IACvD,0BAA0B,EAAE,qBAAqB;IACjD,6BAA6B,EAAE,wBAAwB;IACvD,+BAA+B,EAAE,yBAAyB;IAC1D,sBAAsB,EAAE,kBAAkB;IAC1C,sBAAsB,EAAE,iBAAiB;IACzC,oCAAoC,EAAE,+BAA+B;IACrE,eAAe,EAAE,WAAW;CAC7B,CAAC;AAEF,eAAe,EAAE,KAAK,EAAE,CAAC","sourcesContent":["import awsClientMandatoryConfig from './rules/aws-client-mandatory-config.js';\nimport awsClientSharedConfig from './rules/aws-client-shared-config.js';\nimport jsxNoDollarInterpolation from './rules/jsx-no-dollar-interpolation.js';\nimport noCurrentTargetInCallback from './rules/no-current-target-in-callback.js';\nimport noHydrateResLocals from './rules/no-hydrate-reslocals.js';\nimport noUnusedSqlBlocks from './rules/no-unused-sql-blocks.js';\nimport requireTrpcPermissionMiddleware from './rules/require-trpc-permission-middleware.js';\nimport safeDbTypes from './rules/safe-db-types.js';\n\nconst rules = {\n 'aws-client-mandatory-config': awsClientMandatoryConfig,\n 'aws-client-shared-config': awsClientSharedConfig,\n 'jsx-no-dollar-interpolation': jsxNoDollarInterpolation,\n 'no-current-target-in-callback': noCurrentTargetInCallback,\n 'no-hydrate-reslocals': noHydrateResLocals,\n 'no-unused-sql-blocks': noUnusedSqlBlocks,\n 'require-trpc-permission-middleware': requireTrpcPermissionMiddleware,\n 'safe-db-types': safeDbTypes,\n};\n\nexport default { rules };\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,wBAAwB,MAAM,wCAAwC,CAAC;AAC9E,OAAO,qBAAqB,MAAM,qCAAqC,CAAC;AACxE,OAAO,iBAAiB,MAAM,iCAAiC,CAAC;AAChE,OAAO,wBAAwB,MAAM,wCAAwC,CAAC;AAC9E,OAAO,yBAAyB,MAAM,0CAA0C,CAAC;AACjF,OAAO,kBAAkB,MAAM,iCAAiC,CAAC;AACjE,OAAO,iBAAiB,MAAM,iCAAiC,CAAC;AAChE,OAAO,+BAA+B,MAAM,+CAA+C,CAAC;AAC5F,OAAO,WAAW,MAAM,0BAA0B,CAAC;AAEnD,MAAM,KAAK,GAAG;IACZ,6BAA6B,EAAE,wBAAwB;IACvD,0BAA0B,EAAE,qBAAqB;IACjD,sBAAsB,EAAE,iBAAiB;IACzC,6BAA6B,EAAE,wBAAwB;IACvD,+BAA+B,EAAE,yBAAyB;IAC1D,sBAAsB,EAAE,kBAAkB;IAC1C,sBAAsB,EAAE,iBAAiB;IACzC,oCAAoC,EAAE,+BAA+B;IACrE,eAAe,EAAE,WAAW;CAC7B,CAAC;AAEF,eAAe,EAAE,KAAK,EAAE,CAAC","sourcesContent":["import awsClientMandatoryConfig from './rules/aws-client-mandatory-config.js';\nimport awsClientSharedConfig from './rules/aws-client-shared-config.js';\nimport htmlNoDuplicateId from './rules/html-no-duplicate-id.js';\nimport jsxNoDollarInterpolation from './rules/jsx-no-dollar-interpolation.js';\nimport noCurrentTargetInCallback from './rules/no-current-target-in-callback.js';\nimport noHydrateResLocals from './rules/no-hydrate-reslocals.js';\nimport noUnusedSqlBlocks from './rules/no-unused-sql-blocks.js';\nimport requireTrpcPermissionMiddleware from './rules/require-trpc-permission-middleware.js';\nimport safeDbTypes from './rules/safe-db-types.js';\n\nconst rules = {\n 'aws-client-mandatory-config': awsClientMandatoryConfig,\n 'aws-client-shared-config': awsClientSharedConfig,\n 'html-no-duplicate-id': htmlNoDuplicateId,\n 'jsx-no-dollar-interpolation': jsxNoDollarInterpolation,\n 'no-current-target-in-callback': noCurrentTargetInCallback,\n 'no-hydrate-reslocals': noHydrateResLocals,\n 'no-unused-sql-blocks': noUnusedSqlBlocks,\n 'require-trpc-permission-middleware': requireTrpcPermissionMiddleware,\n 'safe-db-types': safeDbTypes,\n};\n\nexport default { rules };\n"]}
@@ -0,0 +1,10 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ /**
3
+ * Variant of `@html-eslint/no-duplicate-id` that ignores tags whose names start
4
+ * with `pl-`. The `id` attribute on PrairieLearn elements (e.g. `pl-sketch-tool
5
+ * id="fd"`) is not a DOM id, it's an element-specific identifier consumed by
6
+ * the parent element, so duplicates across separate parents are legal.
7
+ */
8
+ declare const _default: ESLintUtils.RuleModule<"duplicateId", [], unknown, ESLintUtils.RuleListener>;
9
+ export default _default;
10
+ //# sourceMappingURL=html-no-duplicate-id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-no-duplicate-id.d.ts","sourceRoot":"","sources":["../../src/rules/html-no-duplicate-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAiCvD;;;;;GAKG","sourcesContent":["import { ESLintUtils } from '@typescript-eslint/utils';\n\ninterface AttributeValueNode {\n type: 'AttributeValue';\n value: string;\n parts?: { type: string }[];\n}\n\ninterface AttributeKeyNode {\n type: 'AttributeKey';\n value: string;\n}\n\ninterface AttributeNode {\n type: 'Attribute';\n key: AttributeKeyNode;\n value?: AttributeValueNode;\n}\n\ninterface TagNode {\n type: 'Tag';\n name: string;\n attributes: AttributeNode[];\n}\n\nfunction findIdAttr(node: TagNode): AttributeNode | undefined {\n return node.attributes.find((attr) => attr.key && attr.key.value.toLowerCase() === 'id');\n}\n\nfunction hasTemplatePart(node: AttributeValueNode): boolean {\n return node.parts?.some((part) => part.type === 'Template') ?? false;\n}\n\n/**\n * Variant of `@html-eslint/no-duplicate-id` that ignores tags whose names start\n * with `pl-`. The `id` attribute on PrairieLearn elements (e.g. `pl-sketch-tool\n * id=\"fd\"`) is not a DOM id, it's an element-specific identifier consumed by\n * the parent element, so duplicates across separate parents are legal.\n */\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n duplicateId: \"The id '{{id}}' is duplicated.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const idAttrs = new Map<string, AttributeValueNode[]>();\n\n return {\n Tag(node: TagNode) {\n if (node.name.toLowerCase().startsWith('pl-')) return;\n const idAttr = findIdAttr(node);\n if (!idAttr?.value) return;\n if (hasTemplatePart(idAttr.value)) return;\n const list = idAttrs.get(idAttr.value.value) ?? [];\n list.push(idAttr.value);\n idAttrs.set(idAttr.value.value, list);\n },\n 'Document:exit'() {\n for (const attrs of idAttrs.values()) {\n if (attrs.length <= 1) continue;\n for (const attr of attrs) {\n context.report({\n node: attr as never,\n data: { id: attr.value },\n messageId: 'duplicateId',\n });\n }\n }\n },\n };\n },\n});\n"]}
@@ -0,0 +1,54 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ function findIdAttr(node) {
3
+ return node.attributes.find((attr) => attr.key && attr.key.value.toLowerCase() === 'id');
4
+ }
5
+ function hasTemplatePart(node) {
6
+ return node.parts?.some((part) => part.type === 'Template') ?? false;
7
+ }
8
+ /**
9
+ * Variant of `@html-eslint/no-duplicate-id` that ignores tags whose names start
10
+ * with `pl-`. The `id` attribute on PrairieLearn elements (e.g. `pl-sketch-tool
11
+ * id="fd"`) is not a DOM id, it's an element-specific identifier consumed by
12
+ * the parent element, so duplicates across separate parents are legal.
13
+ */
14
+ export default ESLintUtils.RuleCreator.withoutDocs({
15
+ meta: {
16
+ type: 'problem',
17
+ messages: {
18
+ duplicateId: "The id '{{id}}' is duplicated.",
19
+ },
20
+ schema: [],
21
+ },
22
+ defaultOptions: [],
23
+ create(context) {
24
+ const idAttrs = new Map();
25
+ return {
26
+ Tag(node) {
27
+ if (node.name.toLowerCase().startsWith('pl-'))
28
+ return;
29
+ const idAttr = findIdAttr(node);
30
+ if (!idAttr?.value)
31
+ return;
32
+ if (hasTemplatePart(idAttr.value))
33
+ return;
34
+ const list = idAttrs.get(idAttr.value.value) ?? [];
35
+ list.push(idAttr.value);
36
+ idAttrs.set(idAttr.value.value, list);
37
+ },
38
+ 'Document:exit'() {
39
+ for (const attrs of idAttrs.values()) {
40
+ if (attrs.length <= 1)
41
+ continue;
42
+ for (const attr of attrs) {
43
+ context.report({
44
+ node: attr,
45
+ data: { id: attr.value },
46
+ messageId: 'duplicateId',
47
+ });
48
+ }
49
+ }
50
+ },
51
+ };
52
+ },
53
+ });
54
+ //# sourceMappingURL=html-no-duplicate-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-no-duplicate-id.js","sourceRoot":"","sources":["../../src/rules/html-no-duplicate-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAyBvD,SAAS,UAAU,CAAC,IAAa;IAC/B,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,IAAI,CAAC,CAAC;AAC3F,CAAC;AAED,SAAS,eAAe,CAAC,IAAwB;IAC/C,OAAO,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,UAAU,CAAC,IAAI,KAAK,CAAC;AACvE,CAAC;AAED;;;;;GAKG;AACH,eAAe,WAAW,CAAC,WAAW,CAAC,WAAW,CAAC;IACjD,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,QAAQ,EAAE;YACR,WAAW,EAAE,gCAAgC;SAC9C;QACD,MAAM,EAAE,EAAE;KACX;IACD,cAAc,EAAE,EAAE;IAClB,MAAM,CAAC,OAAO;QACZ,MAAM,OAAO,GAAG,IAAI,GAAG,EAAgC,CAAC;QAExD,OAAO;YACL,GAAG,CAAC,IAAa;gBACf,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC;oBAAE,OAAO;gBACtD,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;gBAChC,IAAI,CAAC,MAAM,EAAE,KAAK;oBAAE,OAAO;gBAC3B,IAAI,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC;oBAAE,OAAO;gBAC1C,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;gBACnD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACxB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YACxC,CAAC;YACD,eAAe;gBACb,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;oBACrC,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC;wBAAE,SAAS;oBAChC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;wBACzB,OAAO,CAAC,MAAM,CAAC;4BACb,IAAI,EAAE,IAAa;4BACnB,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,KAAK,EAAE;4BACxB,SAAS,EAAE,aAAa;yBACzB,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC","sourcesContent":["import { ESLintUtils } from '@typescript-eslint/utils';\n\ninterface AttributeValueNode {\n type: 'AttributeValue';\n value: string;\n parts?: { type: string }[];\n}\n\ninterface AttributeKeyNode {\n type: 'AttributeKey';\n value: string;\n}\n\ninterface AttributeNode {\n type: 'Attribute';\n key: AttributeKeyNode;\n value?: AttributeValueNode;\n}\n\ninterface TagNode {\n type: 'Tag';\n name: string;\n attributes: AttributeNode[];\n}\n\nfunction findIdAttr(node: TagNode): AttributeNode | undefined {\n return node.attributes.find((attr) => attr.key && attr.key.value.toLowerCase() === 'id');\n}\n\nfunction hasTemplatePart(node: AttributeValueNode): boolean {\n return node.parts?.some((part) => part.type === 'Template') ?? false;\n}\n\n/**\n * Variant of `@html-eslint/no-duplicate-id` that ignores tags whose names start\n * with `pl-`. The `id` attribute on PrairieLearn elements (e.g. `pl-sketch-tool\n * id=\"fd\"`) is not a DOM id, it's an element-specific identifier consumed by\n * the parent element, so duplicates across separate parents are legal.\n */\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n duplicateId: \"The id '{{id}}' is duplicated.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const idAttrs = new Map<string, AttributeValueNode[]>();\n\n return {\n Tag(node: TagNode) {\n if (node.name.toLowerCase().startsWith('pl-')) return;\n const idAttr = findIdAttr(node);\n if (!idAttr?.value) return;\n if (hasTemplatePart(idAttr.value)) return;\n const list = idAttrs.get(idAttr.value.value) ?? [];\n list.push(idAttr.value);\n idAttrs.set(idAttr.value.value, list);\n },\n 'Document:exit'() {\n for (const attrs of idAttrs.values()) {\n if (attrs.length <= 1) continue;\n for (const attr of attrs) {\n context.report({\n node: attr as never,\n data: { id: attr.value },\n messageId: 'duplicateId',\n });\n }\n }\n },\n };\n },\n});\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=html-no-duplicate-id.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-no-duplicate-id.test.d.ts","sourceRoot":"","sources":["../../src/tests/html-no-duplicate-id.test.ts"],"names":[],"mappings":"","sourcesContent":["import htmlParser from '@html-eslint/parser';\nimport { RuleTester } from '@typescript-eslint/rule-tester';\nimport { afterAll, describe, it } from 'vitest';\n\nimport rule from '../rules/html-no-duplicate-id.js';\n\nRuleTester.afterAll = afterAll;\nRuleTester.describe = describe;\nRuleTester.it = it;\n\nconst ruleTester = new RuleTester({\n languageOptions: {\n parser: htmlParser,\n parserOptions: {\n templateEngineSyntax: htmlParser.TEMPLATE_ENGINE_SYNTAX.HANDLEBAR,\n },\n },\n});\n\nruleTester.run('html-no-duplicate-id', rule, {\n valid: [\n {\n code: '<div id=\"a\"></div><div id=\"b\"></div>',\n },\n // Duplicate `id` on `pl-*` elements is a tool/element identifier, not a\n // DOM id, so it should be ignored.\n {\n code: `\n <pl-sketch>\n <pl-sketch-tool id=\"fd\" type=\"free-draw\"></pl-sketch-tool>\n </pl-sketch>\n <pl-sketch>\n <pl-sketch-tool id=\"fd\" type=\"free-draw\"></pl-sketch-tool>\n </pl-sketch>`,\n },\n {\n code: '<div id=\"question-{{params.name}}\"></div><span id=\"question-{{params.name}}\"></span>',\n },\n ],\n invalid: [\n {\n code: '<div id=\"x\"></div><div id=\"x\"></div>',\n errors: [\n { messageId: 'duplicateId', data: { id: 'x' } },\n { messageId: 'duplicateId', data: { id: 'x' } },\n ],\n },\n // Duplicate `id` on non-`pl-*` elements should be reported.\n {\n code: '<div id=\"fd\"></div><span id=\"fd\"></span>',\n errors: [\n { messageId: 'duplicateId', data: { id: 'fd' } },\n { messageId: 'duplicateId', data: { id: 'fd' } },\n ],\n },\n ],\n});\n"]}
@@ -0,0 +1,54 @@
1
+ import htmlParser from '@html-eslint/parser';
2
+ import { RuleTester } from '@typescript-eslint/rule-tester';
3
+ import { afterAll, describe, it } from 'vitest';
4
+ import rule from '../rules/html-no-duplicate-id.js';
5
+ RuleTester.afterAll = afterAll;
6
+ RuleTester.describe = describe;
7
+ RuleTester.it = it;
8
+ const ruleTester = new RuleTester({
9
+ languageOptions: {
10
+ parser: htmlParser,
11
+ parserOptions: {
12
+ templateEngineSyntax: htmlParser.TEMPLATE_ENGINE_SYNTAX.HANDLEBAR,
13
+ },
14
+ },
15
+ });
16
+ ruleTester.run('html-no-duplicate-id', rule, {
17
+ valid: [
18
+ {
19
+ code: '<div id="a"></div><div id="b"></div>',
20
+ },
21
+ // Duplicate `id` on `pl-*` elements is a tool/element identifier, not a
22
+ // DOM id, so it should be ignored.
23
+ {
24
+ code: `
25
+ <pl-sketch>
26
+ <pl-sketch-tool id="fd" type="free-draw"></pl-sketch-tool>
27
+ </pl-sketch>
28
+ <pl-sketch>
29
+ <pl-sketch-tool id="fd" type="free-draw"></pl-sketch-tool>
30
+ </pl-sketch>`,
31
+ },
32
+ {
33
+ code: '<div id="question-{{params.name}}"></div><span id="question-{{params.name}}"></span>',
34
+ },
35
+ ],
36
+ invalid: [
37
+ {
38
+ code: '<div id="x"></div><div id="x"></div>',
39
+ errors: [
40
+ { messageId: 'duplicateId', data: { id: 'x' } },
41
+ { messageId: 'duplicateId', data: { id: 'x' } },
42
+ ],
43
+ },
44
+ // Duplicate `id` on non-`pl-*` elements should be reported.
45
+ {
46
+ code: '<div id="fd"></div><span id="fd"></span>',
47
+ errors: [
48
+ { messageId: 'duplicateId', data: { id: 'fd' } },
49
+ { messageId: 'duplicateId', data: { id: 'fd' } },
50
+ ],
51
+ },
52
+ ],
53
+ });
54
+ //# sourceMappingURL=html-no-duplicate-id.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-no-duplicate-id.test.js","sourceRoot":"","sources":["../../src/tests/html-no-duplicate-id.test.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAEhD,OAAO,IAAI,MAAM,kCAAkC,CAAC;AAEpD,UAAU,CAAC,QAAQ,GAAG,QAAQ,CAAC;AAC/B,UAAU,CAAC,QAAQ,GAAG,QAAQ,CAAC;AAC/B,UAAU,CAAC,EAAE,GAAG,EAAE,CAAC;AAEnB,MAAM,UAAU,GAAG,IAAI,UAAU,CAAC;IAChC,eAAe,EAAE;QACf,MAAM,EAAE,UAAU;QAClB,aAAa,EAAE;YACb,oBAAoB,EAAE,UAAU,CAAC,sBAAsB,CAAC,SAAS;SAClE;KACF;CACF,CAAC,CAAC;AAEH,UAAU,CAAC,GAAG,CAAC,sBAAsB,EAAE,IAAI,EAAE;IAC3C,KAAK,EAAE;QACL;YACE,IAAI,EAAE,sCAAsC;SAC7C;QACD,wEAAwE;QACxE,mCAAmC;QACnC;YACE,IAAI,EAAE;;;;;;qBAMS;SAChB;QACD;YACE,IAAI,EAAE,sFAAsF;SAC7F;KACF;IACD,OAAO,EAAE;QACP;YACE,IAAI,EAAE,sCAAsC;YAC5C,MAAM,EAAE;gBACN,EAAE,SAAS,EAAE,aAAa,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE;gBAC/C,EAAE,SAAS,EAAE,aAAa,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE;aAChD;SACF;QACD,4DAA4D;QAC5D;YACE,IAAI,EAAE,0CAA0C;YAChD,MAAM,EAAE;gBACN,EAAE,SAAS,EAAE,aAAa,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE;gBAChD,EAAE,SAAS,EAAE,aAAa,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE;aACjD;SACF;KACF;CACF,CAAC,CAAC","sourcesContent":["import htmlParser from '@html-eslint/parser';\nimport { RuleTester } from '@typescript-eslint/rule-tester';\nimport { afterAll, describe, it } from 'vitest';\n\nimport rule from '../rules/html-no-duplicate-id.js';\n\nRuleTester.afterAll = afterAll;\nRuleTester.describe = describe;\nRuleTester.it = it;\n\nconst ruleTester = new RuleTester({\n languageOptions: {\n parser: htmlParser,\n parserOptions: {\n templateEngineSyntax: htmlParser.TEMPLATE_ENGINE_SYNTAX.HANDLEBAR,\n },\n },\n});\n\nruleTester.run('html-no-duplicate-id', rule, {\n valid: [\n {\n code: '<div id=\"a\"></div><div id=\"b\"></div>',\n },\n // Duplicate `id` on `pl-*` elements is a tool/element identifier, not a\n // DOM id, so it should be ignored.\n {\n code: `\n <pl-sketch>\n <pl-sketch-tool id=\"fd\" type=\"free-draw\"></pl-sketch-tool>\n </pl-sketch>\n <pl-sketch>\n <pl-sketch-tool id=\"fd\" type=\"free-draw\"></pl-sketch-tool>\n </pl-sketch>`,\n },\n {\n code: '<div id=\"question-{{params.name}}\"></div><span id=\"question-{{params.name}}\"></span>',\n },\n ],\n invalid: [\n {\n code: '<div id=\"x\"></div><div id=\"x\"></div>',\n errors: [\n { messageId: 'duplicateId', data: { id: 'x' } },\n { messageId: 'duplicateId', data: { id: 'x' } },\n ],\n },\n // Duplicate `id` on non-`pl-*` elements should be reported.\n {\n code: '<div id=\"fd\"></div><span id=\"fd\"></span>',\n errors: [\n { messageId: 'duplicateId', data: { id: 'fd' } },\n { messageId: 'duplicateId', data: { id: 'fd' } },\n ],\n },\n ],\n});\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prairielearn/eslint-plugin",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -24,13 +24,13 @@
24
24
  "typescript": "^6.0.3"
25
25
  },
26
26
  "devDependencies": {
27
+ "@html-eslint/parser": "^0.58.1",
27
28
  "@prairielearn/tsconfig": "^2.1.0",
28
29
  "@types/node": "^24.12.2",
29
30
  "@typescript-eslint/rule-tester": "^8.59.2",
30
31
  "@typescript-eslint/types": "^8.59.2",
31
32
  "@typescript/native-preview": "beta",
32
33
  "@vitest/coverage-v8": "^4.1.5",
33
- "tsx": "^4.21.0",
34
34
  "vitest": "^4.1.5"
35
35
  }
36
36
  }
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import awsClientMandatoryConfig from './rules/aws-client-mandatory-config.js';
2
2
  import awsClientSharedConfig from './rules/aws-client-shared-config.js';
3
+ import htmlNoDuplicateId from './rules/html-no-duplicate-id.js';
3
4
  import jsxNoDollarInterpolation from './rules/jsx-no-dollar-interpolation.js';
4
5
  import noCurrentTargetInCallback from './rules/no-current-target-in-callback.js';
5
6
  import noHydrateResLocals from './rules/no-hydrate-reslocals.js';
@@ -10,6 +11,7 @@ import safeDbTypes from './rules/safe-db-types.js';
10
11
  const rules = {
11
12
  'aws-client-mandatory-config': awsClientMandatoryConfig,
12
13
  'aws-client-shared-config': awsClientSharedConfig,
14
+ 'html-no-duplicate-id': htmlNoDuplicateId,
13
15
  'jsx-no-dollar-interpolation': jsxNoDollarInterpolation,
14
16
  'no-current-target-in-callback': noCurrentTargetInCallback,
15
17
  'no-hydrate-reslocals': noHydrateResLocals,
@@ -0,0 +1,76 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+
3
+ interface AttributeValueNode {
4
+ type: 'AttributeValue';
5
+ value: string;
6
+ parts?: { type: string }[];
7
+ }
8
+
9
+ interface AttributeKeyNode {
10
+ type: 'AttributeKey';
11
+ value: string;
12
+ }
13
+
14
+ interface AttributeNode {
15
+ type: 'Attribute';
16
+ key: AttributeKeyNode;
17
+ value?: AttributeValueNode;
18
+ }
19
+
20
+ interface TagNode {
21
+ type: 'Tag';
22
+ name: string;
23
+ attributes: AttributeNode[];
24
+ }
25
+
26
+ function findIdAttr(node: TagNode): AttributeNode | undefined {
27
+ return node.attributes.find((attr) => attr.key && attr.key.value.toLowerCase() === 'id');
28
+ }
29
+
30
+ function hasTemplatePart(node: AttributeValueNode): boolean {
31
+ return node.parts?.some((part) => part.type === 'Template') ?? false;
32
+ }
33
+
34
+ /**
35
+ * Variant of `@html-eslint/no-duplicate-id` that ignores tags whose names start
36
+ * with `pl-`. The `id` attribute on PrairieLearn elements (e.g. `pl-sketch-tool
37
+ * id="fd"`) is not a DOM id, it's an element-specific identifier consumed by
38
+ * the parent element, so duplicates across separate parents are legal.
39
+ */
40
+ export default ESLintUtils.RuleCreator.withoutDocs({
41
+ meta: {
42
+ type: 'problem',
43
+ messages: {
44
+ duplicateId: "The id '{{id}}' is duplicated.",
45
+ },
46
+ schema: [],
47
+ },
48
+ defaultOptions: [],
49
+ create(context) {
50
+ const idAttrs = new Map<string, AttributeValueNode[]>();
51
+
52
+ return {
53
+ Tag(node: TagNode) {
54
+ if (node.name.toLowerCase().startsWith('pl-')) return;
55
+ const idAttr = findIdAttr(node);
56
+ if (!idAttr?.value) return;
57
+ if (hasTemplatePart(idAttr.value)) return;
58
+ const list = idAttrs.get(idAttr.value.value) ?? [];
59
+ list.push(idAttr.value);
60
+ idAttrs.set(idAttr.value.value, list);
61
+ },
62
+ 'Document:exit'() {
63
+ for (const attrs of idAttrs.values()) {
64
+ if (attrs.length <= 1) continue;
65
+ for (const attr of attrs) {
66
+ context.report({
67
+ node: attr as never,
68
+ data: { id: attr.value },
69
+ messageId: 'duplicateId',
70
+ });
71
+ }
72
+ }
73
+ },
74
+ };
75
+ },
76
+ });
@@ -0,0 +1,57 @@
1
+ import htmlParser from '@html-eslint/parser';
2
+ import { RuleTester } from '@typescript-eslint/rule-tester';
3
+ import { afterAll, describe, it } from 'vitest';
4
+
5
+ import rule from '../rules/html-no-duplicate-id.js';
6
+
7
+ RuleTester.afterAll = afterAll;
8
+ RuleTester.describe = describe;
9
+ RuleTester.it = it;
10
+
11
+ const ruleTester = new RuleTester({
12
+ languageOptions: {
13
+ parser: htmlParser,
14
+ parserOptions: {
15
+ templateEngineSyntax: htmlParser.TEMPLATE_ENGINE_SYNTAX.HANDLEBAR,
16
+ },
17
+ },
18
+ });
19
+
20
+ ruleTester.run('html-no-duplicate-id', rule, {
21
+ valid: [
22
+ {
23
+ code: '<div id="a"></div><div id="b"></div>',
24
+ },
25
+ // Duplicate `id` on `pl-*` elements is a tool/element identifier, not a
26
+ // DOM id, so it should be ignored.
27
+ {
28
+ code: `
29
+ <pl-sketch>
30
+ <pl-sketch-tool id="fd" type="free-draw"></pl-sketch-tool>
31
+ </pl-sketch>
32
+ <pl-sketch>
33
+ <pl-sketch-tool id="fd" type="free-draw"></pl-sketch-tool>
34
+ </pl-sketch>`,
35
+ },
36
+ {
37
+ code: '<div id="question-{{params.name}}"></div><span id="question-{{params.name}}"></span>',
38
+ },
39
+ ],
40
+ invalid: [
41
+ {
42
+ code: '<div id="x"></div><div id="x"></div>',
43
+ errors: [
44
+ { messageId: 'duplicateId', data: { id: 'x' } },
45
+ { messageId: 'duplicateId', data: { id: 'x' } },
46
+ ],
47
+ },
48
+ // Duplicate `id` on non-`pl-*` elements should be reported.
49
+ {
50
+ code: '<div id="fd"></div><span id="fd"></span>',
51
+ errors: [
52
+ { messageId: 'duplicateId', data: { id: 'fd' } },
53
+ { messageId: 'duplicateId', data: { id: 'fd' } },
54
+ ],
55
+ },
56
+ ],
57
+ });