@ethlete/eslint-plugin 1.0.0-next.20 → 1.0.0-next.21

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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0-next.21
4
+
5
+ ### Minor Changes
6
+
7
+ - [`3d44765`](https://github.com/ethlete-io/ethdk/commit/3d44765647e6d47b558c1395fe5ebcb20ed3ef83) Thanks [@TomTomB](https://github.com/TomTomB)! - New rule `ethlete/no-async-await`, replacing the `no-restricted-syntax` selectors that could not be
8
+ toggled on their own. It also catches bare `await` and `for await…of`.
9
+
3
10
  ## 1.0.0-next.20
4
11
 
5
12
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethlete/eslint-plugin",
3
- "version": "1.0.0-next.20",
3
+ "version": "1.0.0-next.21",
4
4
  "license": "MIT",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -126,19 +126,8 @@ const recommendedTs = {
126
126
  selector: 'PropertyDefinition > ArrowFunctionExpression',
127
127
  message: 'No arrow function properties in classes. Use regular methods.',
128
128
  },
129
- // No async/await — use RxJS
130
- {
131
- selector: 'FunctionDeclaration[async=true]',
132
- message: 'No async/await. Use RxJS for all async operations.',
133
- },
134
- {
135
- selector: 'ArrowFunctionExpression[async=true]',
136
- message: 'No async/await. Use RxJS for all async operations.',
137
- },
138
- {
139
- selector: 'FunctionExpression[async=true]',
140
- message: 'No async/await. Use RxJS for all async operations.',
141
- },
129
+ // async/await is owned by ethlete/no-async-await, so a lib can keep it on while it works
130
+ // through the rest of this rule's backlog.
142
131
  // No static class members — except ngTemplateContextGuard, which Angular's template
143
132
  // type checker requires to be static (it types a structural directive's `let-` bindings)
144
133
  {
@@ -253,6 +242,9 @@ const recommendedTs = {
253
242
  // No .subscribe() inside a .pipe() callback
254
243
  'ethlete/no-subscribe-in-pipe': 'error',
255
244
 
245
+ // No async/await — asynchronous work is modelled as cold Observables
246
+ 'ethlete/no-async-await': 'error',
247
+
256
248
  // No .subscribe() inside effect() or computed()
257
249
  'ethlete/no-rxjs-in-effect': 'error',
258
250
 
package/src/index.js CHANGED
@@ -58,6 +58,7 @@ const noLegacyPrepareWithoutInjector = require('./rules/no-legacy-prepare-withou
58
58
  const requireFormSubmit = require('./rules/require-form-submit');
59
59
  const noCdkImport = require('./rules/no-cdk-import');
60
60
  const noLegacyQueryImport = require('./rules/no-legacy-query-import');
61
+ const noAsyncAwait = require('./rules/no-async-await');
61
62
  const { recommendedTs, recommendedTemplate, recommendedSpec } = require('./configs/recommended');
62
63
 
63
64
  /** @type {import('eslint').ESLint.Plugin} */
@@ -124,6 +125,7 @@ const plugin = {
124
125
  'require-form-submit': requireFormSubmit,
125
126
  'no-cdk-import': noCdkImport,
126
127
  'no-legacy-query-import': noLegacyQueryImport,
128
+ 'no-async-await': noAsyncAwait,
127
129
  },
128
130
  };
129
131
 
@@ -0,0 +1,80 @@
1
+ // @ts-check
2
+ 'use strict';
3
+
4
+ /**
5
+ * Disallow async/await — asynchronous work is modelled with RxJS.
6
+ *
7
+ * Every async API in the SDK returns a cold Observable, not a Promise, so a consumer can compose,
8
+ * cancel and retry it. Bridge a browser Promise in with from(); never await it.
9
+ *
10
+ * BAD:
11
+ * async copy(text: string) {
12
+ * await navigator.clipboard.writeText(text);
13
+ * this.copied.set(true);
14
+ * }
15
+ *
16
+ * GOOD:
17
+ * copy(text: string) {
18
+ * return from(navigator.clipboard.writeText(text)).pipe(tap(() => this.copied.set(true)));
19
+ * }
20
+ */
21
+
22
+ /** Storybook's play function is called by the test runner and has to be a promise. */
23
+ const isStorybookPlayFunction = (node) => {
24
+ const { parent } = node;
25
+
26
+ return (
27
+ parent?.type === 'Property' &&
28
+ parent.value === node &&
29
+ parent.key.type === 'Identifier' &&
30
+ parent.key.name === 'play'
31
+ );
32
+ };
33
+
34
+ /** @type {import('eslint').Rule.RuleModule} */
35
+ const noAsyncAwait = {
36
+ meta: {
37
+ type: 'problem',
38
+ docs: {
39
+ description: 'Disallow async / await — model asynchronous work as RxJS Observables.',
40
+ recommended: true,
41
+ },
42
+ messages: {
43
+ noAsync:
44
+ "Don't declare an 'async' function. Return a cold Observable instead — 'defer()' for laziness, 'from()' to bridge a Promise-based browser API.",
45
+ noAwait:
46
+ "Don't 'await'. Compose the value into the stream with 'switchMap' / 'mergeMap' / 'from()' instead, and read a one-shot result with 'firstValueFrom()' only in tests.",
47
+ noForAwait: "Don't use 'for await…of'. Bridge the async iterable into a stream and compose it with RxJS.",
48
+ },
49
+ schema: [],
50
+ },
51
+ create(context) {
52
+ const isExempt = (node) => {
53
+ for (let current = node; current; current = current.parent) {
54
+ if (isStorybookPlayFunction(current)) return true;
55
+ }
56
+
57
+ return false;
58
+ };
59
+
60
+ const reportAsync = (node) => {
61
+ if (!node.async || isExempt(node)) return;
62
+
63
+ context.report({ node, messageId: 'noAsync' });
64
+ };
65
+
66
+ return {
67
+ FunctionDeclaration: reportAsync,
68
+ FunctionExpression: reportAsync,
69
+ ArrowFunctionExpression: reportAsync,
70
+ AwaitExpression(node) {
71
+ if (!isExempt(node)) context.report({ node, messageId: 'noAwait' });
72
+ },
73
+ ForOfStatement(node) {
74
+ if (node.await && !isExempt(node)) context.report({ node, messageId: 'noForAwait' });
75
+ },
76
+ };
77
+ },
78
+ };
79
+
80
+ module.exports = noAsyncAwait;