vite-plugin-app-boundaries 0.1.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/README.md ADDED
@@ -0,0 +1,240 @@
1
+ # vite-plugin-app-boundaries
2
+
3
+ Enforce **strict architectural boundaries** between application folders
4
+ in Vite.
5
+
6
+ This plugin prevents one app (e.g. `Front`) from importing code from
7
+ another app (e.g. `Pro`) at **build time and dev time**, not just via
8
+ linting.
9
+
10
+ If a boundary is violated, **Vite errors immediately**.
11
+
12
+ ------------------------------------------------------------------------
13
+
14
+ ## Why this exists
15
+
16
+ Lint rules are easy to bypass. Aliases hide real paths. Large multi-front apps rot over time.
17
+
18
+ This plugin makes architecture **non-negotiable** by enforcing rules
19
+ directly in Vite's module resolution graph.
20
+
21
+ ------------------------------------------------------------------------
22
+
23
+ ## Features
24
+
25
+ - ✅ Enforce boundaries between **any number of apps**
26
+ - ✅ Errors in **dev AND build**
27
+ - ✅ Alias-aware (uses Vite's resolver)
28
+ - ✅ Configurable allowlists
29
+ - ✅ Zero runtime cost
30
+ - ✅ Works with Vite 5 / 6 / 7
31
+
32
+ ------------------------------------------------------------------------
33
+
34
+ ## Example use case
35
+
36
+ ``` txt
37
+ resources/js/Apps/
38
+ ├── Front/
39
+ ├── Pro/
40
+ └── Shared/
41
+ ```
42
+
43
+ Rules:
44
+ - ❌ `Front` cannot import from `Pro`
45
+ - ❌ `Pro` cannot import from `Front`
46
+ - ✅ Both can import from `Shared`
47
+
48
+ ------------------------------------------------------------------------
49
+
50
+ ## Installation
51
+
52
+ ``` bash
53
+ npm install -D vite-plugin-app-boundaries
54
+ ```
55
+
56
+ or
57
+
58
+ ``` bash
59
+ pnpm add -D vite-plugin-app-boundaries
60
+ ```
61
+
62
+ ------------------------------------------------------------------------
63
+
64
+ ## Basic usage
65
+
66
+ ### `vite.config.ts`
67
+
68
+ ``` ts
69
+ import { defineConfig } from "vite";
70
+ import react from "@vitejs/plugin-react";
71
+ import { enforceAppBoundaries } from "vite-plugin-app-boundaries";
72
+
73
+ export default defineConfig({
74
+ plugins: [
75
+ enforceAppBoundaries({
76
+ root: "resources/js/Apps",
77
+
78
+ apps: {
79
+ Front: {
80
+ path: "Front",
81
+ allowImportsFrom: ["Shared"],
82
+ },
83
+
84
+ Pro: {
85
+ path: "Pro",
86
+ allowImportsFrom: ["Shared"],
87
+ },
88
+
89
+ Shared: {
90
+ path: "Shared",
91
+ },
92
+ },
93
+ }),
94
+
95
+ react(),
96
+ ],
97
+ });
98
+ ```
99
+
100
+ ------------------------------------------------------------------------
101
+
102
+ ## What happens on violation
103
+
104
+ ``` ts
105
+ // Apps/Front/pages/welcome.tsx
106
+ import Editor from "@/Apps/Pro/components/editorjs";
107
+ ```
108
+
109
+ ⬇️
110
+
111
+ ``` txt
112
+ 🚫 App boundary violation
113
+
114
+ Importer:
115
+ .../Apps/Front/pages/welcome.tsx
116
+ (app: Front)
117
+
118
+ Imported:
119
+ .../Apps/Pro/components/editorjs.tsx
120
+ (app: Pro)
121
+
122
+ Allowed imports for Front:
123
+ Shared
124
+ ```
125
+
126
+ Vite stops immediately.
127
+
128
+ ------------------------------------------------------------------------
129
+
130
+ ## Configuration reference
131
+
132
+ ### `root`
133
+
134
+ Root directory that contains all apps.
135
+
136
+ ``` ts
137
+ root: "resources/js/Apps"
138
+ ```
139
+
140
+ ------------------------------------------------------------------------
141
+
142
+ ### `apps`
143
+
144
+ Each app has:
145
+
146
+ - `path`: folder name under `root`
147
+ - `allowImportsFrom`: optional list of app names it may import from
148
+
149
+ ``` ts
150
+ apps: {
151
+ Front: {
152
+ path: "Front",
153
+ allowImportsFrom: ["Shared"],
154
+ },
155
+
156
+ Admin: {
157
+ path: "Admin",
158
+ allowImportsFrom: ["Shared", "Pro"],
159
+ },
160
+ }
161
+ ```
162
+
163
+ ------------------------------------------------------------------------
164
+
165
+ ### `debug` (optional)
166
+
167
+ Enable verbose logging to see exactly how Vite resolves imports.
168
+
169
+ ``` ts
170
+ enforceAppBoundaries({
171
+ debug: true,
172
+ ...
173
+ });
174
+ ```
175
+
176
+ Useful when diagnosing aliases or unexpected resolution.
177
+
178
+ ------------------------------------------------------------------------
179
+
180
+ ## ESLint mirror rules (recommended)
181
+
182
+ This plugin enforces boundaries at **build time**.\
183
+ For **editor errors**, mirror the rules using ESLint.
184
+
185
+ ### Install
186
+
187
+ ``` bash
188
+ npm install -D eslint-plugin-boundaries
189
+ ```
190
+
191
+ ### Example `.eslintrc.cjs`
192
+
193
+ ``` js
194
+ module.exports = {
195
+ plugins: ["boundaries"],
196
+
197
+ settings: {
198
+ "boundaries/include": ["resources/js/**/*"],
199
+
200
+ "boundaries/elements": [
201
+ { type: "front", pattern: "resources/js/Apps/Front/**" },
202
+ { type: "pro", pattern: "resources/js/Apps/Pro/**" },
203
+ { type: "shared", pattern: "resources/js/Apps/Shared/**" },
204
+ ],
205
+ },
206
+
207
+ rules: {
208
+ "boundaries/element-types": [
209
+ "error",
210
+ {
211
+ default: "disallow",
212
+ rules: [
213
+ { from: "front", allow: ["shared"] },
214
+ { from: "pro", allow: ["shared"] },
215
+ { from: "shared", allow: ["shared"] },
216
+ ],
217
+ },
218
+ ],
219
+ },
220
+ };
221
+ ```
222
+
223
+ ------------------------------------------------------------------------
224
+
225
+ ## What this plugin does NOT do
226
+
227
+ - ❌ It does not replace ESLint
228
+ - ❌ It does not rewrite imports
229
+ - ❌ It does not enforce runtime isolation
230
+
231
+ Recommended setup:
232
+
233
+ - **This plugin** → build-time enforcement
234
+ - **ESLint boundaries rules** → editor feedback
235
+
236
+ ------------------------------------------------------------------------
237
+
238
+ ## License
239
+
240
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,69 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) {
13
+ __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ }
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+
27
+ //#endregion
28
+ let node_path = require("node:path");
29
+ node_path = __toESM(node_path);
30
+
31
+ //#region src/plugin.ts
32
+ function normalize(p) {
33
+ return p.split(node_path.default.sep).join("/");
34
+ }
35
+ function enforceAppBoundaries(options) {
36
+ const root = normalize(node_path.default.resolve(options.root));
37
+ const apps = Object.entries(options.apps).map(([name, cfg]) => ({
38
+ name,
39
+ root: normalize(node_path.default.join(root, cfg.path)),
40
+ allow: new Set(cfg.allowImportsFrom ?? [])
41
+ }));
42
+ function findApp(file) {
43
+ return apps.find((app) => file.startsWith(app.root));
44
+ }
45
+ return {
46
+ name: "vite-plugin-app-boundaries",
47
+ enforce: "pre",
48
+ async resolveId(source, importer) {
49
+ if (!importer) return null;
50
+ const importerPath = normalize(importer);
51
+ if (!importerPath.startsWith(root)) return null;
52
+ const importerApp = findApp(importerPath);
53
+ if (!importerApp) return null;
54
+ const resolved = await this.resolve(source, importer, { skipSelf: true });
55
+ if (!resolved) return null;
56
+ const resolvedPath = normalize(resolved.id);
57
+ if (!resolvedPath.startsWith(root)) return null;
58
+ const importedApp = findApp(resolvedPath);
59
+ if (!importedApp) return null;
60
+ if (importedApp.name === importerApp.name) return null;
61
+ if (importerApp.allow.has(importedApp.name)) return null;
62
+ throw new Error(`🚫 App boundary violation\n\n${importerPath}\n→ ${resolvedPath}`);
63
+ }
64
+ };
65
+ }
66
+
67
+ //#endregion
68
+ exports.enforceAppBoundaries = enforceAppBoundaries;
69
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["path"],"sources":["../src/plugin.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport type { AppBoundariesOptions } from \"./types\";\n\nfunction normalize(p: string) {\n return p.split(path.sep).join(\"/\");\n}\n\nexport function enforceAppBoundaries(\n options: AppBoundariesOptions\n): Plugin {\n const root = normalize(path.resolve(options.root));\n\n const apps = Object.entries(options.apps).map(\n ([name, cfg]) => ({\n name,\n root: normalize(path.join(root, cfg.path)),\n allow: new Set(cfg.allowImportsFrom ?? []),\n })\n );\n\n function findApp(file: string) {\n return apps.find(app => file.startsWith(app.root));\n }\n\n function log(...args: any[]) {\n if (options.debug) {\n console.log(\"[vite-app-boundaries]\", ...args);\n }\n }\n\n return {\n name: \"vite-plugin-app-boundaries\",\n enforce: \"pre\",\n\n async resolveId(source, importer) {\n if (!importer) return null;\n\n const importerPath = normalize(importer);\n if (!importerPath.startsWith(root)) return null;\n\n const importerApp = findApp(importerPath);\n if (!importerApp) return null;\n\n const resolved = await this.resolve(source, importer, {\n skipSelf: true,\n });\n\n if (!resolved) return null;\n\n const resolvedPath = normalize(resolved.id);\n if (!resolvedPath.startsWith(root)) return null;\n\n const importedApp = findApp(resolvedPath);\n if (!importedApp) return null;\n\n if (importedApp.name === importerApp.name) return null;\n if (importerApp.allow.has(importedApp.name)) return null;\n\n throw new Error(\n `🚫 App boundary violation\\n\\n${importerPath}\\n→ ${resolvedPath}`\n );\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAIA,SAAS,UAAU,GAAW;AAC5B,QAAO,EAAE,MAAMA,kBAAK,IAAI,CAAC,KAAK,IAAI;;AAGpC,SAAgB,qBACd,SACQ;CACR,MAAM,OAAO,UAAUA,kBAAK,QAAQ,QAAQ,KAAK,CAAC;CAElD,MAAM,OAAO,OAAO,QAAQ,QAAQ,KAAK,CAAC,KACvC,CAAC,MAAM,UAAU;EAChB;EACA,MAAM,UAAUA,kBAAK,KAAK,MAAM,IAAI,KAAK,CAAC;EAC1C,OAAO,IAAI,IAAI,IAAI,oBAAoB,EAAE,CAAC;EAC3C,EACF;CAED,SAAS,QAAQ,MAAc;AAC7B,SAAO,KAAK,MAAK,QAAO,KAAK,WAAW,IAAI,KAAK,CAAC;;AASpD,QAAO;EACL,MAAM;EACN,SAAS;EAET,MAAM,UAAU,QAAQ,UAAU;AAChC,OAAI,CAAC,SAAU,QAAO;GAEtB,MAAM,eAAe,UAAU,SAAS;AACxC,OAAI,CAAC,aAAa,WAAW,KAAK,CAAE,QAAO;GAE3C,MAAM,cAAc,QAAQ,aAAa;AACzC,OAAI,CAAC,YAAa,QAAO;GAEzB,MAAM,WAAW,MAAM,KAAK,QAAQ,QAAQ,UAAU,EACpD,UAAU,MACX,CAAC;AAEF,OAAI,CAAC,SAAU,QAAO;GAEtB,MAAM,eAAe,UAAU,SAAS,GAAG;AAC3C,OAAI,CAAC,aAAa,WAAW,KAAK,CAAE,QAAO;GAE3C,MAAM,cAAc,QAAQ,aAAa;AACzC,OAAI,CAAC,YAAa,QAAO;AAEzB,OAAI,YAAY,SAAS,YAAY,KAAM,QAAO;AAClD,OAAI,YAAY,MAAM,IAAI,YAAY,KAAK,CAAE,QAAO;AAEpD,SAAM,IAAI,MACR,gCAAgC,aAAa,MAAM,eACpD;;EAEJ"}
@@ -0,0 +1,18 @@
1
+ import { Plugin } from "vite";
2
+
3
+ //#region src/types.d.ts
4
+ type AppConfig = {
5
+ path: string;
6
+ allowImportsFrom?: string[];
7
+ };
8
+ type AppBoundariesOptions = {
9
+ root: string;
10
+ apps: Record<string, AppConfig>;
11
+ debug?: boolean;
12
+ };
13
+ //#endregion
14
+ //#region src/plugin.d.ts
15
+ declare function enforceAppBoundaries(options: AppBoundariesOptions): Plugin;
16
+ //#endregion
17
+ export { type AppBoundariesOptions, type AppConfig, enforceAppBoundaries };
18
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1,18 @@
1
+ import { Plugin } from "vite";
2
+
3
+ //#region src/types.d.ts
4
+ type AppConfig = {
5
+ path: string;
6
+ allowImportsFrom?: string[];
7
+ };
8
+ type AppBoundariesOptions = {
9
+ root: string;
10
+ apps: Record<string, AppConfig>;
11
+ debug?: boolean;
12
+ };
13
+ //#endregion
14
+ //#region src/plugin.d.ts
15
+ declare function enforceAppBoundaries(options: AppBoundariesOptions): Plugin;
16
+ //#endregion
17
+ export { type AppBoundariesOptions, type AppConfig, enforceAppBoundaries };
18
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,41 @@
1
+ import path from "node:path";
2
+
3
+ //#region src/plugin.ts
4
+ function normalize(p) {
5
+ return p.split(path.sep).join("/");
6
+ }
7
+ function enforceAppBoundaries(options) {
8
+ const root = normalize(path.resolve(options.root));
9
+ const apps = Object.entries(options.apps).map(([name, cfg]) => ({
10
+ name,
11
+ root: normalize(path.join(root, cfg.path)),
12
+ allow: new Set(cfg.allowImportsFrom ?? [])
13
+ }));
14
+ function findApp(file) {
15
+ return apps.find((app) => file.startsWith(app.root));
16
+ }
17
+ return {
18
+ name: "vite-plugin-app-boundaries",
19
+ enforce: "pre",
20
+ async resolveId(source, importer) {
21
+ if (!importer) return null;
22
+ const importerPath = normalize(importer);
23
+ if (!importerPath.startsWith(root)) return null;
24
+ const importerApp = findApp(importerPath);
25
+ if (!importerApp) return null;
26
+ const resolved = await this.resolve(source, importer, { skipSelf: true });
27
+ if (!resolved) return null;
28
+ const resolvedPath = normalize(resolved.id);
29
+ if (!resolvedPath.startsWith(root)) return null;
30
+ const importedApp = findApp(resolvedPath);
31
+ if (!importedApp) return null;
32
+ if (importedApp.name === importerApp.name) return null;
33
+ if (importerApp.allow.has(importedApp.name)) return null;
34
+ throw new Error(`🚫 App boundary violation\n\n${importerPath}\n→ ${resolvedPath}`);
35
+ }
36
+ };
37
+ }
38
+
39
+ //#endregion
40
+ export { enforceAppBoundaries };
41
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport type { AppBoundariesOptions } from \"./types\";\n\nfunction normalize(p: string) {\n return p.split(path.sep).join(\"/\");\n}\n\nexport function enforceAppBoundaries(\n options: AppBoundariesOptions\n): Plugin {\n const root = normalize(path.resolve(options.root));\n\n const apps = Object.entries(options.apps).map(\n ([name, cfg]) => ({\n name,\n root: normalize(path.join(root, cfg.path)),\n allow: new Set(cfg.allowImportsFrom ?? []),\n })\n );\n\n function findApp(file: string) {\n return apps.find(app => file.startsWith(app.root));\n }\n\n function log(...args: any[]) {\n if (options.debug) {\n console.log(\"[vite-app-boundaries]\", ...args);\n }\n }\n\n return {\n name: \"vite-plugin-app-boundaries\",\n enforce: \"pre\",\n\n async resolveId(source, importer) {\n if (!importer) return null;\n\n const importerPath = normalize(importer);\n if (!importerPath.startsWith(root)) return null;\n\n const importerApp = findApp(importerPath);\n if (!importerApp) return null;\n\n const resolved = await this.resolve(source, importer, {\n skipSelf: true,\n });\n\n if (!resolved) return null;\n\n const resolvedPath = normalize(resolved.id);\n if (!resolvedPath.startsWith(root)) return null;\n\n const importedApp = findApp(resolvedPath);\n if (!importedApp) return null;\n\n if (importedApp.name === importerApp.name) return null;\n if (importerApp.allow.has(importedApp.name)) return null;\n\n throw new Error(\n `🚫 App boundary violation\\n\\n${importerPath}\\n→ ${resolvedPath}`\n );\n },\n };\n}\n"],"mappings":";;;AAIA,SAAS,UAAU,GAAW;AAC5B,QAAO,EAAE,MAAM,KAAK,IAAI,CAAC,KAAK,IAAI;;AAGpC,SAAgB,qBACd,SACQ;CACR,MAAM,OAAO,UAAU,KAAK,QAAQ,QAAQ,KAAK,CAAC;CAElD,MAAM,OAAO,OAAO,QAAQ,QAAQ,KAAK,CAAC,KACvC,CAAC,MAAM,UAAU;EAChB;EACA,MAAM,UAAU,KAAK,KAAK,MAAM,IAAI,KAAK,CAAC;EAC1C,OAAO,IAAI,IAAI,IAAI,oBAAoB,EAAE,CAAC;EAC3C,EACF;CAED,SAAS,QAAQ,MAAc;AAC7B,SAAO,KAAK,MAAK,QAAO,KAAK,WAAW,IAAI,KAAK,CAAC;;AASpD,QAAO;EACL,MAAM;EACN,SAAS;EAET,MAAM,UAAU,QAAQ,UAAU;AAChC,OAAI,CAAC,SAAU,QAAO;GAEtB,MAAM,eAAe,UAAU,SAAS;AACxC,OAAI,CAAC,aAAa,WAAW,KAAK,CAAE,QAAO;GAE3C,MAAM,cAAc,QAAQ,aAAa;AACzC,OAAI,CAAC,YAAa,QAAO;GAEzB,MAAM,WAAW,MAAM,KAAK,QAAQ,QAAQ,UAAU,EACpD,UAAU,MACX,CAAC;AAEF,OAAI,CAAC,SAAU,QAAO;GAEtB,MAAM,eAAe,UAAU,SAAS,GAAG;AAC3C,OAAI,CAAC,aAAa,WAAW,KAAK,CAAE,QAAO;GAE3C,MAAM,cAAc,QAAQ,aAAa;AACzC,OAAI,CAAC,YAAa,QAAO;AAEzB,OAAI,YAAY,SAAS,YAAY,KAAM,QAAO;AAClD,OAAI,YAAY,MAAM,IAAI,YAAY,KAAK,CAAE,QAAO;AAEpD,SAAM,IAAI,MACR,gCAAgC,aAAa,MAAM,eACpD;;EAEJ"}
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "vite-plugin-app-boundaries",
3
+ "version": "0.1.0",
4
+ "description": "Enforce architectural boundaries between app folders in Vite",
5
+ "main": "dist/index.cjs",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsdown",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "prepublishOnly": "npm run test && npm run build"
16
+ },
17
+ "peerDependencies": {
18
+ "vite": ">=5"
19
+ },
20
+ "devDependencies": {
21
+ "typescript": "^5.3.0",
22
+ "tsdown": "^0.19.0-beta.1",
23
+ "vitest": "^1.2.0",
24
+ "vite": "^7.0.0"
25
+ },
26
+ "license": "MIT"
27
+ }