@bleedingdev/modern-js-create 3.2.0-ultramodern.11 → 3.2.0-ultramodern.110
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/README.md +158 -35
- package/bin/run.js +0 -0
- package/dist/cjs/index.cjs +1040 -0
- package/dist/cjs/locale/en.cjs +97 -0
- package/dist/cjs/locale/index.cjs +50 -0
- package/dist/cjs/locale/zh.cjs +97 -0
- package/dist/cjs/ultramodern-checks/cli/i18n-check.cjs +73 -0
- package/dist/cjs/ultramodern-checks/cli/oxlint.cjs +174 -0
- package/dist/cjs/ultramodern-checks/cli/workspace-source-check.cjs +179 -0
- package/dist/cjs/ultramodern-checks/index.cjs +58 -0
- package/dist/cjs/ultramodern-checks/oxlint-plugin.cjs +354 -0
- package/dist/cjs/ultramodern-package-source.cjs +133 -0
- package/dist/cjs/ultramodern-workspace.cjs +5616 -0
- package/dist/esm/index.js +1002 -0
- package/dist/esm/locale/en.js +59 -0
- package/dist/esm/locale/index.js +9 -0
- package/dist/esm/locale/zh.js +59 -0
- package/dist/esm/ultramodern-checks/cli/i18n-check.js +26 -0
- package/dist/esm/ultramodern-checks/cli/oxlint.js +118 -0
- package/dist/esm/ultramodern-checks/cli/workspace-source-check.js +124 -0
- package/dist/esm/ultramodern-checks/index.js +3 -0
- package/dist/esm/ultramodern-checks/oxlint-plugin.js +316 -0
- package/dist/esm/ultramodern-package-source.js +61 -0
- package/dist/esm/ultramodern-workspace.js +5554 -0
- package/dist/esm-node/index.js +1003 -0
- package/dist/esm-node/locale/en.js +60 -0
- package/dist/esm-node/locale/index.js +10 -0
- package/dist/esm-node/locale/zh.js +60 -0
- package/dist/esm-node/ultramodern-checks/cli/i18n-check.js +27 -0
- package/dist/esm-node/ultramodern-checks/cli/oxlint.js +119 -0
- package/dist/esm-node/ultramodern-checks/cli/workspace-source-check.js +125 -0
- package/dist/esm-node/ultramodern-checks/index.js +4 -0
- package/dist/esm-node/ultramodern-checks/oxlint-plugin.js +317 -0
- package/dist/esm-node/ultramodern-package-source.js +62 -0
- package/dist/esm-node/ultramodern-workspace.js +5555 -0
- package/dist/types/locale/en.d.ts +3 -0
- package/dist/types/locale/index.d.ts +117 -2
- package/dist/types/locale/zh.d.ts +3 -0
- package/dist/types/ultramodern-checks/cli/i18n-check.d.ts +9 -0
- package/dist/types/ultramodern-checks/cli/oxlint.d.ts +22 -0
- package/dist/types/ultramodern-checks/cli/workspace-source-check.d.ts +8 -0
- package/dist/types/ultramodern-checks/index.d.ts +3 -0
- package/dist/types/ultramodern-checks/oxlint-plugin.d.ts +63 -0
- package/dist/types/ultramodern-package-source.d.ts +28 -0
- package/dist/types/ultramodern-workspace.d.ts +12 -2
- package/package.json +52 -11
- package/template/.codex/hooks.json +16 -0
- package/template/.github/renovate.json +53 -0
- package/template/.github/workflows/ultramodern-gates.yml.handlebars +34 -10
- package/template/.mise.toml.handlebars +2 -0
- package/template/AGENTS.md +9 -6
- package/template/README.md +66 -34
- package/template/api/effect/index.ts.handlebars +20 -9
- package/template/api/lambda/hello.ts.handlebars +5 -5
- package/template/config/favicon.svg +5 -0
- package/template/config/public/assets/ultramodern-logo.svg +6 -0
- package/template/config/public/locales/cs/translation.json +44 -0
- package/template/config/public/locales/en/translation.json +44 -0
- package/template/lefthook.yml +10 -0
- package/template/modern.config.ts.handlebars +35 -3
- package/template/oxfmt.config.ts +8 -1
- package/template/oxlint.config.ts +8 -1
- package/template/package.json.handlebars +36 -30
- package/template/pnpm-workspace.yaml +34 -0
- package/template/rstest.config.mts +5 -0
- package/template/scripts/bootstrap-agent-skills.mjs +148 -15
- package/template/scripts/check-i18n-strings.mjs +3 -0
- package/template/scripts/validate-ultramodern.mjs.handlebars +494 -3
- package/template/src/modern-app-env.d.ts +2 -0
- package/template/src/modern.runtime.ts.handlebars +17 -1
- package/template/src/routes/[lang]/page.tsx.handlebars +209 -0
- package/template/src/routes/index.css.handlebars +192 -55
- package/template/src/routes/layout.tsx.handlebars +2 -1
- package/template/tailwind.config.ts.handlebars +1 -1
- package/template/tests/tsconfig.json +7 -0
- package/template/tests/ultramodern.contract.test.ts.handlebars +160 -0
- package/template/tsconfig.json +2 -1
- package/template-workspace/.agents/agent-reference-repos.json +24 -0
- package/template-workspace/.agents/skills-lock.json +19 -0
- package/template-workspace/.codex/hooks.json +16 -0
- package/template-workspace/.github/renovate.json +29 -0
- package/template-workspace/.github/workflows/ultramodern-workspace-gates.yml.handlebars +54 -0
- package/template-workspace/.gitignore.handlebars +5 -0
- package/template-workspace/.mise.toml.handlebars +2 -0
- package/template-workspace/AGENTS.md +36 -5
- package/template-workspace/README.md.handlebars +70 -11
- package/template-workspace/lefthook.yml +10 -0
- package/template-workspace/oxfmt.config.ts +1 -0
- package/template-workspace/oxlint.config.ts +1 -0
- package/template-workspace/pnpm-workspace.yaml +31 -8
- package/template-workspace/scripts/bootstrap-agent-skills.mjs +190 -21
- package/template-workspace/scripts/setup-agent-reference-repos.mjs +370 -0
- package/dist/index.js +0 -2474
- package/template/src/routes/page.tsx.handlebars +0 -136
- package/template-workspace/scripts/validate-ultramodern-workspace.mjs.handlebars +0 -405
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const EN_LOCALE = {
|
|
2
|
+
prompt: {
|
|
3
|
+
projectName: 'Please enter project name: '
|
|
4
|
+
},
|
|
5
|
+
error: {
|
|
6
|
+
projectNameEmpty: 'Error: Project name cannot be empty',
|
|
7
|
+
directoryExists: 'Error: Directory "{projectName}" already exists and is not empty',
|
|
8
|
+
invalidRouter: 'Error: Unsupported router "{router}". Use "react-router" or "tanstack".',
|
|
9
|
+
invalidBffRuntime: 'Error: Unsupported BFF runtime "{runtime}". Use "hono" or "effect".',
|
|
10
|
+
createFailed: 'Error creating project:'
|
|
11
|
+
},
|
|
12
|
+
message: {
|
|
13
|
+
welcome: '🚀 Welcome to UltraModern.js',
|
|
14
|
+
success: '✨ Created successfully!',
|
|
15
|
+
nextSteps: '📋 Next steps:',
|
|
16
|
+
step1: 'cd {projectName}',
|
|
17
|
+
step2: 'pnpm install',
|
|
18
|
+
step3: 'pnpm dev'
|
|
19
|
+
},
|
|
20
|
+
help: {
|
|
21
|
+
title: '🚀 UltraModern.js Project Creator',
|
|
22
|
+
description: 'Create a new UltraModern.js app with TanStack Router and Effect BFF by default',
|
|
23
|
+
usage: '📖 Usage:',
|
|
24
|
+
usageExample: ' pnpm dlx @bleedingdev/modern-js-create [project-name] [options]',
|
|
25
|
+
options: '⚙️ Options:',
|
|
26
|
+
optionHelp: ' -h, --help Display this help message',
|
|
27
|
+
optionVersion: ' -v, --version Display version information',
|
|
28
|
+
optionLang: ' -l, --lang Set the language (zh or en)',
|
|
29
|
+
optionRouter: ' -r, --router Select router framework (tanstack default; react-router is compatibility mode)',
|
|
30
|
+
optionBff: ' --bff Keep Effect BFF enabled (default for UltraModern apps)',
|
|
31
|
+
optionBffRuntime: ' --bff-runtime Select BFF runtime (hono or effect)',
|
|
32
|
+
optionTailwind: ' --no-tailwind Disable default Tailwind CSS v4 scaffold',
|
|
33
|
+
optionWorkspace: ' --workspace Use workspace protocol for @modern-js dependencies (for local monorepo testing)',
|
|
34
|
+
optionUltramodernWorkspace: ' --ultramodern-workspace Generate an UltraModern SuperApp workspace (default is a full UltraModern single app)',
|
|
35
|
+
optionUltramodernPackageSource: ' --ultramodern-package-source Select UltraModern package source (workspace or install; BleedingDev defaults to install aliases)',
|
|
36
|
+
optionUltramodernPackageScope: ' --ultramodern-package-scope Publish scope for npm alias installs (for example bleedingdev)',
|
|
37
|
+
optionUltramodernPackageNamePrefix: ' --ultramodern-package-name-prefix Prefix for npm alias package names (default: modern-js-)',
|
|
38
|
+
optionVertical: ' --vertical Mutate the current existing UltraModern workspace and wire a MicroVertical named <project-name>',
|
|
39
|
+
optionSub: ' -s, --sub Mark as a subproject (package in monorepo)',
|
|
40
|
+
examples: '💡 Examples:',
|
|
41
|
+
example1: ' pnpm dlx @bleedingdev/modern-js-create my-app',
|
|
42
|
+
example2: ' pnpm dlx @bleedingdev/modern-js-create my-app --lang zh',
|
|
43
|
+
example3: ' pnpm dlx @bleedingdev/modern-js-create my-app --sub',
|
|
44
|
+
example4: ' pnpm dlx @bleedingdev/modern-js-create --help',
|
|
45
|
+
example5: ' pnpm dlx @bleedingdev/modern-js-create .',
|
|
46
|
+
example6: ' pnpm dlx @bleedingdev/modern-js-create my-app --router react-router --no-tailwind',
|
|
47
|
+
example7: ' pnpm dlx @bleedingdev/modern-js-create my-app --bff-runtime hono',
|
|
48
|
+
example8: ' pnpm dlx @bleedingdev/modern-js-create my-app --workspace',
|
|
49
|
+
example9: ' pnpm dlx @bleedingdev/modern-js-create my-super-app --ultramodern-workspace',
|
|
50
|
+
example10: ' pnpm dlx @bleedingdev/modern-js-create my-app --no-tailwind',
|
|
51
|
+
example11: ' pnpm dlx @bleedingdev/modern-js-create my-app --router react-router # compatibility mode',
|
|
52
|
+
example12: ' pnpm dlx @bleedingdev/modern-js-create catalog --vertical',
|
|
53
|
+
moreInfo: '📚 Learn more: https://modernjs.dev'
|
|
54
|
+
},
|
|
55
|
+
version: {
|
|
56
|
+
message: '@bleedingdev/modern-js-create version: {version}'
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
export { EN_LOCALE };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const ZH_LOCALE = {
|
|
2
|
+
prompt: {
|
|
3
|
+
projectName: '请输入项目名称: '
|
|
4
|
+
},
|
|
5
|
+
error: {
|
|
6
|
+
projectNameEmpty: '错误: 项目名称不能为空',
|
|
7
|
+
directoryExists: '错误: 目录 "{projectName}" 已存在且不为空',
|
|
8
|
+
invalidRouter: '错误: 不支持的路由器 "{router}",请使用 "react-router" 或 "tanstack"',
|
|
9
|
+
invalidBffRuntime: '错误: 不支持的 BFF 运行时 "{runtime}",请使用 "hono" 或 "effect"',
|
|
10
|
+
createFailed: '创建项目时出错:'
|
|
11
|
+
},
|
|
12
|
+
message: {
|
|
13
|
+
welcome: '🚀 欢迎使用 UltraModern.js',
|
|
14
|
+
success: '✨ 创建成功!',
|
|
15
|
+
nextSteps: '📋 下一步:',
|
|
16
|
+
step1: 'cd {projectName}',
|
|
17
|
+
step2: 'pnpm install',
|
|
18
|
+
step3: 'pnpm dev'
|
|
19
|
+
},
|
|
20
|
+
help: {
|
|
21
|
+
title: '🚀 UltraModern.js 项目创建工具',
|
|
22
|
+
description: '创建默认包含 TanStack Router 和 Effect BFF 的 UltraModern.js 应用',
|
|
23
|
+
usage: '📖 用法:',
|
|
24
|
+
usageExample: ' pnpm dlx @bleedingdev/modern-js-create [项目名称] [选项]',
|
|
25
|
+
options: '⚙️ 选项:',
|
|
26
|
+
optionHelp: ' -h, --help 显示帮助信息',
|
|
27
|
+
optionVersion: ' -v, --version 显示版本信息',
|
|
28
|
+
optionLang: ' -l, --lang 设置语言 (zh 或 en)',
|
|
29
|
+
optionRouter: ' -r, --router 选择路由框架(默认 tanstack;react-router 为兼容模式)',
|
|
30
|
+
optionBff: ' --bff 保持启用 Effect BFF(UltraModern 应用默认值)',
|
|
31
|
+
optionBffRuntime: ' --bff-runtime 选择 BFF 运行时(hono 或 effect)',
|
|
32
|
+
optionTailwind: ' --no-tailwind 禁用默认 Tailwind CSS v4 模板',
|
|
33
|
+
optionWorkspace: ' --workspace 对 @modern-js 依赖使用 workspace 协议(用于本地 monorepo 联调)',
|
|
34
|
+
optionUltramodernWorkspace: ' --ultramodern-workspace 生成 UltraModern SuperApp 工作区(默认创建完整 UltraModern 单应用)',
|
|
35
|
+
optionUltramodernPackageSource: ' --ultramodern-package-source 选择 UltraModern 依赖来源(workspace 或 install;BleedingDev 默认使用 install alias)',
|
|
36
|
+
optionUltramodernPackageScope: ' --ultramodern-package-scope npm alias 安装使用的发布 scope(例如 bleedingdev)',
|
|
37
|
+
optionUltramodernPackageNamePrefix: ' --ultramodern-package-name-prefix npm alias 包名前缀(默认:modern-js-)',
|
|
38
|
+
optionVertical: ' --vertical 修改当前已有的 UltraModern 工作区,并接入名为 <项目名称> 的 MicroVertical',
|
|
39
|
+
optionSub: ' -s, --sub 标记为子项目(monorepo 中的子包)',
|
|
40
|
+
examples: '💡 示例:',
|
|
41
|
+
example1: ' pnpm dlx @bleedingdev/modern-js-create my-app',
|
|
42
|
+
example2: ' pnpm dlx @bleedingdev/modern-js-create my-app --lang zh',
|
|
43
|
+
example3: ' pnpm dlx @bleedingdev/modern-js-create my-app --sub',
|
|
44
|
+
example4: ' pnpm dlx @bleedingdev/modern-js-create --help',
|
|
45
|
+
example5: ' pnpm dlx @bleedingdev/modern-js-create .',
|
|
46
|
+
example6: ' pnpm dlx @bleedingdev/modern-js-create my-app --router react-router --no-tailwind',
|
|
47
|
+
example7: ' pnpm dlx @bleedingdev/modern-js-create my-app --bff-runtime hono',
|
|
48
|
+
example8: ' pnpm dlx @bleedingdev/modern-js-create my-app --workspace',
|
|
49
|
+
example9: ' pnpm dlx @bleedingdev/modern-js-create my-super-app --ultramodern-workspace',
|
|
50
|
+
example10: ' pnpm dlx @bleedingdev/modern-js-create my-app --no-tailwind',
|
|
51
|
+
example11: ' pnpm dlx @bleedingdev/modern-js-create my-app --router react-router # 兼容模式',
|
|
52
|
+
example12: ' pnpm dlx @bleedingdev/modern-js-create catalog --vertical',
|
|
53
|
+
moreInfo: '📚 更多信息: https://modernjs.dev'
|
|
54
|
+
},
|
|
55
|
+
version: {
|
|
56
|
+
message: '@bleedingdev/modern-js-create 版本: {version}'
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
export { ZH_LOCALE };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { printOxlintOutput, runOxlintRules } from "./oxlint.js";
|
|
2
|
+
const SINGLE_APP_I18N_SUCCESS = 'No hardcoded user-visible JSX strings found.';
|
|
3
|
+
const SINGLE_APP_I18N_FAILURE = 'Hardcoded user-visible JSX strings found. Move copy to locale JSON files.';
|
|
4
|
+
const runSingleAppI18nCheck = ({ cwd = process.cwd(), targets = [
|
|
5
|
+
'src'
|
|
6
|
+
] } = {})=>{
|
|
7
|
+
const result = runOxlintRules({
|
|
8
|
+
cwd,
|
|
9
|
+
targets,
|
|
10
|
+
rules: {
|
|
11
|
+
'ultramodern/no-hardcoded-jsx-text': 'error',
|
|
12
|
+
'ultramodern/no-literal-visible-jsx-attributes': 'error'
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
if (0 === result.exitCode) {
|
|
16
|
+
console.log(SINGLE_APP_I18N_SUCCESS);
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
console.error(SINGLE_APP_I18N_FAILURE);
|
|
20
|
+
printOxlintOutput(result);
|
|
21
|
+
return result.exitCode;
|
|
22
|
+
};
|
|
23
|
+
const main = ()=>{
|
|
24
|
+
process.exitCode = runSingleAppI18nCheck();
|
|
25
|
+
};
|
|
26
|
+
export { SINGLE_APP_I18N_FAILURE, SINGLE_APP_I18N_SUCCESS, main, runSingleAppI18nCheck };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import node_fs from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import node_os from "node:os";
|
|
5
|
+
import node_path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
const oxlint_require = createRequire(import.meta.url);
|
|
8
|
+
const ignoredDirectories = new Set([
|
|
9
|
+
'.modern',
|
|
10
|
+
'.modernjs',
|
|
11
|
+
'.output',
|
|
12
|
+
'dist',
|
|
13
|
+
'node_modules'
|
|
14
|
+
]);
|
|
15
|
+
const packageNames = new Set([
|
|
16
|
+
'@modern-js/create',
|
|
17
|
+
'@bleedingdev/modern-js-create'
|
|
18
|
+
]);
|
|
19
|
+
const resolveExistingPath = (candidates)=>candidates.find((candidate)=>node_fs.existsSync(candidate));
|
|
20
|
+
const findPackageRoot = ()=>{
|
|
21
|
+
let directory = node_path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
while(directory !== node_path.dirname(directory)){
|
|
23
|
+
const packageJsonPath = node_path.join(directory, 'package.json');
|
|
24
|
+
if (node_fs.existsSync(packageJsonPath)) try {
|
|
25
|
+
const packageJson = JSON.parse(node_fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
26
|
+
if (packageNames.has(packageJson.name)) return directory;
|
|
27
|
+
} catch {
|
|
28
|
+
return directory;
|
|
29
|
+
}
|
|
30
|
+
directory = node_path.dirname(directory);
|
|
31
|
+
}
|
|
32
|
+
throw new Error('Unable to resolve @modern-js/create package root.');
|
|
33
|
+
};
|
|
34
|
+
const resolvePluginPath = ()=>{
|
|
35
|
+
const root = findPackageRoot();
|
|
36
|
+
const sourcePluginPath = node_path.join(root, 'src/ultramodern-checks/oxlint-plugin.ts');
|
|
37
|
+
const pluginPath = resolveExistingPath([
|
|
38
|
+
node_path.join(root, 'dist/esm-node/ultramodern-checks/oxlint-plugin.mjs'),
|
|
39
|
+
node_path.join(root, 'dist/esm-node/ultramodern-checks/oxlint-plugin.js'),
|
|
40
|
+
node_path.join(root, 'dist/esm/ultramodern-checks/oxlint-plugin.mjs'),
|
|
41
|
+
node_path.join(root, 'dist/esm/ultramodern-checks/oxlint-plugin.js'),
|
|
42
|
+
node_path.join(root, 'dist/cjs/ultramodern-checks/oxlint-plugin.js'),
|
|
43
|
+
node_path.join(root, 'dist/cjs/ultramodern-checks/oxlint-plugin.cjs'),
|
|
44
|
+
sourcePluginPath
|
|
45
|
+
]);
|
|
46
|
+
if (!pluginPath) throw new Error('Unable to resolve @modern-js/create UltraModern Oxlint plugin.');
|
|
47
|
+
return pluginPath;
|
|
48
|
+
};
|
|
49
|
+
const resolveOxlintBin = ()=>{
|
|
50
|
+
const packageJsonPath = oxlint_require.resolve('oxlint/package.json');
|
|
51
|
+
const packageJson = JSON.parse(node_fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
52
|
+
const binRelativePath = 'string' == typeof packageJson.bin ? packageJson.bin : packageJson.bin?.oxlint;
|
|
53
|
+
if (!binRelativePath) throw new Error('Unable to resolve oxlint binary.');
|
|
54
|
+
return node_path.join(node_path.dirname(packageJsonPath), binRelativePath);
|
|
55
|
+
};
|
|
56
|
+
const existingTargets = (cwd, targets)=>targets.map((target)=>node_path.resolve(cwd, target)).filter((target)=>node_fs.existsSync(target));
|
|
57
|
+
const containsLintableSource = (filePath)=>{
|
|
58
|
+
if (!node_fs.existsSync(filePath)) return false;
|
|
59
|
+
const stats = node_fs.statSync(filePath);
|
|
60
|
+
if (stats.isFile()) return /\.(?:js|jsx|ts|tsx)$/u.test(filePath) && !filePath.endsWith('.d.ts');
|
|
61
|
+
if (!stats.isDirectory()) return false;
|
|
62
|
+
for (const entry of node_fs.readdirSync(filePath, {
|
|
63
|
+
withFileTypes: true
|
|
64
|
+
}))if (!(entry.isDirectory() && ignoredDirectories.has(entry.name))) {
|
|
65
|
+
if (containsLintableSource(node_path.join(filePath, entry.name))) return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
};
|
|
69
|
+
const runOxlintRules = ({ cwd, targets, rules })=>{
|
|
70
|
+
const resolvedTargets = existingTargets(cwd, targets);
|
|
71
|
+
if (0 === resolvedTargets.length || !resolvedTargets.some((target)=>containsLintableSource(target))) return {
|
|
72
|
+
exitCode: 0,
|
|
73
|
+
stdout: '',
|
|
74
|
+
stderr: ''
|
|
75
|
+
};
|
|
76
|
+
const tempDir = node_fs.mkdtempSync(node_path.join(node_os.tmpdir(), 'ultramodern-oxlint-'));
|
|
77
|
+
const configPath = node_path.join(tempDir, 'oxlint.config.mjs');
|
|
78
|
+
const pluginPath = resolvePluginPath();
|
|
79
|
+
node_fs.writeFileSync(configPath, `export default {
|
|
80
|
+
jsPlugins: [${JSON.stringify(pluginPath)}],
|
|
81
|
+
rules: ${JSON.stringify(rules, null, 2)}
|
|
82
|
+
};
|
|
83
|
+
`, 'utf-8');
|
|
84
|
+
try {
|
|
85
|
+
const result = spawnSync(process.execPath, [
|
|
86
|
+
resolveOxlintBin(),
|
|
87
|
+
...resolvedTargets,
|
|
88
|
+
'--config',
|
|
89
|
+
configPath,
|
|
90
|
+
'--format',
|
|
91
|
+
'unix',
|
|
92
|
+
'--quiet'
|
|
93
|
+
], {
|
|
94
|
+
cwd,
|
|
95
|
+
encoding: 'utf-8',
|
|
96
|
+
stdio: [
|
|
97
|
+
'ignore',
|
|
98
|
+
'pipe',
|
|
99
|
+
'pipe'
|
|
100
|
+
]
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
exitCode: result.status ?? 1,
|
|
104
|
+
stdout: result.stdout ?? '',
|
|
105
|
+
stderr: result.stderr ?? ''
|
|
106
|
+
};
|
|
107
|
+
} finally{
|
|
108
|
+
node_fs.rmSync(tempDir, {
|
|
109
|
+
recursive: true,
|
|
110
|
+
force: true
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const printOxlintOutput = ({ stdout, stderr })=>{
|
|
115
|
+
if (stdout) process.stdout.write(stdout);
|
|
116
|
+
if (stderr) process.stderr.write(stderr);
|
|
117
|
+
};
|
|
118
|
+
export { printOxlintOutput, runOxlintRules };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import node_fs from "node:fs";
|
|
2
|
+
import node_path from "node:path";
|
|
3
|
+
import { printOxlintOutput, runOxlintRules } from "./oxlint.js";
|
|
4
|
+
const WORKSPACE_SOURCE_SUCCESS = 'UltraModern i18n and boundary guardrails validated';
|
|
5
|
+
const ignoredDirectories = new Set([
|
|
6
|
+
'.modern',
|
|
7
|
+
'.modernjs',
|
|
8
|
+
'.output',
|
|
9
|
+
'dist',
|
|
10
|
+
'node_modules'
|
|
11
|
+
]);
|
|
12
|
+
const normalizePath = (filePath)=>filePath.replaceAll('\\', '/');
|
|
13
|
+
const relativePath = (root, filePath)=>normalizePath(node_path.relative(root, filePath));
|
|
14
|
+
const walk = (directory, files = [])=>{
|
|
15
|
+
if (!node_fs.existsSync(directory)) return files;
|
|
16
|
+
for (const entry of node_fs.readdirSync(directory, {
|
|
17
|
+
withFileTypes: true
|
|
18
|
+
})){
|
|
19
|
+
if (entry.isDirectory() && ignoredDirectories.has(entry.name)) continue;
|
|
20
|
+
const entryPath = node_path.join(directory, entry.name);
|
|
21
|
+
if (entry.isDirectory()) {
|
|
22
|
+
walk(entryPath, files);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (entry.isFile()) files.push(entryPath);
|
|
26
|
+
}
|
|
27
|
+
return files;
|
|
28
|
+
};
|
|
29
|
+
const isSourceFile = (filePath)=>/\.(?:ts|tsx|js|jsx)$/u.test(filePath);
|
|
30
|
+
const isLocaleJson = (root, filePath)=>/\/locales\/(?:en|cs)\/[^/]+\.json$/u.test(`/${relativePath(root, filePath)}`);
|
|
31
|
+
const readText = (filePath)=>node_fs.readFileSync(filePath, 'utf-8');
|
|
32
|
+
const checkRuntimeResources = (root, filePath, text)=>{
|
|
33
|
+
const relative = relativePath(root, filePath);
|
|
34
|
+
if (!relative.endsWith('/src/modern.runtime.ts')) return;
|
|
35
|
+
const importsLocaleResources = /import\s+csResource\s+from\s+['"]\.\.\/locales\/cs\/[^'"]+\.json['"]/u.test(text) && /import\s+enResource\s+from\s+['"]\.\.\/locales\/en\/[^'"]+\.json['"]/u.test(text);
|
|
36
|
+
if (!importsLocaleResources || !/initOptions\s*:\s*\{[\s\S]*?\bresources\s*,/u.test(text)) throw new Error(`${relative} must register locale JSON resources in modern.runtime.ts so Worker SSR and hydration use the same first-render translations.`);
|
|
37
|
+
};
|
|
38
|
+
const visitLocaleKeys = (value, visitor, pathParts = [])=>{
|
|
39
|
+
if (!value || 'object' != typeof value || Array.isArray(value)) return;
|
|
40
|
+
for (const [key, child] of Object.entries(value)){
|
|
41
|
+
const nextPath = [
|
|
42
|
+
...pathParts,
|
|
43
|
+
key
|
|
44
|
+
];
|
|
45
|
+
visitor(key, child, nextPath);
|
|
46
|
+
visitLocaleKeys(child, visitor, nextPath);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const checkPluralResources = (root, filePath, json)=>{
|
|
50
|
+
const relative = relativePath(root, filePath);
|
|
51
|
+
const language = relative.split('/locales/')[1]?.split('/')[0];
|
|
52
|
+
const requiredSuffixes = 'cs' === language ? [
|
|
53
|
+
'one',
|
|
54
|
+
'few',
|
|
55
|
+
'many',
|
|
56
|
+
'other'
|
|
57
|
+
] : [
|
|
58
|
+
'one',
|
|
59
|
+
'other'
|
|
60
|
+
];
|
|
61
|
+
const groups = new Map();
|
|
62
|
+
visitLocaleKeys(json, (key, value, pathParts)=>{
|
|
63
|
+
if ('string' != typeof value || !value.includes('{{count}}')) return;
|
|
64
|
+
const suffixMatch = key.match(/^(.*)_(one|few|many|other)$/u);
|
|
65
|
+
if (!suffixMatch) throw new Error(`${relative} key ${pathParts.join('.')} contains {{count}} but is not plural-suffixed.`);
|
|
66
|
+
const [, base = '', suffix = ''] = suffixMatch;
|
|
67
|
+
const parentPath = pathParts.slice(0, -1).join('.');
|
|
68
|
+
const groupKey = `${parentPath}.${base}`;
|
|
69
|
+
const existing = groups.get(groupKey) ?? new Set();
|
|
70
|
+
existing.add(suffix);
|
|
71
|
+
groups.set(groupKey, existing);
|
|
72
|
+
});
|
|
73
|
+
for (const [group, suffixes] of groups)for (const suffix of requiredSuffixes)if (!suffixes.has(suffix)) throw new Error(`${relative} plural group ${group} is missing _${suffix}.`);
|
|
74
|
+
};
|
|
75
|
+
const runRuntimeAndLocaleResourceChecks = (root, sourceRoots)=>{
|
|
76
|
+
const files = sourceRoots.flatMap((sourceRoot)=>walk(node_path.join(root, sourceRoot)));
|
|
77
|
+
for (const filePath of files.filter(isSourceFile))checkRuntimeResources(root, filePath, readText(filePath));
|
|
78
|
+
for (const filePath of files.filter((filePath)=>isLocaleJson(root, filePath)))checkPluralResources(root, filePath, JSON.parse(readText(filePath)));
|
|
79
|
+
};
|
|
80
|
+
const runWorkspaceSourceCheck = ({ cwd = process.cwd(), sourceRoots = [
|
|
81
|
+
'apps',
|
|
82
|
+
'verticals'
|
|
83
|
+
] } = {})=>{
|
|
84
|
+
const oxlintResult = runOxlintRules({
|
|
85
|
+
cwd,
|
|
86
|
+
targets: sourceRoots,
|
|
87
|
+
rules: {
|
|
88
|
+
'ultramodern/no-legacy-mf-boundary-attributes': 'error',
|
|
89
|
+
'ultramodern/no-literal-visible-jsx-attributes': [
|
|
90
|
+
'error',
|
|
91
|
+
{
|
|
92
|
+
visibleAttributes: [
|
|
93
|
+
'aria-label',
|
|
94
|
+
"aria-description",
|
|
95
|
+
"aria-roledescription",
|
|
96
|
+
'aria-valuetext',
|
|
97
|
+
'alt',
|
|
98
|
+
'label',
|
|
99
|
+
'placeholder',
|
|
100
|
+
'title'
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
'ultramodern/no-manual-locale-copy-branching': 'error',
|
|
105
|
+
'ultramodern/no-split-translation-keys': 'error'
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
if (0 !== oxlintResult.exitCode) {
|
|
109
|
+
printOxlintOutput(oxlintResult);
|
|
110
|
+
return oxlintResult.exitCode;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
runRuntimeAndLocaleResourceChecks(cwd, sourceRoots);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error(error instanceof Error ? error.message : 'UltraModern workspace source checks failed.');
|
|
116
|
+
return 1;
|
|
117
|
+
}
|
|
118
|
+
console.log(WORKSPACE_SOURCE_SUCCESS);
|
|
119
|
+
return 0;
|
|
120
|
+
};
|
|
121
|
+
const main = ()=>{
|
|
122
|
+
process.exitCode = runWorkspaceSourceCheck();
|
|
123
|
+
};
|
|
124
|
+
export { WORKSPACE_SOURCE_SUCCESS, main, runWorkspaceSourceCheck };
|