@atlaskit/eslint-plugin-platform 2.5.0 → 2.7.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/afm-cc/tsconfig.json +1 -1
  3. package/dist/cjs/index.js +43 -3
  4. package/dist/cjs/rules/ensure-native-and-af-exports-synced/index.js +3 -0
  5. package/dist/cjs/rules/ensure-valid-platform-yarn-protocol-usage/index.js +1 -1
  6. package/dist/cjs/rules/feature-gating/no-preconditioning/index.js +1 -1
  7. package/dist/cjs/rules/no-direct-document-usage/index.js +103 -0
  8. package/dist/cjs/rules/no-set-immediate/index.js +39 -0
  9. package/dist/cjs/rules/no-sparse-checkout/index.js +43 -0
  10. package/dist/cjs/rules/util/file-exclusions.js +45 -0
  11. package/dist/es2019/index.js +43 -3
  12. package/dist/es2019/rules/ensure-native-and-af-exports-synced/index.js +3 -0
  13. package/dist/es2019/rules/ensure-valid-platform-yarn-protocol-usage/index.js +1 -1
  14. package/dist/es2019/rules/feature-gating/no-preconditioning/index.js +1 -1
  15. package/dist/es2019/rules/no-direct-document-usage/index.js +95 -0
  16. package/dist/es2019/rules/no-set-immediate/index.js +33 -0
  17. package/dist/es2019/rules/no-sparse-checkout/index.js +35 -0
  18. package/dist/es2019/rules/util/file-exclusions.js +37 -0
  19. package/dist/esm/index.js +43 -3
  20. package/dist/esm/rules/ensure-native-and-af-exports-synced/index.js +3 -0
  21. package/dist/esm/rules/ensure-valid-platform-yarn-protocol-usage/index.js +1 -1
  22. package/dist/esm/rules/feature-gating/no-preconditioning/index.js +1 -1
  23. package/dist/esm/rules/no-direct-document-usage/index.js +97 -0
  24. package/dist/esm/rules/no-set-immediate/index.js +33 -0
  25. package/dist/esm/rules/no-sparse-checkout/index.js +37 -0
  26. package/dist/esm/rules/util/file-exclusions.js +39 -0
  27. package/dist/types/index.d.ts +22 -0
  28. package/dist/types/rules/no-direct-document-usage/index.d.ts +3 -0
  29. package/dist/types/rules/no-set-immediate/index.d.ts +3 -0
  30. package/dist/types/rules/no-sparse-checkout/index.d.ts +3 -0
  31. package/dist/types/rules/util/file-exclusions.d.ts +13 -0
  32. package/dist/types-ts4.5/index.d.ts +22 -0
  33. package/dist/types-ts4.5/rules/no-direct-document-usage/index.d.ts +3 -0
  34. package/dist/types-ts4.5/rules/no-set-immediate/index.d.ts +3 -0
  35. package/dist/types-ts4.5/rules/no-sparse-checkout/index.d.ts +3 -0
  36. package/dist/types-ts4.5/rules/util/file-exclusions.d.ts +13 -0
  37. package/package.json +10 -1
  38. package/src/index.tsx +47 -2
  39. package/src/rules/ensure-native-and-af-exports-synced/index.tsx +3 -0
  40. package/src/rules/ensure-valid-bin-values/__tests__/unit/rule.test.ts +3 -2
  41. package/src/rules/ensure-valid-platform-yarn-protocol-usage/index.ts +1 -1
  42. package/src/rules/feature-gating/no-preconditioning/index.tsx +1 -1
  43. package/src/rules/no-direct-document-usage/index.tsx +111 -0
  44. package/src/rules/no-set-immediate/index.tsx +43 -0
  45. package/src/rules/no-sparse-checkout/__tests__/unit/rule.test.tsx +48 -0
  46. package/src/rules/no-sparse-checkout/index.tsx +54 -0
  47. package/src/rules/util/file-exclusions.ts +39 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@atlaskit/eslint-plugin-platform",
3
3
  "description": "The essential plugin for use with Atlassian frontend platform tools",
4
- "version": "2.5.0",
4
+ "version": "2.7.0",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "atlassian": {
7
7
  "team": "Build Infra",
@@ -45,6 +45,15 @@
45
45
  "@atlassian/ts-loader": "^0.1.0",
46
46
  "@types/eslint": "^8.56.6",
47
47
  "eslint": "^8.57.0",
48
+ "find-up": "^4 || ^5",
48
49
  "outdent": "^0.5.0"
50
+ },
51
+ "peerDependencies": {
52
+ "find-up": "^4 || ^5"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "find-up": {
56
+ "optional": true
57
+ }
49
58
  }
