@bleedingdev/modern-js-create 3.2.0-ultramodern.62 → 3.2.0-ultramodern.64
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 +4 -0
- package/dist/index.js +408 -201
- package/package.json +3 -3
- package/template/AGENTS.md +1 -1
- package/template/lefthook.yml +2 -7
- package/template-workspace/AGENTS.md +1 -1
- package/template-workspace/lefthook.yml +2 -7
- package/template-workspace/oxfmt.config.ts +1 -0
- package/template-workspace/oxlint.config.ts +1 -0
package/README.md
CHANGED
|
@@ -156,6 +156,10 @@ CSS federation is explicit:
|
|
|
156
156
|
- Tailwind CSS v4 is configured per app through `@tailwindcss/postcss`.
|
|
157
157
|
- Duplicate base styles are forbidden; SSR first paint depends on shared token
|
|
158
158
|
CSS plus Modern/Rspack-emitted app CSS.
|
|
159
|
+
- Apps should not inject remote `async-index.css` paths, hardcode remote
|
|
160
|
+
stylesheet links, or disable filename hashing to make CSS URLs predictable.
|
|
161
|
+
UltraModern SSR resolves federated CSS from build output and MF manifests so
|
|
162
|
+
generated shells and demos can keep normal hashed assets.
|
|
159
163
|
|
|
160
164
|
Version switching evidence must keep UI, Effect API, CSS, i18n JSON, and MF
|
|
161
165
|
manifest markers in lockstep for the same vertical version.
|
package/dist/index.js
CHANGED
|
@@ -610,6 +610,14 @@ const modernPackageNames = [
|
|
|
610
610
|
'@modern-js/plugin-tanstack',
|
|
611
611
|
'@modern-js/runtime'
|
|
612
612
|
];
|
|
613
|
+
function sortJsonValue(value) {
|
|
614
|
+
if (Array.isArray(value)) return value.map(sortJsonValue);
|
|
615
|
+
if (null !== value && 'object' == typeof value) return Object.fromEntries(Object.entries(value).sort(([left], [right])=>left.localeCompare(right)).map(([key, entry])=>[
|
|
616
|
+
key,
|
|
617
|
+
sortJsonValue(entry)
|
|
618
|
+
]));
|
|
619
|
+
return value;
|
|
620
|
+
}
|
|
613
621
|
const ULTRAMODERN_WORKSPACE_FLAG = '--ultramodern-workspace';
|
|
614
622
|
function isRecord(value) {
|
|
615
623
|
return null !== value && 'object' == typeof value && !Array.isArray(value);
|
|
@@ -973,10 +981,10 @@ function createRootPackageJson(scope, packageSource, remotes = []) {
|
|
|
973
981
|
`pnpm --filter ${ultramodern_workspace_packageName(scope, remote.packageSuffix)} dev`
|
|
974
982
|
])),
|
|
975
983
|
build: `${remoteBuildPrefix}pnpm --filter "./apps/shell-super-app" run build && pnpm ultramodern:assert-mf-types`,
|
|
976
|
-
format:
|
|
977
|
-
'format:check':
|
|
978
|
-
lint: 'oxlint
|
|
979
|
-
'lint:fix': 'oxlint
|
|
984
|
+
format: "oxfmt . '!repos/**'",
|
|
985
|
+
'format:check': "oxfmt --check . '!repos/**'",
|
|
986
|
+
lint: 'oxlint apps/*/src verticals/*/src packages/*/src --ignore-pattern "**/modern-tanstack/**"',
|
|
987
|
+
'lint:fix': 'oxlint apps/*/src verticals/*/src packages/*/src --ignore-pattern "**/modern-tanstack/**" --fix',
|
|
980
988
|
typecheck: `pnpm -r --filter "@${scope}/*" typecheck`,
|
|
981
989
|
'cloudflare:build': `${remoteCloudflareBuildPrefix}pnpm --filter "./apps/shell-super-app" run cloudflare:build && pnpm ultramodern:assert-mf-types`,
|
|
982
990
|
'cloudflare:deploy': `${remoteCloudflareDeployPrefix}pnpm --filter "./apps/shell-super-app" run cloudflare:deploy`,
|
|
@@ -987,8 +995,9 @@ function createRootPackageJson(scope, packageSource, remotes = []) {
|
|
|
987
995
|
'agents:refs:check': "node ./scripts/setup-agent-reference-repos.mjs --check",
|
|
988
996
|
'ultramodern:assert-mf-types': "node ./scripts/assert-mf-types.mjs",
|
|
989
997
|
'ultramodern:check': "node ./scripts/validate-ultramodern-workspace.mjs",
|
|
990
|
-
|
|
991
|
-
|
|
998
|
+
'ultramodern:i18n-boundaries': "node ./scripts/check-ultramodern-i18n-boundaries.mjs",
|
|
999
|
+
postinstall: "oxfmt . '!repos/**' && node ./scripts/bootstrap-agent-skills.mjs && node ./scripts/setup-agent-reference-repos.mjs && (git rev-parse --is-inside-work-tree >/dev/null 2>&1 && lefthook install || true)",
|
|
1000
|
+
check: 'pnpm format:check && pnpm lint && pnpm typecheck && pnpm skills:check && pnpm ultramodern:i18n-boundaries && pnpm ultramodern:check'
|
|
992
1001
|
},
|
|
993
1002
|
engines: {
|
|
994
1003
|
node: '>=20',
|
|
@@ -1160,7 +1169,7 @@ function createAppPackage(scope, app, packageSource, enableTailwind, remotes = [
|
|
|
1160
1169
|
dev: 'modern dev',
|
|
1161
1170
|
build: app.exposes ? `modern build && node ${relativeRootFor(app.directory)}/scripts/assert-mf-types.mjs` : 'modern build',
|
|
1162
1171
|
'cloudflare:build': 'ULTRAMODERN_ZEPHYR=false MODERNJS_DEPLOY=cloudflare modern build && ULTRAMODERN_ZEPHYR=false MODERNJS_DEPLOY=cloudflare modern deploy',
|
|
1163
|
-
'cloudflare:deploy': 'ULTRAMODERN_CLOUDFLARE_REQUIRE_PUBLIC_URLS=true
|
|
1172
|
+
'cloudflare:deploy': 'ULTRAMODERN_CLOUDFLARE_REQUIRE_PUBLIC_URLS=true pnpm run cloudflare:build && wrangler deploy --config .output/wrangler.json',
|
|
1164
1173
|
'cloudflare:preview': 'pnpm run cloudflare:build && wrangler dev --config .output/wrangler.json',
|
|
1165
1174
|
'cloudflare:proof': `node ${relativeRootFor(app.directory)}/scripts/proof-cloudflare-version.mjs --app ${app.id}`,
|
|
1166
1175
|
serve: 'modern serve',
|
|
@@ -1325,6 +1334,7 @@ ${bffConfig} output: {
|
|
|
1325
1334
|
i18nPlugin({
|
|
1326
1335
|
backend: {
|
|
1327
1336
|
enabled: true,
|
|
1337
|
+
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
|
1328
1338
|
},
|
|
1329
1339
|
reactI18next: false,
|
|
1330
1340
|
localeDetection: {
|
|
@@ -1365,9 +1375,7 @@ ${bffPluginEntry} moduleFederationPlugin(),
|
|
|
1365
1375
|
...(cloudflareDeployEnabled
|
|
1366
1376
|
? {
|
|
1367
1377
|
deploy: {
|
|
1368
|
-
target: 'cloudflare',
|
|
1369
1378
|
worker: {
|
|
1370
|
-
name: cloudflareWorkerName,
|
|
1371
1379
|
ssr: true,
|
|
1372
1380
|
},
|
|
1373
1381
|
},
|
|
@@ -1377,7 +1385,7 @@ ${bffPluginEntry} moduleFederationPlugin(),
|
|
|
1377
1385
|
port,
|
|
1378
1386
|
publicDir: ['./locales', './assets'],
|
|
1379
1387
|
ssr: {
|
|
1380
|
-
mode: '
|
|
1388
|
+
mode: 'string',
|
|
1381
1389
|
moduleFederationAppSSR: true,
|
|
1382
1390
|
},
|
|
1383
1391
|
},
|
|
@@ -1547,10 +1555,10 @@ function createBuildMarker(scope, app) {
|
|
|
1547
1555
|
function createUltramodernBuildModule(scope, app) {
|
|
1548
1556
|
return `export const ultramodernVerticalIdentity = {
|
|
1549
1557
|
appId: '${app.id}',
|
|
1550
|
-
packageName: '${ultramodern_workspace_packageName(scope, app.packageSuffix)}',
|
|
1551
|
-
version: '0.1.0',
|
|
1552
1558
|
build: '${createBuildMarker(scope, app)}',
|
|
1553
1559
|
deployProfile: 'cloudflare-ssr-mf-effect-v1',
|
|
1560
|
+
packageName: '${ultramodern_workspace_packageName(scope, app.packageSuffix)}',
|
|
1561
|
+
version: '0.1.0',
|
|
1554
1562
|
} as const;
|
|
1555
1563
|
|
|
1556
1564
|
export const ultramodernUiMarker = {
|
|
@@ -1791,8 +1799,8 @@ function createLocalisedUrlsMap(app) {
|
|
|
1791
1799
|
}));
|
|
1792
1800
|
}
|
|
1793
1801
|
function createRouteMetadataModule(app) {
|
|
1794
|
-
const routes = createRouteOwnedI18nPaths(app);
|
|
1795
|
-
const localisedUrls = createLocalisedUrlsMap(app);
|
|
1802
|
+
const routes = sortJsonValue(createRouteOwnedI18nPaths(app));
|
|
1803
|
+
const localisedUrls = sortJsonValue(createLocalisedUrlsMap(app));
|
|
1796
1804
|
const namespace = appI18nNamespace(app);
|
|
1797
1805
|
return `export const ultramodernRouteNamespace = '${namespace}' as const;
|
|
1798
1806
|
|
|
@@ -1801,10 +1809,10 @@ export const ultramodernRouteMetadata = ${JSON.stringify(routes, null, 2)} as co
|
|
|
1801
1809
|
export const ultramodernLocalisedUrls = ${JSON.stringify(localisedUrls, null, 2)} as const;
|
|
1802
1810
|
|
|
1803
1811
|
export const ultramodernRouteConfig = {
|
|
1804
|
-
source: 'route-owned',
|
|
1805
|
-
namespace: ultramodernRouteNamespace,
|
|
1806
1812
|
localisedUrls: ultramodernLocalisedUrls,
|
|
1813
|
+
namespace: ultramodernRouteNamespace,
|
|
1807
1814
|
routes: ultramodernRouteMetadata,
|
|
1815
|
+
source: 'route-owned',
|
|
1808
1816
|
} as const;
|
|
1809
1817
|
`;
|
|
1810
1818
|
}
|
|
@@ -1830,37 +1838,40 @@ function createRouteAliasPage(canonicalPath) {
|
|
|
1830
1838
|
}
|
|
1831
1839
|
function createBoundaryDebugMetadata(scope, remotes = []) {
|
|
1832
1840
|
return {
|
|
1833
|
-
schemaVersion: 1,
|
|
1834
1841
|
appId: shellApp.id,
|
|
1835
1842
|
boundaries: [
|
|
1836
1843
|
shellApp,
|
|
1837
1844
|
...remotes
|
|
1838
1845
|
].map((app)=>({
|
|
1839
1846
|
appId: app.id,
|
|
1847
|
+
label: app.displayName,
|
|
1840
1848
|
mfName: app.mfName,
|
|
1841
|
-
packageName: ultramodern_workspace_packageName(scope, app.packageSuffix),
|
|
1842
|
-
role: 'shell' === app.kind ? 'host' : 'vertical',
|
|
1843
1849
|
ownerTeam: app.ownership.team,
|
|
1844
|
-
|
|
1845
|
-
|
|
1850
|
+
packageName: ultramodern_workspace_packageName(scope, app.packageSuffix),
|
|
1851
|
+
role: 'shell' === app.kind ? 'host' : 'vertical'
|
|
1852
|
+
})),
|
|
1853
|
+
schemaVersion: 1
|
|
1846
1854
|
};
|
|
1847
1855
|
}
|
|
1848
1856
|
function createAppEnvDts(app, remotes = []) {
|
|
1849
1857
|
const remoteModuleDeclarations = resolveRemoteRefs(app, remotes).flatMap((remote)=>Object.keys(remote.exposes ?? {}).filter((expose)=>'./Route' !== expose).map((expose)=>{
|
|
1850
1858
|
const moduleName = `${remoteDependencyAlias(remote)}/${expose.replace(/^\.\//u, '')}`;
|
|
1851
1859
|
return `declare module '${moduleName}' {
|
|
1852
|
-
const Component:
|
|
1860
|
+
const Component: React.ComponentType<Record<string, never>>;
|
|
1853
1861
|
export default Component;
|
|
1854
1862
|
}
|
|
1855
1863
|
`;
|
|
1856
1864
|
})).join('\n');
|
|
1857
|
-
|
|
1865
|
+
const reactTypeReference = remoteModuleDeclarations ? "/// <reference types='react' />\n" : '';
|
|
1866
|
+
const siteUrlDeclaration = 'declare const ULTRAMODERN_SITE_URL: string;';
|
|
1867
|
+
return `${reactTypeReference}/// <reference types='@modern-js/app-tools/types' />
|
|
1858
1868
|
|
|
1859
|
-
|
|
1869
|
+
${siteUrlDeclaration}
|
|
1860
1870
|
declare module '*.svg' {
|
|
1861
1871
|
const url: string;
|
|
1862
1872
|
export default url;
|
|
1863
1873
|
}
|
|
1874
|
+
declare module '*.css';
|
|
1864
1875
|
${remoteModuleDeclarations ? `\n${remoteModuleDeclarations}` : ''}`;
|
|
1865
1876
|
}
|
|
1866
1877
|
function createAppRuntimeConfig(app, scope, remotes = []) {
|
|
@@ -1889,10 +1900,10 @@ export default defineRuntimeConfig({
|
|
|
1889
1900
|
supportedLngs: ['en', 'cs'],
|
|
1890
1901
|
},
|
|
1891
1902
|
},
|
|
1903
|
+
${pluginsConfig}
|
|
1892
1904
|
router: {
|
|
1893
1905
|
framework: 'tanstack',
|
|
1894
1906
|
},
|
|
1895
|
-
${pluginsConfig}
|
|
1896
1907
|
});
|
|
1897
1908
|
`;
|
|
1898
1909
|
}
|
|
@@ -1953,7 +1964,7 @@ function createTw(prefix) {
|
|
|
1953
1964
|
function workspaceAssetsForApp(_app) {
|
|
1954
1965
|
return {};
|
|
1955
1966
|
}
|
|
1956
|
-
function createLocalizedHeadComponent() {
|
|
1967
|
+
function createLocalizedHeadComponent(includeLocationSuffix = true) {
|
|
1957
1968
|
return `const fallbackLanguage = 'en';
|
|
1958
1969
|
const supportedLanguages = ['en', 'cs'] as const;
|
|
1959
1970
|
type SupportedLanguage = (typeof supportedLanguages)[number];
|
|
@@ -1967,7 +1978,7 @@ const isSupportedLanguage = (value: string): value is SupportedLanguage =>
|
|
|
1967
1978
|
supportedLanguages.includes(value as SupportedLanguage);
|
|
1968
1979
|
|
|
1969
1980
|
const normalisePath = (pathname: string) => {
|
|
1970
|
-
const normalised = pathname.
|
|
1981
|
+
const normalised = pathname.replaceAll(/\\/+/gu, '/').replace(/\\/+$/u, '');
|
|
1971
1982
|
return normalised.length > 0 ? normalised : '/';
|
|
1972
1983
|
};
|
|
1973
1984
|
|
|
@@ -1980,7 +1991,7 @@ const stripLanguagePrefix = (pathname: string) => {
|
|
|
1980
1991
|
};
|
|
1981
1992
|
|
|
1982
1993
|
const escapeRegExp = (value: string) =>
|
|
1983
|
-
value.
|
|
1994
|
+
value.replaceAll(/[.*+?^\${}()|[\\]\\\\]/gu, '\\\\$&');
|
|
1984
1995
|
|
|
1985
1996
|
const paramName = (segment: string) => segment.slice(1).replace(/\\?$/u, '');
|
|
1986
1997
|
|
|
@@ -1997,16 +2008,19 @@ const matchPattern = (pathname: string, pattern: string) => {
|
|
|
1997
2008
|
return \`/\${escapeRegExp(segment)}\`;
|
|
1998
2009
|
})
|
|
1999
2010
|
.join('');
|
|
2000
|
-
const match = new RegExp(\`^\${source || '/'}
|
|
2011
|
+
const match = new RegExp(\`^\${source || '/'}$\`, 'u').exec(
|
|
2012
|
+
normalisePath(pathname),
|
|
2013
|
+
);
|
|
2001
2014
|
|
|
2002
|
-
if (
|
|
2003
|
-
return
|
|
2015
|
+
if (match === null) {
|
|
2016
|
+
return;
|
|
2004
2017
|
}
|
|
2005
2018
|
|
|
2006
|
-
|
|
2019
|
+
const params: Record<string, string> = {};
|
|
2020
|
+
for (const [index, name] of names.entries()) {
|
|
2007
2021
|
params[name] = decodeURIComponent(match[index + 1] ?? '');
|
|
2008
|
-
|
|
2009
|
-
|
|
2022
|
+
}
|
|
2023
|
+
return params;
|
|
2010
2024
|
};
|
|
2011
2025
|
|
|
2012
2026
|
const buildPath = (pattern: string, params: Record<string, string>) => {
|
|
@@ -2018,7 +2032,9 @@ const buildPath = (pattern: string, params: Record<string, string>) => {
|
|
|
2018
2032
|
return segment;
|
|
2019
2033
|
}
|
|
2020
2034
|
const value = params[paramName(segment)];
|
|
2021
|
-
return value
|
|
2035
|
+
return value !== undefined && value.length > 0
|
|
2036
|
+
? encodeURIComponent(value)
|
|
2037
|
+
: '';
|
|
2022
2038
|
})
|
|
2023
2039
|
.filter(Boolean)
|
|
2024
2040
|
.join('/');
|
|
@@ -2034,16 +2050,17 @@ const resolveLocalisedPath = (
|
|
|
2034
2050
|
|
|
2035
2051
|
for (const entry of Object.values(localisedUrls)) {
|
|
2036
2052
|
const targetPattern = entry[targetLanguage];
|
|
2037
|
-
if (
|
|
2053
|
+
if (targetPattern === undefined) {
|
|
2038
2054
|
continue;
|
|
2039
2055
|
}
|
|
2040
2056
|
|
|
2041
2057
|
for (const language of supportedLanguages) {
|
|
2042
2058
|
const sourcePattern = entry[language];
|
|
2043
|
-
const params =
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2059
|
+
const params =
|
|
2060
|
+
sourcePattern === undefined
|
|
2061
|
+
? undefined
|
|
2062
|
+
: matchPattern(pathWithoutLanguage, sourcePattern);
|
|
2063
|
+
if (params !== undefined) {
|
|
2047
2064
|
return buildPath(targetPattern, params);
|
|
2048
2065
|
}
|
|
2049
2066
|
}
|
|
@@ -2062,21 +2079,22 @@ const absoluteUrl = (pathname: string) => {
|
|
|
2062
2079
|
return \`\${origin}\${pathname}\`;
|
|
2063
2080
|
};
|
|
2064
2081
|
|
|
2065
|
-
const locationSuffix = (location: {
|
|
2082
|
+
${includeLocationSuffix ? `const locationSuffix = (location: {
|
|
2066
2083
|
hash?: unknown;
|
|
2067
2084
|
search?: unknown;
|
|
2068
2085
|
searchStr?: unknown;
|
|
2069
2086
|
}) => {
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2087
|
+
let locationSearch = '';
|
|
2088
|
+
if (typeof location.searchStr === 'string') {
|
|
2089
|
+
locationSearch = location.searchStr;
|
|
2090
|
+
} else if (typeof location.search === 'string') {
|
|
2091
|
+
locationSearch = location.search;
|
|
2092
|
+
}
|
|
2076
2093
|
const locationHash = typeof location.hash === 'string' ? location.hash : '';
|
|
2077
2094
|
|
|
2078
2095
|
return \`\${locationSearch}\${locationHash}\`;
|
|
2079
2096
|
};
|
|
2097
|
+
` : ''}
|
|
2080
2098
|
|
|
2081
2099
|
const LocalizedHead = () => {
|
|
2082
2100
|
const location = useLocation();
|
|
@@ -2114,7 +2132,7 @@ import { VerticalShowcase } from '../vertical-components';
|
|
|
2114
2132
|
import { ultramodernLocalisedUrls } from '../ultramodern-route-metadata';
|
|
2115
2133
|
import { ultramodernUiMarker } from '../../ultramodern-build';
|
|
2116
2134
|
|
|
2117
|
-
${createLocalizedHeadComponent()}
|
|
2135
|
+
${createLocalizedHeadComponent(false)}
|
|
2118
2136
|
export default function ShellHome() {
|
|
2119
2137
|
const { i18nInstance } = useModernI18n();
|
|
2120
2138
|
const t = i18nInstance['t'].bind(i18nInstance);
|
|
@@ -2172,9 +2190,9 @@ import { ultramodernLocalisedUrls } from './ultramodern-route-metadata';
|
|
|
2172
2190
|
const supportedLanguages = ['en', 'cs'] as const;
|
|
2173
2191
|
type SupportedLanguage = (typeof supportedLanguages)[number];
|
|
2174
2192
|
|
|
2175
|
-
|
|
2193
|
+
interface ShellFrameProps {
|
|
2176
2194
|
children: ReactNode;
|
|
2177
|
-
}
|
|
2195
|
+
}
|
|
2178
2196
|
|
|
2179
2197
|
const localisedUrls = ultramodernLocalisedUrls as Record<
|
|
2180
2198
|
string,
|
|
@@ -2185,7 +2203,7 @@ const isSupportedLanguage = (value: string): value is SupportedLanguage =>
|
|
|
2185
2203
|
supportedLanguages.includes(value as SupportedLanguage);
|
|
2186
2204
|
|
|
2187
2205
|
const normalisePath = (pathname: string) => {
|
|
2188
|
-
const normalised = pathname.
|
|
2206
|
+
const normalised = pathname.replaceAll(/\\/+/gu, '/').replace(/\\/+$/u, '');
|
|
2189
2207
|
return normalised.length > 0 ? normalised : '/';
|
|
2190
2208
|
};
|
|
2191
2209
|
|
|
@@ -2198,7 +2216,7 @@ const stripLanguagePrefix = (pathname: string) => {
|
|
|
2198
2216
|
};
|
|
2199
2217
|
|
|
2200
2218
|
const escapeRegExp = (value: string) =>
|
|
2201
|
-
value.
|
|
2219
|
+
value.replaceAll(/[.*+?^\${}()|[\\]\\\\]/gu, '\\\\$&');
|
|
2202
2220
|
|
|
2203
2221
|
const paramName = (segment: string) => segment.slice(1).replace(/\\?$/u, '');
|
|
2204
2222
|
|
|
@@ -2215,16 +2233,19 @@ const matchPattern = (pathname: string, pattern: string) => {
|
|
|
2215
2233
|
return \`/\${escapeRegExp(segment)}\`;
|
|
2216
2234
|
})
|
|
2217
2235
|
.join('');
|
|
2218
|
-
const match = new RegExp(\`^\${source || '/'}
|
|
2236
|
+
const match = new RegExp(\`^\${source || '/'}$\`, 'u').exec(
|
|
2237
|
+
normalisePath(pathname),
|
|
2238
|
+
);
|
|
2219
2239
|
|
|
2220
|
-
if (
|
|
2221
|
-
return
|
|
2240
|
+
if (match === null) {
|
|
2241
|
+
return;
|
|
2222
2242
|
}
|
|
2223
2243
|
|
|
2224
|
-
|
|
2244
|
+
const params: Record<string, string> = {};
|
|
2245
|
+
for (const [index, name] of names.entries()) {
|
|
2225
2246
|
params[name] = decodeURIComponent(match[index + 1] ?? '');
|
|
2226
|
-
|
|
2227
|
-
|
|
2247
|
+
}
|
|
2248
|
+
return params;
|
|
2228
2249
|
};
|
|
2229
2250
|
|
|
2230
2251
|
const buildPath = (pattern: string, params: Record<string, string>) => {
|
|
@@ -2236,7 +2257,9 @@ const buildPath = (pattern: string, params: Record<string, string>) => {
|
|
|
2236
2257
|
return segment;
|
|
2237
2258
|
}
|
|
2238
2259
|
const value = params[paramName(segment)];
|
|
2239
|
-
return value
|
|
2260
|
+
return value !== undefined && value.length > 0
|
|
2261
|
+
? encodeURIComponent(value)
|
|
2262
|
+
: '';
|
|
2240
2263
|
})
|
|
2241
2264
|
.filter(Boolean)
|
|
2242
2265
|
.join('/');
|
|
@@ -2252,16 +2275,17 @@ const resolveLocalisedPath = (
|
|
|
2252
2275
|
|
|
2253
2276
|
for (const entry of Object.values(localisedUrls)) {
|
|
2254
2277
|
const targetPattern = entry[targetLanguage];
|
|
2255
|
-
if (
|
|
2278
|
+
if (targetPattern === undefined) {
|
|
2256
2279
|
continue;
|
|
2257
2280
|
}
|
|
2258
2281
|
|
|
2259
2282
|
for (const language of supportedLanguages) {
|
|
2260
2283
|
const sourcePattern = entry[language];
|
|
2261
|
-
const params =
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2284
|
+
const params =
|
|
2285
|
+
sourcePattern === undefined
|
|
2286
|
+
? undefined
|
|
2287
|
+
: matchPattern(pathWithoutLanguage, sourcePattern);
|
|
2288
|
+
if (params !== undefined) {
|
|
2265
2289
|
return buildPath(targetPattern, params);
|
|
2266
2290
|
}
|
|
2267
2291
|
}
|
|
@@ -2280,12 +2304,12 @@ const locationSuffix = (location: {
|
|
|
2280
2304
|
search?: unknown;
|
|
2281
2305
|
searchStr?: unknown;
|
|
2282
2306
|
}) => {
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2307
|
+
let locationSearch = '';
|
|
2308
|
+
if (typeof location.searchStr === 'string') {
|
|
2309
|
+
locationSearch = location.searchStr;
|
|
2310
|
+
} else if (typeof location.search === 'string') {
|
|
2311
|
+
locationSearch = location.search;
|
|
2312
|
+
}
|
|
2289
2313
|
const locationHash = typeof location.hash === 'string' ? location.hash : '';
|
|
2290
2314
|
|
|
2291
2315
|
return \`\${locationSearch}\${locationHash}\`;
|
|
@@ -2351,21 +2375,19 @@ function createShellRemoteComponents(scope, remotes = []) {
|
|
|
2351
2375
|
const remoteCount = String(widgetRemotes.length);
|
|
2352
2376
|
return `import { createLazyComponent } from '@module-federation/modern-js-v3/react';
|
|
2353
2377
|
import { getInstance, loadRemote } from '@module-federation/modern-js-v3/runtime';
|
|
2354
|
-
import { Suspense, useEffect, useMemo, useState
|
|
2378
|
+
import { Suspense, useEffect, useMemo, useState } from 'react';
|
|
2379
|
+
import type { ComponentType } from 'react';
|
|
2355
2380
|
import { I18nLink, useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2356
2381
|
${serverImports}
|
|
2357
2382
|
|
|
2358
|
-
|
|
2383
|
+
interface RemoteComponentModule {
|
|
2359
2384
|
default: ComponentType;
|
|
2360
|
-
}
|
|
2385
|
+
}
|
|
2361
2386
|
|
|
2362
|
-
const
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
}
|
|
2367
|
-
return module;
|
|
2368
|
-
};
|
|
2387
|
+
const widgetCount = Number('${remoteCount}');
|
|
2388
|
+
|
|
2389
|
+
const loadRemoteComponent = (specifier: string) =>
|
|
2390
|
+
loadRemote<RemoteComponentModule>(specifier) as Promise<RemoteComponentModule>;
|
|
2369
2391
|
|
|
2370
2392
|
const remoteFallback =
|
|
2371
2393
|
({ error }: { error: Error }) => {
|
|
@@ -2374,11 +2396,9 @@ const remoteFallback =
|
|
|
2374
2396
|
return <div className="${tw('rounded-xl border border-red-900/20 bg-red-50 px-4 py-3 text-sm font-semibold text-red-900')}" data-remote-error={error.name}>{t('shell.remoteUnavailable')}</div>;
|
|
2375
2397
|
};
|
|
2376
2398
|
|
|
2377
|
-
const createHydratedRemote =
|
|
2378
|
-
ServerComponent: ComponentType,
|
|
2379
|
-
|
|
2380
|
-
) => {
|
|
2381
|
-
return function HydratedRemote() {
|
|
2399
|
+
const createHydratedRemote =
|
|
2400
|
+
(ServerComponent: ComponentType, specifier: string) =>
|
|
2401
|
+
function HydratedRemote() {
|
|
2382
2402
|
const [hydrated, setHydrated] = useState(false);
|
|
2383
2403
|
|
|
2384
2404
|
useEffect(() => {
|
|
@@ -2387,11 +2407,11 @@ const createHydratedRemote = (
|
|
|
2387
2407
|
|
|
2388
2408
|
const FederatedComponent = useMemo(() => {
|
|
2389
2409
|
if (!hydrated) {
|
|
2390
|
-
return
|
|
2410
|
+
return null;
|
|
2391
2411
|
}
|
|
2392
2412
|
const instance = getInstance();
|
|
2393
|
-
if (
|
|
2394
|
-
return
|
|
2413
|
+
if (instance === null || instance === undefined) {
|
|
2414
|
+
return null;
|
|
2395
2415
|
}
|
|
2396
2416
|
return createLazyComponent({
|
|
2397
2417
|
export: 'default',
|
|
@@ -2402,7 +2422,7 @@ const createHydratedRemote = (
|
|
|
2402
2422
|
});
|
|
2403
2423
|
}, [hydrated]);
|
|
2404
2424
|
|
|
2405
|
-
if (
|
|
2425
|
+
if (FederatedComponent === null) {
|
|
2406
2426
|
return <ServerComponent />;
|
|
2407
2427
|
}
|
|
2408
2428
|
|
|
@@ -2412,11 +2432,10 @@ const createHydratedRemote = (
|
|
|
2412
2432
|
</Suspense>
|
|
2413
2433
|
);
|
|
2414
2434
|
};
|
|
2415
|
-
};
|
|
2416
2435
|
|
|
2417
2436
|
${hydratedExports}
|
|
2418
2437
|
|
|
2419
|
-
export
|
|
2438
|
+
export const Header = () => {
|
|
2420
2439
|
const { i18nInstance } = useModernI18n();
|
|
2421
2440
|
const t = i18nInstance['t'].bind(i18nInstance);
|
|
2422
2441
|
|
|
@@ -2425,24 +2444,24 @@ export function Header() {
|
|
|
2425
2444
|
<I18nLink className="${tw('whitespace-nowrap text-xl font-black tracking-normal text-stone-950 no-underline')}" to="/">{t('shell.title')}</I18nLink>
|
|
2426
2445
|
</header>
|
|
2427
2446
|
);
|
|
2428
|
-
}
|
|
2447
|
+
};
|
|
2429
2448
|
|
|
2430
|
-
export
|
|
2449
|
+
export const StatusBadge = () => {
|
|
2431
2450
|
const { i18nInstance } = useModernI18n();
|
|
2432
2451
|
const t = i18nInstance['t'].bind(i18nInstance);
|
|
2433
2452
|
|
|
2434
2453
|
return (
|
|
2435
2454
|
<span className="${tw('inline-flex h-10 shrink-0 items-center justify-center rounded-full border border-stone-900/15 bg-white px-4 text-sm font-extrabold text-stone-950 shadow-lg shadow-stone-900/5')}">
|
|
2436
|
-
{
|
|
2455
|
+
{widgetCount} {t('shell.hero.cardOneKicker')}
|
|
2437
2456
|
</span>
|
|
2438
2457
|
);
|
|
2439
|
-
}
|
|
2458
|
+
};
|
|
2440
2459
|
|
|
2441
|
-
export
|
|
2460
|
+
export const VerticalShowcase = () => {
|
|
2442
2461
|
const { i18nInstance } = useModernI18n();
|
|
2443
2462
|
const t = i18nInstance['t'].bind(i18nInstance);
|
|
2444
2463
|
|
|
2445
|
-
if (
|
|
2464
|
+
if (widgetCount === 0) {
|
|
2446
2465
|
return (
|
|
2447
2466
|
<section className="${tw('mx-auto mt-12 max-w-7xl rounded-2xl bg-white/90 p-6 shadow-xl shadow-stone-900/10')}">
|
|
2448
2467
|
<p className="${tw('text-lg font-bold text-stone-700')}">{t('shell.hero.empty')}</p>
|
|
@@ -2457,39 +2476,50 @@ ${showcaseItems}
|
|
|
2457
2476
|
</div>
|
|
2458
2477
|
</section>
|
|
2459
2478
|
);
|
|
2460
|
-
}
|
|
2479
|
+
};
|
|
2461
2480
|
`;
|
|
2462
2481
|
}
|
|
2463
2482
|
function createRemotePage(app) {
|
|
2464
2483
|
const tw = createTw(tailwindPrefixForApp(app));
|
|
2484
|
+
const listEffectItems = `list${toPascalCase(effectApiStem(app))}`;
|
|
2465
2485
|
const effectBffImport = appHasEffectApi(app) ? `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2466
2486
|
import { Helmet } from '@modern-js/runtime/head';
|
|
2467
2487
|
import { useLocation } from '@modern-js/plugin-tanstack/runtime';
|
|
2468
2488
|
import { useEffect, useState } from 'react';
|
|
2489
|
+
import {
|
|
2490
|
+
Effect,
|
|
2491
|
+
${listEffectItems},
|
|
2492
|
+
runEffectRequest,
|
|
2493
|
+
} from '../../effect/${effectApiStem(app)}-client';
|
|
2469
2494
|
import { ultramodernLocalisedUrls } from '../ultramodern-route-metadata';
|
|
2470
2495
|
import { ultramodernUiMarker } from '../../ultramodern-build';
|
|
2471
2496
|
` : "import { useModernI18n } from '@modern-js/plugin-i18n/runtime';\nimport { Helmet } from '@modern-js/runtime/head';\nimport { useLocation } from '@modern-js/plugin-tanstack/runtime';\nimport { ultramodernLocalisedUrls } from '../ultramodern-route-metadata';\nimport { ultramodernUiMarker } from '../../ultramodern-build';\n";
|
|
2472
2497
|
const effectBffState = appHasEffectApi(app) ? ` const [effectApiStatus, setEffectApiStatus] = useState('pending');
|
|
2473
2498
|
|
|
2474
2499
|
useEffect(() => {
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2500
|
+
let cancelled = false;
|
|
2501
|
+
void runEffectRequest(
|
|
2502
|
+
${listEffectItems}({ limit: 1 }).pipe(
|
|
2503
|
+
Effect.match({
|
|
2504
|
+
onFailure: () => {
|
|
2505
|
+
if (cancelled) {
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
setEffectApiStatus('unavailable');
|
|
2509
|
+
},
|
|
2510
|
+
onSuccess: data => {
|
|
2511
|
+
if (cancelled) {
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
setEffectApiStatus(data.items.at(0)?.title ?? 'empty');
|
|
2515
|
+
},
|
|
2516
|
+
}),
|
|
2517
|
+
),
|
|
2518
|
+
);
|
|
2484
2519
|
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
setEffectApiStatus(data.items[0]?.title ?? 'empty');
|
|
2489
|
-
})
|
|
2490
|
-
.catch(() => {
|
|
2491
|
-
setEffectApiStatus('unavailable');
|
|
2492
|
-
});
|
|
2520
|
+
return () => {
|
|
2521
|
+
cancelled = true;
|
|
2522
|
+
};
|
|
2493
2523
|
}, []);
|
|
2494
2524
|
|
|
2495
2525
|
` : '';
|
|
@@ -2518,7 +2548,7 @@ ${effectBffState} return (
|
|
|
2518
2548
|
))}
|
|
2519
2549
|
</nav>
|
|
2520
2550
|
<h1 className="${tw('mt-10 text-5xl font-black')}">{t('${app.domain}.title')}</h1>
|
|
2521
|
-
<p className="${tw('mt-3 text-lg text-stone-600')}" data-mf-role="${app.kind}">{t('${app.domain}.role')}</p>
|
|
2551
|
+
<p className="${tw('mt-3 text-lg text-stone-600')}" data-modern-mf-role="${app.kind}">{t('${app.domain}.role')}</p>
|
|
2522
2552
|
<p className="${tw('sr-only')}" data-build-marker={ultramodernUiMarker.build} data-testid="ultramodern-ui-marker">
|
|
2523
2553
|
{ultramodernUiMarker.appId}:{ultramodernUiMarker.version}
|
|
2524
2554
|
</p>
|
|
@@ -2554,7 +2584,7 @@ export default function ${toPascalCase(domain)}Route() {
|
|
|
2554
2584
|
const t = i18nInstance['t'].bind(i18nInstance);
|
|
2555
2585
|
|
|
2556
2586
|
return (
|
|
2557
|
-
<section className="${tw('rounded-2xl bg-white/90 p-5 shadow-xl shadow-stone-900/10')}" data-
|
|
2587
|
+
<section className="${tw('rounded-2xl bg-white/90 p-5 shadow-xl shadow-stone-900/10')}" data-modern-boundary-id="${app.mfName}" data-modern-mf-expose="./Route">
|
|
2558
2588
|
<h2 className="${tw('text-2xl font-black')}">{t('${domain}.title')}</h2>
|
|
2559
2589
|
<p className="${tw('mt-2 text-stone-600')}">{t('${domain}.routeSurface')}</p>
|
|
2560
2590
|
</section>
|
|
@@ -2573,7 +2603,7 @@ export default function ${componentName}() {
|
|
|
2573
2603
|
const t = i18nInstance['t'].bind(i18nInstance);
|
|
2574
2604
|
|
|
2575
2605
|
return (
|
|
2576
|
-
<section className="${tw('rounded-2xl bg-white/90 p-5 shadow-xl shadow-stone-900/10')}" data-
|
|
2606
|
+
<section className="${tw('rounded-2xl bg-white/90 p-5 shadow-xl shadow-stone-900/10')}" data-modern-boundary-id="${app.mfName}" data-modern-mf-expose="./Widget">
|
|
2577
2607
|
<h2 className="${tw('text-2xl font-black')}">{t('${domain}.title')}</h2>
|
|
2578
2608
|
<p className="${tw('mt-2 text-stone-600')}">{t('${domain}.widgetBody')}</p>
|
|
2579
2609
|
</section>
|
|
@@ -2672,7 +2702,7 @@ export default function ${componentName}() {
|
|
|
2672
2702
|
|
|
2673
2703
|
return (
|
|
2674
2704
|
<>
|
|
2675
|
-
<section className="${tw('mx-auto mt-10 grid max-w-7xl items-center gap-8 md:grid-cols-[1fr_0.95fr] lg:gap-14')}" data-modern-boundary-id="${app.mfName}" data-modern-mf-expose="${expose}"
|
|
2705
|
+
<section className="${tw('mx-auto mt-10 grid max-w-7xl items-center gap-8 md:grid-cols-[1fr_0.95fr] lg:gap-14')}" data-modern-boundary-id="${app.mfName}" data-modern-mf-expose="${expose}">
|
|
2676
2706
|
<div className="${tw('rounded-3xl border-[18px] border-amber-200 bg-white/90 p-8 shadow-2xl shadow-stone-900/15')}">
|
|
2677
2707
|
<p className="${tw('text-xs font-black uppercase tracking-[0.18em] text-emerald-800')}">{t('records.record.lifecycle')}</p>
|
|
2678
2708
|
<dl className="${tw('mt-6 grid gap-4')}">
|
|
@@ -2741,7 +2771,7 @@ export default function ${componentName}() {
|
|
|
2741
2771
|
const queue = useActionQueue();
|
|
2742
2772
|
|
|
2743
2773
|
return (
|
|
2744
|
-
<section className="${tw('mx-auto mt-10 max-w-7xl')}" data-modern-boundary-id="${app.mfName}" data-modern-mf-expose="${expose}"
|
|
2774
|
+
<section className="${tw('mx-auto mt-10 max-w-7xl')}" data-modern-boundary-id="${app.mfName}" data-modern-mf-expose="${expose}">
|
|
2745
2775
|
<h1 className="${tw('text-5xl font-black leading-none tracking-normal text-stone-950 md:text-7xl')}">{t('actions.queue.title')}</h1>
|
|
2746
2776
|
<div className="${tw('mt-8 rounded-2xl bg-white/90 p-5 shadow-xl shadow-stone-900/10')}">
|
|
2747
2777
|
{queue.lines.length === 0 ? (
|
|
@@ -2783,7 +2813,7 @@ export default function ${componentName}() {
|
|
|
2783
2813
|
const t = i18nInstance['t'].bind(i18nInstance);
|
|
2784
2814
|
|
|
2785
2815
|
return (
|
|
2786
|
-
<section className="${tw('rounded-2xl bg-white/90 p-5 shadow-xl shadow-stone-900/10')}" data-modern-boundary-id="${app.mfName}" data-modern-mf-expose="${expose}"
|
|
2816
|
+
<section className="${tw('rounded-2xl bg-white/90 p-5 shadow-xl shadow-stone-900/10')}" data-modern-boundary-id="${app.mfName}" data-modern-mf-expose="${expose}">
|
|
2787
2817
|
<h2 className="${tw('text-2xl font-black')}">{t('${domain}.title')}</h2>
|
|
2788
2818
|
<p className="${tw('mt-2 text-stone-600')}">{t('${domain}.federatedSurface')}</p>
|
|
2789
2819
|
</section>
|
|
@@ -2795,22 +2825,18 @@ function createRecordsRemoteComponents(scope, app) {
|
|
|
2795
2825
|
const tw = createTw(tailwindPrefixForApp(app));
|
|
2796
2826
|
return `import { createLazyComponent } from '@module-federation/modern-js-v3/react';
|
|
2797
2827
|
import { getInstance, loadRemote } from '@module-federation/modern-js-v3/runtime';
|
|
2798
|
-
import { Suspense, useEffect, useMemo, useState
|
|
2828
|
+
import { Suspense, useEffect, useMemo, useState } from 'react';
|
|
2829
|
+
import type { ComponentType } from 'react';
|
|
2799
2830
|
import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2800
2831
|
import HighlightsServer from '${ultramodern_workspace_packageName(scope, 'workspace')}/Highlights';
|
|
2801
2832
|
import StartActionServer from '${ultramodern_workspace_packageName(scope, 'actions')}/StartAction';
|
|
2802
2833
|
|
|
2803
|
-
|
|
2834
|
+
interface RemoteComponentModule {
|
|
2804
2835
|
default: ComponentType;
|
|
2805
|
-
}
|
|
2836
|
+
}
|
|
2806
2837
|
|
|
2807
|
-
const loadRemoteComponent =
|
|
2808
|
-
|
|
2809
|
-
if (!module) {
|
|
2810
|
-
throw new Error(\`Remote module unavailable: \${specifier}\`);
|
|
2811
|
-
}
|
|
2812
|
-
return module;
|
|
2813
|
-
};
|
|
2838
|
+
const loadRemoteComponent = (specifier: string) =>
|
|
2839
|
+
loadRemote<RemoteComponentModule>(specifier) as Promise<RemoteComponentModule>;
|
|
2814
2840
|
|
|
2815
2841
|
const remoteFallback =
|
|
2816
2842
|
({ error }: { error: Error }) => {
|
|
@@ -2819,11 +2845,9 @@ const remoteFallback =
|
|
|
2819
2845
|
return <div className="${tw('rounded-xl border border-red-900/20 bg-red-50 px-4 py-3 text-sm font-semibold text-red-900')}" data-remote-error={error.name}>{t('records.remoteUnavailable')}</div>;
|
|
2820
2846
|
};
|
|
2821
2847
|
|
|
2822
|
-
const createHydratedRemote =
|
|
2823
|
-
ServerComponent: ComponentType,
|
|
2824
|
-
|
|
2825
|
-
) => {
|
|
2826
|
-
return function HydratedRemote() {
|
|
2848
|
+
const createHydratedRemote =
|
|
2849
|
+
(ServerComponent: ComponentType, specifier: string) =>
|
|
2850
|
+
function HydratedRemote() {
|
|
2827
2851
|
const [hydrated, setHydrated] = useState(false);
|
|
2828
2852
|
|
|
2829
2853
|
useEffect(() => {
|
|
@@ -2832,11 +2856,11 @@ const createHydratedRemote = (
|
|
|
2832
2856
|
|
|
2833
2857
|
const FederatedComponent = useMemo(() => {
|
|
2834
2858
|
if (!hydrated) {
|
|
2835
|
-
return
|
|
2859
|
+
return null;
|
|
2836
2860
|
}
|
|
2837
2861
|
const instance = getInstance();
|
|
2838
|
-
if (
|
|
2839
|
-
return
|
|
2862
|
+
if (instance === null || instance === undefined) {
|
|
2863
|
+
return null;
|
|
2840
2864
|
}
|
|
2841
2865
|
return createLazyComponent({
|
|
2842
2866
|
export: 'default',
|
|
@@ -2847,7 +2871,7 @@ const createHydratedRemote = (
|
|
|
2847
2871
|
});
|
|
2848
2872
|
}, [hydrated]);
|
|
2849
2873
|
|
|
2850
|
-
if (
|
|
2874
|
+
if (FederatedComponent === null) {
|
|
2851
2875
|
return <ServerComponent />;
|
|
2852
2876
|
}
|
|
2853
2877
|
|
|
@@ -2857,7 +2881,6 @@ const createHydratedRemote = (
|
|
|
2857
2881
|
</Suspense>
|
|
2858
2882
|
);
|
|
2859
2883
|
};
|
|
2860
|
-
};
|
|
2861
2884
|
|
|
2862
2885
|
export const Highlights = createHydratedRemote(HighlightsServer, 'workspace/Highlights');
|
|
2863
2886
|
export const StartAction = createHydratedRemote(StartActionServer, 'actions/StartAction');
|
|
@@ -2960,7 +2983,7 @@ const generatedLocaleResources = {
|
|
|
2960
2983
|
},
|
|
2961
2984
|
shell: {
|
|
2962
2985
|
boundaries: {
|
|
2963
|
-
toggle: 'zobrazit hranice
|
|
2986
|
+
toggle: 'zobrazit hranice týmů'
|
|
2964
2987
|
},
|
|
2965
2988
|
hero: {
|
|
2966
2989
|
cardOne: 'Přidejte první business vertical příkazem create <domain> --vertical, až ho opravdu potřebujete.',
|
|
@@ -3069,7 +3092,7 @@ const generatedLocaleResources = {
|
|
|
3069
3092
|
},
|
|
3070
3093
|
shell: {
|
|
3071
3094
|
boundaries: {
|
|
3072
|
-
toggle: 'show
|
|
3095
|
+
toggle: 'show team boundaries'
|
|
3073
3096
|
},
|
|
3074
3097
|
hero: {
|
|
3075
3098
|
cardOne: 'Add the first business vertical with create <domain> --vertical when the product needs one.',
|
|
@@ -3454,8 +3477,7 @@ const ${groupName}Items = [
|
|
|
3454
3477
|
},
|
|
3455
3478
|
];
|
|
3456
3479
|
|
|
3457
|
-
const operationAttributes = (operationContext: OperationContext) => {
|
|
3458
|
-
return {
|
|
3480
|
+
const operationAttributes = (operationContext: OperationContext) => ({
|
|
3459
3481
|
'modernjs.operation.id': operationContext.operationId,
|
|
3460
3482
|
'modernjs.operation.method': operationContext.method,
|
|
3461
3483
|
'modernjs.operation.route': operationContext.routePath,
|
|
@@ -3463,8 +3485,7 @@ const operationAttributes = (operationContext: OperationContext) => {
|
|
|
3463
3485
|
...(typeof operationContext.traceId === 'string'
|
|
3464
3486
|
? { 'modernjs.trace.id': operationContext.traceId }
|
|
3465
3487
|
: {}),
|
|
3466
|
-
};
|
|
3467
|
-
};
|
|
3488
|
+
});
|
|
3468
3489
|
|
|
3469
3490
|
const ${groupName}Layer = HttpApiBuilder.group(
|
|
3470
3491
|
${apiExport},
|
|
@@ -3555,6 +3576,7 @@ function createEffectClient(service, contractImportPath) {
|
|
|
3555
3576
|
const getName = `get${toPascalCase(singular)}`;
|
|
3556
3577
|
const createName = `create${toPascalCase(singular)}`;
|
|
3557
3578
|
return `import {
|
|
3579
|
+
Effect,
|
|
3558
3580
|
makeEffectHttpApiClient,
|
|
3559
3581
|
runEffectRequest,
|
|
3560
3582
|
} from '@modern-js/plugin-bff/effect-client';
|
|
@@ -3562,85 +3584,74 @@ import {
|
|
|
3562
3584
|
${contractExport}ApiContract,
|
|
3563
3585
|
${apiExport},
|
|
3564
3586
|
${groupName}OperationContexts,
|
|
3565
|
-
type OperationContext,
|
|
3566
3587
|
} from '${contractImportPath}';
|
|
3588
|
+
import type { OperationContext } from '${contractImportPath}';
|
|
3589
|
+
|
|
3590
|
+
export { Effect, runEffectRequest };
|
|
3567
3591
|
|
|
3568
|
-
export
|
|
3592
|
+
export interface ${clientOptionsName} {
|
|
3569
3593
|
baseUrl?: string | URL;
|
|
3570
3594
|
locale?: string;
|
|
3571
3595
|
operationContext?: OperationContext;
|
|
3572
3596
|
traceparent?: string;
|
|
3573
|
-
}
|
|
3597
|
+
}
|
|
3574
3598
|
|
|
3575
|
-
export
|
|
3599
|
+
export const ${createClientName} = (
|
|
3576
3600
|
options: ${clientOptionsName} = {},
|
|
3577
|
-
)
|
|
3578
|
-
|
|
3601
|
+
) =>
|
|
3602
|
+
makeEffectHttpApiClient(${apiExport}, {
|
|
3579
3603
|
baseUrl: options.baseUrl ?? ${contractExport}ApiContract.apiPrefix,
|
|
3580
3604
|
});
|
|
3581
|
-
}
|
|
3582
3605
|
|
|
3583
|
-
export
|
|
3606
|
+
export const ${listName} = (
|
|
3584
3607
|
options: ${clientOptionsName} & { limit?: number } = {},
|
|
3585
|
-
)
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
operationContext
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
).then(client =>
|
|
3593
|
-
runEffectRequest(
|
|
3608
|
+
) =>
|
|
3609
|
+
${createClientName}({
|
|
3610
|
+
...options,
|
|
3611
|
+
operationContext:
|
|
3612
|
+
options.operationContext ?? ${groupName}OperationContexts.list,
|
|
3613
|
+
}).pipe(
|
|
3614
|
+
Effect.flatMap(client =>
|
|
3594
3615
|
client.${groupName}.list({ query: { limit: options.limit } }),
|
|
3595
3616
|
),
|
|
3596
3617
|
);
|
|
3597
|
-
}
|
|
3598
3618
|
|
|
3599
|
-
export
|
|
3619
|
+
export const ${readinessName} = (
|
|
3600
3620
|
options: ${clientOptionsName} = {},
|
|
3601
|
-
)
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
operationContext
|
|
3606
|
-
|
|
3607
|
-
}),
|
|
3608
|
-
).then(client =>
|
|
3609
|
-
runEffectRequest(client.${groupName}.readiness({})),
|
|
3621
|
+
) =>
|
|
3622
|
+
${createClientName}({
|
|
3623
|
+
...options,
|
|
3624
|
+
operationContext:
|
|
3625
|
+
options.operationContext ?? ${groupName}OperationContexts.readiness,
|
|
3626
|
+
}).pipe(
|
|
3627
|
+
Effect.flatMap(client => client.${groupName}.readiness({})),
|
|
3610
3628
|
);
|
|
3611
|
-
}
|
|
3612
3629
|
|
|
3613
|
-
export
|
|
3630
|
+
export const ${getName} = (
|
|
3614
3631
|
id: string,
|
|
3615
3632
|
options: ${clientOptionsName} = {},
|
|
3616
|
-
)
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
operationContext
|
|
3621
|
-
|
|
3622
|
-
}),
|
|
3623
|
-
).then(client =>
|
|
3624
|
-
runEffectRequest(client.${groupName}.get({ params: { id } })),
|
|
3633
|
+
) =>
|
|
3634
|
+
${createClientName}({
|
|
3635
|
+
...options,
|
|
3636
|
+
operationContext:
|
|
3637
|
+
options.operationContext ?? ${groupName}OperationContexts.get,
|
|
3638
|
+
}).pipe(
|
|
3639
|
+
Effect.flatMap(client => client.${groupName}.get({ params: { id } })),
|
|
3625
3640
|
);
|
|
3626
|
-
}
|
|
3627
3641
|
|
|
3628
|
-
export
|
|
3642
|
+
export const ${createName} = (
|
|
3629
3643
|
title: string,
|
|
3630
3644
|
options: ${clientOptionsName} = {},
|
|
3631
|
-
)
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
operationContext
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
).then(client =>
|
|
3639
|
-
runEffectRequest(
|
|
3645
|
+
) =>
|
|
3646
|
+
${createClientName}({
|
|
3647
|
+
...options,
|
|
3648
|
+
operationContext:
|
|
3649
|
+
options.operationContext ?? ${groupName}OperationContexts.create,
|
|
3650
|
+
}).pipe(
|
|
3651
|
+
Effect.flatMap(client =>
|
|
3640
3652
|
client.${groupName}.create({ payload: { title } }),
|
|
3641
3653
|
),
|
|
3642
3654
|
);
|
|
3643
|
-
}
|
|
3644
3655
|
`;
|
|
3645
3656
|
}
|
|
3646
3657
|
function createShellEffectClient(scope, remotes = []) {
|
|
@@ -3850,6 +3861,7 @@ function createTopology(scope, remotes = []) {
|
|
|
3850
3861
|
validation: {
|
|
3851
3862
|
script: "scripts/validate-ultramodern-workspace.mjs",
|
|
3852
3863
|
commands: [
|
|
3864
|
+
'pnpm ultramodern:i18n-boundaries',
|
|
3853
3865
|
'pnpm ultramodern:check'
|
|
3854
3866
|
]
|
|
3855
3867
|
}
|
|
@@ -4193,7 +4205,7 @@ function createAppGeneratedContract(scope, app, apps, enableTailwind) {
|
|
|
4193
4205
|
}
|
|
4194
4206
|
},
|
|
4195
4207
|
ssr: {
|
|
4196
|
-
mode: '
|
|
4208
|
+
mode: 'string',
|
|
4197
4209
|
moduleFederationAppSSR: true
|
|
4198
4210
|
},
|
|
4199
4211
|
i18n: {
|
|
@@ -4415,6 +4427,7 @@ function createTemplateManifest(modernVersion, packageSource) {
|
|
|
4415
4427
|
expectedCommands: [
|
|
4416
4428
|
'mise install',
|
|
4417
4429
|
'pnpm install',
|
|
4430
|
+
'pnpm run ultramodern:i18n-boundaries',
|
|
4418
4431
|
'pnpm run ultramodern:check'
|
|
4419
4432
|
]
|
|
4420
4433
|
}
|
|
@@ -4492,6 +4505,197 @@ for (const appDir of appDirs) {
|
|
|
4492
4505
|
}
|
|
4493
4506
|
`;
|
|
4494
4507
|
}
|
|
4508
|
+
function createWorkspaceI18nBoundaryValidationScript() {
|
|
4509
|
+
return `#!/usr/bin/env node
|
|
4510
|
+
import fs from 'node:fs';
|
|
4511
|
+
import path from 'node:path';
|
|
4512
|
+
import { fileURLToPath } from 'node:url';
|
|
4513
|
+
|
|
4514
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
4515
|
+
const sourceRoots = ['apps', 'verticals'];
|
|
4516
|
+
const languageConditionalPattern =
|
|
4517
|
+
/\\b(language|locale|lng|currentLanguage)\\s*={0,2}={1,2}\\s*['"][a-z-]+['"]\\s*\\?\\s*([^:;\\n]+)\\s*:\\s*([^;\\n})]+)/gu;
|
|
4518
|
+
const allowedLanguageConditionalBranches = new Set([
|
|
4519
|
+
"'page'",
|
|
4520
|
+
'"page"',
|
|
4521
|
+
'undefined',
|
|
4522
|
+
'null',
|
|
4523
|
+
'true',
|
|
4524
|
+
'false',
|
|
4525
|
+
]);
|
|
4526
|
+
const visibleCopyAttributes = new Set([
|
|
4527
|
+
'alt',
|
|
4528
|
+
'aria-label',
|
|
4529
|
+
'label',
|
|
4530
|
+
'placeholder',
|
|
4531
|
+
'title',
|
|
4532
|
+
]);
|
|
4533
|
+
|
|
4534
|
+
function fail(message) {
|
|
4535
|
+
throw new Error(message);
|
|
4536
|
+
}
|
|
4537
|
+
|
|
4538
|
+
function walk(directory, files = []) {
|
|
4539
|
+
if (!fs.existsSync(directory)) {
|
|
4540
|
+
return files;
|
|
4541
|
+
}
|
|
4542
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
4543
|
+
if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.output') {
|
|
4544
|
+
continue;
|
|
4545
|
+
}
|
|
4546
|
+
const entryPath = path.join(directory, entry.name);
|
|
4547
|
+
if (entry.isDirectory()) {
|
|
4548
|
+
walk(entryPath, files);
|
|
4549
|
+
} else {
|
|
4550
|
+
files.push(entryPath);
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4553
|
+
return files;
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
function relative(filePath) {
|
|
4557
|
+
return path.relative(root, filePath).replace(/\\\\/gu, '/');
|
|
4558
|
+
}
|
|
4559
|
+
|
|
4560
|
+
function isSourceFile(filePath) {
|
|
4561
|
+
return /\\.(?:ts|tsx|js|jsx)$/u.test(filePath);
|
|
4562
|
+
}
|
|
4563
|
+
|
|
4564
|
+
function isLocaleJson(filePath) {
|
|
4565
|
+
const normalized = relative(filePath);
|
|
4566
|
+
return /\\/locales\\/(en|cs)\\/[^/]+\\.json$/u.test(normalized);
|
|
4567
|
+
}
|
|
4568
|
+
|
|
4569
|
+
function readText(filePath) {
|
|
4570
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
4571
|
+
}
|
|
4572
|
+
|
|
4573
|
+
function branchIsUserCopy(branch) {
|
|
4574
|
+
const value = branch.trim().replace(/,$/u, '');
|
|
4575
|
+
if (allowedLanguageConditionalBranches.has(value)) {
|
|
4576
|
+
return false;
|
|
4577
|
+
}
|
|
4578
|
+
return /^['"][^'"]{2,}['"]$/u.test(value);
|
|
4579
|
+
}
|
|
4580
|
+
|
|
4581
|
+
function checkRuntimeResources(filePath, text) {
|
|
4582
|
+
if (!relative(filePath).endsWith('/src/modern.runtime.ts')) {
|
|
4583
|
+
return;
|
|
4584
|
+
}
|
|
4585
|
+
if (/initOptions\\s*:\\s*\\{[\\s\\S]*?\\bresources\\s*:/u.test(text)) {
|
|
4586
|
+
fail(\`\${relative(filePath)} must not inline i18n resources in modern.runtime.ts; use locale JSON files.\`);
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4589
|
+
|
|
4590
|
+
function checkLanguageConditionals(filePath, text) {
|
|
4591
|
+
for (const match of text.matchAll(languageConditionalPattern)) {
|
|
4592
|
+
const [, name, whenTrue = '', whenFalse = ''] = match;
|
|
4593
|
+
if (branchIsUserCopy(whenTrue) || branchIsUserCopy(whenFalse)) {
|
|
4594
|
+
fail(
|
|
4595
|
+
\`\${relative(filePath)} contains manual \${name} copy branching. Put user-facing copy in i18n JSON resources.\`,
|
|
4596
|
+
);
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4600
|
+
|
|
4601
|
+
function checkLiteralVisibleAttributes(filePath, text) {
|
|
4602
|
+
if (!filePath.endsWith('.tsx') && !filePath.endsWith('.jsx')) {
|
|
4603
|
+
return;
|
|
4604
|
+
}
|
|
4605
|
+
for (const attribute of visibleCopyAttributes) {
|
|
4606
|
+
const pattern = new RegExp(\`\\\\b\${attribute}=["'][^"'{}]*[A-Za-z][^"'{}]*["']\`, 'u');
|
|
4607
|
+
if (pattern.test(text)) {
|
|
4608
|
+
fail(
|
|
4609
|
+
\`\${relative(filePath)} contains literal \${attribute} copy. Use t(...) or route metadata for visible text.\`,
|
|
4610
|
+
);
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
|
|
4615
|
+
function checkSplitPhraseKeys(filePath, text) {
|
|
4616
|
+
if (/t\\(\\s*['"][^'"]+\\.(?:prefix|suffix|before|after)['"]\\s*\\)/u.test(text)) {
|
|
4617
|
+
fail(
|
|
4618
|
+
\`\${relative(filePath)} uses split phrase translation keys. Keep translator-owned phrases whole.\`,
|
|
4619
|
+
);
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
|
|
4623
|
+
function checkBoundaryAttributes(filePath, text) {
|
|
4624
|
+
if (!filePath.endsWith('.tsx') && !filePath.endsWith('.jsx')) {
|
|
4625
|
+
return;
|
|
4626
|
+
}
|
|
4627
|
+
if (/\\bdata-mf-(?:remote|expose)=/u.test(text)) {
|
|
4628
|
+
fail(
|
|
4629
|
+
\`\${relative(filePath)} uses legacy data-mf-* boundary attributes. Use data-modern-boundary-id and data-modern-mf-expose.\`,
|
|
4630
|
+
);
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
4633
|
+
|
|
4634
|
+
function visitLocaleKeys(value, visitor, pathParts = []) {
|
|
4635
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
4636
|
+
return;
|
|
4637
|
+
}
|
|
4638
|
+
for (const [key, child] of Object.entries(value)) {
|
|
4639
|
+
const nextPath = [...pathParts, key];
|
|
4640
|
+
visitor(key, child, nextPath);
|
|
4641
|
+
visitLocaleKeys(child, visitor, nextPath);
|
|
4642
|
+
}
|
|
4643
|
+
}
|
|
4644
|
+
|
|
4645
|
+
function checkPluralResources(filePath, json) {
|
|
4646
|
+
const language = relative(filePath).split('/locales/')[1]?.split('/')[0];
|
|
4647
|
+
const requiredSuffixes =
|
|
4648
|
+
language === 'cs' ? ['one', 'few', 'many', 'other'] : ['one', 'other'];
|
|
4649
|
+
const groups = new Map();
|
|
4650
|
+
|
|
4651
|
+
visitLocaleKeys(json, (key, value, pathParts) => {
|
|
4652
|
+
if (typeof value === 'string' && value.includes('{{count}}')) {
|
|
4653
|
+
const suffixMatch = key.match(/^(.*)_(one|few|many|other)$/u);
|
|
4654
|
+
if (!suffixMatch) {
|
|
4655
|
+
fail(
|
|
4656
|
+
\`\${relative(filePath)} key \${pathParts.join('.')} contains {{count}} but is not plural-suffixed.\`,
|
|
4657
|
+
);
|
|
4658
|
+
}
|
|
4659
|
+
const [, base = '', suffix = ''] = suffixMatch;
|
|
4660
|
+
const parentPath = pathParts.slice(0, -1).join('.');
|
|
4661
|
+
const groupKey = \`\${parentPath}.\${base}\`;
|
|
4662
|
+
const existing = groups.get(groupKey) ?? new Set();
|
|
4663
|
+
existing.add(suffix);
|
|
4664
|
+
groups.set(groupKey, existing);
|
|
4665
|
+
}
|
|
4666
|
+
});
|
|
4667
|
+
|
|
4668
|
+
for (const [group, suffixes] of groups) {
|
|
4669
|
+
for (const suffix of requiredSuffixes) {
|
|
4670
|
+
if (!suffixes.has(suffix)) {
|
|
4671
|
+
fail(\`\${relative(filePath)} plural group \${group} is missing _\${suffix}.\`);
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
|
|
4677
|
+
const sourceFiles = sourceRoots.flatMap(sourceRoot =>
|
|
4678
|
+
walk(path.join(root, sourceRoot)).filter(filePath => isSourceFile(filePath)),
|
|
4679
|
+
);
|
|
4680
|
+
for (const filePath of sourceFiles) {
|
|
4681
|
+
const text = readText(filePath);
|
|
4682
|
+
checkRuntimeResources(filePath, text);
|
|
4683
|
+
checkLanguageConditionals(filePath, text);
|
|
4684
|
+
checkLiteralVisibleAttributes(filePath, text);
|
|
4685
|
+
checkSplitPhraseKeys(filePath, text);
|
|
4686
|
+
checkBoundaryAttributes(filePath, text);
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4689
|
+
const localeFiles = sourceRoots.flatMap(sourceRoot =>
|
|
4690
|
+
walk(path.join(root, sourceRoot)).filter(filePath => isLocaleJson(filePath)),
|
|
4691
|
+
);
|
|
4692
|
+
for (const filePath of localeFiles) {
|
|
4693
|
+
checkPluralResources(filePath, JSON.parse(readText(filePath)));
|
|
4694
|
+
}
|
|
4695
|
+
|
|
4696
|
+
console.log('UltraModern i18n and boundary guardrails validated');
|
|
4697
|
+
`;
|
|
4698
|
+
}
|
|
4495
4699
|
function createWorkspaceValidationScript(scope, enableTailwind, remotes = []) {
|
|
4496
4700
|
const verticals = remotes.filter(appHasEffectApi).map((remote)=>({
|
|
4497
4701
|
id: remote.id,
|
|
@@ -4579,6 +4783,7 @@ const requiredPaths = [
|
|
|
4579
4783
|
'.modernjs/ultramodern-generated-contract.json',
|
|
4580
4784
|
'scripts/assert-mf-types.mjs',
|
|
4581
4785
|
'scripts/bootstrap-agent-skills.mjs',
|
|
4786
|
+
'scripts/check-ultramodern-i18n-boundaries.mjs',
|
|
4582
4787
|
'scripts/proof-cloudflare-version.mjs',
|
|
4583
4788
|
'scripts/setup-agent-reference-repos.mjs',
|
|
4584
4789
|
'apps/shell-super-app/package.json',
|
|
@@ -4662,12 +4867,13 @@ assert(
|
|
|
4662
4867
|
);
|
|
4663
4868
|
assert(rootPackage.scripts?.['cloudflare:build'] === expectedCloudflareBuildScript, 'Root cloudflare:build script is incorrect');
|
|
4664
4869
|
assert(rootPackage.scripts?.['ultramodern:check'] === 'node ./scripts/validate-ultramodern-workspace.mjs', 'Root must expose ultramodern:check');
|
|
4870
|
+
assert(rootPackage.scripts?.['ultramodern:i18n-boundaries'] === 'node ./scripts/check-ultramodern-i18n-boundaries.mjs', 'Root must expose ultramodern:i18n-boundaries');
|
|
4665
4871
|
assert(rootPackage.scripts?.['ultramodern:assert-mf-types'] === 'node ./scripts/assert-mf-types.mjs', 'Root must expose ultramodern:assert-mf-types');
|
|
4666
4872
|
assert(rootPackage.scripts?.['cloudflare:deploy'] === expectedCloudflareDeployScript, 'Root must expose cloudflare:deploy');
|
|
4667
4873
|
assert(rootPackage.scripts?.['cloudflare:proof'] === 'node ./scripts/proof-cloudflare-version.mjs --out .codex/reports/cloudflare-version-proof/public-url-proof.json', 'Root must expose cloudflare:proof');
|
|
4668
4874
|
assert(rootPackage.scripts?.['skills:install'] === 'node ./scripts/bootstrap-agent-skills.mjs', 'Root must expose skills:install');
|
|
4669
4875
|
assert(rootPackage.scripts?.['skills:check'] === 'node ./scripts/bootstrap-agent-skills.mjs --check', 'Root must expose skills:check');
|
|
4670
|
-
assert(rootPackage.scripts?.postinstall === 'node ./scripts/bootstrap-agent-skills.mjs && (git rev-parse --is-inside-work-tree >/dev/null 2>&1 && lefthook install || true)
|
|
4876
|
+
assert(rootPackage.scripts?.postinstall === "oxfmt . '!repos/**' && node ./scripts/bootstrap-agent-skills.mjs && node ./scripts/setup-agent-reference-repos.mjs && (git rev-parse --is-inside-work-tree >/dev/null 2>&1 && lefthook install || true)", 'Root postinstall must format, bootstrap agent skills, install reference repositories, and enable hooks last');
|
|
4671
4877
|
|
|
4672
4878
|
const expectedAppIds = ['shell-super-app', ...fullStackVerticals.map(vertical => vertical.id)];
|
|
4673
4879
|
assert(
|
|
@@ -4721,7 +4927,7 @@ assert(!('effectServices' in topology), 'Default APIs must be vertical-owned, no
|
|
|
4721
4927
|
for (const vertical of fullStackVerticals) {
|
|
4722
4928
|
const packageJson = readJson(\`\${vertical.path}/package.json\`);
|
|
4723
4929
|
assert(packageJson.name === vertical.packageName, \`\${vertical.id} package name is incorrect\`);
|
|
4724
|
-
assert(packageJson.scripts?.['cloudflare:deploy'] === 'ULTRAMODERN_CLOUDFLARE_REQUIRE_PUBLIC_URLS=true
|
|
4930
|
+
assert(packageJson.scripts?.['cloudflare:deploy'] === 'ULTRAMODERN_CLOUDFLARE_REQUIRE_PUBLIC_URLS=true pnpm run cloudflare:build && wrangler deploy --config .output/wrangler.json', \`\${vertical.id} must expose cloudflare:deploy\`);
|
|
4725
4931
|
assert(packageJson.scripts?.['cloudflare:proof']?.includes(\`--app \${vertical.id}\`), \`\${vertical.id} must expose cloudflare:proof\`);
|
|
4726
4932
|
assert(packageJson.dependencies?.['@modern-js/plugin-bff'], \`\${vertical.id} must depend on plugin-bff\`);
|
|
4727
4933
|
assert(packageJson.exports?.['./effect/client'] === \`./src/effect/\${vertical.stem}-client.ts\`, \`\${vertical.id} must export its Effect client\`);
|
|
@@ -5124,6 +5330,7 @@ main().then(
|
|
|
5124
5330
|
function writeGeneratedWorkspaceScripts(targetDir, scope, enableTailwind, remotes = []) {
|
|
5125
5331
|
writeFileReplacing(targetDir, "scripts/assert-mf-types.mjs", createAssertMfTypesScript(remotes));
|
|
5126
5332
|
writeFileReplacing(targetDir, "scripts/validate-ultramodern-workspace.mjs", createWorkspaceValidationScript(scope, enableTailwind, remotes));
|
|
5333
|
+
writeFileReplacing(targetDir, "scripts/check-ultramodern-i18n-boundaries.mjs", createWorkspaceI18nBoundaryValidationScript());
|
|
5127
5334
|
writeFileReplacing(targetDir, "scripts/proof-cloudflare-version.mjs", createCloudflareVersionProofScript());
|
|
5128
5335
|
}
|
|
5129
5336
|
function writeApp(targetDir, scope, app, packageSource, enableTailwind, remotes = []) {
|
package/package.json
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=20"
|
|
23
23
|
},
|
|
24
|
-
"version": "3.2.0-ultramodern.
|
|
24
|
+
"version": "3.2.0-ultramodern.64",
|
|
25
25
|
"types": "./dist/types/index.d.ts",
|
|
26
26
|
"main": "./dist/index.js",
|
|
27
27
|
"bin": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@types/node": "^25.9.1",
|
|
42
42
|
"@typescript/native-preview": "7.0.0-dev.20260527.2",
|
|
43
43
|
"tsx": "^4.22.3",
|
|
44
|
-
"@modern-js/i18n-utils": "npm:@bleedingdev/modern-js-i18n-utils@3.2.0-ultramodern.
|
|
44
|
+
"@modern-js/i18n-utils": "npm:@bleedingdev/modern-js-i18n-utils@3.2.0-ultramodern.64"
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|
|
47
47
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -54,6 +54,6 @@
|
|
|
54
54
|
"start": "node ./dist/index.js"
|
|
55
55
|
},
|
|
56
56
|
"ultramodern": {
|
|
57
|
-
"frameworkVersion": "3.2.0-ultramodern.
|
|
57
|
+
"frameworkVersion": "3.2.0-ultramodern.64"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/template/AGENTS.md
CHANGED
|
@@ -10,7 +10,7 @@ This project is generated for Codex-first UltraModern.js work.
|
|
|
10
10
|
- `pnpm i18n:check` rejects hardcoded user-visible JSX text.
|
|
11
11
|
- `pnpm ultramodern:check` verifies the generated contract.
|
|
12
12
|
- Generated Codex stop hooks and subagent-stop hooks run `pnpm format && pnpm lint:fix && pnpm ultramodern:check`.
|
|
13
|
-
- `postinstall` installs `lefthook` when the app is inside a Git worktree. Generated `lefthook.yml` runs `pnpm format
|
|
13
|
+
- `postinstall` installs `lefthook` when the app is inside a Git worktree. Generated `lefthook.yml` runs `pnpm format && pnpm lint:fix && pnpm ultramodern:check` on pre-commit; pre-push runs `pnpm ultramodern:check`.
|
|
14
14
|
|
|
15
15
|
## Internationalization
|
|
16
16
|
|
package/template/lefthook.yml
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
pre-commit:
|
|
2
2
|
commands:
|
|
3
|
-
|
|
4
|
-
run: pnpm format
|
|
3
|
+
fix-and-check:
|
|
4
|
+
run: pnpm format && pnpm lint:fix && pnpm ultramodern:check
|
|
5
5
|
stage_fixed: true
|
|
6
|
-
lint:
|
|
7
|
-
run: pnpm lint:fix
|
|
8
|
-
stage_fixed: true
|
|
9
|
-
check:
|
|
10
|
-
run: pnpm ultramodern:check
|
|
11
6
|
|
|
12
7
|
pre-push:
|
|
13
8
|
commands:
|
|
@@ -11,7 +11,7 @@ instructions, not optional reading.
|
|
|
11
11
|
- `pnpm typecheck` runs effect-tsgo as the TypeScript checker.
|
|
12
12
|
- `pnpm check` runs formatting, linting, effect-tsgo, private-skill availability checks, and the generated workspace contract.
|
|
13
13
|
- Generated Codex stop hooks and subagent-stop hooks run `pnpm format && pnpm lint:fix && pnpm check`.
|
|
14
|
-
- `postinstall` installs `lefthook` when the workspace is inside a Git worktree. Generated `lefthook.yml` runs `pnpm format
|
|
14
|
+
- `postinstall` formats the generated tree, installs agent skills and reference repos, then installs `lefthook` when the workspace is inside a Git worktree. Generated `lefthook.yml` runs `pnpm format && pnpm lint:fix && pnpm check` on pre-commit; pre-push runs `pnpm check`.
|
|
15
15
|
|
|
16
16
|
## Localized Routes
|
|
17
17
|
|