@backstage/eslint-plugin 0.1.7 → 0.1.9-next.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
  # @backstage/eslint-plugin
2
2
 
3
+ ## 0.1.9-next.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 08895e3: Added support for linting dependencies on workspace packages with the `backstage.inline` flag.
8
+
9
+ ## 0.1.8
10
+
11
+ ### Patch Changes
12
+
13
+ - 65ec043: add some `pickers` fixes
14
+
3
15
  ## 0.1.7
4
16
 
5
17
  ### Patch Changes
@@ -41,3 +41,50 @@ import {
41
41
  import Typography from '@material-ui/core/Typography';
42
42
  import Box from '@material-ui/core/Box';
43
43
  ```
44
+
45
+ ## --fix known issues
46
+
47
+ This rule provides automatic fixes for the imports, but it has some known issues:
48
+
49
+ ### Non Props types import
50
+
51
+ The fix will handle correctly 3 groups of imports:
52
+
53
+ - Any import from related to styles (i.e `makeStyles`, `styled`, `WithStyles`) will be auto fixed to the `@material-ui/core/styles` import.
54
+ - Any import with `Props` suffix will be auto fixed to actual component for example `DialogProps` will be imported from `@material-ui/core/Dialog`.
55
+ - Any other import will be considered as a component import and will be auto fixed to the actual component import.
56
+
57
+ This means that some types of imports without `Props` suffix will be wrongly auto fixed to the component import, for example this fix will be wrong:
58
+
59
+ ```diff
60
+ - import { Alert, Color } from '@material-ui/lab';
61
+ + import Alert from '@material-ui/lab/Alert';
62
+ + import Color from '@material-ui/lab/Color'; // this import is wrong
63
+ ```
64
+
65
+ The correct import should look like this:
66
+
67
+ ```diff
68
+ - import { Alert, Color } from '@material-ui/lab';
69
+ + import Alert, {Color} from '@material-ui/lab/Alert';
70
+ ```
71
+
72
+ Because `Color` is a type coming from the Alert component.
73
+
74
+ ### No default export available
75
+
76
+ Some components do not have a default export, for example `@material-ui/pickers/DateTimePicker` does not have a default export, so the fix will not work for these cases.
77
+
78
+ The fix will be wrong for this import:
79
+
80
+ ```diff
81
+ - import { DateTimePicker } from '@material-ui/pickers';
82
+ + import DateTimePicker from '@material-ui/pickers/DateTimePicker'; // this default import does not exist
83
+ ```
84
+
85
+ The correct import should look like this:
86
+
87
+ ```diff
88
+ - import { DateTimePicker } from '@material-ui/pickers';
89
+ + import { DateTimePicker } from '@material-ui/pickers/DateTimePicker'; // this is the correct import
90
+ ```
@@ -21,7 +21,7 @@ const manypkg = require('@manypkg/get-packages');
21
21
 
22
22
  /**
23
23
  * @typedef ExtendedPackage
24
- * @type {import('@manypkg/get-packages').Package & { packageJson: { exports?: Record<string, string>, files?: Array<string> }}} packageJson
24
+ * @type {import('@manypkg/get-packages').Package & { packageJson: { exports?: Record<string, string>, files?: Array<string>, backstage?: { inline?: boolean } }}} packageJson
25
25
  */
26
26
 
27
27
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/eslint-plugin",
3
- "version": "0.1.7",
3
+ "version": "0.1.9-next.0",
4
4
  "description": "Backstage ESLint plugin",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -22,7 +22,7 @@
22
22
  "minimatch": "^9.0.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@backstage/cli": "^0.26.3",
25
+ "@backstage/cli": "^0.27.1-next.2",
26
26
  "@types/estree": "^1.0.5",
27
27
  "eslint": "^8.33.0"
28
28
  }
@@ -27,7 +27,23 @@
27
27
  */
28
28
 
29
29
  const KNOWN_STYLES = [
30
- // TODO: add exports from colorManipulator and transitions
30
+ // colorManipulator
31
+ 'hexToRgb',
32
+ 'rgbToHex',
33
+ 'hslToRgb',
34
+ 'decomposeColor',
35
+ 'recomposeColor',
36
+ 'getContrastRatio',
37
+ 'getLuminance',
38
+ 'emphasize',
39
+ 'fade',
40
+ 'alpha',
41
+ 'darken',
42
+ 'lighten',
43
+ // transitions
44
+ 'easing',
45
+ 'duration',
46
+ // styles
31
47
  'createTheme',
32
48
  'unstable_createMuiStrictModeTheme',
33
49
  'createMuiTheme',
@@ -139,7 +155,11 @@ module.exports = {
139
155
  const value = s.imported.name;
140
156
  const alias = s.local.name === value ? undefined : s.local.name;
141
157
 
142
- const propsMatch = /^([A-Z]\w+)Props$/.exec(value);
158
+ const propsMatch =
159
+ /^([A-Z]\w+)Props$/.exec(value) ??
160
+ (node.source.value === '@material-ui/pickers'
161
+ ? /^Keyboard([A-Z]\w+Picker)$/.exec(value)
162
+ : null);
143
163
 
144
164
  const emitProp = propsMatch !== null;
145
165
  const emitComponent = !emitProp;
@@ -188,7 +208,7 @@ module.exports = {
188
208
  if (specifier.emitProp && !specifier.emitComponent) {
189
209
  const replacement = `import { ${getNamedImportValue(
190
210
  specifier,
191
- )} } from '@material-ui/core/${specifier.componentValue}';`;
211
+ )} } from '${node.source.value}/${specifier.componentValue}';`;
192
212
  replacements.push(replacement);
193
213
  }