50
59
  }
package/src/index.tsx CHANGED
@@ -28,6 +28,31 @@ import useEntrypointsInExamples from './rules/use-entrypoints-in-examples';
28
28
  import useRecommendedUtils from './rules/feature-gating/use-recommended-utils';
29
29
  import expandBackgroundShorthand from './rules/compiled/expand-background-shorthand';
30
30
  import expandSpacingShorthand from './rules/compiled/expand-spacing-shorthand';
31
+ import noSparseCheckout from './rules/no-sparse-checkout';
32
+ import noDirectDocumentUsage from './rules/no-direct-document-usage';
33
+ import noSetImmediate from './rules/no-set-immediate';
34
+ import { join, normalize } from 'node:path';
35
+ import { readFileSync } from 'node:fs';
36
+
37
+ let jiraRoot: string | undefined;
38
+
39
+ try {
40
+ const findUp = require('find-up') as typeof import('find-up');
41
+ findUp.sync((dir) => {
42
+ const productsJsonPath = join(dir, 'products.json');
43
+ if (findUp.sync.exists(productsJsonPath)) {
44
+ const productJson: Record<string, { path: string }> = JSON.parse(
45
+ readFileSync(productsJsonPath, 'utf-8'),
46
+ );
47
+ if (productJson.Jira) {
48
+ jiraRoot = normalize(join(dir, productJson.Jira.path));
49
+ return findUp.stop;
50
+ }
51
+ }
52
+ });
53
+ } catch {
54
+ // we aren't running inside of AFM, so we can ignore this.
55
+ }
31
56
 
