@bleedingdev/modern-js-code-tools 3.4.0-ultramodern.9 → 3.5.0-ultramodern.1
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 +68 -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 +68 -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 +68 -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|verticals)\/[^/]+\/(?:api\/(?:effect|lambda)|shared\/effect|src\/effect))(?:\/|$)/u;
|
|
92
|
+
const API_ENTRY_PATTERN = /(?:^|\/)(?:apps|verticals)\/[^/]+\/api\/index\.[cm]?[jt]sx?$/u;
|
|
93
|
+
const SHARED_API_CONTRACT_PATTERN = /(?:^|\/)(?:apps|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 isApiEntryFile = (filePath)=>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,62 @@ 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 (isApiEntryFile(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
|
+
reportProgramPattern(context, node, source, /\bSchema\.(?:UnknownFromJsonString|Unknown|Any)\b/u, 'API modules must use concrete request, response and error schemas; Schema.UnknownFromJsonString, Schema.Unknown and Schema.Any are forbidden in UltraModern API code.');
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
});
|
|
332
397
|
const plugin = {
|
|
333
398
|
meta: {
|
|
334
399
|
name: 'ultramodern'
|
|
@@ -338,7 +403,8 @@ const plugin = {
|
|
|
338
403
|
'no-legacy-mf-boundary-attributes': createNoLegacyMfBoundaryAttributesRule(),
|
|
339
404
|
'no-literal-visible-jsx-attributes': createNoLiteralVisibleJsxAttributesRule(),
|
|
340
405
|
'no-manual-locale-copy-branching': createNoManualLocaleCopyBranchingRule(),
|
|
341
|
-
'no-split-translation-keys': createNoSplitTranslationKeysRule()
|
|
406
|
+
'no-split-translation-keys': createNoSplitTranslationKeysRule(),
|
|
407
|
+
'strict-effect-api-boundaries': createStrictEffectApiBoundariesRule()
|
|
342
408
|
}
|
|
343
409
|
};
|
|
344
410
|
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|verticals)\/[^/]+\/(?:api\/(?:effect|lambda)|shared\/effect|src\/effect))(?:\/|$)/u;
|
|
63
|
+
const API_ENTRY_PATTERN = /(?:^|\/)(?:apps|verticals)\/[^/]+\/api\/index\.[cm]?[jt]sx?$/u;
|
|
64
|
+
const SHARED_API_CONTRACT_PATTERN = /(?:^|\/)(?:apps|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 isApiEntryFile = (filePath)=>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,62 @@ 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 (isApiEntryFile(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
|
+
reportProgramPattern(context, node, source, /\bSchema\.(?:UnknownFromJsonString|Unknown|Any)\b/u, 'API modules must use concrete request, response and error schemas; Schema.UnknownFromJsonString, Schema.Unknown and Schema.Any are forbidden in UltraModern API code.');
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
});
|
|
303
368
|
const oxlint_plugin_plugin = {
|
|
304
369
|
meta: {
|
|
305
370
|
name: 'ultramodern'
|
|
@@ -309,7 +374,8 @@ const oxlint_plugin_plugin = {
|
|
|
309
374
|
'no-legacy-mf-boundary-attributes': createNoLegacyMfBoundaryAttributesRule(),
|
|
310
375
|
'no-literal-visible-jsx-attributes': createNoLiteralVisibleJsxAttributesRule(),
|
|
311
376
|
'no-manual-locale-copy-branching': createNoManualLocaleCopyBranchingRule(),
|
|
312
|
-
'no-split-translation-keys': createNoSplitTranslationKeysRule()
|
|
377
|
+
'no-split-translation-keys': createNoSplitTranslationKeysRule(),
|
|
378
|
+
'strict-effect-api-boundaries': createStrictEffectApiBoundariesRule()
|
|
313
379
|
}
|
|
314
380
|
};
|
|
315
381
|
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|verticals)\/[^/]+\/(?:api\/(?:effect|lambda)|shared\/effect|src\/effect))(?:\/|$)/u;
|
|
64
|
+
const API_ENTRY_PATTERN = /(?:^|\/)(?:apps|verticals)\/[^/]+\/api\/index\.[cm]?[jt]sx?$/u;
|
|
65
|
+
const SHARED_API_CONTRACT_PATTERN = /(?:^|\/)(?:apps|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 isApiEntryFile = (filePath)=>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,62 @@ 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 (isApiEntryFile(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
|
+
reportProgramPattern(context, node, source, /\bSchema\.(?:UnknownFromJsonString|Unknown|Any)\b/u, 'API modules must use concrete request, response and error schemas; Schema.UnknownFromJsonString, Schema.Unknown and Schema.Any are forbidden in UltraModern API code.');
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
});
|
|
304
369
|
const oxlint_plugin_plugin = {
|
|
305
370
|
meta: {
|
|
306
371
|
name: 'ultramodern'
|
|
@@ -310,7 +375,8 @@ const oxlint_plugin_plugin = {
|
|
|
310
375
|
'no-legacy-mf-boundary-attributes': createNoLegacyMfBoundaryAttributesRule(),
|
|
311
376
|
'no-literal-visible-jsx-attributes': createNoLiteralVisibleJsxAttributesRule(),
|
|
312
377
|
'no-manual-locale-copy-branching': createNoManualLocaleCopyBranchingRule(),
|
|
313
|
-
'no-split-translation-keys': createNoSplitTranslationKeysRule()
|
|
378
|
+
'no-split-translation-keys': createNoSplitTranslationKeysRule(),
|
|
379
|
+
'strict-effect-api-boundaries': createStrictEffectApiBoundariesRule()
|
|
314
380
|
}
|
|
315
381
|
};
|
|
316
382
|
const oxlint_plugin = oxlint_plugin_plugin;
|