@bleedingdev/modern-js-code-tools 3.4.0-ultramodern.9 → 3.5.0-ultramodern.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.
@@ -83,13 +83,13 @@ const resolvePluginPath = ()=>{
83
83
  const root = findPackageRoot();
84
84
  const sourcePluginPath = external_node_path_default().join(root, 'src/oxlint-plugin.ts');
85
85
  const pluginPath = resolveExistingPath([
86
+ sourcePluginPath,
86
87
  external_node_path_default().join(root, 'dist/esm-node/oxlint-plugin.mjs'),
87
88
  external_node_path_default().join(root, 'dist/esm-node/oxlint-plugin.js'),
88
89
  external_node_path_default().join(root, 'dist/esm/oxlint-plugin.mjs'),
89
90
  external_node_path_default().join(root, 'dist/esm/oxlint-plugin.js'),
90
91
  external_node_path_default().join(root, 'dist/cjs/oxlint-plugin.js'),
91
- external_node_path_default().join(root, 'dist/cjs/oxlint-plugin.cjs'),
92
- sourcePluginPath
92
+ external_node_path_default().join(root, 'dist/cjs/oxlint-plugin.cjs')
93
93
  ]);
94
94
  if (!pluginPath) throw new Error('Unable to resolve @modern-js/code-tools Oxlint plugin.');
95
95
  return pluginPath;
@@ -105,7 +105,7 @@ const existingTargets = (cwd, targets)=>targets.map((target)=>external_node_path
105
105
  const containsLintableSource = (filePath)=>{
106
106
  if (!external_node_fs_default().existsSync(filePath)) return false;
107
107
  const stats = external_node_fs_default().statSync(filePath);
108
- if (stats.isFile()) return /\.(?:js|jsx|ts|tsx)$/u.test(filePath) && !filePath.endsWith('.d.ts');
108
+ if (stats.isFile()) return /\.(?:[cm]?[jt]sx?)$/u.test(filePath) && !filePath.endsWith('.d.ts');
109
109
  if (!stats.isDirectory()) return false;
110
110
  for (const entry of external_node_fs_default().readdirSync(filePath, {
111
111
  withFileTypes: true
@@ -76,7 +76,7 @@ const walk = (directory, files = [])=>{
76
76
  }
77
77
  return files;
78
78
  };
79
- const isSourceFile = (filePath)=>/\.(?:ts|tsx|js|jsx)$/u.test(filePath);
79
+ const isSourceFile = (filePath)=>/\.(?:[cm]?[jt]sx?)$/u.test(filePath);
80
80
  const createLocaleJsonMatcher = (locales)=>{
81
81
  const pattern = new RegExp(`/locales/(?:${locales.map(escapeRegExp).join('|')})/[^/]+\\.json$`, 'u');
82
82
  return (root, filePath)=>pattern.test(`/${relativePath(root, filePath)}`);
@@ -143,7 +143,8 @@ const runRuntimeAndLocaleResourceChecks = (root, sourceRoots, locales, pluralCat
143
143
  };
144
144
  const runWorkspaceSourceCheck = ({ cwd = process.cwd(), sourceRoots = [
145
145
  'apps',
146
- 'verticals'
146
+ 'verticals',
147
+ 'packages'
147
148
  ], locales = DEFAULT_LOCALES, pluralCategories } = {})=>{
148
149
  const oxlintResult = (0, external_oxlint_cjs_namespaceObject.runOxlintRules)({
149
150
  cwd,
@@ -166,7 +167,8 @@ const runWorkspaceSourceCheck = ({ cwd = process.cwd(), sourceRoots = [
166
167
  }
167
168
  ],
168
169
  'ultramodern/no-manual-locale-copy-branching': 'error',
169
- 'ultramodern/no-split-translation-keys': 'error'
170
+ 'ultramodern/no-split-translation-keys': 'error',
171
+ 'ultramodern/strict-effect-api-boundaries': 'error'
170
172
  }
171
173
  });
172
174
  if (0 !== oxlintResult.exitCode) {
@@ -87,6 +87,10 @@ const DEFAULT_TRANSLATION_FUNCTIONS = [
87
87
  const DEFAULT_IGNORE_COMMENT_PATTERN = 'i18n-ignore';
88
88
  const LETTER_PATTERN = /\p{L}/u;
89
89
  const SPLIT_TRANSLATION_KEY_PATTERN = /\.(?:prefix|suffix|before|after)$/u;
90
+ const API_SOURCE_FILE_PATTERN = /(?:^|\/)(?:api\/|shared\/api\.[cm]?[jt]sx?$)/u;
91
+ const FORBIDDEN_GENERATED_EFFECT_PATH_PATTERN = /(?:^|\/)(?:apps\/shell-super-app\/src\/effect|verticals\/[^/]+\/(?:api\/(?:effect|lambda)|shared\/effect|src\/effect))(?:\/|$)/u;
92
+ const VERTICAL_API_ENTRY_PATTERN = /(?:^|\/)verticals\/[^/]+\/api\/index\.[cm]?[jt]sx?$/u;
93
+ const SHARED_API_CONTRACT_PATTERN = /(?:^|\/)verticals\/[^/]+\/shared\/api\.[cm]?[jt]sx?$/u;
90
94
  const isRecord = (value)=>Boolean(value) && 'object' == typeof value && !Array.isArray(value);
91
95
  const asStringArray = (value, fallback)=>Array.isArray(value) && value.every((item)=>'string' == typeof item) ? value : fallback;
92
96
  const getCommonRuleOption = (context, defaults)=>{
@@ -237,7 +241,12 @@ const createNoLiteralVisibleJsxAttributesRule = ()=>({
237
241
  };
238
242
  }
239
243
  });
240
- const getSourceText = (context, node)=>context.getSourceCode?.().getText?.(node) ?? '';
244
+ const getSourceText = (context, node)=>context.getSourceCode?.().getText?.(node) ?? context.getSourceCode?.().text ?? '';
245
+ const normalizeFilename = (filePath)=>(filePath ?? '').replaceAll('\\', '/');
246
+ const isApiSourceFile = (filePath)=>API_SOURCE_FILE_PATTERN.test(normalizeFilename(filePath));
247
+ const isForbiddenGeneratedEffectPath = (filePath)=>FORBIDDEN_GENERATED_EFFECT_PATH_PATTERN.test(normalizeFilename(filePath));
248
+ const isVerticalApiEntryFile = (filePath)=>VERTICAL_API_ENTRY_PATTERN.test(normalizeFilename(filePath));
249
+ const isSharedApiContractFile = (filePath)=>SHARED_API_CONTRACT_PATTERN.test(normalizeFilename(filePath));
241
250
  const looksLikeLocaleTest = (context, node)=>{
242
251
  const text = getSourceText(context, node);
243
252
  return /\b(?:language|locale|lng|currentLanguage)\b/u.test(text) && /(?:={2,3}|!==?)/u.test(text) && /['"][a-z]{2}(?:-[A-Za-z0-9]+)?['"]/u.test(text);
@@ -329,6 +338,61 @@ const createNoLegacyMfBoundaryAttributesRule = ()=>({
329
338
  };
330
339
  }
331
340
  });
341
+ const reportProgramPattern = (context, node, source, pattern, message)=>{
342
+ if (pattern.test(source)) context.report({
343
+ node,
344
+ message
345
+ });
346
+ };
347
+ const reportMissingProgramPattern = (context, node, source, pattern, message)=>{
348
+ if (!pattern.test(source)) context.report({
349
+ node,
350
+ message
351
+ });
352
+ };
353
+ const createStrictEffectApiBoundariesRule = ()=>({
354
+ meta: {
355
+ type: 'problem',
356
+ docs: {
357
+ description: 'Disallow raw HTTP handlers and non-Effect API runtime drift in UltraModern workspaces.'
358
+ },
359
+ schema: []
360
+ },
361
+ create (context) {
362
+ return {
363
+ Program (node) {
364
+ const source = getSourceText(context, node);
365
+ const filename = normalizeFilename(context.filename);
366
+ const apiFile = isApiSourceFile(filename);
367
+ if (isForbiddenGeneratedEffectPath(filename)) context.report({
368
+ node,
369
+ message: 'Generated UltraModern workspaces use direct api/index.ts, shared/api.ts and src/api/* paths; api/effect, api/lambda, shared/effect and src/effect are forbidden.'
370
+ });
371
+ reportProgramPattern(context, node, source, /@modern-js\/plugin-bff\/hono-server/u, 'UltraModern API workspaces must not import Hono server helpers; use @modern-js/plugin-bff/effect-edge and HttpApi.');
372
+ reportProgramPattern(context, node, source, /\bruntimeFramework\s*(?::|=)\s*['"]hono['"]/u, 'UltraModern API apps must use bff.runtimeFramework: "effect".');
373
+ reportProgramPattern(context, node, source, /(?:from|import)\s*['"][^'"]*(?:api\/effect|shared\/effect)[^'"]*['"]|shared-effect-api/u, 'Import API code from direct api/index.ts, shared/api.ts or src/api/* paths, not api/effect, shared/effect or shared Effect API packages.');
374
+ reportProgramPattern(context, node, source, /\bstrictEffectApproach\s*(?::|=)\s*false\b/u, 'UltraModern API apps must keep strictEffectApproach enabled.');
375
+ if (isVerticalApiEntryFile(filename)) {
376
+ reportMissingProgramPattern(context, node, source, /\bdefineEffectBff\b/u, 'Generated API entries must export defineEffectBff(...).');
377
+ reportMissingProgramPattern(context, node, source, /\bHttpApiBuilder\b/u, 'Generated API entries must implement handlers through HttpApiBuilder.');
378
+ reportMissingProgramPattern(context, node, source, /\bLayer\b/u, 'Generated API entries must compose dependencies with Effect Layer.');
379
+ reportMissingProgramPattern(context, node, source, /from\s+['"]\.\.\/shared\/api\.ts['"]/u, 'Generated API entries must import the contract from ../shared/api.ts.');
380
+ }
381
+ if (isSharedApiContractFile(filename)) {
382
+ reportMissingProgramPattern(context, node, source, /\bHttpApi\.make\b/u, 'Generated shared API contracts must declare an HttpApi contract.');
383
+ reportMissingProgramPattern(context, node, source, /\bHttpApiGroup\.make\b/u, 'Generated shared API contracts must declare HttpApi groups.');
384
+ reportMissingProgramPattern(context, node, source, /\bHttpApiEndpoint\./u, 'Generated shared API contracts must declare endpoints through HttpApiEndpoint.');
385
+ reportMissingProgramPattern(context, node, source, /\bSchema\./u, 'Generated shared API contracts must model request, response and error shapes with Schema.');
386
+ }
387
+ if (!apiFile) return;
388
+ reportProgramPattern(context, node, source, /\bnew\s+Response\s*\(|\bResponse\.json\s*\(/u, 'API modules must not hand-build Response objects; model endpoints through Effect HttpApi and schemas.');
389
+ reportProgramPattern(context, node, source, /\b(?:request|req)\.(?:json|text|formData|arrayBuffer)\s*\(/u, 'API modules must not manually parse request bodies; use HttpApiEndpoint payload/query/params schemas.');
390
+ reportProgramPattern(context, node, source, /\bexport\s+const\s+handler\b|\bexport\s+default\s+async\b/u, 'API modules must not export raw request handlers; export defineEffectBff(...) from api/index.ts.');
391
+ reportProgramPattern(context, node, source, /\bcreateHandler\s*[:=]\s*(?!defineEffectBff\b)/u, 'API modules must not define unbranded handler factories; use defineEffectBff(...).');
392
+ }
393
+ };
394
+ }
395
+ });
332
396
  const plugin = {
333
397
  meta: {
334
398
  name: 'ultramodern'
@@ -338,7 +402,8 @@ const plugin = {
338
402
  'no-legacy-mf-boundary-attributes': createNoLegacyMfBoundaryAttributesRule(),
339
403
  'no-literal-visible-jsx-attributes': createNoLiteralVisibleJsxAttributesRule(),
340
404
  'no-manual-locale-copy-branching': createNoManualLocaleCopyBranchingRule(),
341
- 'no-split-translation-keys': createNoSplitTranslationKeysRule()
405
+ 'no-split-translation-keys': createNoSplitTranslationKeysRule(),
406
+ 'strict-effect-api-boundaries': createStrictEffectApiBoundariesRule()
342
407
  }
343
408
  };
344
409
  const __rspack_default_export = plugin;
@@ -35,13 +35,13 @@ const resolvePluginPath = ()=>{
35
35
  const root = findPackageRoot();
36
36
  const sourcePluginPath = node_path.join(root, 'src/oxlint-plugin.ts');
37
37
  const pluginPath = resolveExistingPath([
38
+ sourcePluginPath,
38
39
  node_path.join(root, 'dist/esm-node/oxlint-plugin.mjs'),
39
40
  node_path.join(root, 'dist/esm-node/oxlint-plugin.js'),
40
41
  node_path.join(root, 'dist/esm/oxlint-plugin.mjs'),
41
42
  node_path.join(root, 'dist/esm/oxlint-plugin.js'),
42
43
  node_path.join(root, 'dist/cjs/oxlint-plugin.js'),
43
- node_path.join(root, 'dist/cjs/oxlint-plugin.cjs'),
44
- sourcePluginPath
44
+ node_path.join(root, 'dist/cjs/oxlint-plugin.cjs')
45
45
  ]);
46
46
  if (!pluginPath) throw new Error('Unable to resolve @modern-js/code-tools Oxlint plugin.');
47
47
  return pluginPath;
@@ -57,7 +57,7 @@ const existingTargets = (cwd, targets)=>targets.map((target)=>node_path.resolve(
57
57
  const containsLintableSource = (filePath)=>{
58
58
  if (!node_fs.existsSync(filePath)) return false;
59
59
  const stats = node_fs.statSync(filePath);
60
- if (stats.isFile()) return /\.(?:js|jsx|ts|tsx)$/u.test(filePath) && !filePath.endsWith('.d.ts');
60
+ if (stats.isFile()) return /\.(?:[cm]?[jt]sx?)$/u.test(filePath) && !filePath.endsWith('.d.ts');
61
61
  if (!stats.isDirectory()) return false;
62
62
  for (const entry of node_fs.readdirSync(filePath, {
63
63
  withFileTypes: true
@@ -31,7 +31,7 @@ const walk = (directory, files = [])=>{
31
31
  }
32
32
  return files;
33
33
  };
34
- const isSourceFile = (filePath)=>/\.(?:ts|tsx|js|jsx)$/u.test(filePath);
34
+ const isSourceFile = (filePath)=>/\.(?:[cm]?[jt]sx?)$/u.test(filePath);
35
35
  const createLocaleJsonMatcher = (locales)=>{
36
36
  const pattern = new RegExp(`/locales/(?:${locales.map(escapeRegExp).join('|')})/[^/]+\\.json$`, 'u');
37
37
  return (root, filePath)=>pattern.test(`/${relativePath(root, filePath)}`);
@@ -98,7 +98,8 @@ const runRuntimeAndLocaleResourceChecks = (root, sourceRoots, locales, pluralCat
98
98
  };
99
99
  const runWorkspaceSourceCheck = ({ cwd = process.cwd(), sourceRoots = [
100
100
  'apps',
101
- 'verticals'
101
+ 'verticals',
102
+ 'packages'
102
103
  ], locales = DEFAULT_LOCALES, pluralCategories } = {})=>{
103
104
  const oxlintResult = runOxlintRules({
104
105
  cwd,
@@ -121,7 +122,8 @@ const runWorkspaceSourceCheck = ({ cwd = process.cwd(), sourceRoots = [
121
122
  }
122
123
  ],
123
124
  'ultramodern/no-manual-locale-copy-branching': 'error',
124
- 'ultramodern/no-split-translation-keys': 'error'
125
+ 'ultramodern/no-split-translation-keys': 'error',
126
+ 'ultramodern/strict-effect-api-boundaries': 'error'
125
127
  }
126
128
  });
127
129
  if (0 !== oxlintResult.exitCode) {
@@ -58,6 +58,10 @@ const DEFAULT_TRANSLATION_FUNCTIONS = [
58
58
  const DEFAULT_IGNORE_COMMENT_PATTERN = 'i18n-ignore';
59
59
  const LETTER_PATTERN = /\p{L}/u;
60
60
  const SPLIT_TRANSLATION_KEY_PATTERN = /\.(?:prefix|suffix|before|after)$/u;
61
+ const API_SOURCE_FILE_PATTERN = /(?:^|\/)(?:api\/|shared\/api\.[cm]?[jt]sx?$)/u;
62
+ const FORBIDDEN_GENERATED_EFFECT_PATH_PATTERN = /(?:^|\/)(?:apps\/shell-super-app\/src\/effect|verticals\/[^/]+\/(?:api\/(?:effect|lambda)|shared\/effect|src\/effect))(?:\/|$)/u;
63
+ const VERTICAL_API_ENTRY_PATTERN = /(?:^|\/)verticals\/[^/]+\/api\/index\.[cm]?[jt]sx?$/u;
64
+ const SHARED_API_CONTRACT_PATTERN = /(?:^|\/)verticals\/[^/]+\/shared\/api\.[cm]?[jt]sx?$/u;
61
65
  const isRecord = (value)=>Boolean(value) && 'object' == typeof value && !Array.isArray(value);
62
66
  const asStringArray = (value, fallback)=>Array.isArray(value) && value.every((item)=>'string' == typeof item) ? value : fallback;
63
67
  const getCommonRuleOption = (context, defaults)=>{
@@ -208,7 +212,12 @@ const createNoLiteralVisibleJsxAttributesRule = ()=>({
208
212
  };
209
213
  }
210
214
  });
211
- const getSourceText = (context, node)=>context.getSourceCode?.().getText?.(node) ?? '';
215
+ const getSourceText = (context, node)=>context.getSourceCode?.().getText?.(node) ?? context.getSourceCode?.().text ?? '';
216
+ const normalizeFilename = (filePath)=>(filePath ?? '').replaceAll('\\', '/');
217
+ const isApiSourceFile = (filePath)=>API_SOURCE_FILE_PATTERN.test(normalizeFilename(filePath));
218
+ const isForbiddenGeneratedEffectPath = (filePath)=>FORBIDDEN_GENERATED_EFFECT_PATH_PATTERN.test(normalizeFilename(filePath));
219
+ const isVerticalApiEntryFile = (filePath)=>VERTICAL_API_ENTRY_PATTERN.test(normalizeFilename(filePath));
220
+ const isSharedApiContractFile = (filePath)=>SHARED_API_CONTRACT_PATTERN.test(normalizeFilename(filePath));
212
221
  const looksLikeLocaleTest = (context, node)=>{
213
222
  const text = getSourceText(context, node);
214
223
  return /\b(?:language|locale|lng|currentLanguage)\b/u.test(text) && /(?:={2,3}|!==?)/u.test(text) && /['"][a-z]{2}(?:-[A-Za-z0-9]+)?['"]/u.test(text);
@@ -300,6 +309,61 @@ const createNoLegacyMfBoundaryAttributesRule = ()=>({
300
309
  };
301
310
  }
302
311
  });
312
+ const reportProgramPattern = (context, node, source, pattern, message)=>{
313
+ if (pattern.test(source)) context.report({
314
+ node,
315
+ message
316
+ });
317
+ };
318
+ const reportMissingProgramPattern = (context, node, source, pattern, message)=>{
319
+ if (!pattern.test(source)) context.report({
320
+ node,
321
+ message
322
+ });
323
+ };
324
+ const createStrictEffectApiBoundariesRule = ()=>({
325
+ meta: {
326
+ type: 'problem',
327
+ docs: {
328
+ description: 'Disallow raw HTTP handlers and non-Effect API runtime drift in UltraModern workspaces.'
329
+ },
330
+ schema: []
331
+ },
332
+ create (context) {
333
+ return {
334
+ Program (node) {
335
+ const source = getSourceText(context, node);
336
+ const filename = normalizeFilename(context.filename);
337
+ const apiFile = isApiSourceFile(filename);
338
+ if (isForbiddenGeneratedEffectPath(filename)) context.report({
339
+ node,
340
+ message: 'Generated UltraModern workspaces use direct api/index.ts, shared/api.ts and src/api/* paths; api/effect, api/lambda, shared/effect and src/effect are forbidden.'
341
+ });
342
+ reportProgramPattern(context, node, source, /@modern-js\/plugin-bff\/hono-server/u, 'UltraModern API workspaces must not import Hono server helpers; use @modern-js/plugin-bff/effect-edge and HttpApi.');
343
+ reportProgramPattern(context, node, source, /\bruntimeFramework\s*(?::|=)\s*['"]hono['"]/u, 'UltraModern API apps must use bff.runtimeFramework: "effect".');
344
+ reportProgramPattern(context, node, source, /(?:from|import)\s*['"][^'"]*(?:api\/effect|shared\/effect)[^'"]*['"]|shared-effect-api/u, 'Import API code from direct api/index.ts, shared/api.ts or src/api/* paths, not api/effect, shared/effect or shared Effect API packages.');
345
+ reportProgramPattern(context, node, source, /\bstrictEffectApproach\s*(?::|=)\s*false\b/u, 'UltraModern API apps must keep strictEffectApproach enabled.');
346
+ if (isVerticalApiEntryFile(filename)) {
347
+ reportMissingProgramPattern(context, node, source, /\bdefineEffectBff\b/u, 'Generated API entries must export defineEffectBff(...).');
348
+ reportMissingProgramPattern(context, node, source, /\bHttpApiBuilder\b/u, 'Generated API entries must implement handlers through HttpApiBuilder.');
349
+ reportMissingProgramPattern(context, node, source, /\bLayer\b/u, 'Generated API entries must compose dependencies with Effect Layer.');
350
+ reportMissingProgramPattern(context, node, source, /from\s+['"]\.\.\/shared\/api\.ts['"]/u, 'Generated API entries must import the contract from ../shared/api.ts.');
351
+ }
352
+ if (isSharedApiContractFile(filename)) {
353
+ reportMissingProgramPattern(context, node, source, /\bHttpApi\.make\b/u, 'Generated shared API contracts must declare an HttpApi contract.');
354
+ reportMissingProgramPattern(context, node, source, /\bHttpApiGroup\.make\b/u, 'Generated shared API contracts must declare HttpApi groups.');
355
+ reportMissingProgramPattern(context, node, source, /\bHttpApiEndpoint\./u, 'Generated shared API contracts must declare endpoints through HttpApiEndpoint.');
356
+ reportMissingProgramPattern(context, node, source, /\bSchema\./u, 'Generated shared API contracts must model request, response and error shapes with Schema.');
357
+ }
358
+ if (!apiFile) return;
359
+ reportProgramPattern(context, node, source, /\bnew\s+Response\s*\(|\bResponse\.json\s*\(/u, 'API modules must not hand-build Response objects; model endpoints through Effect HttpApi and schemas.');
360
+ reportProgramPattern(context, node, source, /\b(?:request|req)\.(?:json|text|formData|arrayBuffer)\s*\(/u, 'API modules must not manually parse request bodies; use HttpApiEndpoint payload/query/params schemas.');
361
+ reportProgramPattern(context, node, source, /\bexport\s+const\s+handler\b|\bexport\s+default\s+async\b/u, 'API modules must not export raw request handlers; export defineEffectBff(...) from api/index.ts.');
362
+ reportProgramPattern(context, node, source, /\bcreateHandler\s*[:=]\s*(?!defineEffectBff\b)/u, 'API modules must not define unbranded handler factories; use defineEffectBff(...).');
363
+ }
364
+ };
365
+ }
366
+ });
303
367
  const oxlint_plugin_plugin = {
304
368
  meta: {
305
369
  name: 'ultramodern'
@@ -309,7 +373,8 @@ const oxlint_plugin_plugin = {
309
373
  'no-legacy-mf-boundary-attributes': createNoLegacyMfBoundaryAttributesRule(),
310
374
  'no-literal-visible-jsx-attributes': createNoLiteralVisibleJsxAttributesRule(),
311
375
  'no-manual-locale-copy-branching': createNoManualLocaleCopyBranchingRule(),
312
- 'no-split-translation-keys': createNoSplitTranslationKeysRule()
376
+ 'no-split-translation-keys': createNoSplitTranslationKeysRule(),
377
+ 'strict-effect-api-boundaries': createStrictEffectApiBoundariesRule()
313
378
  }
314
379
  };
315
380
  const oxlint_plugin = oxlint_plugin_plugin;
@@ -36,13 +36,13 @@ const resolvePluginPath = ()=>{
36
36
  const root = findPackageRoot();
37
37
  const sourcePluginPath = node_path.join(root, 'src/oxlint-plugin.ts');
38
38
  const pluginPath = resolveExistingPath([
39
+ sourcePluginPath,
39
40
  node_path.join(root, 'dist/esm-node/oxlint-plugin.mjs'),
40
41
  node_path.join(root, 'dist/esm-node/oxlint-plugin.js'),
41
42
  node_path.join(root, 'dist/esm/oxlint-plugin.mjs'),
42
43
  node_path.join(root, 'dist/esm/oxlint-plugin.js'),
43
44
  node_path.join(root, 'dist/cjs/oxlint-plugin.js'),
44
- node_path.join(root, 'dist/cjs/oxlint-plugin.cjs'),
45
- sourcePluginPath
45
+ node_path.join(root, 'dist/cjs/oxlint-plugin.cjs')
46
46
  ]);
47
47
  if (!pluginPath) throw new Error('Unable to resolve @modern-js/code-tools Oxlint plugin.');
48
48
  return pluginPath;
@@ -58,7 +58,7 @@ const existingTargets = (cwd, targets)=>targets.map((target)=>node_path.resolve(
58
58
  const containsLintableSource = (filePath)=>{
59
59
  if (!node_fs.existsSync(filePath)) return false;
60
60
  const stats = node_fs.statSync(filePath);
61
- if (stats.isFile()) return /\.(?:js|jsx|ts|tsx)$/u.test(filePath) && !filePath.endsWith('.d.ts');
61
+ if (stats.isFile()) return /\.(?:[cm]?[jt]sx?)$/u.test(filePath) && !filePath.endsWith('.d.ts');
62
62
  if (!stats.isDirectory()) return false;
63
63
  for (const entry of node_fs.readdirSync(filePath, {
64
64
  withFileTypes: true
@@ -32,7 +32,7 @@ const walk = (directory, files = [])=>{
32
32
  }
33
33
  return files;
34
34
  };
35
- const isSourceFile = (filePath)=>/\.(?:ts|tsx|js|jsx)$/u.test(filePath);
35
+ const isSourceFile = (filePath)=>/\.(?:[cm]?[jt]sx?)$/u.test(filePath);
36
36
  const createLocaleJsonMatcher = (locales)=>{
37
37
  const pattern = new RegExp(`/locales/(?:${locales.map(escapeRegExp).join('|')})/[^/]+\\.json$`, 'u');
38
38
  return (root, filePath)=>pattern.test(`/${relativePath(root, filePath)}`);
@@ -99,7 +99,8 @@ const runRuntimeAndLocaleResourceChecks = (root, sourceRoots, locales, pluralCat
99
99
  };
100
100
  const runWorkspaceSourceCheck = ({ cwd = process.cwd(), sourceRoots = [
101
101
  'apps',
102
- 'verticals'
102
+ 'verticals',
103
+ 'packages'
103
104
  ], locales = DEFAULT_LOCALES, pluralCategories } = {})=>{
104
105
  const oxlintResult = runOxlintRules({
105
106
  cwd,
@@ -122,7 +123,8 @@ const runWorkspaceSourceCheck = ({ cwd = process.cwd(), sourceRoots = [
122
123
  }
123
124
  ],
124
125
  'ultramodern/no-manual-locale-copy-branching': 'error',
125
- 'ultramodern/no-split-translation-keys': 'error'
126
+ 'ultramodern/no-split-translation-keys': 'error',
127
+ 'ultramodern/strict-effect-api-boundaries': 'error'
126
128
  }
127
129
  });
128
130
  if (0 !== oxlintResult.exitCode) {
@@ -59,6 +59,10 @@ const DEFAULT_TRANSLATION_FUNCTIONS = [
59
59
  const DEFAULT_IGNORE_COMMENT_PATTERN = 'i18n-ignore';
60
60
  const LETTER_PATTERN = /\p{L}/u;
61
61
  const SPLIT_TRANSLATION_KEY_PATTERN = /\.(?:prefix|suffix|before|after)$/u;
62
+ const API_SOURCE_FILE_PATTERN = /(?:^|\/)(?:api\/|shared\/api\.[cm]?[jt]sx?$)/u;
63
+ const FORBIDDEN_GENERATED_EFFECT_PATH_PATTERN = /(?:^|\/)(?:apps\/shell-super-app\/src\/effect|verticals\/[^/]+\/(?:api\/(?:effect|lambda)|shared\/effect|src\/effect))(?:\/|$)/u;
64
+ const VERTICAL_API_ENTRY_PATTERN = /(?:^|\/)verticals\/[^/]+\/api\/index\.[cm]?[jt]sx?$/u;
65
+ const SHARED_API_CONTRACT_PATTERN = /(?:^|\/)verticals\/[^/]+\/shared\/api\.[cm]?[jt]sx?$/u;
62
66
  const isRecord = (value)=>Boolean(value) && 'object' == typeof value && !Array.isArray(value);
63
67
  const asStringArray = (value, fallback)=>Array.isArray(value) && value.every((item)=>'string' == typeof item) ? value : fallback;
64
68
  const getCommonRuleOption = (context, defaults)=>{
@@ -209,7 +213,12 @@ const createNoLiteralVisibleJsxAttributesRule = ()=>({
209
213
  };
210
214
  }
211
215
  });
212
- const getSourceText = (context, node)=>context.getSourceCode?.().getText?.(node) ?? '';
216
+ const getSourceText = (context, node)=>context.getSourceCode?.().getText?.(node) ?? context.getSourceCode?.().text ?? '';
217
+ const normalizeFilename = (filePath)=>(filePath ?? '').replaceAll('\\', '/');
218
+ const isApiSourceFile = (filePath)=>API_SOURCE_FILE_PATTERN.test(normalizeFilename(filePath));
219
+ const isForbiddenGeneratedEffectPath = (filePath)=>FORBIDDEN_GENERATED_EFFECT_PATH_PATTERN.test(normalizeFilename(filePath));
220
+ const isVerticalApiEntryFile = (filePath)=>VERTICAL_API_ENTRY_PATTERN.test(normalizeFilename(filePath));
221
+ const isSharedApiContractFile = (filePath)=>SHARED_API_CONTRACT_PATTERN.test(normalizeFilename(filePath));
213
222
  const looksLikeLocaleTest = (context, node)=>{
214
223
  const text = getSourceText(context, node);
215
224
  return /\b(?:language|locale|lng|currentLanguage)\b/u.test(text) && /(?:={2,3}|!==?)/u.test(text) && /['"][a-z]{2}(?:-[A-Za-z0-9]+)?['"]/u.test(text);
@@ -301,6 +310,61 @@ const createNoLegacyMfBoundaryAttributesRule = ()=>({
301
310
  };
302
311
  }
303
312
  });
313
+ const reportProgramPattern = (context, node, source, pattern, message)=>{
314
+ if (pattern.test(source)) context.report({
315
+ node,
316
+ message
317
+ });
318
+ };
319
+ const reportMissingProgramPattern = (context, node, source, pattern, message)=>{
320
+ if (!pattern.test(source)) context.report({
321
+ node,
322
+ message
323
+ });
324
+ };
325
+ const createStrictEffectApiBoundariesRule = ()=>({
326
+ meta: {
327
+ type: 'problem',
328
+ docs: {
329
+ description: 'Disallow raw HTTP handlers and non-Effect API runtime drift in UltraModern workspaces.'
330
+ },
331
+ schema: []
332
+ },
333
+ create (context) {
334
+ return {
335
+ Program (node) {
336
+ const source = getSourceText(context, node);
337
+ const filename = normalizeFilename(context.filename);
338
+ const apiFile = isApiSourceFile(filename);
339
+ if (isForbiddenGeneratedEffectPath(filename)) context.report({
340
+ node,
341
+ message: 'Generated UltraModern workspaces use direct api/index.ts, shared/api.ts and src/api/* paths; api/effect, api/lambda, shared/effect and src/effect are forbidden.'
342
+ });
343
+ reportProgramPattern(context, node, source, /@modern-js\/plugin-bff\/hono-server/u, 'UltraModern API workspaces must not import Hono server helpers; use @modern-js/plugin-bff/effect-edge and HttpApi.');
344
+ reportProgramPattern(context, node, source, /\bruntimeFramework\s*(?::|=)\s*['"]hono['"]/u, 'UltraModern API apps must use bff.runtimeFramework: "effect".');
345
+ reportProgramPattern(context, node, source, /(?:from|import)\s*['"][^'"]*(?:api\/effect|shared\/effect)[^'"]*['"]|shared-effect-api/u, 'Import API code from direct api/index.ts, shared/api.ts or src/api/* paths, not api/effect, shared/effect or shared Effect API packages.');
346
+ reportProgramPattern(context, node, source, /\bstrictEffectApproach\s*(?::|=)\s*false\b/u, 'UltraModern API apps must keep strictEffectApproach enabled.');
347
+ if (isVerticalApiEntryFile(filename)) {
348
+ reportMissingProgramPattern(context, node, source, /\bdefineEffectBff\b/u, 'Generated API entries must export defineEffectBff(...).');
349
+ reportMissingProgramPattern(context, node, source, /\bHttpApiBuilder\b/u, 'Generated API entries must implement handlers through HttpApiBuilder.');
350
+ reportMissingProgramPattern(context, node, source, /\bLayer\b/u, 'Generated API entries must compose dependencies with Effect Layer.');
351
+ reportMissingProgramPattern(context, node, source, /from\s+['"]\.\.\/shared\/api\.ts['"]/u, 'Generated API entries must import the contract from ../shared/api.ts.');
352
+ }
353
+ if (isSharedApiContractFile(filename)) {
354
+ reportMissingProgramPattern(context, node, source, /\bHttpApi\.make\b/u, 'Generated shared API contracts must declare an HttpApi contract.');
355
+ reportMissingProgramPattern(context, node, source, /\bHttpApiGroup\.make\b/u, 'Generated shared API contracts must declare HttpApi groups.');
356
+ reportMissingProgramPattern(context, node, source, /\bHttpApiEndpoint\./u, 'Generated shared API contracts must declare endpoints through HttpApiEndpoint.');
357
+ reportMissingProgramPattern(context, node, source, /\bSchema\./u, 'Generated shared API contracts must model request, response and error shapes with Schema.');
358
+ }
359
+ if (!apiFile) return;
360
+ reportProgramPattern(context, node, source, /\bnew\s+Response\s*\(|\bResponse\.json\s*\(/u, 'API modules must not hand-build Response objects; model endpoints through Effect HttpApi and schemas.');
361
+ reportProgramPattern(context, node, source, /\b(?:request|req)\.(?:json|text|formData|arrayBuffer)\s*\(/u, 'API modules must not manually parse request bodies; use HttpApiEndpoint payload/query/params schemas.');
362
+ reportProgramPattern(context, node, source, /\bexport\s+const\s+handler\b|\bexport\s+default\s+async\b/u, 'API modules must not export raw request handlers; export defineEffectBff(...) from api/index.ts.');
363
+ reportProgramPattern(context, node, source, /\bcreateHandler\s*[:=]\s*(?!defineEffectBff\b)/u, 'API modules must not define unbranded handler factories; use defineEffectBff(...).');
364
+ }
365
+ };
366
+ }
367
+ });
304
368
  const oxlint_plugin_plugin = {
305
369
  meta: {
306
370
  name: 'ultramodern'
@@ -310,7 +374,8 @@ const oxlint_plugin_plugin = {
310
374
  'no-legacy-mf-boundary-attributes': createNoLegacyMfBoundaryAttributesRule(),
311
375
  'no-literal-visible-jsx-attributes': createNoLiteralVisibleJsxAttributesRule(),
312
376
  'no-manual-locale-copy-branching': createNoManualLocaleCopyBranchingRule(),
313
- 'no-split-translation-keys': createNoSplitTranslationKeysRule()
377
+ 'no-split-translation-keys': createNoSplitTranslationKeysRule(),
378
+ 'strict-effect-api-boundaries': createStrictEffectApiBoundariesRule()
314
379
  }
315
380
  };
316
381
  const oxlint_plugin = oxlint_plugin_plugin;
@@ -58,6 +58,7 @@ declare const plugin: {
58
58
  'no-literal-visible-jsx-attributes': Rule;
59
59
  'no-manual-locale-copy-branching': Rule;
60
60
  'no-split-translation-keys': Rule;
61
+ 'strict-effect-api-boundaries': Rule;
61
62
  };
62
63
  };
63
64
  export default plugin;
package/package.json CHANGED
@@ -23,7 +23,7 @@
23
23
  "engines": {
24
24
  "node": ">=20"
25
25
  },
26
- "version": "3.4.0-ultramodern.9",
26
+ "version": "3.5.0-ultramodern.0",
27
27
  "types": "./dist/types/index.d.ts",
28
28
  "main": "./dist/esm-node/index.js",
29
29
  "typesVersions": {