@bleedingdev/modern-js-code-tools 3.4.0-ultramodern.8 → 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.
- package/dist/cjs/cli/oxlint.cjs +3 -3
- package/dist/cjs/cli/workspace-source-check.cjs +5 -3
- package/dist/cjs/oxlint-plugin.cjs +67 -2
- package/dist/esm/cli/oxlint.js +3 -3
- package/dist/esm/cli/workspace-source-check.js +5 -3
- package/dist/esm/oxlint-plugin.js +67 -2
- package/dist/esm-node/cli/oxlint.js +3 -3
- package/dist/esm-node/cli/workspace-source-check.js +5 -3
- package/dist/esm-node/oxlint-plugin.js +67 -2
- package/dist/types/oxlint-plugin.d.ts +1 -0
- package/package.json +1 -1
package/dist/cjs/cli/oxlint.cjs
CHANGED
|
@@ -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 /\.(?:
|
|
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)=>/\.(?:
|
|
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;
|
package/dist/esm/cli/oxlint.js
CHANGED
|
@@ -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 /\.(?:
|
|
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)=>/\.(?:
|
|
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 /\.(?:
|
|
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)=>/\.(?:
|
|
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;
|