@atlaskit/eslint-plugin-platform 0.4.0 → 0.5.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,17 @@
1
1
  # @atlaskit/eslint-plugin-platform
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#81166](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/pull-requests/81166) [`a249a1bd29a6`](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/commits/a249a1bd29a6) - Upgrade ESLint to version 8
8
+
9
+ ## 0.4.1
10
+
11
+ ### Patch Changes
12
+
13
+ - [#78702](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/pull-requests/78702) [`6b76dabb8255`](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/commits/6b76dabb8255) - Add rule to check for invalid flag usages in exports
14
+
3
15
  ## 0.4.0
4
16
 
5
17
  ### Minor Changes
@@ -0,0 +1,21 @@
1
+ {
2
+ "extends": "../../../../tsconfig.entry-points.confluence.json",
3
+ "compilerOptions": {
4
+ "target": "es5",
5
+ "composite": true,
6
+ "outDir": "../dist",
7
+ "rootDir": "../",
8
+ "baseUrl": "../"
9
+ },
10
+ "include": [
11
+ "../src/**/*.ts",
12
+ "../src/**/*.tsx"
13
+ ],
14
+ "exclude": [
15
+ "../src/**/__tests__/*",
16
+ "../__tests__/*",
17
+ "../src/**/*.test.*",
18
+ "../src/**/test.*"
19
+ ],
20
+ "references": []
21
+ }
package/dist/cjs/index.js CHANGED
@@ -17,6 +17,7 @@ var _ensureFeatureFlagPrefix = _interopRequireDefault(require("./rules/ensure-fe
17
17
  var _ensureCriticalDependencyResolutions = _interopRequireDefault(require("./rules/ensure-critical-dependency-resolutions"));
18
18
  var _noInvalidStorybookDecoratorUsage = _interopRequireDefault(require("./rules/no-invalid-storybook-decorator-usage"));
19
19
  var _ensurePublishValid = _interopRequireDefault(require("./rules/ensure-publish-valid"));
20
+ var _ensureNativeAndAfExportsSynced = _interopRequireDefault(require("./rules/ensure-native-and-af-exports-synced"));
20
21
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
21
22
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } // eslint-disable-next-line import/no-extraneous-dependencies
22
23
  var rules = exports.rules = {
@@ -30,7 +31,8 @@ var rules = exports.rules = {
30
31
  'no-invalid-feature-flag-usage': _noInvalidFeatureFlagUsage.default,
31
32
  'no-pre-post-install-scripts': _noPrePostInstalls.default,
32
33
  'no-invalid-storybook-decorator-usage': _noInvalidStorybookDecoratorUsage.default,
33
- 'ensure-publish-valid': _ensurePublishValid.default
34
+ 'ensure-publish-valid': _ensurePublishValid.default,
35
+ 'ensure-native-and-af-exports-synced': _ensureNativeAndAfExportsSynced.default
34
36
  };
35
37
  var configs = exports.configs = {
36
38
  recommended: {
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.default = void 0;
8
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
+ var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
10
+ var _path = _interopRequireDefault(require("path"));
11
+ var _registrationUtils = require("../util/registration-utils");
12
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
13
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
14
+ var exportsValidationExceptions = {
15
+ '@atlassian/sizemap': {
16
+ ignoredAfExportKeys: ['.', './lmdb-cache-manager']
17
+ },
18
+ '@atlaskit/tokens': {
19
+ ignoredAfExportKeys: ['./babel-plugin']
20
+ }
21
+ };
22
+ var rule = {
23
+ meta: {
24
+ docs: {
25
+ recommended: false
26
+ },
27
+ type: 'problem',
28
+ messages: {
29
+ missingExportsProperty: "The exports property must be defined for {{pkgName}}; it most likely can just be a duplicate of the \"af:exports\" property. See http://go/eslint-exports for details",
30
+ missingExportsKey: "Missing package.json exports key \"{{expectedKey}}\" in {{pkgName}}. The exports entry should be \"{{expectedKey}}\": \"{{expectedValue}}\". See http://go/eslint-exports for details",
31
+ unexpectedExportsKey: "Unexpected package.json exports key \"{{key}}\" in {{pkgName}}. The exports entry should be \"{{expectedKey}}\": \"{{expectedValue}}\". See http://go/eslint-exports for details",
32
+ unexpectedExportsValue: "Unexpected package.json exports value in {{pkgName}} for the \"{{key}}\" key. The exports entry should be \"{{key}}\": \"{{expectedValue}}\". See http://go/eslint-exports for details"
33
+ }
34
+ },
35
+ create: function create(context) {
36
+ var fileName = context.getFilename();
37
+ if (!fileName.endsWith('package.json')) {
38
+ return {};
39
+ }
40
+ var _getMetadataForFilena = (0, _registrationUtils.getMetadataForFilename)(fileName),
41
+ packageJson = _getMetadataForFilena.pkgJson;
42
+ var pkgName = packageJson.name;
43
+
44
+ // TODO: remove '|| !packageJson['exports']' once all package.json files have 'exports'
45
+ if (!pkgName || !packageJson['af:exports'] || !packageJson['exports']) {
46
+ return {};
47
+ }
48
+
49
+ // TODO: Add back in once all package.json files have 'exports'
50
+ // if (!packageJson['exports']) {
51
+ // context.report({
52
+ // node: context.getSourceCode().ast,
53
+ // messageId: 'missingExportsProperty',
54
+ // data: { pkgName },
55
+ // });
56
+ // return {};
57
+ // }
58
+
59
+ var afExports = packageJson['af:exports'];
60
+ var nativeExports = packageJson['exports'];
61
+ return {
62
+ Program: function Program(node) {
63
+ for (var _i = 0, _Object$entries = Object.entries(afExports); _i < _Object$entries.length; _i++) {
64
+ var _exportsValidationExc;
65
+ var _Object$entries$_i = (0, _slicedToArray2.default)(_Object$entries[_i], 2),
66
+ afExportsKey = _Object$entries$_i[0],
67
+ afExportsValue = _Object$entries$_i[1];
68
+ if ((_exportsValidationExc = exportsValidationExceptions[pkgName]) !== null && _exportsValidationExc !== void 0 && _exportsValidationExc.ignoredAfExportKeys.includes(afExportsKey)) {
69
+ continue;
70
+ }
71
+ var exportKeyViolations = getExportKeyViolation(afExportsKey, afExportsValue, nativeExports);
72
+ if (exportKeyViolations) {
73
+ context.report({
74
+ data: _objectSpread(_objectSpread({}, exportKeyViolations), {}, {
75
+ key: afExportsKey,
76
+ pkgName: pkgName
77
+ }),
78
+ node: node,
79
+ messageId: exportKeyViolations.messageId
80
+ });
81
+ continue;
82
+ }
83
+ var exportValueViolations = getExportValueViolation(pkgName, afExportsKey, afExportsValue, nativeExports);
84
+ if (exportValueViolations) {
85
+ context.report({
86
+ data: _objectSpread(_objectSpread({}, exportValueViolations), {}, {
87
+ pkgName: pkgName
88
+ }),
89
+ node: node,
90
+ messageId: 'unexpectedExportsValue'
91
+ });
92
+ continue;
93
+ }
94
+ }
95
+ }
96
+ };
97
+ }
98
+ };
99
+ function getExportKeyViolation(afExportsKey, afExportsValue, nativeExports) {
100
+ var afExportsValueHasExtension = _path.default.extname(afExportsValue);
101
+ if (afExportsValueHasExtension && !nativeExports.hasOwnProperty(afExportsKey)) {
102
+ return {
103
+ messageId: 'missingExportsKey',
104
+ expectedKey: afExportsKey,
105
+ expectedValue: afExportsValue
106
+ };
107
+ }
108
+ if (!afExportsValueHasExtension && nativeExports.hasOwnProperty(afExportsKey)) {
109
+ return {
110
+ messageId: 'unexpectedExportsKey',
111
+ expectedKey: "".concat(afExportsKey, "/*"),
112
+ expectedValue: "".concat(afExportsValue, "/*")
113
+ };
114
+ }
115
+ if (!afExportsValueHasExtension && !nativeExports.hasOwnProperty("".concat(afExportsKey, "/*"))) {
116
+ return {
117
+ messageId: 'missingExportsKey',
118
+ expectedKey: "".concat(afExportsKey, "/*"),
119
+ expectedValue: "".concat(afExportsValue, "/*")
120
+ };
121
+ }
122
+ }
123
+ function getExportValueViolation(pkgName, afExportsKey, afExportsValue, nativeExports) {
124
+ var afExportsValueHasExtension = _path.default.extname(afExportsValue);
125
+
126
+ // Some entrypoints have been updated to an index.js file that registers ts-node
127
+ // Use path.basename to get the file name to see if it is equal to 'index.js'
128
+ if (afExportsValueHasExtension && _path.default.basename(nativeExports[afExportsKey]) === 'index.js') {
129
+ return;
130
+ }
131
+ if (afExportsValueHasExtension && nativeExports[afExportsKey] !== afExportsValue) {
132
+ return {
133
+ key: afExportsKey,
134
+ expectedValue: afExportsValue
135
+ };
136
+ }
137
+
138
+ // af:exports entrypoints without a file extension export the whole directory so check to ensure the exports value includes the wildcard
139
+ if (!afExportsValueHasExtension && !nativeExports["".concat(afExportsKey, "/*")].startsWith("".concat(afExportsValue, "/*"))) {
140
+ return {
141
+ key: "".concat(afExportsKey, "/*"),
142
+ expectedValue: "".concat(afExportsValue, "/*")
143
+ };
144
+ }
145
+ }
146
+ var _default = exports.default = rule;
@@ -50,11 +50,13 @@ var rule = {
50
50
  messages: {
51
51
  onlyInlineIf: "Only call feature flags as part of an expression, don't assign to a variable! See http://go/pff-eslint for more details",
52
52
  onlyStringLiteral: "Only get feature flags by string literal, don't use variables! See http://go/pff-eslint for more details",
53
- multipleFlagCheckInExpression: "Only check one flag per expression! See http://go/pff-eslint for more details"
53
+ multipleFlagCheckInExpression: "Only check one flag per expression! See http://go/pff-eslint for more details",
54
+ noModuleScope: "Don't use platform feature flags in module scope! See http://go/pff-eslint for more details"
54
55
  }
55
56
  },
56
57
  create: function create(context) {
57
58
  return (0, _defineProperty2.default)({}, "CallExpression[callee.name=/".concat(FF_GETTER_BOOLEAN_IDENTIFIER, "/]"), function CallExpressionCalleeName(node) {
59
+ var _node$parent2;
58
60
  // to make typescript happy
59
61
  if (node.type === 'CallExpression') {
60
62
  var _node$parent;
@@ -67,7 +69,26 @@ var rule = {
67
69
  }
68
70
  switch ((_node$parent = node.parent) === null || _node$parent === void 0 ? void 0 : _node$parent.type) {
69
71
  case 'IfStatement':
72
+ break;
70
73
  case 'ConditionalExpression':
74
+ switch ((_node$parent2 = node.parent) === null || _node$parent2 === void 0 ? void 0 : _node$parent2.parent.type) {
75
+ case 'ExportDefaultDeclaration':
76
+ // handles "export default getBooleanFF('test-flag') ? "this is" : "not good";"
77
+ context.report({
78
+ node: node,
79
+ messageId: 'noModuleScope'
80
+ });
81
+ break;
82
+ case 'VariableDeclarator':
83
+ // handles "export const foo = getBooleanFF('test-flag') ? 'this is' : 'not good';"
84
+ if (node.parent.parent.parent.type === 'VariableDeclaration' && node.parent.parent.parent.parent.type === 'ExportNamedDeclaration') {
85
+ context.report({
86
+ node: node,
87
+ messageId: 'noModuleScope'
88
+ });
89
+ }
90
+ break;
91
+ }
71
92
  break;
72
93
  case 'UnaryExpression':
73
94
  case 'LogicalExpression':
@@ -11,6 +11,7 @@ import ensureFeatureFlagPrefix from './rules/ensure-feature-flag-prefix';
11
11
  import ensureCriticalDependencyResolutions from './rules/ensure-critical-dependency-resolutions';
12
12
  import noInvalidStorybookDecoratorUsage from './rules/no-invalid-storybook-decorator-usage';
13
13
  import ensurePublishValid from './rules/ensure-publish-valid';
14
+ import ensureNativeAndAfExportsSynced from './rules/ensure-native-and-af-exports-synced';
14
15
  export const rules = {
15
16
  'ensure-feature-flag-registration': ensureFeatureFlagRegistration,
16
17
  'ensure-feature-flag-prefix': ensureFeatureFlagPrefix,
@@ -22,7 +23,8 @@ export const rules = {
22
23
  'no-invalid-feature-flag-usage': noInvalidFeatureFlagUsage,
23
24
  'no-pre-post-install-scripts': noPreAndPostInstallScripts,
24
25
  'no-invalid-storybook-decorator-usage': noInvalidStorybookDecoratorUsage,
25
- 'ensure-publish-valid': ensurePublishValid
26
+ 'ensure-publish-valid': ensurePublishValid,
27
+ 'ensure-native-and-af-exports-synced': ensureNativeAndAfExportsSynced
26
28
  };
27
29
  export const configs = {
28
30
  recommended: {
@@ -0,0 +1,135 @@
1
+ import path from 'path';
2
+ import { getMetadataForFilename } from '../util/registration-utils';
3
+ const exportsValidationExceptions = {
4
+ '@atlassian/sizemap': {
5
+ ignoredAfExportKeys: ['.', './lmdb-cache-manager']
6
+ },
7
+ '@atlaskit/tokens': {
8
+ ignoredAfExportKeys: ['./babel-plugin']
9
+ }
10
+ };
11
+ const rule = {
12
+ meta: {
13
+ docs: {
14
+ recommended: false
15
+ },
16
+ type: 'problem',
17
+ messages: {
18
+ missingExportsProperty: `The exports property must be defined for {{pkgName}}; it most likely can just be a duplicate of the "af:exports" property. See http://go/eslint-exports for details`,
19
+ missingExportsKey: `Missing package.json exports key "{{expectedKey}}" in {{pkgName}}. The exports entry should be "{{expectedKey}}": "{{expectedValue}}". See http://go/eslint-exports for details`,
20
+ unexpectedExportsKey: `Unexpected package.json exports key "{{key}}" in {{pkgName}}. The exports entry should be "{{expectedKey}}": "{{expectedValue}}". See http://go/eslint-exports for details`,
21
+ unexpectedExportsValue: `Unexpected package.json exports value in {{pkgName}} for the "{{key}}" key. The exports entry should be "{{key}}": "{{expectedValue}}". See http://go/eslint-exports for details`
22
+ }
23
+ },
24
+ create(context) {
25
+ const fileName = context.getFilename();
26
+ if (!fileName.endsWith('package.json')) {
27
+ return {};
28
+ }
29
+ const {
30
+ pkgJson: packageJson
31
+ } = getMetadataForFilename(fileName);
32
+ const pkgName = packageJson.name;
33
+
34
+ // TODO: remove '|| !packageJson['exports']' once all package.json files have 'exports'
35
+ if (!pkgName || !packageJson['af:exports'] || !packageJson['exports']) {
36
+ return {};
37
+ }
38
+
39
+ // TODO: Add back in once all package.json files have 'exports'
40
+ // if (!packageJson['exports']) {
41
+ // context.report({
42
+ // node: context.getSourceCode().ast,
43
+ // messageId: 'missingExportsProperty',
44
+ // data: { pkgName },
45
+ // });
46
+ // return {};
47
+ // }
48
+
49
+ const afExports = packageJson['af:exports'];
50
+ const nativeExports = packageJson['exports'];
51
+ return {
52
+ Program(node) {
53
+ for (const [afExportsKey, afExportsValue] of Object.entries(afExports)) {
54
+ var _exportsValidationExc;
55
+ if ((_exportsValidationExc = exportsValidationExceptions[pkgName]) !== null && _exportsValidationExc !== void 0 && _exportsValidationExc.ignoredAfExportKeys.includes(afExportsKey)) {
56
+ continue;
57
+ }
58
+ const exportKeyViolations = getExportKeyViolation(afExportsKey, afExportsValue, nativeExports);
59
+ if (exportKeyViolations) {
60
+ context.report({
61
+ data: {
62
+ ...exportKeyViolations,
63
+ key: afExportsKey,
64
+ pkgName
65
+ },
66
+ node,
67
+ messageId: exportKeyViolations.messageId
68
+ });
69
+ continue;
70
+ }
71
+ const exportValueViolations = getExportValueViolation(pkgName, afExportsKey, afExportsValue, nativeExports);
72
+ if (exportValueViolations) {
73
+ context.report({
74
+ data: {
75
+ ...exportValueViolations,
76
+ pkgName
77
+ },
78
+ node,
79
+ messageId: 'unexpectedExportsValue'
80
+ });
81
+ continue;
82
+ }
83
+ }
84
+ }
85
+ };
86
+ }
87
+ };
88
+ function getExportKeyViolation(afExportsKey, afExportsValue, nativeExports) {
89
+ const afExportsValueHasExtension = path.extname(afExportsValue);
90
+ if (afExportsValueHasExtension && !nativeExports.hasOwnProperty(afExportsKey)) {
91
+ return {
92
+ messageId: 'missingExportsKey',
93
+ expectedKey: afExportsKey,
94
+ expectedValue: afExportsValue
95
+ };
96
+ }
97
+ if (!afExportsValueHasExtension && nativeExports.hasOwnProperty(afExportsKey)) {
98
+ return {
99
+ messageId: 'unexpectedExportsKey',
100
+ expectedKey: `${afExportsKey}/*`,
101
+ expectedValue: `${afExportsValue}/*`
102
+ };
103
+ }
104
+ if (!afExportsValueHasExtension && !nativeExports.hasOwnProperty(`${afExportsKey}/*`)) {
105
+ return {
106
+ messageId: 'missingExportsKey',
107
+ expectedKey: `${afExportsKey}/*`,
108
+ expectedValue: `${afExportsValue}/*`
109
+ };
110
+ }
111
+ }
112
+ function getExportValueViolation(pkgName, afExportsKey, afExportsValue, nativeExports) {
113
+ const afExportsValueHasExtension = path.extname(afExportsValue);
114
+
115
+ // Some entrypoints have been updated to an index.js file that registers ts-node
116
+ // Use path.basename to get the file name to see if it is equal to 'index.js'
117
+ if (afExportsValueHasExtension && path.basename(nativeExports[afExportsKey]) === 'index.js') {
118
+ return;
119
+ }
120
+ if (afExportsValueHasExtension && nativeExports[afExportsKey] !== afExportsValue) {
121
+ return {
122
+ key: afExportsKey,
123
+ expectedValue: afExportsValue
124
+ };
125
+ }
126
+
127
+ // af:exports entrypoints without a file extension export the whole directory so check to ensure the exports value includes the wildcard
128
+ if (!afExportsValueHasExtension && !nativeExports[`${afExportsKey}/*`].startsWith(`${afExportsValue}/*`)) {
129
+ return {
130
+ key: `${afExportsKey}/*`,
131
+ expectedValue: `${afExportsValue}/*`
132
+ };
133
+ }
134
+ }
135
+ export default rule;
@@ -42,12 +42,14 @@ const rule = {
42
42
  messages: {
43
43
  onlyInlineIf: "Only call feature flags as part of an expression, don't assign to a variable! See http://go/pff-eslint for more details",
44
44
  onlyStringLiteral: "Only get feature flags by string literal, don't use variables! See http://go/pff-eslint for more details",
45
- multipleFlagCheckInExpression: `Only check one flag per expression! See http://go/pff-eslint for more details`
45
+ multipleFlagCheckInExpression: `Only check one flag per expression! See http://go/pff-eslint for more details`,
46
+ noModuleScope: `Don't use platform feature flags in module scope! See http://go/pff-eslint for more details`
46
47
  }
47
48
  },
48
49
  create(context) {
49
50
  return {
50
51
  [`CallExpression[callee.name=/${FF_GETTER_BOOLEAN_IDENTIFIER}/]`]: node => {
52
+ var _node$parent2;
51
53
  // to make typescript happy
52
54
  if (node.type === 'CallExpression') {
53
55
  var _node$parent;
@@ -60,7 +62,26 @@ const rule = {
60
62
  }
61
63
  switch ((_node$parent = node.parent) === null || _node$parent === void 0 ? void 0 : _node$parent.type) {
62
64
  case 'IfStatement':
65
+ break;
63
66
  case 'ConditionalExpression':
67
+ switch ((_node$parent2 = node.parent) === null || _node$parent2 === void 0 ? void 0 : _node$parent2.parent.type) {
68
+ case 'ExportDefaultDeclaration':
69
+ // handles "export default getBooleanFF('test-flag') ? "this is" : "not good";"
70
+ context.report({
71
+ node,
72
+ messageId: 'noModuleScope'
73
+ });
74
+ break;
75
+ case 'VariableDeclarator':
76
+ // handles "export const foo = getBooleanFF('test-flag') ? 'this is' : 'not good';"
77
+ if (node.parent.parent.parent.type === 'VariableDeclaration' && node.parent.parent.parent.parent.type === 'ExportNamedDeclaration') {
78
+ context.report({
79
+ node,
80
+ messageId: 'noModuleScope'
81
+ });
82
+ }
83
+ break;
84
+ }
64
85
  break;
65
86
  case 'UnaryExpression':
66
87
  case 'LogicalExpression':
package/dist/esm/index.js CHANGED
@@ -14,6 +14,7 @@ import ensureFeatureFlagPrefix from './rules/ensure-feature-flag-prefix';
14
14
  import ensureCriticalDependencyResolutions from './rules/ensure-critical-dependency-resolutions';
15
15
  import noInvalidStorybookDecoratorUsage from './rules/no-invalid-storybook-decorator-usage';
16
16
  import ensurePublishValid from './rules/ensure-publish-valid';
17
+ import ensureNativeAndAfExportsSynced from './rules/ensure-native-and-af-exports-synced';
17
18
  export var rules = {
18
19
  'ensure-feature-flag-registration': ensureFeatureFlagRegistration,
19
20
  'ensure-feature-flag-prefix': ensureFeatureFlagPrefix,
@@ -25,7 +26,8 @@ export var rules = {
25
26
  'no-invalid-feature-flag-usage': noInvalidFeatureFlagUsage,
26
27
  'no-pre-post-install-scripts': noPreAndPostInstallScripts,
27
28
  'no-invalid-storybook-decorator-usage': noInvalidStorybookDecoratorUsage,
28
- 'ensure-publish-valid': ensurePublishValid
29
+ 'ensure-publish-valid': ensurePublishValid,
30
+ 'ensure-native-and-af-exports-synced': ensureNativeAndAfExportsSynced
29
31
  };
30
32
  export var configs = {
31
33
  recommended: {
@@ -0,0 +1,139 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
3
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
4
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
5
+ import path from 'path';
6
+ import { getMetadataForFilename } from '../util/registration-utils';
7
+ var exportsValidationExceptions = {
8
+ '@atlassian/sizemap': {
9
+ ignoredAfExportKeys: ['.', './lmdb-cache-manager']
10
+ },
11
+ '@atlaskit/tokens': {
12
+ ignoredAfExportKeys: ['./babel-plugin']
13
+ }
14
+ };
15
+ var rule = {
16
+ meta: {
17
+ docs: {
18
+ recommended: false
19
+ },
20
+ type: 'problem',
21
+ messages: {
22
+ missingExportsProperty: "The exports property must be defined for {{pkgName}}; it most likely can just be a duplicate of the \"af:exports\" property. See http://go/eslint-exports for details",
23
+ missingExportsKey: "Missing package.json exports key \"{{expectedKey}}\" in {{pkgName}}. The exports entry should be \"{{expectedKey}}\": \"{{expectedValue}}\". See http://go/eslint-exports for details",
24
+ unexpectedExportsKey: "Unexpected package.json exports key \"{{key}}\" in {{pkgName}}. The exports entry should be \"{{expectedKey}}\": \"{{expectedValue}}\". See http://go/eslint-exports for details",
25
+ unexpectedExportsValue: "Unexpected package.json exports value in {{pkgName}} for the \"{{key}}\" key. The exports entry should be \"{{key}}\": \"{{expectedValue}}\". See http://go/eslint-exports for details"
26
+ }
27
+ },
28
+ create: function create(context) {
29
+ var fileName = context.getFilename();
30
+ if (!fileName.endsWith('package.json')) {
31
+ return {};
32
+ }
33
+ var _getMetadataForFilena = getMetadataForFilename(fileName),
34
+ packageJson = _getMetadataForFilena.pkgJson;
35
+ var pkgName = packageJson.name;
36
+
37
+ // TODO: remove '|| !packageJson['exports']' once all package.json files have 'exports'
38
+ if (!pkgName || !packageJson['af:exports'] || !packageJson['exports']) {
39
+ return {};
40
+ }
41
+
42
+ // TODO: Add back in once all package.json files have 'exports'
43
+ // if (!packageJson['exports']) {
44
+ // context.report({
45
+ // node: context.getSourceCode().ast,
46
+ // messageId: 'missingExportsProperty',
47
+ // data: { pkgName },
48
+ // });
49
+ // return {};
50
+ // }
51
+
52
+ var afExports = packageJson['af:exports'];
53
+ var nativeExports = packageJson['exports'];
54
+ return {
55
+ Program: function Program(node) {
56
+ for (var _i = 0, _Object$entries = Object.entries(afExports); _i < _Object$entries.length; _i++) {
57
+ var _exportsValidationExc;
58
+ var _Object$entries$_i = _slicedToArray(_Object$entries[_i], 2),
59
+ afExportsKey = _Object$entries$_i[0],
60
+ afExportsValue = _Object$entries$_i[1];
61
+ if ((_exportsValidationExc = exportsValidationExceptions[pkgName]) !== null && _exportsValidationExc !== void 0 && _exportsValidationExc.ignoredAfExportKeys.includes(afExportsKey)) {
62
+ continue;
63
+ }
64
+ var exportKeyViolations = getExportKeyViolation(afExportsKey, afExportsValue, nativeExports);
65
+ if (exportKeyViolations) {
66
+ context.report({
67
+ data: _objectSpread(_objectSpread({}, exportKeyViolations), {}, {
68
+ key: afExportsKey,
69
+ pkgName: pkgName
70
+ }),
71
+ node: node,
72
+ messageId: exportKeyViolations.messageId
73
+ });
74
+ continue;
75
+ }
76
+ var exportValueViolations = getExportValueViolation(pkgName, afExportsKey, afExportsValue, nativeExports);
77
+ if (exportValueViolations) {
78
+ context.report({
79
+ data: _objectSpread(_objectSpread({}, exportValueViolations), {}, {
80
+ pkgName: pkgName
81
+ }),
82
+ node: node,
83
+ messageId: 'unexpectedExportsValue'
84
+ });
85
+ continue;
86
+ }
87
+ }
88
+ }
89
+ };
90
+ }
91
+ };
92
+ function getExportKeyViolation(afExportsKey, afExportsValue, nativeExports) {
93
+ var afExportsValueHasExtension = path.extname(afExportsValue);
94
+ if (afExportsValueHasExtension && !nativeExports.hasOwnProperty(afExportsKey)) {
95
+ return {
96
+ messageId: 'missingExportsKey',
97
+ expectedKey: afExportsKey,
98
+ expectedValue: afExportsValue
99
+ };
100
+ }
101
+ if (!afExportsValueHasExtension && nativeExports.hasOwnProperty(afExportsKey)) {
102
+ return {
103
+ messageId: 'unexpectedExportsKey',
104
+ expectedKey: "".concat(afExportsKey, "/*"),
105
+ expectedValue: "".concat(afExportsValue, "/*")
106
+ };
107
+ }
108
+ if (!afExportsValueHasExtension && !nativeExports.hasOwnProperty("".concat(afExportsKey, "/*"))) {
109
+ return {
110
+ messageId: 'missingExportsKey',
111
+ expectedKey: "".concat(afExportsKey, "/*"),
112
+ expectedValue: "".concat(afExportsValue, "/*")
113
+ };
114
+ }
115
+ }
116
+ function getExportValueViolation(pkgName, afExportsKey, afExportsValue, nativeExports) {
117
+ var afExportsValueHasExtension = path.extname(afExportsValue);
118
+
119
+ // Some entrypoints have been updated to an index.js file that registers ts-node
120
+ // Use path.basename to get the file name to see if it is equal to 'index.js'
121
+ if (afExportsValueHasExtension && path.basename(nativeExports[afExportsKey]) === 'index.js') {
122
+ return;
123
+ }
124
+ if (afExportsValueHasExtension && nativeExports[afExportsKey] !== afExportsValue) {
125
+ return {
126
+ key: afExportsKey,
127
+ expectedValue: afExportsValue
128
+ };
129
+ }
130
+
131
+ // af:exports entrypoints without a file extension export the whole directory so check to ensure the exports value includes the wildcard
132
+ if (!afExportsValueHasExtension && !nativeExports["".concat(afExportsKey, "/*")].startsWith("".concat(afExportsValue, "/*"))) {
133
+ return {
134
+ key: "".concat(afExportsKey, "/*"),
135
+ expectedValue: "".concat(afExportsValue, "/*")
136
+ };
137
+ }
138
+ }
139
+ export default rule;
@@ -43,11 +43,13 @@ var rule = {
43
43
  messages: {
44
44
  onlyInlineIf: "Only call feature flags as part of an expression, don't assign to a variable! See http://go/pff-eslint for more details",
45
45
  onlyStringLiteral: "Only get feature flags by string literal, don't use variables! See http://go/pff-eslint for more details",
46
- multipleFlagCheckInExpression: "Only check one flag per expression! See http://go/pff-eslint for more details"
46
+ multipleFlagCheckInExpression: "Only check one flag per expression! See http://go/pff-eslint for more details",
47
+ noModuleScope: "Don't use platform feature flags in module scope! See http://go/pff-eslint for more details"
47
48
  }
48
49
  },
49
50
  create: function create(context) {
50
51
  return _defineProperty({}, "CallExpression[callee.name=/".concat(FF_GETTER_BOOLEAN_IDENTIFIER, "/]"), function CallExpressionCalleeName(node) {
52
+ var _node$parent2;
51
53
  // to make typescript happy
52
54
  if (node.type === 'CallExpression') {
53
55
  var _node$parent;
@@ -60,7 +62,26 @@ var rule = {
60
62
  }
61
63
  switch ((_node$parent = node.parent) === null || _node$parent === void 0 ? void 0 : _node$parent.type) {
62
64
  case 'IfStatement':
65
+ break;
63
66
  case 'ConditionalExpression':
67
+ switch ((_node$parent2 = node.parent) === null || _node$parent2 === void 0 ? void 0 : _node$parent2.parent.type) {
68
+ case 'ExportDefaultDeclaration':
69
+ // handles "export default getBooleanFF('test-flag') ? "this is" : "not good";"
70
+ context.report({
71
+ node: node,
72
+ messageId: 'noModuleScope'
73
+ });
74
+ break;
75
+ case 'VariableDeclarator':
76
+ // handles "export const foo = getBooleanFF('test-flag') ? 'this is' : 'not good';"
77
+ if (node.parent.parent.parent.type === 'VariableDeclaration' && node.parent.parent.parent.parent.type === 'ExportNamedDeclaration') {
78
+ context.report({
79
+ node: node,
80
+ messageId: 'noModuleScope'
81
+ });
82
+ }
83
+ break;
84
+ }
64
85
  break;
65
86
  case 'UnaryExpression':
66
87
  case 'LogicalExpression':
@@ -11,6 +11,7 @@ export declare const rules: {
11
11
  'no-pre-post-install-scripts': import("eslint").Rule.RuleModule;
12
12
  'no-invalid-storybook-decorator-usage': import("eslint").Rule.RuleModule;
13
13
  'ensure-publish-valid': import("eslint").Rule.RuleModule;
14
+ 'ensure-native-and-af-exports-synced': import("eslint").Rule.RuleModule;
14
15
  };
15
16
  export declare const configs: {
16
17
  recommended: {
@@ -0,0 +1,3 @@
1
+ import type { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
@@ -11,6 +11,7 @@ export declare const rules: {
11
11
  'no-pre-post-install-scripts': import("eslint").Rule.RuleModule;
12
12
  'no-invalid-storybook-decorator-usage': import("eslint").Rule.RuleModule;
13
13
  'ensure-publish-valid': import("eslint").Rule.RuleModule;
14
+ 'ensure-native-and-af-exports-synced': import("eslint").Rule.RuleModule;
14
15
  };
15
16
  export declare const configs: {
16
17
  recommended: {
@@ -0,0 +1,3 @@
1
+ import type { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
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": "0.4.0",
4
+ "version": "0.5.0",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "atlassian": {
7
7
  "team": "UIP - Platform Integration Trust (PITa)",
@@ -38,7 +38,7 @@
38
38
  "devDependencies": {
39
39
  "@atlassian/atlassian-frontend-prettier-config-1.0.1": "npm:@atlassian/atlassian-frontend-prettier-config@1.0.1",
40
40
  "@types/eslint": "^8.4.5",
41
- "eslint": "^7.7.0",
41
+ "eslint": "^8.49.0",
42
42
  "tsconfig-paths": "^4.2.0"
43
43
  },
44
44
  "prettier": "@atlassian/atlassian-frontend-prettier-config-1.0.1"
@@ -18,7 +18,7 @@ import { RuleTester } from 'eslint';
18
18
  };
19
19
 
20
20
  export const tester = new RuleTester({
21
- parser: require.resolve('babel-eslint'),
21
+ parser: require.resolve('@babel/eslint-parser'),
22
22
  parserOptions: {
23
23
  ecmaVersion: 6,
24
24
  sourceType: 'module',
package/src/index.tsx CHANGED
@@ -11,6 +11,7 @@ import ensureFeatureFlagPrefix from './rules/ensure-feature-flag-prefix';
11
11
  import ensureCriticalDependencyResolutions from './rules/ensure-critical-dependency-resolutions';
12
12
  import noInvalidStorybookDecoratorUsage from './rules/no-invalid-storybook-decorator-usage';
13
13
  import ensurePublishValid from './rules/ensure-publish-valid';
14
+ import ensureNativeAndAfExportsSynced from './rules/ensure-native-and-af-exports-synced';
14
15
 
15
16
  export const rules = {
16
17
  'ensure-feature-flag-registration': ensureFeatureFlagRegistration,
@@ -24,6 +25,7 @@ export const rules = {
24
25
  'no-pre-post-install-scripts': noPreAndPostInstallScripts,
25
26
  'no-invalid-storybook-decorator-usage': noInvalidStorybookDecoratorUsage,
26
27
  'ensure-publish-valid': ensurePublishValid,
28
+ 'ensure-native-and-af-exports-synced': ensureNativeAndAfExportsSynced,
27
29
  };
28
30
 
29
31
  export const configs = {
@@ -0,0 +1,199 @@
1
+ import { tester } from '../../../../__tests__/utils/_tester';
2
+ import rule from '../../index';
3
+ import { PackageJson } from 'read-pkg-up';
4
+
5
+ let mockPath = 'packages/test/package.json';
6
+
7
+ let mockPackageJson: PackageJson = {};
8
+ jest.mock('read-pkg-up', () => ({
9
+ sync: () => ({
10
+ path: mockPath,
11
+ packageJson: mockPackageJson,
12
+ }),
13
+ }));
14
+
15
+ describe('valid test cases', () => {
16
+ describe('allows @atlaskit/tokens babel-plugin entrypoint', () => {
17
+ beforeEach(() => {
18
+ mockPackageJson = {
19
+ name: '@atlaskit/tokens',
20
+ 'af:exports': {
21
+ './babel-plugin': './src/entry-points/babel-plugin.tsx',
22
+ },
23
+ exports: {
24
+ './babel-plugin': './babel-plugin.js',
25
+ },
26
+ };
27
+ });
28
+ tester.run('ensure-native-and-af-exports-synced', rule, {
29
+ valid: [{ code: '' }],
30
+ invalid: [],
31
+ });
32
+ });
33
+
34
+ describe('passes for valid directory export', () => {
35
+ beforeEach(() => {
36
+ mockPackageJson = {
37
+ name: '@atlaskit/tokens',
38
+ 'af:exports': {
39
+ './glyph': './glyph',
40
+ './test/icon': './test/icon',
41
+ './button': './button',
42
+ '.': './src',
43
+ },
44
+ exports: {
45
+ './glyph/*': './glyph/*',
46
+ './test/icon/*': './test/icon/*',
47
+ './button/*': './button/*.js',
48
+ './*': './src/*',
49
+ },
50
+ };
51
+ });
52
+ tester.run('ensure-native-and-af-exports-synced', rule, {
53
+ valid: [
54
+ {
55
+ code: '',
56
+ filename: 'packages/test/package.json',
57
+ },
58
+ ],
59
+ invalid: [],
60
+ });
61
+ });
62
+
63
+ describe('passes for index.js files', () => {
64
+ beforeEach(() => {
65
+ mockPackageJson = {
66
+ name: '@atlaskit/tokens',
67
+ 'af:exports': {
68
+ '.': './src/index.tsx',
69
+ './gas-v3': './src/gas-v3/index.ts',
70
+ './reader': './src/reader/reader.ts',
71
+ './writer': './src/writer/writer.ts',
72
+ './report': './src/report/report.ts',
73
+ },
74
+ exports: {
75
+ '.': './index.js',
76
+ './gas-v3': './gas-v3/index.js',
77
+ './reader': './reader/index.js',
78
+ './writer': './writer/index.js',
79
+ './report': './report/index.js',
80
+ },
81
+ };
82
+ });
83
+ tester.run('ensure-native-and-af-exports-synced', rule, {
84
+ valid: [
85
+ {
86
+ code: '',
87
+ filename: 'packages/test/package.json',
88
+ },
89
+ ],
90
+ invalid: [],
91
+ });
92
+ });
93
+
94
+ describe('should pass for multiple valid entrypoints', () => {
95
+ beforeEach(() => {
96
+ mockPackageJson = {
97
+ name: '@atlaskit/tokens',
98
+ 'af:exports': {
99
+ '.': './src/index.tsx',
100
+ './rename-mapping': './src/entry-points/rename-mapping.tsx',
101
+ './babel-plugin': './src/entry-points/babel-plugin.tsx',
102
+ './glyph': './glyph',
103
+ './button': './button',
104
+ },
105
+ exports: {
106
+ '.': './index.js',
107
+ './rename-mapping': './src/entry-points/rename-mapping.tsx',
108
+ './babel-plugin': './src/entry-points/babel-plugin.tsx',
109
+ './glyph/*': './glyph/*',
110
+ './button/*': './button/*.js',
111
+ },
112
+ };
113
+ });
114
+ tester.run('ensure-native-and-af-exports-synced', rule, {
115
+ valid: [
116
+ {
117
+ code: '',
118
+ filename: 'packages/test/package.json',
119
+ },
120
+ ],
121
+ invalid: [],
122
+ });
123
+ });
124
+ });
125
+
126
+ describe('invalid test cases', () => {
127
+ describe('should fail for mismatched invalid entrypoints', () => {
128
+ beforeEach(() => {
129
+ mockPackageJson = {
130
+ name: '@atlaskit/test',
131
+ 'af:exports': {
132
+ '.': './src/index.ts',
133
+ },
134
+ exports: {
135
+ '.': './test/index.ts',
136
+ },
137
+ };
138
+ });
139
+ tester.run('ensure-native-and-af-exports-synced', rule, {
140
+ valid: [],
141
+ invalid: [
142
+ {
143
+ code: '',
144
+ filename: 'packages/test/package.json',
145
+ errors: [{ messageId: 'unexpectedExportsValue' }],
146
+ },
147
+ ],
148
+ });
149
+ });
150
+
151
+ describe('should fail for missing entrypoints', () => {
152
+ beforeEach(() => {
153
+ mockPackageJson = {
154
+ name: '@atlaskit/tokens',
155
+ 'af:exports': {
156
+ '.': './src/index.ts',
157
+ './button': './button',
158
+ },
159
+ exports: {
160
+ '.': './src/index.ts',
161
+ },
162
+ };
163
+ });
164
+ tester.run('ensure-native-and-af-exports-synced', rule, {
165
+ valid: [],
166
+ invalid: [
167
+ {
168
+ code: '',
169
+ filename: 'packages/test/package.json',
170
+ errors: [{ messageId: 'missingExportsKey' }],
171
+ },
172
+ ],
173
+ });
174
+ });
175
+
176
+ describe('should fail for invalid directory export', () => {
177
+ beforeEach(() => {
178
+ mockPackageJson = {
179
+ name: '@atlaskit/tokens',
180
+ 'af:exports': {
181
+ './button': './button',
182
+ },
183
+ exports: {
184
+ './button/*': './src/button/*',
185
+ },
186
+ };
187
+ });
188
+ tester.run('ensure-native-and-af-exports-synced', rule, {
189
+ valid: [],
190
+ invalid: [
191
+ {
192
+ code: '',
193
+ filename: 'packages/test/package.json',
194
+ errors: [{ messageId: 'unexpectedExportsValue' }],
195
+ },
196
+ ],
197
+ });
198
+ });
199
+ });
@@ -0,0 +1,193 @@
1
+ import type { Rule } from 'eslint';
2
+ import path from 'path';
3
+
4
+ import { getMetadataForFilename } from '../util/registration-utils';
5
+
6
+ interface ExportsValidationExceptions {
7
+ [key: string]: { ignoredAfExportKeys: string[] };
8
+ }
9
+
10
+ const exportsValidationExceptions: ExportsValidationExceptions = {
11
+ '@atlassian/sizemap': {
12
+ ignoredAfExportKeys: ['.', './lmdb-cache-manager'],
13
+ },
14
+ '@atlaskit/tokens': {
15
+ ignoredAfExportKeys: ['./babel-plugin'],
16
+ },
17
+ };
18
+
19
+ const rule: Rule.RuleModule = {
20
+ meta: {
21
+ docs: {
22
+ recommended: false,
23
+ },
24
+ type: 'problem',
25
+ messages: {
26
+ missingExportsProperty: `The exports property must be defined for {{pkgName}}; it most likely can just be a duplicate of the "af:exports" property. See http://go/eslint-exports for details`,
27
+ missingExportsKey: `Missing package.json exports key "{{expectedKey}}" in {{pkgName}}. The exports entry should be "{{expectedKey}}": "{{expectedValue}}". See http://go/eslint-exports for details`,
28
+ unexpectedExportsKey: `Unexpected package.json exports key "{{key}}" in {{pkgName}}. The exports entry should be "{{expectedKey}}": "{{expectedValue}}". See http://go/eslint-exports for details`,
29
+ unexpectedExportsValue: `Unexpected package.json exports value in {{pkgName}} for the "{{key}}" key. The exports entry should be "{{key}}": "{{expectedValue}}". See http://go/eslint-exports for details`,
30
+ },
31
+ },
32
+
33
+ create(context) {
34
+ const fileName = context.getFilename();
35
+
36
+ if (!fileName.endsWith('package.json')) {
37
+ return {};
38
+ }
39
+
40
+ const { pkgJson: packageJson } = getMetadataForFilename(fileName);
41
+
42
+ const pkgName = packageJson.name;
43
+
44
+ // TODO: remove '|| !packageJson['exports']' once all package.json files have 'exports'
45
+ if (!pkgName || !packageJson['af:exports'] || !packageJson['exports']) {
46
+ return {};
47
+ }
48
+
49
+ // TODO: Add back in once all package.json files have 'exports'
50
+ // if (!packageJson['exports']) {
51
+ // context.report({
52
+ // node: context.getSourceCode().ast,
53
+ // messageId: 'missingExportsProperty',
54
+ // data: { pkgName },
55
+ // });
56
+ // return {};
57
+ // }
58
+
59
+ const afExports: { [key: string]: any } = packageJson['af:exports'];
60
+ const nativeExports: { [key: string]: any } = packageJson['exports'];
61
+
62
+ return {
63
+ Program(node) {
64
+ for (const [afExportsKey, afExportsValue] of Object.entries(
65
+ afExports,
66
+ )) {
67
+ if (
68
+ exportsValidationExceptions[pkgName]?.ignoredAfExportKeys.includes(
69
+ afExportsKey,
70
+ )
71
+ ) {
72
+ continue;
73
+ }
74
+
75
+ const exportKeyViolations = getExportKeyViolation(
76
+ afExportsKey,
77
+ afExportsValue,
78
+ nativeExports,
79
+ );
80
+
81
+ if (exportKeyViolations) {
82
+ context.report({
83
+ data: { ...exportKeyViolations, key: afExportsKey, pkgName },
84
+ node,
85
+ messageId: exportKeyViolations.messageId,
86
+ });
87
+
88
+ continue;
89
+ }
90
+
91
+ const exportValueViolations = getExportValueViolation(
92
+ pkgName,
93
+ afExportsKey,
94
+ afExportsValue,
95
+ nativeExports,
96
+ );
97
+
98
+ if (exportValueViolations) {
99
+ context.report({
100
+ data: { ...exportValueViolations, pkgName },
101
+ node,
102
+ messageId: 'unexpectedExportsValue',
103
+ });
104
+
105
+ continue;
106
+ }
107
+ }
108
+ },
109
+ };
110
+ },
111
+ };
112
+
113
+ function getExportKeyViolation(
114
+ afExportsKey: string,
115
+ afExportsValue: string,
116
+ nativeExports: { [key: string]: any },
117
+ ) {
118
+ const afExportsValueHasExtension = path.extname(afExportsValue);
119
+
120
+ if (
121
+ afExportsValueHasExtension &&
122
+ !nativeExports.hasOwnProperty(afExportsKey)
123
+ ) {
124
+ return {
125
+ messageId: 'missingExportsKey',
126
+ expectedKey: afExportsKey,
127
+ expectedValue: afExportsValue,
128
+ };
129
+ }
130
+
131
+ if (
132
+ !afExportsValueHasExtension &&
133
+ nativeExports.hasOwnProperty(afExportsKey)
134
+ ) {
135
+ return {
136
+ messageId: 'unexpectedExportsKey',
137
+ expectedKey: `${afExportsKey}/*`,
138
+ expectedValue: `${afExportsValue}/*`,
139
+ };
140
+ }
141
+
142
+ if (
143
+ !afExportsValueHasExtension &&
144
+ !nativeExports.hasOwnProperty(`${afExportsKey}/*`)
145
+ ) {
146
+ return {
147
+ messageId: 'missingExportsKey',
148
+ expectedKey: `${afExportsKey}/*`,
149
+ expectedValue: `${afExportsValue}/*`,
150
+ };
151
+ }
152
+ }
153
+
154
+ function getExportValueViolation(
155
+ pkgName: string,
156
+ afExportsKey: string,
157
+ afExportsValue: string,
158
+ nativeExports: { [key: string]: any },
159
+ ) {
160
+ const afExportsValueHasExtension = path.extname(afExportsValue);
161
+
162
+ // Some entrypoints have been updated to an index.js file that registers ts-node
163
+ // Use path.basename to get the file name to see if it is equal to 'index.js'
164
+ if (
165
+ afExportsValueHasExtension &&
166
+ path.basename(nativeExports[afExportsKey]) === 'index.js'
167
+ ) {
168
+ return;
169
+ }
170
+
171
+ if (
172
+ afExportsValueHasExtension &&
173
+ nativeExports[afExportsKey] !== afExportsValue
174
+ ) {
175
+ return {
176
+ key: afExportsKey,
177
+ expectedValue: afExportsValue,
178
+ };
179
+ }
180
+
181
+ // af:exports entrypoints without a file extension export the whole directory so check to ensure the exports value includes the wildcard
182
+ if (
183
+ !afExportsValueHasExtension &&
184
+ !nativeExports[`${afExportsKey}/*`].startsWith(`${afExportsValue}/*`)
185
+ ) {
186
+ return {
187
+ key: `${afExportsKey}/*`,
188
+ expectedValue: `${afExportsValue}/*`,
189
+ };
190
+ }
191
+ }
192
+
193
+ export default rule;
@@ -55,6 +55,15 @@ describe('enforce-feature-flag-usage-structure tests', () => {
55
55
  { messageId: 'multipleFlagCheckInExpression' },
56
56
  ],
57
57
  },
58
+ {
59
+ code: `export default getBooleanFF('test-flag') ? "this is" : "not good";`,
60
+ errors: [{ messageId: 'noModuleScope' }],
61
+ },
62
+ {
63
+ only: true,
64
+ code: `export const foo = getBooleanFF('test-flag') ? "this is" : "not good";`,
65
+ errors: [{ messageId: 'noModuleScope' }],
66
+ },
58
67
  ],
59
68
  });
60
69
  });
@@ -63,6 +63,7 @@ const rule: Rule.RuleModule = {
63
63
  onlyStringLiteral:
64
64
  "Only get feature flags by string literal, don't use variables! See http://go/pff-eslint for more details",
65
65
  multipleFlagCheckInExpression: `Only check one flag per expression! See http://go/pff-eslint for more details`,
66
+ noModuleScope: `Don't use platform feature flags in module scope! See http://go/pff-eslint for more details`,
66
67
  },
67
68
  },
68
69
  create(context) {
@@ -83,7 +84,30 @@ const rule: Rule.RuleModule = {
83
84
 
84
85
  switch (node.parent?.type) {
85
86
  case 'IfStatement':
87
+ break;
86
88
  case 'ConditionalExpression':
89
+ switch (node.parent?.parent.type) {
90
+ case 'ExportDefaultDeclaration':
91
+ // handles "export default getBooleanFF('test-flag') ? "this is" : "not good";"
92
+ context.report({
93
+ node,
94
+ messageId: 'noModuleScope',
95
+ });
96
+ break;
97
+ case 'VariableDeclarator':
98
+ // handles "export const foo = getBooleanFF('test-flag') ? 'this is' : 'not good';"
99
+ if (
100
+ node.parent.parent.parent.type === 'VariableDeclaration' &&
101
+ node.parent.parent.parent.parent.type ===
102
+ 'ExportNamedDeclaration'
103
+ ) {
104
+ context.report({
105
+ node,
106
+ messageId: 'noModuleScope',
107
+ });
108
+ }
109
+ break;
110
+ }
87
111
  break;
88
112
  case 'UnaryExpression':
89
113
  case 'LogicalExpression':
@@ -1,50 +0,0 @@
1
- ## API Report File for "@atlaskit/eslint-plugin-platform"
2
-
3
- > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4
-
5
- ```ts
6
-
7
- import type { Linter } from 'eslint';
8
- import { Rule } from 'eslint';
9
-
10
- // @public (undocumented)
11
- export const configs: {
12
- recommended: {
13
- plugins: string[];
14
- rules: {
15
- '@atlaskit/platform/ensure-feature-flag-registration': string;
16
- '@atlaskit/platform/ensure-feature-flag-prefix': (string | {
17
- allowedPrefixes: string[];
18
- })[];
19
- '@atlaskit/platform/ensure-test-runner-arguments': string;
20
- '@atlaskit/platform/ensure-test-runner-nested-count': string;
21
- '@atlaskit/platform/no-invalid-feature-flag-usage': string;
22
- '@atlaskit/platform/no-invalid-storybook-decorator-usage': string;
23
- '@atlaskit/platform/ensure-atlassian-team': string;
24
- };
25
- };
26
- };
27
-
28
- // @public (undocumented)
29
- export const processors: {
30
- 'package-json-processor': Linter.Processor<Linter.ProcessorFile | string>;
31
- };
32
-
33
- // @public (undocumented)
34
- export const rules: {
35
- 'ensure-feature-flag-registration': Rule.RuleModule;
36
- 'ensure-feature-flag-prefix': Rule.RuleModule;
37
- 'ensure-test-runner-arguments': Rule.RuleModule;
38
- 'ensure-test-runner-nested-count': Rule.RuleModule;
39
- 'ensure-atlassian-team': Rule.RuleModule;
40
- 'ensure-critical-dependency-resolutions': Rule.RuleModule;
41
- 'no-duplicate-dependencies': Rule.RuleModule;
42
- 'no-invalid-feature-flag-usage': Rule.RuleModule;
43
- 'no-pre-post-install-scripts': Rule.RuleModule;
44
- 'no-invalid-storybook-decorator-usage': Rule.RuleModule;
45
- 'ensure-publish-valid': Rule.RuleModule;
46
- };
47
-
48
- // (No @packageDocumentation comment for this package)
49
-
50
- ```