194
214
 
@@ -197,9 +217,9 @@ module.exports = {
197
217
  replacements.push(
198
218
  `import ${
199
219
  specifier.componentAlias ?? specifier.componentValue
200
- }, { ${getNamedImportValue(
201
- specifier,
202
- )} } from '@material-ui/core/${specifier.componentValue}';`,
220
+ }, { ${getNamedImportValue(specifier)} } from '${
221
+ node.source.value
222
+ }/${specifier.componentValue}';`,
203
223
  );
204
224
  }
205
225
  }
@@ -22,11 +22,11 @@ const visitImports = require('../lib/visitImports');
22
22
  const minimatch = require('minimatch');
23
23
  const { execFileSync } = require('child_process');
24
24
 
25
- const depFields = {
25
+ const depFields = /** @type {const} */ ({
26
26
  dep: 'dependencies',
27
27
  dev: 'devDependencies',
28
28
  peer: 'peerDependencies',
29
- };
29
+ });
30
30
 
31
31
  const devModulePatterns = [
32
32
  new minimatch.Minimatch('!src/**'),
@@ -154,6 +154,124 @@ function addVersionQuery(name, flag, packages) {
154
154
  return `${name}@${mostCommonRange}`;
155
155
  }
156
156
 
157
+ /**
158
+ * Add missing package imports
159
+ * @param {Array<{name: string, flag: string, node: import('estree').Node}>} toAdd
160
+ * @param {import('../lib/getPackages').PackageMap} packages
161
+ * @param {import('../lib/getPackages').ExtendedPackage} localPkg
162
+ */
163
+ function addMissingImports(toAdd, packages, localPkg) {
164
+ /** @type Record<string, Set<string>> */
165
+ const byFlag = {};
166
+
167
+ for (const { name, flag } of toAdd) {
168
+ byFlag[flag] = byFlag[flag] ?? new Set();
169
+ byFlag[flag].add(name);
170
+ }
171
+
172
+ for (const name of byFlag[''] ?? []) {
173
+ byFlag['--dev']?.delete(name);
174
+ }
175
+ for (const name of byFlag['--peer'] ?? []) {
176
+ byFlag['']?.delete(name);
177
+ byFlag['--dev']?.delete(name);
178
+ }
179
+
180
+ for (const [flag, names] of Object.entries(byFlag)) {
181
+ // Look up existing version queries in the repo for the same dependency
182
+ const namesWithQuery = [...names].map(name =>
183
+ addVersionQuery(name, flag, packages),
184
+ );
185
+
186
+ // The security implication of this is a bit interesting, as crafted add-import
187
+ // directives could be used to install malicious packages. However, the same is true
188
+ // for adding malicious packages to package.json, so there's no significant difference.
189
+ execFileSync('yarn', ['add', ...(flag ? [flag] : []), ...namesWithQuery], {
190
+ cwd: localPkg.dir,
191
+ stdio: 'inherit',
192
+ });
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Removes dependency entries pointing to inlined workspace packages.
198
+ * @param {Array<{pkg: import('../lib/getPackages').ExtendedPackage, node: import('estree').Node}>} toInline
199
+ * @param {import('../lib/getPackages').ExtendedPackage} localPkg
200
+ */
201
+ function removeInlineImports(toInline, localPkg) {
202
+ /** @type Set<string> */
203
+ const toRemove = new Set();
204
+
205
+ for (const { pkg } of toInline) {
206
+ const name = pkg.packageJson.name;
207
+ for (const depType of Object.values(depFields)) {
208
+ if (localPkg.packageJson[depType]?.[name]) {
209
+ toRemove.add(name);
210
+ }
211
+ }
212
+ }
213
+ if (toRemove.size > 0) {
214
+ execFileSync('yarn', ['remove', ...toRemove], {
215
+ cwd: localPkg.dir,
216
+ stdio: 'inherit',
217
+ });
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Adds dependencies that are not properly forwarded from inline dependencies.
223
+ * @param {Array<{pkg: import('../lib/getPackages').ExtendedPackage, node: import('estree').Node}>} toInline
224
+ * @param {import('../lib/getPackages').ExtendedPackage} localPkg
225
+ */
226
+ function addForwardedInlineImports(toInline, localPkg) {
227
+ const declaredProdDeps = new Set([
228
+ ...Object.keys(localPkg.packageJson.dependencies ?? {}),
229
+ ...Object.keys(localPkg.packageJson.peerDependencies ?? {}),
230
+ localPkg.packageJson.name, // include self
231
+ ]);
232
+
233
+ /** @type Map<string, Map<string, string>> */
234
+ const byFlagByName = new Map();
235
+
236
+ for (const { pkg } of toInline) {
237
+ for (const depType of /** @type {const} */ ([
238
+ 'dependencies',
239
+ 'peerDependencies',
240
+ ])) {
241
+ for (const [depName, depQuery] of Object.entries(
242
+ pkg.packageJson[depType] ?? {},
243
+ )) {
244
+ if (!declaredProdDeps.has(depName)) {
245
+ const flag = getAddFlagForDepsField(depType);
246
+ const byName = byFlagByName.get(flag);
247
+ if (byName) {
248
+ const query = byName.get(depName);
249
+ if (query && query !== depQuery) {
250
+ throw new Error(
251
+ `Conflicting dependency queries for inlined package dep ${depName}, got ${query} and ${depQuery}`,
252
+ );
253
+ } else {
254
+ byName.set(depName, depQuery);
255
+ }
256
+ } else {
257
+ byFlagByName.set(flag, new Map([[depName, depQuery]]));
258
+ }
259
+ }
260
+ }
261
+ }
262
+ }
263
+
264
+ for (const [flag, byName] of byFlagByName) {
265
+ const namesWithQuery = [...byName.entries()].map(
266
+ ([name, query]) => `${name}@${query}`,
267
+ );
268
+ execFileSync('yarn', ['add', ...(flag ? [flag] : []), ...namesWithQuery], {
269
+ cwd: localPkg.dir,
270
+ stdio: 'inherit',
271
+ });
272
+ }
273
+ }
274
+
157
275
  /** @type {import('eslint').Rule.RuleModule} */
158
276
  module.exports = {
159
277
  meta: {
@@ -165,6 +283,8 @@ module.exports = {
165
283
  switch:
166
284
  '{{ packageName }} is declared in {{ oldDepsField }}, but should be moved to {{ depsField }} in {{ packageJsonPath }}.',
167
285
  switchBack: 'Switch back to import declaration',
286
+ inlineDirect: `The dependency on the inline package {{ packageName }} must not be declared in package dependencies.`,
287
+ inlineMissing: `Each production dependency from the inline package {{ packageName }} must be re-declared by this package, the following dependencies are missing: {{ missingDeps }}`,
168
288
  },
169
289
  docs: {
170
290
  description:
@@ -189,57 +309,48 @@ module.exports = {
189
309
  /** @type Array<{name: string, flag: string, node: import('estree').Node}> */
190
310
  const importsToAdd = [];
191
311
 
312
+ /** @type Array<{pkg: import('../lib/getPackages').ExtendedPackage, node: import('estree').Node}> */
313
+ const importsToInline = [];
314
+
192
315
  return {
193
316
  // All missing imports that we detect are collected as we traverse, and then we use
194
317
  // the program exit to execute all install directives that have been found.
195
318
  ['Program:exit']() {
196
- /** @type Record<string, Set<string>> */
197
- const byFlag = {};
319
+ if (importsToAdd.length > 0) {
320
+ addMissingImports(importsToAdd, packages, localPkg);
198
321
 
199
- for (const { name, flag } of importsToAdd) {
200
- byFlag[flag] = byFlag[flag] ?? new Set();
201
- byFlag[flag].add(name);
202
- }
203
-
204
- for (const name of byFlag[''] ?? []) {
205
- byFlag['--dev']?.delete(name);
206
- }
207
- for (const name of byFlag['--peer'] ?? []) {
208
- byFlag['']?.delete(name);
209
- byFlag['--dev']?.delete(name);
322
+ // This switches all import directives back to the original import.
323
+ for (const added of importsToAdd) {
324
+ context.report({
325
+ node: added.node,
326
+ messageId: 'switchBack',
327
+ fix(fixer) {
328
+ return fixer.replaceText(added.node, `'${added.name}'`);
329
+ },
330
+ });
331
+ }
332
+ importsToAdd.length = 0;
210
333
  }
211
334
 
212
- for (const [flag, names] of Object.entries(byFlag)) {
213
- // Look up existing version queries in the repo for the same dependency
214
- const namesWithQuery = [...names].map(name =>
215
- addVersionQuery(name, flag, packages),
216
- );
217
-
218
- // The security implication of this is a bit interesting, as crafted add-import
219
- // directives could be used to install malicious packages. However, the same is true
220
- // for adding malicious packages to package.json, so there's no significant difference.
221
- execFileSync(
222
- 'yarn',
223
- ['add', ...(flag ? [flag] : []), ...namesWithQuery],
224
- {
225
- cwd: localPkg.dir,
226
- stdio: 'inherit',
227
- },
228
- );
229
- }
335
+ if (importsToInline.length > 0) {
336
+ removeInlineImports(importsToInline, localPkg);
337
+ addForwardedInlineImports(importsToInline, localPkg);
230
338
 
231
- // This switches all import directives back to the original import.
232
- for (const added of importsToAdd) {
233
- context.report({
234
- node: added.node,
235
- messageId: 'switchBack',
236
- fix(fixer) {
237
- return fixer.replaceText(added.node, `'${added.name}'`);
238
- },
239
- });
339
+ for (const inlined of importsToInline) {
340
+ context.report({
341
+ node: inlined.node,
342
+ messageId: 'switchBack',
343
+ fix(fixer) {
344
+ return fixer.replaceText(
345
+ inlined.node,
346
+ `'${inlined.pkg.packageJson.name}'`,
347
+ );
348
+ },
349
+ });
350
+ }
351
+ importsToInline.length = 0;
240
352
  }
241
353
 
242
- importsToAdd.length = 0;
243
354
  packages.clearCache();
244
355
  },
245
356
  ...visitImports(context, (node, imp) => {
@@ -255,22 +366,99 @@ module.exports = {
255
366
 
256
367
  // Any import directive that is found is collected for processing later
257
368
  if (imp.type === 'directive') {
258
- const parts = imp.path.split(':');
259
- if (parts[1] !== 'add-import') {
260
- return;
369
+ const [, directive, ...args] = imp.path.split(':');
370
+
371
+ if (directive === 'add-import') {
372
+ const [type, name] = args;
373
+ if (!name.match(/^(@[-\w\.~]+\/)?[-\w\.~]*$/i)) {
374
+ throw new Error(
375
+ `Invalid package name to add as dependency: '${name}'`,
376
+ );
377
+ }
378
+
379
+ importsToAdd.push({
380
+ flag: getAddFlagForDepsField(type).trim(),
381
+ name,
382
+ node: imp.node,
383
+ });
384
+ }
385
+
386
+ if (directive === 'inline-imports') {
387
+ const [name] = args;
388
+ const pkg = packages.map.get(name);
389
+ if (!pkg) {
390
+ throw new Error(`Unexpectedly missing inline package: ${name}`);
391
+ }
392
+
393
+ importsToInline.push({
394
+ pkg: pkg,
395
+ node: imp.node,
396
+ });
397
+ }
398
+
399
+ return;
400
+ }
401
+
402
+ // Importing an internal inlined package, whose imports are inlined too
403
+ if (
404
+ imp.type === 'internal' &&
405
+ imp.package.packageJson.backstage?.inline
406
+ ) {
407
+ for (const depType of Object.values(depFields)) {
408
+ if (localPkg.packageJson[depType]?.[imp.packageName]) {
409
+ context.report({
410
+ node,
411
+ messageId: 'inlineDirect',
412
+ data: {
413
+ packageName: imp.packageName,
414
+ },
415
+ fix: fixer => {
416
+ return fixer.replaceText(
417
+ imp.node,
418
+ `'directive:inline-imports:${imp.packageName}'`,
419
+ );
420
+ },
421
+ });
422
+ return;
423
+ }
261
424
  }
262
- const [type, name] = parts.slice(2);
263
- if (!name.match(/^(@[-\w\.~]+\/)?[-\w\.~]*$/i)) {
264
- throw new Error(
265
- `Invalid package name to add as dependency: '${name}'`,
266
- );
425
+
426
+ const missingDeps = [];
427
+ const declaredProdDeps = new Set([
428
+ ...Object.keys(localPkg.packageJson.dependencies ?? {}),
429
+ ...Object.keys(localPkg.packageJson.peerDependencies ?? {}),
430
+ localPkg.packageJson.name, // include self
431
+ ]);
432
+ for (const depType of /** @type {const} */ ([
433
+ 'dependencies',
434
+ 'peerDependencies',
435
+ ])) {
436
+ for (const depName of Object.keys(
437
+ imp.package.packageJson[depType] ?? {},
438
+ )) {
439
+ if (!declaredProdDeps.has(depName)) {
440
+ missingDeps.push(depName);
441
+ }
442
+ }
443
+ }
444
+
445
+ if (missingDeps.length > 0) {
446
+ context.report({
447
+ node,
448
+ messageId: 'inlineMissing',
449
+ data: {
450
+ packageName: imp.packageName,
451
+ missingDeps: missingDeps.join(', '),
452
+ },
453
+ fix: fixer => {
454
+ return fixer.replaceText(
455
+ imp.node,
456
+ `'directive:inline-imports:${imp.packageName}'`,
457
+ );
458
+ },
459
+ });
267
460
  }
268
461
 
269
- importsToAdd.push({
270
- flag: getAddFlagForDepsField(type).trim(),
271
- name,
272
- node: imp.node,
273
- });
274
462
  return;
275
463
  }
276
464
 
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "@internal/bar",
3
+ "backstage": {
4
+ "role": "frontend-plugin"
5
+ },
3
6
  "exports": {
4
7
  ".": "./src/index.ts",
5
8
  "./BarPage": "./src/components/Bar.tsx",
6
9
  "./package.json": "./package.json"
7
10
  },
8
- "backstage": {
9
- "role": "frontend-plugin"
10
- },
11
11
  "dependencies": {
12
+ "inline-dep": "*",
12
13
  "react-router": "*"
13
14
  },
14
15
  "devDependencies": {
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@internal/inline",
3
+ "backstage": {
4
+ "inline": true
5
+ },
6
+ "files": [
7
+ "dist",
8
+ "type-utils"
9
+ ],
10
+ "dependencies": {
11
+ "@internal/inline-dep-valid": "workspace:^"
12
+ },
13
+ "peerDependencies": {
14
+ "react": "*"
15
+ }
16
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@internal/inline-dep-direct",
3
+ "files": [
4
+ "dist",
5
+ "type-utils"
6
+ ],
7
+ "dependencies": {
8
+ "@internal/inline": "workspace:^",
9
+ "@internal/inline-dep-valid": "workspace:^",
10
+ "react": "*"
11
+ }
12
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "@internal/inline-dep-missing",
3
+ "files": [
4
+ "dist",
5
+ "type-utils"
6
+ ]
7
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "@internal/inline-dep-valid",
3
+ "files": [
4
+ "dist",
5
+ "type-utils"
6
+ ],
7
+ "peerDependencies": {
8
+ "react": "*"
9
+ }
10
+ }
@@ -93,6 +93,8 @@ import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';`,
93
93
  ThemeProvider,
94
94
  WithStyles,
95
95
  Tooltip as MaterialTooltip,
96
+ alpha,
97
+ easing
96
98
  } from '@material-ui/core';`,
97
99
  errors: [{ messageId: 'topLevelImport' }],
98
100
  output: `import Box from '@material-ui/core/Box';
@@ -101,7 +103,7 @@ import DialogContent from '@material-ui/core/DialogContent';
101
103
  import DialogTitle from '@material-ui/core/DialogTitle';
102
104
  import Grid from '@material-ui/core/Grid';
103
105
  import MaterialTooltip from '@material-ui/core/Tooltip';
104
- import { makeStyles, ThemeProvider, WithStyles } from '@material-ui/core/styles';`,
106
+ import { makeStyles, ThemeProvider, WithStyles, alpha, easing } from '@material-ui/core/styles';`,
105
107
  },
106
108
  {
107
109
  code: `import { Box, Button, makeStyles } from '@material-ui/core';`,
@@ -111,11 +113,11 @@ import Button from '@material-ui/core/Button';
111
113
  import { makeStyles } from '@material-ui/core/styles';`,
112
114
  },
113
115
  {
114
- code: `import { Paper, Typography, styled, withStyles } from '@material-ui/core';`,
116
+ code: `import { Paper, Typography, styled, withStyles, alpha, duration} from '@material-ui/core';`,
115
117
  errors: [{ messageId: 'topLevelImport' }],
116
118
  output: `import Paper from '@material-ui/core/Paper';
117
119
  import Typography from '@material-ui/core/Typography';
118
- import { styled, withStyles } from '@material-ui/core/styles';`,
120
+ import { styled, withStyles, alpha, duration } from '@material-ui/core/styles';`,
119
121
  },
120
122
  {
121
123
  code: `import { styled } from '@material-ui/core';`,
@@ -152,5 +154,17 @@ import { styled, withStyles } from '@material-ui/core/styles';`,
152
154
  errors: [{ messageId: 'topLevelImport' }],
153
155
  output: `import { styled as s } from '@material-ui/core/styles';`,
154
156
  },
157
+ {
158
+ code: `import { TreeItem, TreeItemProps, TreeView, AlertProps } from '@material-ui/lab';`,
159
+ errors: [{ messageId: 'topLevelImport' }],
160
+ output: `import TreeItem, { TreeItemProps } from '@material-ui/lab/TreeItem';
161
+ import TreeView from '@material-ui/lab/TreeView';
162
+ import { AlertProps } from '@material-ui/lab/Alert';`,
163
+ },
164
+ {
165
+ code: `import { KeyboardDatePicker } from '@material-ui/pickers';`,
166
+ errors: [{ messageId: 'topLevelImport' }],
167
+ output: `import { KeyboardDatePicker } from '@material-ui/pickers/DatePicker';`,
168
+ },
155
169
  ],
156
170
  });
@@ -52,6 +52,12 @@ const ERR_SWITCHED = (
52
52
  const ERR_SWITCH_BACK = () => ({
53
53
  message: 'Switch back to import declaration',
54
54
  });
55
+ const ERR_INLINE_DIRECT = (name: string) => ({
56
+ message: `The dependency on the inline package ${name} must not be declared in package dependencies.`,
57
+ });
58
+ const ERR_INLINE_MISSING = (name: string, missing: string) => ({
59
+ message: `Each production dependency from the inline package ${name} must be re-declared by this package, the following dependencies are missing: ${missing}`,
60
+ });
55
61
 
56
62
  // cwd must be restored
57
63
  const origDir = process.cwd();
@@ -102,6 +108,17 @@ ruleTester.run(RULE, rule, {
102
108
  code: `require('lod' + 'ash')`,
103
109
  filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'),
104
110
  },
111
+ {
112
+ code: `import '@internal/inline'`,
113
+ filename: joinPath(FIXTURE, 'packages/inline-dep-valid/src/index.ts'),
114
+ },
115
+ {
116
+ code: `import '@internal/inline'`,
117
+ filename: joinPath(
118
+ FIXTURE,
119
+ 'packages/inline-dep-valid/src/index.test.ts',
120
+ ),
121
+ },
105
122
  ],
106
123
  invalid: [
107
124
  {
@@ -264,6 +281,29 @@ ruleTester.run(RULE, rule, {
264
281
  ),
265
282
  ],
266
283
  },
284
+ {
285
+ code: `import '@internal/inline'`,
286
+ output: `import 'directive:inline-imports:@internal/inline'`,
287
+ filename: joinPath(
288
+ FIXTURE,
289
+ 'packages/inline-dep-invalid-direct/src/index.ts',
290
+ ),
291
+ errors: [ERR_INLINE_DIRECT('@internal/inline')],
292
+ },
293
+ {
294
+ code: `import '@internal/inline'`,
295
+ output: `import 'directive:inline-imports:@internal/inline'`,
296
+ filename: joinPath(
297
+ FIXTURE,
298
+ 'packages/inline-dep-invalid-missing/src/index.ts',
299
+ ),
300
+ errors: [
301
+ ERR_INLINE_MISSING(
302
+ '@internal/inline',
303
+ '@internal/inline-dep-valid, react',
304
+ ),
305
+ ],
306
+ },
267
307
 
268
308
  // Switching back to original import declarations
269
309
  {
@@ -302,5 +342,14 @@ ruleTester.run(RULE, rule, {
302
342
  filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'),
303
343
  errors: [ERR_SWITCH_BACK()],
304
344
  },
345
+ {
346
+ code: `import 'directive:inline-imports:@internal/inline'`,
347
+ output: `import '@internal/inline'`,
348
+ filename: joinPath(
349
+ FIXTURE,
350
+ 'packages/inline-dep-invalid-direct/src/index.ts',
351
+ ),
352
+ errors: [ERR_SWITCH_BACK()],
353
+ },
305
354
  ],
306
355
  });