@cmssy/eslint-plugin 12.12.0 → 12.14.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 CHANGED
@@ -2,10 +2,15 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var tsParser = require('@typescript-eslint/parser');
5
6
  var fs = require('fs');
6
7
  var path = require('path');
7
8
 
8
- // src/edit-route-provider-parity.ts
9
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
+
11
+ var tsParser__default = /*#__PURE__*/_interopDefault(tsParser);
12
+
13
+ // src/index.ts
9
14
  var LAYOUT = /\/layout\.(tsx|jsx|js|ts)$/;
10
15
  var EDIT_SEGMENT = "cmssy-edit";
11
16
  var WRAPPERS = /* @__PURE__ */ new Set([
@@ -91,6 +96,11 @@ var editRouteProviderParity = {
91
96
  };
92
97
  var SERVER_MODULES = [/^@cmssy\/next\/server$/];
93
98
  var SERVER_SYMBOLS = /* @__PURE__ */ new Set(["defineCmssyConfig"]);
99
+ var SERVER_ENV_NAME = "CMSSY_[A-Z0-9_]+";
100
+ var SERVER_ENV = new RegExp(`^${SERVER_ENV_NAME}$`);
101
+ var SERVER_ENV_READ = new RegExp(
102
+ `\\bprocess\\s*\\.\\s*env\\s*(?:\\.\\s*${SERVER_ENV_NAME}|\\[\\s*["'\`]${SERVER_ENV_NAME}["'\`]\\s*\\])`
103
+ );
94
104
  var EXTENSIONS = ["", ".ts", ".tsx", ".js", ".jsx", ".mjs"];
95
105
  var aliasCache = /* @__PURE__ */ new Map();
96
106
  function stripJsonComments(text) {
@@ -218,7 +228,25 @@ function valueImports(code) {
218
228
  ...withoutTypes.matchAll(/(?:from|import)\s*["']([^"']+)["']/g)
219
229
  ].map(([, specifier]) => specifier ?? "");
220
230
  }
231
+ function readsServerEnv(code) {
232
+ return SERVER_ENV_READ.test(code);
233
+ }
234
+ function serverEnvName(node) {
235
+ const member = node;
236
+ const env = member.object;
237
+ if (env?.type !== "MemberExpression" || env.computed) return null;
238
+ if (env.object?.type !== "Identifier" || env.object.name !== "process") {
239
+ return null;
240
+ }
241
+ if (env.property?.type !== "Identifier" || env.property.name !== "env") {
242
+ return null;
243
+ }
244
+ const property = member.property;
245
+ const name = member.computed ? typeof property?.value === "string" ? property.value : null : property?.type === "Identifier" ? property.name ?? null : null;
246
+ return name && SERVER_ENV.test(name) ? name : null;
247
+ }
221
248
  function importsServerConfig(code) {
249
+ if (readsServerEnv(code)) return true;
222
250
  if (SERVER_MODULES.some(
223
251
  (pattern) => valueImports(code).some((s) => pattern.test(s))
224
252
  )) {
@@ -285,7 +313,7 @@ var noServerConfigInClient = {
285
313
  meta: {
286
314
  type: "problem",
287
315
  docs: {
288
- description: "Disallow a client component from importing values that read the cmssy server config"
316
+ description: "Disallow a client component from importing values that read the cmssy server config, or reading a CMSSY_* variable itself"
289
317
  },
290
318
  schema: [
291
319
  {
@@ -300,8 +328,9 @@ var noServerConfigInClient = {
300
328
  }
301
329
  ],
302
330
  messages: {
303
- 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.',
304
- reachesConfigFromEntry: "This module is loaded in the browser, so it pulls the cmssy config there, where server env does not exist.\n {{chain}}\nImport the type instead (types are erased), or load the value with a dynamic import inside the function that needs it."
331
+ reachesConfig: 'This client component pulls the cmssy config into the browser bundle, where server env does not exist - the page fails at runtime with "missing required configuration", or reads whatever the config falls back to, which is usually "".\n {{chain}}\nImport the type instead (types are erased), or move the value into a module that does not touch the config.',
332
+ reachesConfigFromEntry: "This module is loaded in the browser, so it pulls the cmssy config there, where server env does not exist.\n {{chain}}\nImport the type instead (types are erased), or load the value with a dynamic import inside the function that needs it.",
333
+ readsServerEnv: "{{name}} is read here and this module runs in the browser, where it is undefined - the bundler only inlines NEXT_PUBLIC_* names, and a draft secret must never be one.\nRead it in a server component and pass the value down as a prop."
305
334
  }
306
335
  },
307
336
  create(context) {
@@ -313,6 +342,11 @@ var noServerConfigInClient = {
313
342
  if (!client && !entry) return {};
314
343
  const messageId = client ? "reachesConfig" : "reachesConfigFromEntry";
315
344
  return {
345
+ MemberExpression(node) {
346
+ const name = serverEnvName(node);
347
+ if (!name) return;
348
+ context.report({ node, messageId: "readsServerEnv", data: { name } });
349
+ },
316
350
  ImportDeclaration(node) {
317
351
  const typed = node;
318
352
  const onlyTypes = typed.importKind === "type" || node.specifiers.length > 0 && node.specifiers.every(
@@ -363,6 +397,29 @@ plugin.configs.recommended = [
363
397
  }
364
398
  }
365
399
  ];
400
+ plugin.configs.standalone = [
401
+ {
402
+ ignores: [
403
+ "**/.next/**",
404
+ "**/.astro/**",
405
+ "**/.react-router/**",
406
+ "**/.vercel/**",
407
+ "**/build/**",
408
+ "**/coverage/**",
409
+ "**/dist/**",
410
+ "**/out/**"
411
+ ]
412
+ },
413
+ {
414
+ files: ["**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}"],
415
+ languageOptions: {
416
+ parser: tsParser__default.default,
417
+ sourceType: "module",
418
+ parserOptions: { ecmaFeatures: { jsx: true } }
419
+ }
420
+ },
421
+ ...plugin.configs.recommended
422
+ ];
366
423
  var index_default = plugin;
367
424
 
368
425
  exports.default = index_default;
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
+ import tsParser from '@typescript-eslint/parser';
1
2
  import { readFileSync, existsSync } from 'fs';
2
3
  import { isAbsolute, resolve, dirname, sep } from 'path';
3
4
 
4
- // src/edit-route-provider-parity.ts
5
+ // src/index.ts
5
6
  var LAYOUT = /\/layout\.(tsx|jsx|js|ts)$/;
6
7
  var EDIT_SEGMENT = "cmssy-edit";
7
8
  var WRAPPERS = /* @__PURE__ */ new Set([
@@ -87,6 +88,11 @@ var editRouteProviderParity = {
87
88
  };
88
89
  var SERVER_MODULES = [/^@cmssy\/next\/server$/];
89
90
  var SERVER_SYMBOLS = /* @__PURE__ */ new Set(["defineCmssyConfig"]);
91
+ var SERVER_ENV_NAME = "CMSSY_[A-Z0-9_]+";
92
+ var SERVER_ENV = new RegExp(`^${SERVER_ENV_NAME}$`);
93
+ var SERVER_ENV_READ = new RegExp(
94
+ `\\bprocess\\s*\\.\\s*env\\s*(?:\\.\\s*${SERVER_ENV_NAME}|\\[\\s*["'\`]${SERVER_ENV_NAME}["'\`]\\s*\\])`
95
+ );
90
96
  var EXTENSIONS = ["", ".ts", ".tsx", ".js", ".jsx", ".mjs"];
91
97
  var aliasCache = /* @__PURE__ */ new Map();
92
98
  function stripJsonComments(text) {
@@ -214,7 +220,25 @@ function valueImports(code) {
214
220
  ...withoutTypes.matchAll(/(?:from|import)\s*["']([^"']+)["']/g)
215
221
  ].map(([, specifier]) => specifier ?? "");
216
222
  }
223
+ function readsServerEnv(code) {
224
+ return SERVER_ENV_READ.test(code);
225
+ }
226
+ function serverEnvName(node) {
227
+ const member = node;
228
+ const env = member.object;
229
+ if (env?.type !== "MemberExpression" || env.computed) return null;
230
+ if (env.object?.type !== "Identifier" || env.object.name !== "process") {
231
+ return null;
232
+ }
233
+ if (env.property?.type !== "Identifier" || env.property.name !== "env") {
234
+ return null;
235
+ }
236
+ const property = member.property;
237
+ const name = member.computed ? typeof property?.value === "string" ? property.value : null : property?.type === "Identifier" ? property.name ?? null : null;
238
+ return name && SERVER_ENV.test(name) ? name : null;
239
+ }
217
240
  function importsServerConfig(code) {
241
+ if (readsServerEnv(code)) return true;
218
242
  if (SERVER_MODULES.some(
219
243
  (pattern) => valueImports(code).some((s) => pattern.test(s))
220
244
  )) {
@@ -281,7 +305,7 @@ var noServerConfigInClient = {
281
305
  meta: {
282
306
  type: "problem",
283
307
  docs: {
284
- description: "Disallow a client component from importing values that read the cmssy server config"
308
+ description: "Disallow a client component from importing values that read the cmssy server config, or reading a CMSSY_* variable itself"
285
309
  },
286
310
  schema: [
287
311
  {
@@ -296,8 +320,9 @@ var noServerConfigInClient = {
296
320
  }
297
321
  ],
298
322
  messages: {
299
- 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.',
300
- reachesConfigFromEntry: "This module is loaded in the browser, so it pulls the cmssy config there, where server env does not exist.\n {{chain}}\nImport the type instead (types are erased), or load the value with a dynamic import inside the function that needs it."
323
+ reachesConfig: 'This client component pulls the cmssy config into the browser bundle, where server env does not exist - the page fails at runtime with "missing required configuration", or reads whatever the config falls back to, which is usually "".\n {{chain}}\nImport the type instead (types are erased), or move the value into a module that does not touch the config.',
324
+ reachesConfigFromEntry: "This module is loaded in the browser, so it pulls the cmssy config there, where server env does not exist.\n {{chain}}\nImport the type instead (types are erased), or load the value with a dynamic import inside the function that needs it.",
325
+ readsServerEnv: "{{name}} is read here and this module runs in the browser, where it is undefined - the bundler only inlines NEXT_PUBLIC_* names, and a draft secret must never be one.\nRead it in a server component and pass the value down as a prop."
301
326
  }
302
327
  },
303
328
  create(context) {
@@ -309,6 +334,11 @@ var noServerConfigInClient = {
309
334
  if (!client && !entry) return {};
310
335
  const messageId = client ? "reachesConfig" : "reachesConfigFromEntry";
311
336
  return {
337
+ MemberExpression(node) {
338
+ const name = serverEnvName(node);
339
+ if (!name) return;
340
+ context.report({ node, messageId: "readsServerEnv", data: { name } });
341
+ },
312
342
  ImportDeclaration(node) {
313
343
  const typed = node;
314
344
  const onlyTypes = typed.importKind === "type" || node.specifiers.length > 0 && node.specifiers.every(
@@ -359,6 +389,29 @@ plugin.configs.recommended = [
359
389
  }
360
390
  }
361
391
  ];
392
+ plugin.configs.standalone = [
393
+ {
394
+ ignores: [
395
+ "**/.next/**",
396
+ "**/.astro/**",
397
+ "**/.react-router/**",
398
+ "**/.vercel/**",
399
+ "**/build/**",
400
+ "**/coverage/**",
401
+ "**/dist/**",
402
+ "**/out/**"
403
+ ]
404
+ },
405
+ {
406
+ files: ["**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}"],
407
+ languageOptions: {
408
+ parser: tsParser,
409
+ sourceType: "module",
410
+ parserOptions: { ecmaFeatures: { jsx: true } }
411
+ }
412
+ },
413
+ ...plugin.configs.recommended
414
+ ];
362
415
  var index_default = plugin;
363
416
 
364
417
  export { index_default as default, editRouteProviderParity, noServerConfigInClient, rules };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmssy/eslint-plugin",
3
- "version": "12.12.0",
3
+ "version": "12.14.0",
4
4
  "description": "Catches the cmssy wiring mistakes that a build cannot: server config dragged into a client bundle, a provider the editor route never gets.",
5
5
  "keywords": [
6
6
  "cmssy",
@@ -37,9 +37,11 @@
37
37
  "files": [
38
38
  "dist"
39
39
  ],
40
+ "dependencies": {
41
+ "@typescript-eslint/parser": "^8.0.0"
42
+ },
40
43
  "devDependencies": {
41
44
  "@types/node": "^20.0.0",
42
- "@typescript-eslint/parser": "^8.0.0",
43
45
  "eslint": "^9.0.0",
44
46
  "tsup": "^8.3.0",
45
47
  "typescript": "^5.6.0",