@cmssy/eslint-plugin 5.0.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/dist/index.cjs ADDED
@@ -0,0 +1,147 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var fs = require('fs');
6
+ var path = require('path');
7
+
8
+ // src/no-server-config-in-client.ts
9
+ var SERVER_MODULES = [/^@cmssy\/next\/server$/];
10
+ var SERVER_SYMBOLS = /* @__PURE__ */ new Set(["defineCmssyConfig"]);
11
+ var EXTENSIONS = ["", ".ts", ".tsx", ".js", ".jsx", ".mjs"];
12
+ function resolveLocal(fromFile, specifier) {
13
+ if (!specifier.startsWith(".")) return null;
14
+ const base = path.resolve(path.dirname(fromFile), specifier);
15
+ for (const ext of EXTENSIONS) {
16
+ const candidate = `${base}${ext}`;
17
+ if (fs.existsSync(candidate) && !candidate.endsWith("/")) return candidate;
18
+ }
19
+ for (const ext of EXTENSIONS.slice(1)) {
20
+ const candidate = `${base}/index${ext}`;
21
+ if (fs.existsSync(candidate)) return candidate;
22
+ }
23
+ return null;
24
+ }
25
+ function valueImports(code) {
26
+ const withoutTypes = code.replace(
27
+ /^\s*(?:import|export)\s+type\s[^;]*;/gm,
28
+ ""
29
+ );
30
+ return [
31
+ ...withoutTypes.matchAll(/(?:from|import)\s*\(?\s*["']([^"']+)["']/g)
32
+ ].map(([, specifier]) => specifier ?? "");
33
+ }
34
+ function importsServerConfig(code) {
35
+ if (SERVER_MODULES.some(
36
+ (pattern) => valueImports(code).some((s) => pattern.test(s))
37
+ )) {
38
+ return true;
39
+ }
40
+ return [...SERVER_SYMBOLS].some(
41
+ (symbol) => new RegExp(`\\b${symbol}\\b`).test(
42
+ code.replace(/^\s*(?:import|export)\s+type\s[^;]*;/gm, "")
43
+ )
44
+ );
45
+ }
46
+ function chainToServerConfig(file, cache2, seen = /* @__PURE__ */ new Set()) {
47
+ if (seen.has(file)) return null;
48
+ seen.add(file);
49
+ const cached = cache2.get(file);
50
+ if (cached !== void 0) return cached;
51
+ let code;
52
+ try {
53
+ code = fs.readFileSync(file, "utf8");
54
+ } catch {
55
+ cache2.set(file, null);
56
+ return null;
57
+ }
58
+ if (importsServerConfig(code)) {
59
+ const chain = [file];
60
+ cache2.set(file, chain);
61
+ return chain;
62
+ }
63
+ for (const specifier of valueImports(code)) {
64
+ const next = resolveLocal(file, specifier);
65
+ if (!next) continue;
66
+ const deeper = chainToServerConfig(next, cache2, seen);
67
+ if (deeper) {
68
+ const chain = [file, ...deeper];
69
+ cache2.set(file, chain);
70
+ return chain;
71
+ }
72
+ }
73
+ cache2.set(file, null);
74
+ return null;
75
+ }
76
+ function isClientFile(source) {
77
+ return /^\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*["']use client["']/.test(
78
+ source
79
+ );
80
+ }
81
+ var cache = /* @__PURE__ */ new Map();
82
+ var noServerConfigInClient = {
83
+ meta: {
84
+ type: "problem",
85
+ docs: {
86
+ description: "Disallow a client component from importing values that read the cmssy server config"
87
+ },
88
+ schema: [],
89
+ messages: {
90
+ reachesConfig: 'This client component pulls the cmssy config into the browser bundle, where server env does not exist - the page will fail at runtime with "missing required configuration".\n {{chain}}\nImport the type instead (types are erased), or move the value into a module that does not touch the config.'
91
+ }
92
+ },
93
+ create(context) {
94
+ const filename = context.filename ?? context.getFilename();
95
+ if (!isClientFile(context.sourceCode.getText())) return {};
96
+ return {
97
+ ImportDeclaration(node) {
98
+ const typed = node;
99
+ const onlyTypes = typed.importKind === "type" || node.specifiers.length > 0 && node.specifiers.every(
100
+ (s) => s.importKind === "type"
101
+ );
102
+ if (onlyTypes) return;
103
+ const specifier = String(node.source.value);
104
+ if (SERVER_MODULES.some((pattern) => pattern.test(specifier))) {
105
+ context.report({
106
+ node,
107
+ messageId: "reachesConfig",
108
+ data: { chain: `${specifier} is server-only` }
109
+ });
110
+ return;
111
+ }
112
+ const target = resolveLocal(filename, specifier);
113
+ if (!target) return;
114
+ const chain = chainToServerConfig(target, cache);
115
+ if (!chain) return;
116
+ context.report({
117
+ node,
118
+ messageId: "reachesConfig",
119
+ data: {
120
+ chain: [filename, ...chain].map((f) => f.split("/").slice(-2).join("/")).join(" -> ")
121
+ }
122
+ });
123
+ }
124
+ };
125
+ }
126
+ };
127
+
128
+ // src/index.ts
129
+ var rules = {
130
+ "no-server-config-in-client": noServerConfigInClient
131
+ };
132
+ var plugin = {
133
+ meta: { name: "@cmssy/eslint-plugin" },
134
+ rules,
135
+ configs: {}
136
+ };
137
+ plugin.configs.recommended = [
138
+ {
139
+ plugins: { cmssy: plugin },
140
+ rules: { "cmssy/no-server-config-in-client": "error" }
141
+ }
142
+ ];
143
+ var index_default = plugin;
144
+
145
+ exports.default = index_default;
146
+ exports.noServerConfigInClient = noServerConfigInClient;
147
+ exports.rules = rules;
@@ -0,0 +1,19 @@
1
+ import * as eslint from 'eslint';
2
+ import { Rule } from 'eslint';
3
+
4
+ declare const noServerConfigInClient: Rule.RuleModule;
5
+
6
+ declare const rules: {
7
+ "no-server-config-in-client": eslint.Rule.RuleModule;
8
+ };
9
+ declare const plugin: {
10
+ meta: {
11
+ name: string;
12
+ };
13
+ rules: {
14
+ "no-server-config-in-client": eslint.Rule.RuleModule;
15
+ };
16
+ configs: Record<string, unknown>;
17
+ };
18
+
19
+ export { plugin as default, noServerConfigInClient, rules };
@@ -0,0 +1,19 @@
1
+ import * as eslint from 'eslint';
2
+ import { Rule } from 'eslint';
3
+
4
+ declare const noServerConfigInClient: Rule.RuleModule;
5
+
6
+ declare const rules: {
7
+ "no-server-config-in-client": eslint.Rule.RuleModule;
8
+ };
9
+ declare const plugin: {
10
+ meta: {
11
+ name: string;
12
+ };
13
+ rules: {
14
+ "no-server-config-in-client": eslint.Rule.RuleModule;
15
+ };
16
+ configs: Record<string, unknown>;
17
+ };
18
+
19
+ export { plugin as default, noServerConfigInClient, rules };
package/dist/index.js ADDED
@@ -0,0 +1,141 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { resolve, dirname } from 'path';
3
+
4
+ // src/no-server-config-in-client.ts
5
+ var SERVER_MODULES = [/^@cmssy\/next\/server$/];
6
+ var SERVER_SYMBOLS = /* @__PURE__ */ new Set(["defineCmssyConfig"]);
7
+ var EXTENSIONS = ["", ".ts", ".tsx", ".js", ".jsx", ".mjs"];
8
+ function resolveLocal(fromFile, specifier) {
9
+ if (!specifier.startsWith(".")) return null;
10
+ const base = resolve(dirname(fromFile), specifier);
11
+ for (const ext of EXTENSIONS) {
12
+ const candidate = `${base}${ext}`;
13
+ if (existsSync(candidate) && !candidate.endsWith("/")) return candidate;
14
+ }
15
+ for (const ext of EXTENSIONS.slice(1)) {
16
+ const candidate = `${base}/index${ext}`;
17
+ if (existsSync(candidate)) return candidate;
18
+ }
19
+ return null;
20
+ }
21
+ function valueImports(code) {
22
+ const withoutTypes = code.replace(
23
+ /^\s*(?:import|export)\s+type\s[^;]*;/gm,
24
+ ""
25
+ );
26
+ return [
27
+ ...withoutTypes.matchAll(/(?:from|import)\s*\(?\s*["']([^"']+)["']/g)
28
+ ].map(([, specifier]) => specifier ?? "");
29
+ }
30
+ function importsServerConfig(code) {
31
+ if (SERVER_MODULES.some(
32
+ (pattern) => valueImports(code).some((s) => pattern.test(s))
33
+ )) {
34
+ return true;
35
+ }
36
+ return [...SERVER_SYMBOLS].some(
37
+ (symbol) => new RegExp(`\\b${symbol}\\b`).test(
38
+ code.replace(/^\s*(?:import|export)\s+type\s[^;]*;/gm, "")
39
+ )
40
+ );
41
+ }
42
+ function chainToServerConfig(file, cache2, seen = /* @__PURE__ */ new Set()) {
43
+ if (seen.has(file)) return null;
44
+ seen.add(file);
45
+ const cached = cache2.get(file);
46
+ if (cached !== void 0) return cached;
47
+ let code;
48
+ try {
49
+ code = readFileSync(file, "utf8");
50
+ } catch {
51
+ cache2.set(file, null);
52
+ return null;
53
+ }
54
+ if (importsServerConfig(code)) {
55
+ const chain = [file];
56
+ cache2.set(file, chain);
57
+ return chain;
58
+ }
59
+ for (const specifier of valueImports(code)) {
60
+ const next = resolveLocal(file, specifier);
61
+ if (!next) continue;
62
+ const deeper = chainToServerConfig(next, cache2, seen);
63
+ if (deeper) {
64
+ const chain = [file, ...deeper];
65
+ cache2.set(file, chain);
66
+ return chain;
67
+ }
68
+ }
69
+ cache2.set(file, null);
70
+ return null;
71
+ }
72
+ function isClientFile(source) {
73
+ return /^\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*["']use client["']/.test(
74
+ source
75
+ );
76
+ }
77
+ var cache = /* @__PURE__ */ new Map();
78
+ var noServerConfigInClient = {
79
+ meta: {
80
+ type: "problem",
81
+ docs: {
82
+ description: "Disallow a client component from importing values that read the cmssy server config"
83
+ },
84
+ schema: [],
85
+ messages: {
86
+ reachesConfig: 'This client component pulls the cmssy config into the browser bundle, where server env does not exist - the page will fail at runtime with "missing required configuration".\n {{chain}}\nImport the type instead (types are erased), or move the value into a module that does not touch the config.'
87
+ }
88
+ },
89
+ create(context) {
90
+ const filename = context.filename ?? context.getFilename();
91
+ if (!isClientFile(context.sourceCode.getText())) return {};
92
+ return {
93
+ ImportDeclaration(node) {
94
+ const typed = node;
95
+ const onlyTypes = typed.importKind === "type" || node.specifiers.length > 0 && node.specifiers.every(
96
+ (s) => s.importKind === "type"
97
+ );
98
+ if (onlyTypes) return;
99
+ const specifier = String(node.source.value);
100
+ if (SERVER_MODULES.some((pattern) => pattern.test(specifier))) {
101
+ context.report({
102
+ node,
103
+ messageId: "reachesConfig",
104
+ data: { chain: `${specifier} is server-only` }
105
+ });
106
+ return;
107
+ }
108
+ const target = resolveLocal(filename, specifier);
109
+ if (!target) return;
110
+ const chain = chainToServerConfig(target, cache);
111
+ if (!chain) return;
112
+ context.report({
113
+ node,
114
+ messageId: "reachesConfig",
115
+ data: {
116
+ chain: [filename, ...chain].map((f) => f.split("/").slice(-2).join("/")).join(" -> ")
117
+ }
118
+ });
119
+ }
120
+ };
121
+ }
122
+ };
123
+
124
+ // src/index.ts
125
+ var rules = {
126
+ "no-server-config-in-client": noServerConfigInClient
127
+ };
128
+ var plugin = {
129
+ meta: { name: "@cmssy/eslint-plugin" },
130
+ rules,
131
+ configs: {}
132
+ };
133
+ plugin.configs.recommended = [
134
+ {
135
+ plugins: { cmssy: plugin },
136
+ rules: { "cmssy/no-server-config-in-client": "error" }
137
+ }
138
+ ];
139
+ var index_default = plugin;
140
+
141
+ export { index_default as default, noServerConfigInClient, rules };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@cmssy/eslint-plugin",
3
+ "version": "5.0.0",
4
+ "description": "Catches the cmssy wiring mistakes that a build cannot: server config dragged into a client bundle.",
5
+ "keywords": [
6
+ "cmssy",
7
+ "eslint",
8
+ "eslintplugin",
9
+ "next"
10
+ ],
11
+ "homepage": "https://cmssy.com",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/cmssy-io/cmssy-sdk.git",
15
+ "directory": "packages/eslint-plugin"
16
+ },
17
+ "type": "module",
18
+ "license": "MIT",
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js",
26
+ "require": "./dist/index.cjs"
27
+ }
28
+ },
29
+ "main": "./dist/index.cjs",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "devDependencies": {
36
+ "@types/node": "^20.0.0",
37
+ "@typescript-eslint/parser": "^8.0.0",
38
+ "eslint": "^9.0.0",
39
+ "tsup": "^8.3.0",
40
+ "typescript": "^5.6.0",
41
+ "vitest": "^2.1.0"
42
+ },
43
+ "peerDependencies": {
44
+ "eslint": ">=8"
45
+ },
46
+ "scripts": {
47
+ "build": "tsup",
48
+ "test": "vitest run",
49
+ "typecheck": "tsc --noEmit"
50
+ }
51
+ }