32
57
  const packageJson: {
33
58
  name: string;
@@ -63,6 +88,9 @@ const rules = {
63
88
  'no-alias': noAlias,
64
89
  'use-entrypoints-in-examples': useEntrypointsInExamples,
65
90
  'use-recommended-utils': useRecommendedUtils,
91
+ 'no-sparse-checkout': noSparseCheckout,
92
+ 'no-direct-document-usage': noDirectDocumentUsage,
93
+ 'no-set-immediate': noSetImmediate,
66
94
  };
67
95
 
68
96
  const commonConfig = {
@@ -72,6 +100,8 @@ const commonConfig = {
72
100
  '@atlaskit/platform/no-invalid-storybook-decorator-usage': 'error',
73
101
  '@atlaskit/platform/ensure-atlassian-team': 'error',
74
102
  '@atlaskit/platform/no-module-level-eval-nav4': 'error',
103
+ '@atlaskit/platform/no-direct-document-usage': 'warn',
104
+ '@atlaskit/platform/no-set-immediate': 'error',
75
105
  // Compiled: rules that are not included via `@compiled/recommended
76
106
  '@atlaskit/platform/expand-border-shorthand': 'error',
77
107
  '@atlaskit/platform/expand-background-shorthand': 'error',
@@ -112,6 +142,8 @@ const jsonPrefix =
112
142
  const jsonPrefixForFlatConfig =
113
143
  '/* eslint-disable quote-props, comma-dangle, quotes, semi, eol-last, no-template-curly-in-string */ module.exports = ';
114
144
 
145
+ const jsonPrefixForJira = 'module.exports = ';
146
+
115
147
  const { name, version } = packageJson;
116
148
  const plugin = {
117
149
  meta: {
@@ -151,7 +183,14 @@ const plugin = {
151
183
  },
152
184
  processors: {
153
185
  'package-json-processor': {
154
- preprocess: (source: string) => {
186
+ preprocess: (source, filename) => {
187
+ // we only need to check for jiraRoot because it uses a different
188
+ // ESLint version and produces fake errors due to how this processor handles JSON
189
+ if (jiraRoot && filename.startsWith(jiraRoot)) {
190
+ // augment the json into a js file
191
+ return [jsonPrefixForJira + source.trim()];
192
+ }
193
+
155
194
  // augment the json into a js file
156
195
  return [jsonPrefix + source.trim()];
157
196
  },
@@ -177,7 +216,13 @@ const plugin = {
177
216
  // This processor is used for ESLint FlatConfig,
178
217
  // once we roll out FlatConfig, we can remove the above processor
179
218
  'package-json-processor-for-flat-config': {
180
- preprocess: (source: string) => {
219
+ // we only need to check for jiraRoot because it uses a different
220
+ // ESLint version and produces fake errors due to how this processor handles JSON
221
+ preprocess: (source, filename) => {
222
+ if (jiraRoot && filename.startsWith(jiraRoot)) {
223
+ // augment the json into a js file
224
+ return [jsonPrefixForJira + source.trim()];
225
+ }
181
226
  // augment the json into a js file
182
227
  return [jsonPrefixForFlatConfig + source.trim()];
183
228
  },
@@ -8,6 +8,9 @@ interface ExportsValidationExceptions {
8
8
  }
9
9
 
10
10
  const exportsValidationExceptions: ExportsValidationExceptions = {
11
+ '@af/yarn-workspace': {
12
+ ignoredAfExportKeys: ['./lock-parser'],
13
+ },
11
14
  '@atlaskit/tokens': {
12
15
  ignoredAfExportKeys: ['./babel-plugin'],
13
16
  },
@@ -16,8 +16,9 @@ jest.mock('fs', () => {
16
16
  const actual = jest.requireActual('fs');
17
17
  return {
18
18
  ...actual,
19
- statSync: jest.fn((stat: string) => ({
20
- isFile: jest.fn(() => mockValidBinPaths.includes(stat)),
19
+ statSync: jest.fn((p: string) => ({
20
+ isFile: jest.fn(() => mockValidBinPaths.includes(p)),
21
+ isDirectory: jest.fn(() => actual.statSync(p).isDirectory()),
21
22
  })),
22
23
  };
23
24
  });
@@ -39,7 +39,7 @@ const rule: Rule.RuleModule = {
39
39
  },
40
40
  hasSuggestions: false,
41
41
  messages: {
42
- invalidWorkspaceProtocolUsage: `The 'workspace:^' or 'workspace:~' protocol is Used. To resolve this error, please use the 'workspace:*' protocol instead.`,
42
+ invalidWorkspaceProtocolUsage: `The 'workspace:^'protocol is Used. To resolve this error, please use the 'workspace:*' protocol instead.`,
43
43
  invalidRootProtocolUsage: `The 'root:' protocol is not allowed in platform packages. To resolve this error, replace the 'root:' protocol with specific package versions (e.g. '^1.0.0').`,
44
44
  },
45
45
  },
@@ -68,7 +68,7 @@ const rule: Rule.RuleModule = {
68
68
  },
69
69
  messages: {
70
70
  useConfig:
71
- 'Do not precondition gates or experiments with another gate. Configure this in Statsig instead to reduce unnecessary code and simplify cleanup.',
71
+ 'Do not precondition gates or experiments with another gate. Configure this in Statsig instead to reduce unnecessary code, simplify cleanup and to ensure accurate exposures in Statsig.',
72
72
  incorrectExposure:
73
73
  'Evaluate gates or experiments at the end of your logical expression to ensure exposure is tracked correctly.',
74
74
  },
@@ -0,0 +1,111 @@
1
+ import type { Rule } from 'eslint';
2
+ import { skipForExampleFiles, skipForTestFiles } from '../util/file-exclusions';
3
+
4
+ const rule: Rule.RuleModule = {
5
+ meta: {
6
+ type: 'problem',
7
+ docs: {
8
+ description:
9
+ 'Enforce using getDocument from @atlaskit/browser-apis instead of direct document usage',
10
+ recommended: true,
11
+ },
12
+ messages: {
13
+ useGetDocument:
14
+ 'Use getDocument from @atlaskit/browser-apis instead of direct document usage',
15
+ },
16
+ schema: [],
17
+ },
18
+ create(context) {
19
+ let hasGetDocumentImport = false;
20
+ const filename = context.filename;
21
+
22
+ // Skip test files
23
+ const skipResult = skipForTestFiles(context);
24
+ if (skipResult) {
25
+ return skipResult;
26
+ }
27
+
28
+ // Skip example files
29
+ const skipResult2 = skipForExampleFiles(context);
30
+ if (skipResult2) {
31
+ return skipResult2;
32
+ }
33
+
34
+ // Skip the getDocument.ts file itself
35
+ if (filename.endsWith('getDocument.ts')) {
36
+ return {};
37
+ }
38
+
39
+ return {
40
+ ImportDeclaration(node) {
41
+ if (
42
+ node.source.value === '@atlaskit/browser-apis' &&
43
+ node.specifiers.some(
44
+ (specifier) =>
45
+ specifier.type === 'ImportSpecifier' &&
46
+ specifier.imported.type === 'Identifier' &&
47
+ specifier.imported.name === 'getDocument',
48
+ )
49
+ ) {
50
+ hasGetDocumentImport = true;
51
+ }
52
+ },
53
+ Identifier(node) {
54
+ if (node.name === 'document' && !hasGetDocumentImport) {
55
+ const parent = node.parent;
56
+
57
+ // Skip if 'document' is used as a property key in an object literal
58
+ if (parent?.type === 'Property' && parent.key === node) {
59
+ return;
60
+ }
61
+
62
+ // Skip if 'document' is used as a shorthand property value
63
+ if (parent?.type === 'Property' && parent.value === node && parent.shorthand) {
64
+ return;
65
+ }
66
+
67
+ // Skip if 'document' is used as a property being accessed in a member expression
68
+ if (parent?.type === 'MemberExpression' && parent.property === node && !parent.computed) {
69
+ return;
70
+ }
71
+
72
+ // Skip if 'document' is being declared as a variable
73
+ if (parent?.type === 'VariableDeclarator' && parent.id === node) {
74
+ return;
75
+ }
76
+
77
+ // Skip if 'document' is a function name
78
+ if (parent?.type === 'FunctionDeclaration' && 'id' in parent && parent.id === node) {
79
+ return;
80
+ }
81
+
82
+ if (parent?.type === 'FunctionExpression' && 'id' in parent && parent.id === node) {
83
+ return;
84
+ }
85
+
86
+ // Skip if 'document' is a method name in a class or object
87
+ if (parent?.type === 'MethodDefinition' && parent.key === node) {
88
+ return;
89
+ }
90
+
91
+ // Skip if 'document' is being assigned to (shadowing the global)
92
+ if (parent?.type === 'AssignmentExpression' && parent.left === node) {
93
+ return;
94
+ }
95
+
96
+ // Skip if 'document' is in a destructuring pattern (could be destructuring from an object)
97
+ if (parent?.type === 'ObjectPattern' || parent?.type === 'ArrayPattern') {
98
+ return;
99
+ }
100
+
101
+ context.report({
102
+ node,
103
+ messageId: 'useGetDocument',
104
+ });
105
+ }
106
+ },
107
+ };
108
+ },
109
+ };
110
+
111
+ export default rule;
@@ -0,0 +1,43 @@
1
+ import type { Rule } from 'eslint';
2
+
3
+ const rule: Rule.RuleModule = {
4
+ meta: {
5
+ docs: {
6
+ description:
7
+ "Prevent usage of setImmediate in favor of React Testing Library's `waitFor` or similar",
8
+ recommended: true,
9
+ },
10
+ type: 'problem',
11
+ messages: {
12
+ noSetImmediate:
13
+ "Avoid using setImmediate. Use React Testing Library's waitFor or similar instead for better test reliability.",
14
+ suggestWaitFor: 'Replace with waitFor from @testing-library/react or similar',
15
+ },
16
+ hasSuggestions: true,
17
+ },
18
+ create(context) {
19
+ return {
20
+ CallExpression(node) {
21
+ if (node.callee.type === 'Identifier' && node.callee.name === 'setImmediate') {
22
+ context.report({
23
+ node,
24
+ messageId: 'noSetImmediate',
25
+ suggest: [
26
+ {
27
+ messageId: 'suggestWaitFor',
28
+ fix(fixer) {
29
+ return fixer.replaceText(
30
+ node,
31
+ 'await waitFor(() => { /* your assertion here */ })',
32
+ );
33
+ },
34
+ },
35
+ ],
36
+ });
37
+ }
38
+ },
39
+ };
40
+ },
41
+ };
42
+
43
+ export default rule;
@@ -0,0 +1,48 @@
1
+ import { tester } from '../../../../__tests__/utils/_tester';
2
+ import rule from '../../index';
3
+
4
+ describe('test no-sparse-checkout rule', () => {
5
+ tester.run('no-sparse-checkout', rule, {
6
+ valid: [
7
+ {
8
+ code: `
9
+ const config = {
10
+ clone: alias.afmClone({ sparseCheckout: false })
11
+ };
12
+ `,
13
+ filename: 'hello/foo.ts',
14
+ },
15
+ {
16
+ code: `
17
+ const config = {
18
+ clone: alias.afmClone({ cloneDepth: 1})
19
+ };
20
+ `,
21
+ filename: 'hello/foo.ts',
22
+ },
23
+ ],
24
+ invalid: [
25
+ {
26
+ code: `
27
+ const config = {
28
+ clone: alias.afmClone({ sparseCheckout: true })
29
+ };
30
+ `,
31
+ filename: 'hello/foo.ts',
32
+ errors: [{ messageId: 'noSparseCheckout' }],
33
+ },
34
+ {
35
+ code: `
36
+ const config = {
37
+ clone: alias.afmClone({
38
+ cloneDepth: 'full',
39
+ sparseCheckout: true
40
+ })
41
+ };
42
+ `,
43
+ filename: 'hello/foo.ts',
44
+ errors: [{ messageId: 'noSparseCheckout' }],
45
+ },
46
+ ],
47
+ });
48
+ });
@@ -0,0 +1,54 @@
1
+ import type { Rule } from 'eslint';
2
+ import type { Property } from 'estree';
3
+
4
+ // We will be removing sparse checkout from pipelines in CI completely due to the load it causes on BBC.
5
+ // We will be incrementally removing sparse-checkout from the files below as it is probably unnecessasry.
6
+ // If you must add an exception below, please go through the chopper process before doing so
7
+ const sparseCheckoutExceptions = [
8
+ 'bitbucket-pipelines/pipelines/custom/run-issue-automat.ts',
9
+ 'bitbucket-pipelines/pipelines/custom/marketplace/utils.ts',
10
+ 'bitbucket-pipelines/pipelines/custom/confluence/utils/index.ts',
11
+ 'bitbucket-pipelines/pipelines/custom/afm-tools/upload-afm-dependency-graph-cache.ts',
12
+ 'bitbucket-pipelines/pipelines/custom/afm-tools/default-afm-tools.ts',
13
+ 'bitbucket-pipelines/pipelines/custom/marketplace/utils.ts',
14
+ 'bitbucket-pipelines/pipelines/custom/afm-git-hooks.ts',
15
+ 'bitbucket-pipelines/pipelines/custom/update-codeowners-and-teams-gen.ts',
16
+ 'bitbucket-pipelines/pipelines/custom/run-issue-automat.ts',
17
+ ];
18
+
19
+ const rule: Rule.RuleModule = {
20
+ meta: {
21
+ docs: {
22
+ recommended: false,
23
+ },
24
+ type: 'problem',
25
+ messages: {
26
+ noSparseCheckout:
27
+ 'Sparse checkout is not allowed in pipeline configurations. Use git-alternates instead by setting sparseCheckout to false or add this file to exceptions.',
28
+ },
29
+ },
30
+
31
+ create(context) {
32
+ const fileName = context.filename;
33
+ if (sparseCheckoutExceptions.some((exception) => fileName.endsWith(exception))) {
34
+ return {};
35
+ }
36
+
37
+ return {
38
+ // Look for calls to afmClone or objects that match AFMCloneConfig type
39
+ 'CallExpression[callee.object.name=alias][callee.property.name=afmClone] ObjectExpression Property':
40
+ (node: Property) => {
41
+ if (node.key.type === 'Identifier' && node.key.name === 'sparseCheckout') {
42
+ if (node.value.type === 'Literal' && node.value.value === true) {
43
+ context.report({
44
+ node,
45
+ messageId: 'noSparseCheckout',
46
+ });
47
+ }
48
+ }
49
+ },
50
+ };
51
+ },
52
+ };
53
+
54
+ export default rule;
@@ -0,0 +1,39 @@
1
+ import type { Rule } from 'eslint';
2
+
3
+ /**
4
+ * Common patterns for test files that should be excluded from rules
5
+ */
6
+ const TEST_FILE_PATTERNS = ['__tests__', 'test', 'spec'] as const;
7
+
8
+ /**
9
+ * Checks if a file should be excluded from rules based on test file patterns
10
+ * @param filename The filename to check
11
+ * @returns true if the file should be excluded, false otherwise
12
+ */
13
+ const isTestFile = (filename: string): boolean => {
14
+ return TEST_FILE_PATTERNS.some((pattern) => filename.includes(pattern));
15
+ };
16
+
17
+ /**
18
+ * Helper function to skip rules for test files
19
+ * @param context The ESLint rule context
20
+ * @returns An empty RuleListener if the file is a test file, undefined otherwise
21
+ */
22
+ export const skipForTestFiles = (context: Rule.RuleContext): Rule.RuleListener | undefined => {
23
+ if (isTestFile(context.filename)) {
24
+ return {};
25
+ }
26
+ return undefined;
27
+ };
28
+
29
+ /**
30
+ * Helper function to skip rules for example files
31
+ * @param context The ESLint rule context
32
+ * @returns An empty RuleListener if the file is an example file, undefined otherwise
33
+ */
34
+ export const skipForExampleFiles = (context: Rule.RuleContext): Rule.RuleListener | undefined => {
35
+ if (context.filename.includes('example')) {
36
+ return {};
37
+ }
38
+ return undefined;
39
+ };