@ultimat3/cli 28.0.0 → 29.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -1
- package/package.json +31 -31
- package/src/app-root.ts +1 -1
- package/src/cmd-deploy-compose.ts +10 -1
- package/src/cmd-deploy-start-first.ts +63 -19
- package/src/cmd-deploy.ts +10 -1
- package/src/cmd-i18n.ts +28 -6
- package/src/dev-route-table.ts +6 -0
- package/src/document-options.ts +10 -0
- package/src/error-codes.ts +6 -0
- package/src/i18n-locale-parity.ts +134 -0
- package/src/i18n-registration.ts +6 -0
- package/src/mcp-errors.ts +6 -0
- package/src/messages.ts +4 -0
- package/src/page-navigation.ts +54 -10
- package/src/page-speculation.ts +8 -4
- package/src/prerender.ts +5 -0
- package/src/runtime-jobs.ts +83 -7
- package/src/runtime-render.ts +58 -56
- package/src/serve-web.ts +5 -0
- package/src/site-config.ts +14 -4
- package/src/templates/scaffold-container.ts +3 -3
- package/src/templates/scaffold-repo.ts +2 -2
- package/src/workspace-graph.ts +56 -9
package/src/site-config.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// The public origin and the crawler rules out of `app.config.ts` — `site.origin`,
|
|
2
|
-
// `seo.robots.disallow` and `seo.robots.groups` — and the one resolution of "which origin is this site served on" every
|
|
1
|
+
// The public origin, the app's favicon and the crawler rules out of `app.config.ts` — `site.origin`,
|
|
2
|
+
// `site.favicon`, `seo.robots.disallow` and `seo.robots.groups` — and the one resolution of "which origin is this site served on" every
|
|
3
3
|
// absolute URL a document, a sitemap or a robots file carries is built against. Read off the one
|
|
4
4
|
// loader (`app-config-load.ts`).
|
|
5
5
|
|
|
6
|
-
import type { Environment, SeoRobotsGroup, SeoSitemapConfig } from '@ultimat3/core';
|
|
6
|
+
import type { Environment, SeoRobotsGroup, SeoSitemapConfig, SiteFavicon } from '@ultimat3/core';
|
|
7
7
|
import { loadAppConfig } from './app-config-load';
|
|
8
8
|
|
|
9
9
|
export interface SiteSettings {
|
|
@@ -18,6 +18,14 @@ export interface SiteSettings {
|
|
|
18
18
|
readonly robotsGroups?: readonly SeoRobotsGroup[] | undefined;
|
|
19
19
|
/** `seo.sitemap` — public `app/` pages to list, where `<lastmod>` comes from, sections, page size. */
|
|
20
20
|
readonly sitemap: SeoSitemapConfig;
|
|
21
|
+
/**
|
|
22
|
+
* `site.favicon` — the app's SVG favicon, named as a site asset path (`assets/brand/icon.svg`).
|
|
23
|
+
* `undefined` when the app declares none. The boot resolves it ONCE, through the site asset
|
|
24
|
+
* table (`siteAssetTable(root).resolve(path).url` — the table `/assets/*` serves and `asset()`
|
|
25
|
+
* answers from), into the content-hashed URL every document carries. Optional so a
|
|
26
|
+
* `SiteSettings` literal written before it existed still compiles.
|
|
27
|
+
*/
|
|
28
|
+
readonly favicon?: SiteFavicon | undefined;
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
export const NO_SITE_SETTINGS: SiteSettings = {
|
|
@@ -25,6 +33,7 @@ export const NO_SITE_SETTINGS: SiteSettings = {
|
|
|
25
33
|
disallow: [],
|
|
26
34
|
robotsGroups: [],
|
|
27
35
|
sitemap: { extra: [], lastmod: 'none', sections: [], pageSize: 50_000 },
|
|
36
|
+
favicon: undefined,
|
|
28
37
|
};
|
|
29
38
|
|
|
30
39
|
const trimSlash = (origin: string): string => origin.replace(/\/+$/, '');
|
|
@@ -32,12 +41,13 @@ const trimSlash = (origin: string): string => origin.replace(/\/+$/, '');
|
|
|
32
41
|
export async function loadSiteSettings(root: string): Promise<SiteSettings> {
|
|
33
42
|
const config = await loadAppConfig(root);
|
|
34
43
|
if (config === undefined) return NO_SITE_SETTINGS;
|
|
35
|
-
const { origin } = config.site;
|
|
44
|
+
const { origin, favicon } = config.site;
|
|
36
45
|
return {
|
|
37
46
|
origin: origin === null ? null : trimSlash(origin),
|
|
38
47
|
disallow: [...config.seo.robots.disallow],
|
|
39
48
|
robotsGroups: [...(config.seo.robots.groups ?? [])],
|
|
40
49
|
sitemap: config.seo.sitemap,
|
|
50
|
+
favicon,
|
|
41
51
|
};
|
|
42
52
|
}
|
|
43
53
|
|
|
@@ -32,16 +32,16 @@ const dockerfile = (
|
|
|
32
32
|
# from the file itself, and without it the frozen install cannot find what the lockfile names.
|
|
33
33
|
# The tag names the series for the reader; the digest is what builds — a tag is a pointer its
|
|
34
34
|
# publisher can move. Dependabot's docker ecosystem moves the digest together with the tag.
|
|
35
|
-
# The digest is \`oven/bun:1.4.
|
|
35
|
+
# The digest is \`oven/bun:1.4.4-alpine\`'s, the patch the framework that wrote this file was
|
|
36
36
|
# tested on — the same patch as \`engines.bun\` in package.json.
|
|
37
|
-
FROM oven/bun:1.4-alpine@sha256:
|
|
37
|
+
FROM oven/bun:1.4-alpine@sha256:4cf82f5686b196504fe119db3fcd9185b191f82ece82c40680e597228ec6070a AS manifests
|
|
38
38
|
WORKDIR /app
|
|
39
39
|
COPY . .
|
|
40
40
|
RUN find . ! -type d ! -name package.json ! -name bunfig.toml ! -name '*.tgz' -delete \\
|
|
41
41
|
&& find . -type d -empty -delete
|
|
42
42
|
|
|
43
43
|
# ---------- deps: runtime dependencies only, cached on the lockfile and the manifests ----------
|
|
44
|
-
FROM oven/bun:1.4-alpine@sha256:
|
|
44
|
+
FROM oven/bun:1.4-alpine@sha256:4cf82f5686b196504fe119db3fcd9185b191f82ece82c40680e597228ec6070a AS deps
|
|
45
45
|
WORKDIR /app
|
|
46
46
|
COPY --from=manifests /app ./
|
|
47
47
|
COPY bun.lock ./
|
|
@@ -45,11 +45,11 @@ const TYPESCRIPT_VERSION = '^7.0.2';
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* The Solid runtime every island and `@ultimat3/ui` component renders with, exact for the reason
|
|
48
|
-
* `BIOME_VERSION` is: the CLI compiles islands with `babel-preset-solid ^1.9.
|
|
48
|
+
* `BIOME_VERSION` is: the CLI compiles islands with `babel-preset-solid ^1.9.16`, and a scaffold
|
|
49
49
|
* that installed an older runtime than the compiler targets drifted as a buried literal (1.9.14
|
|
50
50
|
* beside the repository's 1.9.15). `scaffold-repo.test.ts` holds it equal to the root manifest's.
|
|
51
51
|
*/
|
|
52
|
-
const SOLID_VERSION = '1.9.
|
|
52
|
+
const SOLID_VERSION = '1.9.17';
|
|
53
53
|
|
|
54
54
|
// `engines.bun` is `REQUIRED_BUN`, the floor the SHIPPED `x` enforces
|
|
55
55
|
// (`packages/cli/src/app-root.ts`), and not a second literal. It was one: `>=1.3.0`, a whole minor
|
package/src/workspace-graph.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// The workspace dependency graph, in the two forms that must agree: the edges every manifest
|
|
2
2
|
// DECLARES, the edges every shipped source file actually IMPORTS, and the gate rule that the
|
|
3
|
-
// second is
|
|
4
|
-
// for `tsc`
|
|
3
|
+
// second is covered by the first, with no unused framework sibling declarations. A tsconfig-only
|
|
4
|
+
// edge resolves for `tsc` but is invisible to bun, `--filter` ordering and change-detection tools.
|
|
5
5
|
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { ERROR_DOCS_URL, maskLiterals } from '@ultimat3/core';
|
|
8
|
+
import { CATALOG_PACKAGES } from './error-catalog';
|
|
8
9
|
import { scanAllImports } from './import-scan';
|
|
9
10
|
import type { Finding } from './output';
|
|
10
|
-
import {
|
|
11
|
+
import { FIXTURE_SUFFIX } from './publish-closure';
|
|
12
|
+
import { quoteArg } from './shell-quote';
|
|
13
|
+
import { eachSourceFile, isGenerated, isTest, isVendored, SOURCE_GLOBS } from './source-files';
|
|
11
14
|
|
|
12
15
|
export interface WorkspaceNode {
|
|
13
16
|
/** package.json "name". */
|
|
@@ -176,7 +179,7 @@ export const undeclaredWorkspaceDepFinding = (
|
|
|
176
179
|
cause: `${at} imports ${to.name}, which ${from.dir}/package.json does not declare`,
|
|
177
180
|
// The exact line to paste, at the version the target really carries: a `workspace:*` range would
|
|
178
181
|
// resolve and then fail `checkLockstep`, which compares a sibling pin against the version.
|
|
179
|
-
fix: `add "${to.name}": "${to.version ?? '0.0.0'}" to "dependencies" in ${from.dir}/package.json`,
|
|
182
|
+
fix: `add "${to.name}": "${to.version ?? '0.0.0'}" to "${at.endsWith(FIXTURE_SUFFIX) ? 'devDependencies' : 'dependencies'}" in ${from.dir}/package.json`,
|
|
180
183
|
docs: ERROR_DOCS_URL,
|
|
181
184
|
at: `${from.dir}/package.json`,
|
|
182
185
|
});
|
|
@@ -186,10 +189,9 @@ export const undeclaredWorkspaceDepFinding = (
|
|
|
186
189
|
* declared none of it — the imports resolved through the root tsconfig's `paths`, so the edges
|
|
187
190
|
* existed only inside `tsc` and every tool that asks what a change affects answered short.
|
|
188
191
|
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* judging test imports would demand 29 of them to describe a resolution that already works.
|
|
192
|
+
* Tests resolve through the root's hoisted devDependencies and are excluded here. Fixtures still
|
|
193
|
+
* need forward declarations, but their imports justify only development dependencies: they are
|
|
194
|
+
* excluded from tarballs and must not keep unused shipped dependency fields alive.
|
|
193
195
|
*
|
|
194
196
|
* One finding per (workspace, dependency) pair, never one per import site: the fix is one line in
|
|
195
197
|
* one manifest, and repeating it per file is noise an author has to deduplicate by hand.
|
|
@@ -202,6 +204,8 @@ export async function checkWorkspaceDependencies(root: string): Promise<readonly
|
|
|
202
204
|
// Deepest directory first: a nested workspace's files are its own, never its parent's.
|
|
203
205
|
const owners = [...graph].sort((left, right) => right.dir.length - left.dir.length);
|
|
204
206
|
const reported = new Set<string>();
|
|
207
|
+
const used = new Set<string>();
|
|
208
|
+
const fixtureUsed = new Set<string>();
|
|
205
209
|
for await (const path of eachSourceFile(root)) {
|
|
206
210
|
if (isTest(path) || isGenerated(path)) continue;
|
|
207
211
|
const owner = owners.find((node) => path.startsWith(`${node.dir}/`));
|
|
@@ -209,12 +213,55 @@ export async function checkWorkspaceDependencies(root: string): Promise<readonly
|
|
|
209
213
|
for (const name of importedPackages(await Bun.file(join(root, path)).text(), path)) {
|
|
210
214
|
const target = byName.get(name);
|
|
211
215
|
if (target === undefined || target.name === owner.name) continue;
|
|
212
|
-
if (owner.dependencies.includes(target.name)) continue;
|
|
213
216
|
const pair = `${owner.dir} ${target.name}`;
|
|
217
|
+
// Fixtures still owe forward declarations, but never justify a shipped dependency.
|
|
218
|
+
(path.endsWith(FIXTURE_SUFFIX) ? fixtureUsed : used).add(pair);
|
|
219
|
+
if (owner.dependencies.includes(target.name)) continue;
|
|
214
220
|
if (reported.has(pair)) continue;
|
|
215
221
|
reported.add(pair);
|
|
216
222
|
findings.push(undeclaredWorkspaceDepFinding(owner, target, path));
|
|
217
223
|
}
|
|
218
224
|
}
|
|
225
|
+
for (const owner of graph) {
|
|
226
|
+
// Only infer absence where the source contract scans. Match directory prefixes rather than
|
|
227
|
+
// observed files so a covered workspace with no source still owes removal of unused siblings.
|
|
228
|
+
const covered = SOURCE_GLOBS.some((pattern) => {
|
|
229
|
+
const directories = pattern.split('/').slice(0, -1);
|
|
230
|
+
return directories.some((_, index) =>
|
|
231
|
+
new Bun.Glob(directories.slice(0, index + 1).join('/')).match(owner.dir),
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
if (!covered || isVendored(owner.dir)) continue;
|
|
235
|
+
const unused = owner.dependencies.filter(
|
|
236
|
+
(name) =>
|
|
237
|
+
name.startsWith('@ultimat3/') &&
|
|
238
|
+
!used.has(`${owner.dir} ${name}`) &&
|
|
239
|
+
!(owner.name === '@ultimat3/cli' && CATALOG_PACKAGES.some((root) => root === name)),
|
|
240
|
+
);
|
|
241
|
+
if (unused.length === 0) continue;
|
|
242
|
+
const path = `${owner.dir}/package.json`;
|
|
243
|
+
const manifest = await readManifest(join(root, path));
|
|
244
|
+
for (const name of unused) {
|
|
245
|
+
const fields = DEPENDENCY_FIELDS.filter(
|
|
246
|
+
(field) =>
|
|
247
|
+
Object.hasOwn(asRecord(manifest?.[field]) ?? {}, name) &&
|
|
248
|
+
!(field === 'devDependencies' && fixtureUsed.has(`${owner.dir} ${name}`)),
|
|
249
|
+
);
|
|
250
|
+
if (fields.length === 0) continue;
|
|
251
|
+
// A package-wide remove would also delete a valid fixture-only development declaration.
|
|
252
|
+
const script =
|
|
253
|
+
'const [path, name, fields] = Bun.argv.slice(-3); ' +
|
|
254
|
+
'const manifest = await Bun.file(path).json(); ' +
|
|
255
|
+
'for (const field of JSON.parse(fields)) delete manifest[field]?.[name]; ' +
|
|
256
|
+
'await Bun.write(path, JSON.stringify(manifest, null, 2) + "\\n");';
|
|
257
|
+
findings.push({
|
|
258
|
+
code: 'X_PACKAGE_SHAPE',
|
|
259
|
+
cause: `${path} declares ${name} in ${fields.join(', ')}, but no eligible source in ${owner.dir} imports it`,
|
|
260
|
+
fix: `bun -e ${quoteArg(script)} -- ${quoteArg(path)} ${quoteArg(name)} ${quoteArg(JSON.stringify(fields))}`,
|
|
261
|
+
docs: ERROR_DOCS_URL,
|
|
262
|
+
at: path,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
219
266
|
return findings;
|
|
220
267
|
}
|