Skip to content

Commit e641ebd

Browse files
authored
Merge pull request #8 from joshuajaco/add-scopes
Enable scoped sharing of workspaces
2 parents c606209 + 65622df commit e641ebd

File tree

4 files changed

+205
-15
lines changed

4 files changed

+205
-15
lines changed

docs/rules/no-cross-imports.md

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ This rule takes one argument:
88

99
```
1010
...
11-
"workspaces/no-cross-imports": ["error", { allow: ["@project/A", "@project/B"] }]
11+
"workspaces/no-cross-imports": ["error", { allow: ["@project/A", "@project/B"], scopes: { enable: true, folderName: 'shared' } }]
1212
...
1313
```
1414

1515
### allow
1616

1717
Takes a single or a list of package names to exclude from this rule.
1818

19-
## Example
19+
#### Example
2020

2121
These examples have the following project structure:
2222

@@ -46,3 +46,91 @@ import bar from '../B/bar';
4646
// inside "project/index.js"
4747
import foo from './packages/B/foo';
4848
```
49+
50+
### scopes
51+
52+
Takes either a boolean or an options object. Defaults to `false`.
53+
54+
Scopes are a way to partially allow imports across workspace boundaries.
55+
In larger monorepos, you might run into a situation where you want to group code
56+
across _some_ packages, but not all of them. A natural way to do this would be
57+
to create folder structure that visualizes this. So your structure might look
58+
like this:
59+
60+
```
61+
project
62+
└─── packages
63+
└─── shared-components/
64+
└─── package.json
65+
└─── welcome-page/
66+
└─── package.json
67+
└─── user-management/
68+
└─── registration/
69+
└─── package.json
70+
└─── login/
71+
└─── package.json
72+
```
73+
74+
Now, we may want to share code across the packages in the `user-management`
75+
section (e.g. fetching the user object, user form components etc.). With scopes,
76+
i am always allowed to import from a package with a **special folder name**
77+
(see below) given that it shares a common folder parent. So for
78+
the above case, I would be able to do this:
79+
80+
```
81+
project
82+
└─── packages
83+
└─── shared-components/
84+
└─── package.json
85+
└─── welcome-page/
86+
└─── package.json
87+
└─── user-management/
88+
└─── shared/
89+
└─── package.json
90+
└─── registration/
91+
└─── package.json
92+
└─── login/
93+
└─── package.json
94+
```
95+
96+
When passing a boolean, the default folder name `shared` will be used. If you
97+
want to configure this, pass another string via the `folderName` key.
98+
99+
#### Example
100+
101+
These examples have the following project structure:
102+
103+
```
104+
project
105+
└─── packages
106+
└─── shared-components/
107+
└─── package.json
108+
└─── welcome-page/
109+
└─── package.json
110+
└─── user-management/
111+
└─── shared/
112+
└─── package.json
113+
└─── registration/
114+
└─── package.json
115+
└─── login/
116+
└─── package.json
117+
```
118+
119+
Examples of **incorrect** code for this rule:
120+
121+
```js
122+
// inside "project/packages/welcome-page/index.js"
123+
// configuration: [{ allow: "@project/user-management-shared", scopes: true }]
124+
import foo from '@project/user-management-shared';
125+
```
126+
127+
Examples of **correct** code for this rule:
128+
129+
```js
130+
// inside "project/packages/user-management/registration/index.js"
131+
// configuration: [{ allow: "@project/user-management-shared", scopes: true }]
132+
import foo from '@project/user-management-shared';
133+
134+
// inside "project/index.js"
135+
import foo from './packages/user-management/registration';
136+
```

lib/rules/no-cross-imports.js

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,31 +26,70 @@ module.exports.meta = {
2626
},
2727
],
2828
},
29+
scopes: {
30+
anyOf: [
31+
{ type: 'boolean', additionalProperties: false },
32+
{
33+
type: 'object',
34+
additionalProperties: false,
35+
properties: {
36+
enable: { type: 'boolean' },
37+
folderName: { type: 'string' },
38+
},
39+
},
40+
],
41+
},
2942
},
3043
},
3144
],
3245
};
3346

47+
const filterSharedPackagesInCurrentScope = (
48+
{ location: currentLocation },
49+
scopedEnabled,
50+
scopedSharingFolderName,
51+
) => ({ location }) => {
52+
if (!scopedEnabled) return true;
53+
const locationArray = location.split('/');
54+
const forbiddenPackageParent = locationArray.slice(0, -1).join('/');
55+
if (!isSubPath(forbiddenPackageParent, currentLocation)) {
56+
return true;
57+
}
58+
59+
return locationArray[locationArray.length - 1] !== scopedSharingFolderName;
60+
};
61+
3462
module.exports.create = (context) => {
3563
const {
36-
options: [{ allow = [] } = {}],
64+
options: [{ allow = [], scopes = { enable: false } } = {}],
3765
} = context;
3866

3967
const allowed = typeof allow === 'string' ? [allow] : allow;
68+
const scopedEnabled = scopes === true || !!scopes.enable;
69+
const scopedSharingFolderName = scopes.folderName || 'shared';
70+
4071
const forbidden = packages.filter(({ name }) => !allowed.includes(name));
4172

4273
return getImport(context, ({ node, value, path, currentPackage }) => {
43-
forbidden.forEach(({ name, location }) => {
44-
if (
45-
name !== currentPackage.name &&
46-
(isSubPath(name, value) || isSubPath(location, path))
47-
) {
48-
context.report({
49-
node,
50-
message: 'Import from package "{{name}}" is not allowed',
51-
data: { name },
52-
});
53-
}
54-
});
74+
forbidden
75+
.filter(
76+
filterSharedPackagesInCurrentScope(
77+
currentPackage,
78+
scopedEnabled,
79+
scopedSharingFolderName,
80+
),
81+
)
82+
.forEach(({ name, location }) => {
83+
if (
84+
name !== currentPackage.name &&
85+
(isSubPath(name, value) || isSubPath(location, path))
86+
) {
87+
context.report({
88+
node,
89+
message: 'Import from package "{{name}}" is not allowed',
90+
data: { name },
91+
});
92+
}
93+
});
5594
});
5695
};

tests/rules/no-cross-imports.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ ruleTester.run('no-cross-imports', rule, {
3838
filename: '/some/file.js',
3939
code: "import '@test/workspace';",
4040
},
41+
{
42+
options: [{ scopes: true }],
43+
filename: '/test/scope/workspace/file.js',
44+
code: "import '@test/shared-in-scope';",
45+
},
46+
{
47+
options: [{ scopes: { enable: true } }],
48+
filename: '/test/scope/workspace/file.js',
49+
code: "import '@test/shared-in-scope';",
50+
},
51+
{
52+
options: [{ scopes: { enable: true, folderName: 'shared' } }],
53+
filename: '/test/scope/workspace/file.js',
54+
code: "import '@test/shared-in-scope';",
55+
},
4156
],
4257

4358
invalid: [
@@ -184,5 +199,35 @@ ruleTester.run('no-cross-imports', rule, {
184199
},
185200
],
186201
},
202+
{
203+
options: [{ scopes: true }],
204+
filename: '/test/scope/workspace/file.js',
205+
code: "import '@test/shared-outside-scope';",
206+
errors: [
207+
{
208+
message:
209+
'Import from package "@test/shared-outside-scope" is not allowed',
210+
},
211+
],
212+
},
213+
{
214+
filename: '/test/scope/workspace/file.js',
215+
code: "import '@test/shared-in-scope';",
216+
errors: [
217+
{
218+
message: 'Import from package "@test/shared-in-scope" is not allowed',
219+
},
220+
],
221+
},
222+
{
223+
options: [{ scopes: { enable: true, folderName: 'something-else' } }],
224+
filename: '/test/scope/workspace/file.js',
225+
code: "import '@test/shared-in-scope';",
226+
errors: [
227+
{
228+
message: 'Import from package "@test/shared-in-scope" is not allowed',
229+
},
230+
],
231+
},
187232
],
188233
});

tests/setup.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,22 @@ mock('get-monorepo-packages', () => [
2222
name: '@test/third-workspace',
2323
},
2424
},
25+
{
26+
location: '/test/scope/shared',
27+
package: {
28+
name: '@test/shared-in-scope',
29+
},
30+
},
31+
{
32+
location: '/test/other-scope/shared',
33+
package: {
34+
name: '@test/shared-outside-scope',
35+
},
36+
},
37+
{
38+
location: '/test/scope/workspace',
39+
package: {
40+
name: '@test/scoped-workspace',
41+
},
42+
},
2543
]);

0 commit comments

Comments
 (0)