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