@ultimat3/cli 22.3.3 → 22.3.5
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/package.json +28 -28
- package/src/app-load.ts +49 -29
- package/src/island-routes.ts +4 -2
- package/src/island-store.ts +31 -4
- package/src/island-styles.ts +12 -1
- package/src/style-bundle.ts +23 -1
- package/src/style-routes.ts +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cli",
|
|
3
|
-
"version": "22.3.
|
|
3
|
+
"version": "22.3.5",
|
|
4
4
|
"description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -38,33 +38,33 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@babel/core": "^8.0.1",
|
|
41
|
-
"@ultimat3/action": "22.3.
|
|
42
|
-
"@ultimat3/admin": "22.3.
|
|
43
|
-
"@ultimat3/ai": "22.3.
|
|
44
|
-
"@ultimat3/auth": "22.3.
|
|
45
|
-
"@ultimat3/cache": "22.3.
|
|
46
|
-
"@ultimat3/core": "22.3.
|
|
47
|
-
"@ultimat3/db": "22.3.
|
|
48
|
-
"@ultimat3/entity": "22.3.
|
|
49
|
-
"@ultimat3/flags": "22.3.
|
|
50
|
-
"@ultimat3/http": "22.3.
|
|
51
|
-
"@ultimat3/i18n": "22.3.
|
|
52
|
-
"@ultimat3/jobs": "22.3.
|
|
53
|
-
"@ultimat3/mail": "22.3.
|
|
54
|
-
"@ultimat3/manifest": "22.3.
|
|
55
|
-
"@ultimat3/mcp": "22.3.
|
|
56
|
-
"@ultimat3/money": "22.3.
|
|
57
|
-
"@ultimat3/notify": "22.3.
|
|
58
|
-
"@ultimat3/policy": "22.3.
|
|
59
|
-
"@ultimat3/pwa": "22.3.
|
|
60
|
-
"@ultimat3/query": "22.3.
|
|
61
|
-
"@ultimat3/realtime": "22.3.
|
|
62
|
-
"@ultimat3/render": "22.3.
|
|
63
|
-
"@ultimat3/schema": "22.3.
|
|
64
|
-
"@ultimat3/seo": "22.3.
|
|
65
|
-
"@ultimat3/storage": "22.3.
|
|
66
|
-
"@ultimat3/testing": "22.3.
|
|
67
|
-
"@ultimat3/time": "22.3.
|
|
41
|
+
"@ultimat3/action": "22.3.5",
|
|
42
|
+
"@ultimat3/admin": "22.3.5",
|
|
43
|
+
"@ultimat3/ai": "22.3.5",
|
|
44
|
+
"@ultimat3/auth": "22.3.5",
|
|
45
|
+
"@ultimat3/cache": "22.3.5",
|
|
46
|
+
"@ultimat3/core": "22.3.5",
|
|
47
|
+
"@ultimat3/db": "22.3.5",
|
|
48
|
+
"@ultimat3/entity": "22.3.5",
|
|
49
|
+
"@ultimat3/flags": "22.3.5",
|
|
50
|
+
"@ultimat3/http": "22.3.5",
|
|
51
|
+
"@ultimat3/i18n": "22.3.5",
|
|
52
|
+
"@ultimat3/jobs": "22.3.5",
|
|
53
|
+
"@ultimat3/mail": "22.3.5",
|
|
54
|
+
"@ultimat3/manifest": "22.3.5",
|
|
55
|
+
"@ultimat3/mcp": "22.3.5",
|
|
56
|
+
"@ultimat3/money": "22.3.5",
|
|
57
|
+
"@ultimat3/notify": "22.3.5",
|
|
58
|
+
"@ultimat3/policy": "22.3.5",
|
|
59
|
+
"@ultimat3/pwa": "22.3.5",
|
|
60
|
+
"@ultimat3/query": "22.3.5",
|
|
61
|
+
"@ultimat3/realtime": "22.3.5",
|
|
62
|
+
"@ultimat3/render": "22.3.5",
|
|
63
|
+
"@ultimat3/schema": "22.3.5",
|
|
64
|
+
"@ultimat3/seo": "22.3.5",
|
|
65
|
+
"@ultimat3/storage": "22.3.5",
|
|
66
|
+
"@ultimat3/testing": "22.3.5",
|
|
67
|
+
"@ultimat3/time": "22.3.5",
|
|
68
68
|
"babel-preset-solid": "^1.9.15"
|
|
69
69
|
}
|
|
70
70
|
}
|
package/src/app-load.ts
CHANGED
|
@@ -115,6 +115,28 @@ export function resetAppLoad(): void {
|
|
|
115
115
|
routeSources.clear();
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Every module path the app globs match, SORTED. `Bun.Glob` answers in directory order, which is
|
|
120
|
+
* the filesystem's — ext4 hashes names with a per-filesystem seed — so two pods of one image
|
|
121
|
+
* imported the app in two orders, registered its stylesheets in two orders, and served two
|
|
122
|
+
* different `/styles/<hash>.css` for one page (notificado.co, 22.3.2). Import order is the
|
|
123
|
+
* stylesheet cascade's order, so it must be the same everywhere.
|
|
124
|
+
*/
|
|
125
|
+
export async function appModulePaths(root: string): Promise<readonly string[]> {
|
|
126
|
+
// Pattern by pattern, as before — only the order WITHIN a pattern was the filesystem's.
|
|
127
|
+
const found: string[] = [];
|
|
128
|
+
const seen = new Set<string>();
|
|
129
|
+
for (const pattern of APP_GLOBS) {
|
|
130
|
+
const matched: string[] = [];
|
|
131
|
+
for await (const absolute of new Bun.Glob(pattern).scan({ cwd: root, absolute: true })) {
|
|
132
|
+
if (!seen.has(absolute)) matched.push(absolute);
|
|
133
|
+
seen.add(absolute);
|
|
134
|
+
}
|
|
135
|
+
found.push(...matched.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)));
|
|
136
|
+
}
|
|
137
|
+
return found;
|
|
138
|
+
}
|
|
139
|
+
|
|
118
140
|
export async function loadApp(root: string): Promise<LoadedApp> {
|
|
119
141
|
// Before any import: a stylesheet's surface is read below the app root, never off its absolute
|
|
120
142
|
// path — under the container's `WORKDIR /app` that path's first segment is `app/`, and every
|
|
@@ -127,36 +149,34 @@ export async function loadApp(root: string): Promise<LoadedApp> {
|
|
|
127
149
|
const files: string[] = [];
|
|
128
150
|
const findings: Finding[] = [];
|
|
129
151
|
|
|
130
|
-
for (const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
files.push(file);
|
|
157
|
-
const finding = await register(absolute, file, module, snapshot);
|
|
158
|
-
if (finding !== undefined) findings.push(finding);
|
|
152
|
+
for (const absolute of await appModulePaths(root)) {
|
|
153
|
+
// A SEGMENT, never a substring: an app checked out under
|
|
154
|
+
// `~/dev/node_modules-experiments/myapp` answered `includes('node_modules')` for every
|
|
155
|
+
// file it holds, so this loop imported none of them and the app registered nothing.
|
|
156
|
+
// The ROOT-RELATIVE path is tested, never the absolute one: an app checked out under
|
|
157
|
+
// `~/work/my.test.app` matched `.test.` on every file and loaded none of them.
|
|
158
|
+
const file = relative(root, absolute).split(sep).join('/');
|
|
159
|
+
if (hasPathSegment(absolute, 'node_modules') || isTest(file)) continue;
|
|
160
|
+
if (ENTRY_POINT.test(file) || CLIENT_ENTRY_POINT.test(file) || STATES_FILE.test(file)) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
// The source is read BEFORE the import, and only on the file's first pass — a rescan of a
|
|
164
|
+
// registered module reads nothing here. A route entry is bound to the bytes the module was
|
|
165
|
+
// evaluated from, and a read AFTER the import cannot know which bytes those were: a save
|
|
166
|
+
// landing between the two bound V1's component to V2's hash, so the next scan saw nothing to
|
|
167
|
+
// do and served V1 until the save after. Read first, the worst case is one re-import the
|
|
168
|
+
// next tick, of a file that did change.
|
|
169
|
+
const snapshot = registered.has(absolute) ? undefined : await Bun.file(absolute).text();
|
|
170
|
+
let module: Record<string, unknown>;
|
|
171
|
+
try {
|
|
172
|
+
module = (await import(absolute)) as Record<string, unknown>;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
findings.push({ ...findingFrom(error), at: file });
|
|
175
|
+
continue;
|
|
159
176
|
}
|
|
177
|
+
files.push(file);
|
|
178
|
+
const finding = await register(absolute, file, module, snapshot);
|
|
179
|
+
if (finding !== undefined) findings.push(finding);
|
|
160
180
|
}
|
|
161
181
|
|
|
162
182
|
files.sort();
|
package/src/island-routes.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// would be a page that boots in `x dev` and 404s in the image.
|
|
4
4
|
|
|
5
5
|
import type { Route, UltimateRequest } from '@ultimat3/http';
|
|
6
|
-
import { applyCacheHeaders, json } from '@ultimat3/http';
|
|
6
|
+
import { applyCacheHeaders, json, NO_STORE } from '@ultimat3/http';
|
|
7
7
|
import type { IslandBundle } from './island-bundle';
|
|
8
8
|
import { ISLAND_BASE_PATH } from './island-bundle';
|
|
9
9
|
|
|
@@ -28,7 +28,7 @@ export function islandRoutes(source: IslandSource): readonly Route[] {
|
|
|
28
28
|
handler: (request: UltimateRequest): Response => {
|
|
29
29
|
const chunk = source().chunkAt(request.pathname);
|
|
30
30
|
if (chunk === undefined) {
|
|
31
|
-
|
|
31
|
+
const missing = json(
|
|
32
32
|
{
|
|
33
33
|
ok: false,
|
|
34
34
|
error: {
|
|
@@ -45,6 +45,8 @@ export function islandRoutes(source: IslandSource): readonly Route[] {
|
|
|
45
45
|
},
|
|
46
46
|
{ status: 404 },
|
|
47
47
|
);
|
|
48
|
+
// `no-store`: a miss on a content-hashed URL must not be pinned by a CDN or a proxy.
|
|
49
|
+
return applyCacheHeaders(missing, NO_STORE);
|
|
48
50
|
}
|
|
49
51
|
return applyCacheHeaders(
|
|
50
52
|
new Response(chunk.code, { headers: { 'content-type': 'text/javascript' } }),
|
package/src/island-store.ts
CHANGED
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
// that names a different set of islands than the app has, or a chunk whose bytes do not hash to
|
|
8
8
|
// what the index recorded is refused, and the boot builds instead — with the reason logged.
|
|
9
9
|
|
|
10
|
-
import { join } from 'node:path'; // why: Bun ships no path-join primitive.
|
|
10
|
+
import { join, relative, sep } from 'node:path'; // why: Bun ships no path-join primitive.
|
|
11
11
|
import { frameworkVersion, logger } from '@ultimat3/core';
|
|
12
|
-
import { contentHash } from '@ultimat3/render/server';
|
|
12
|
+
import { contentHash, loadStylesheet } from '@ultimat3/render/server';
|
|
13
13
|
import type { IslandBundle, IslandChunk } from './island-bundle';
|
|
14
14
|
import { buildIslands, discoverIslands, islandBundle } from './island-bundle';
|
|
15
|
+
import { islandStylesheets } from './island-styles';
|
|
15
16
|
|
|
16
17
|
/** App-root-relative: `COPY . .` carries it into the image with the rest of `.x/`. */
|
|
17
18
|
export const ISLAND_STORE_DIR = '.x/islands';
|
|
@@ -28,6 +29,13 @@ interface StoreIndex {
|
|
|
28
29
|
readonly framework: string;
|
|
29
30
|
readonly bun: string;
|
|
30
31
|
readonly chunks: readonly StoredChunk[];
|
|
32
|
+
/**
|
|
33
|
+
* The stylesheets the island build registered, app-relative and sorted. Registered again at boot
|
|
34
|
+
* from the app's own sources, so a container serving these chunks joins the SAME surface
|
|
35
|
+
* stylesheet a build would — without them an island-only sheet was missing from every pod that
|
|
36
|
+
* read the store, and present on any that rebuilt.
|
|
37
|
+
*/
|
|
38
|
+
readonly stylesheets: readonly string[];
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
const chunkFile = (url: string): string => url.slice(url.lastIndexOf('/') + 1);
|
|
@@ -50,7 +58,16 @@ export async function writeIslandStore(
|
|
|
50
58
|
identity: contentHash(chunk.code),
|
|
51
59
|
});
|
|
52
60
|
}
|
|
53
|
-
const
|
|
61
|
+
const stylesheets = islandStylesheets()
|
|
62
|
+
.map((path) => relative(root, path).split(sep).join('/'))
|
|
63
|
+
.filter((path) => !path.startsWith('..'))
|
|
64
|
+
.sort();
|
|
65
|
+
const index: StoreIndex = {
|
|
66
|
+
framework: frameworkVersion(),
|
|
67
|
+
bun: Bun.version,
|
|
68
|
+
chunks,
|
|
69
|
+
stylesheets,
|
|
70
|
+
};
|
|
54
71
|
await Bun.write(join(dir, INDEX), `${JSON.stringify(index, null, 2)}\n`);
|
|
55
72
|
return [...written, `${ISLAND_STORE_DIR}/${INDEX}`];
|
|
56
73
|
}
|
|
@@ -74,7 +91,10 @@ function parseIndex(value: unknown): StoreIndex | undefined {
|
|
|
74
91
|
return undefined;
|
|
75
92
|
parsed.push({ file, moduleId, url, identity });
|
|
76
93
|
}
|
|
77
|
-
|
|
94
|
+
const sheets = record['stylesheets'];
|
|
95
|
+
// Absent in a 22.3.2 store: read as none, and the boot builds when it matters (see below).
|
|
96
|
+
const stylesheets = Array.isArray(sheets) ? sheets.filter(isString) : [];
|
|
97
|
+
return { framework: record['framework'], bun: record['bun'], chunks: parsed, stylesheets };
|
|
78
98
|
}
|
|
79
99
|
|
|
80
100
|
/** A verified store, or the one sentence saying why it cannot be served. */
|
|
@@ -113,6 +133,13 @@ export async function readIslandStore(root: string): Promise<StoreRead> {
|
|
|
113
133
|
bytes: new TextEncoder().encode(code).byteLength,
|
|
114
134
|
});
|
|
115
135
|
}
|
|
136
|
+
// In sorted order, as `island` sheets: `stylesFor` orders those by path, so this is the same
|
|
137
|
+
// surface stylesheet the build that wrote the store joined.
|
|
138
|
+
for (const sheet of index.stylesheets) {
|
|
139
|
+
const source = Bun.file(join(root, sheet));
|
|
140
|
+
if (!(await source.exists())) return { stale: `${sheet}, an island stylesheet, is missing` };
|
|
141
|
+
loadStylesheet(join(root, sheet), await source.text(), 'island');
|
|
142
|
+
}
|
|
116
143
|
return { bundle: islandBundle(chunks) };
|
|
117
144
|
}
|
|
118
145
|
|
package/src/island-styles.ts
CHANGED
|
@@ -16,6 +16,16 @@ import { IslandBuildFailedError } from './errors';
|
|
|
16
16
|
*/
|
|
17
17
|
const STYLESHEET = /\.s?css$/;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Every stylesheet an island build has loaded in this process, absolute. `x build --target docker`
|
|
21
|
+
* writes them into the island store, so a container that serves the stored chunks registers the
|
|
22
|
+
* same sheets a build would have — and every pod of one image joins the same surface stylesheet.
|
|
23
|
+
*/
|
|
24
|
+
const islandSheets = new Set<string>();
|
|
25
|
+
|
|
26
|
+
/** Sorted, so the store index is byte-identical for identical input. */
|
|
27
|
+
export const islandStylesheets = (): readonly string[] => [...islandSheets].sort();
|
|
28
|
+
|
|
19
29
|
/**
|
|
20
30
|
* `loadStylesheet`, not `compileStylesheet`: compiling alone answers the class names and drops the
|
|
21
31
|
* RULES on the floor, and an island is the one importer a document's own module graph never sees.
|
|
@@ -28,7 +38,8 @@ export const islandStylesPlugin: BunPlugin = {
|
|
|
28
38
|
build.onLoad({ filter: STYLESHEET }, async ({ path }) => {
|
|
29
39
|
const source = await Bun.file(path).text();
|
|
30
40
|
try {
|
|
31
|
-
|
|
41
|
+
islandSheets.add(path);
|
|
42
|
+
return { contents: loadStylesheet(path, source, 'island'), loader: 'js' };
|
|
32
43
|
} catch (error) {
|
|
33
44
|
// A coded failure already names the file and carries a fix — re-wrapping it would bury
|
|
34
45
|
// both. Anything else is rendered through `renderThrowable`, because this is the plugin's
|
package/src/style-bundle.ts
CHANGED
|
@@ -72,15 +72,37 @@ let memo: { readonly revision: number; readonly bundle: StyleBundle } | undefine
|
|
|
72
72
|
export function styleBundle(): StyleBundle {
|
|
73
73
|
const revision = stylesheetsRevision();
|
|
74
74
|
if (memo !== undefined && memo.revision === revision) return memo.bundle;
|
|
75
|
-
const
|
|
75
|
+
const current = styleBundleOf(
|
|
76
76
|
STYLED_SURFACES.map((surface) => ({ surface, css: stylesFor(surface) })).filter(
|
|
77
77
|
(sheet) => sheet.css.length > 0,
|
|
78
78
|
),
|
|
79
79
|
);
|
|
80
|
+
for (const chunk of current.chunks) {
|
|
81
|
+
minted.delete(chunk.url);
|
|
82
|
+
minted.set(chunk.url, chunk);
|
|
83
|
+
}
|
|
84
|
+
while (minted.size > MINTED_KEPT) {
|
|
85
|
+
const oldest = minted.keys().next().value;
|
|
86
|
+
if (oldest === undefined) break;
|
|
87
|
+
minted.delete(oldest);
|
|
88
|
+
}
|
|
89
|
+
const bundle: StyleBundle = {
|
|
90
|
+
...current,
|
|
91
|
+
chunkAt: (url: string): StyleChunk | undefined => current.chunkAt(url) ?? minted.get(url),
|
|
92
|
+
};
|
|
80
93
|
memo = { revision, bundle };
|
|
81
94
|
return bundle;
|
|
82
95
|
}
|
|
83
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Every stylesheet this process has minted, newest last, so a URL it once linked keeps answering
|
|
99
|
+
* after the registry moves — a module imported late, an island rebuilt under `x dev`. The worker
|
|
100
|
+
* precached the boot-time URL and a document rendered a minute ago names it; a 404 there is a page
|
|
101
|
+
* with no CSS. Bounded, because `x dev` mints one per edit.
|
|
102
|
+
*/
|
|
103
|
+
const minted = new Map<string, StyleChunk>();
|
|
104
|
+
const MINTED_KEPT = 32;
|
|
105
|
+
|
|
84
106
|
/**
|
|
85
107
|
* Test seam, and the shape `islandBundle` has: a table built from what the caller supplies.
|
|
86
108
|
*
|
package/src/style-routes.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// image.
|
|
5
5
|
|
|
6
6
|
import type { Route, UltimateRequest } from '@ultimat3/http';
|
|
7
|
-
import { applyCacheHeaders, json } from '@ultimat3/http';
|
|
7
|
+
import { applyCacheHeaders, json, NO_STORE } from '@ultimat3/http';
|
|
8
8
|
import type { StyleBundle } from './style-bundle';
|
|
9
9
|
import { STYLE_BASE_PATH } from './style-bundle';
|
|
10
10
|
|
|
@@ -32,7 +32,7 @@ export function styleRoutes(source: StyleSource): readonly Route[] {
|
|
|
32
32
|
handler: (request: UltimateRequest): Response => {
|
|
33
33
|
const chunk = source().chunkAt(request.pathname);
|
|
34
34
|
if (chunk === undefined) {
|
|
35
|
-
|
|
35
|
+
const missing = json(
|
|
36
36
|
{
|
|
37
37
|
ok: false,
|
|
38
38
|
error: {
|
|
@@ -45,6 +45,8 @@ export function styleRoutes(source: StyleSource): readonly Route[] {
|
|
|
45
45
|
},
|
|
46
46
|
{ status: 404 },
|
|
47
47
|
);
|
|
48
|
+
// `no-store`: a miss on a content-hashed URL must not be pinned by a CDN or a proxy.
|
|
49
|
+
return applyCacheHeaders(missing, NO_STORE);
|
|
48
50
|
}
|
|
49
51
|
return applyCacheHeaders(
|
|
50
52
|
new Response(chunk.css, { headers: { 'content-type': 'text/css; charset=utf-8' } }),
|