@uniflowed/host 0.0.0-alpha.8 → 0.0.0-alpha.9
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/assets.js +248 -0
- package/package.json +3 -1
package/assets.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// Plain JavaScript: this runs in the host that runs Vite, beside
|
|
4
|
+
// `transform.js`, and reaching the Flow transform is what that file is for.
|
|
5
|
+
//
|
|
6
|
+
// The JavaScript side of the `uf assets` service.
|
|
7
|
+
//
|
|
8
|
+
// Decoding, resizing and re-encoding images lives in `crates/uf_assets` and is
|
|
9
|
+
// reached exactly the way the Flow transform is reached: one long-lived `uf`
|
|
10
|
+
// process per host process, newline-delimited JSON in, replies in request
|
|
11
|
+
// order out. The protocol is documented on the other side, in
|
|
12
|
+
// `crates/uf_cli/src/commands/assets.rs`.
|
|
13
|
+
//
|
|
14
|
+
// Why a second service rather than a second message on the transform one: the
|
|
15
|
+
// transform service runs on a thread with half a gigabyte of stack because
|
|
16
|
+
// every stage of the Flow chain recurses, and it is started for every host
|
|
17
|
+
// that touches a module — the Node loader hook and the Bun preload included,
|
|
18
|
+
// neither of which has any use for an image pipeline. A build that imports no
|
|
19
|
+
// images should not start a process that can resize them, and a `uf test` run
|
|
20
|
+
// should not start one at all.
|
|
21
|
+
|
|
22
|
+
import { spawn } from "node:child_process";
|
|
23
|
+
import { createInterface } from "node:readline";
|
|
24
|
+
|
|
25
|
+
import { ufBinary, ufBinaryIdentity } from "./transform.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* File extensions the pipeline claims.
|
|
29
|
+
*
|
|
30
|
+
* `.svg` is deliberately not among them. uf could only ever copy one through —
|
|
31
|
+
* it is already resolution independent, so there is nothing to resize — and
|
|
32
|
+
* claiming it would take `.svg` away from `vite-plugin-svgr` and everything
|
|
33
|
+
* like it, which turn one into a component. A plugin that claims an extension
|
|
34
|
+
* to do nothing with it is the shape of red line 8 in `docs/red-lines.md`: if
|
|
35
|
+
* Vite can do it, a uf project can do it. So an SVG import stays Vite's, and
|
|
36
|
+
* `<Image src={url} width={…} height={…} />` is how you render one.
|
|
37
|
+
*
|
|
38
|
+
* `.gif` and `.avif` *are* claimed even though no decoder for them is compiled
|
|
39
|
+
* in, and that is the opposite decision for a reason: uf has something to say
|
|
40
|
+
* about them. The import still evaluates to a manifest, and the manifest
|
|
41
|
+
* carries a `note` saying the file was served unchanged and why — which is
|
|
42
|
+
* what tells an author their AVIF is not being resized, rather than leaving
|
|
43
|
+
* them to notice.
|
|
44
|
+
*/
|
|
45
|
+
export const IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".avif"];
|
|
46
|
+
|
|
47
|
+
/** Font file extensions the pipeline claims. */
|
|
48
|
+
export const FONT_EXTENSIONS = [".woff2", ".woff", ".ttf", ".otf"];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether this import is one uf's asset pipeline handles, and as what.
|
|
52
|
+
*
|
|
53
|
+
* Returns `"image"`, `"font"`, or `null`. The query string is stripped first:
|
|
54
|
+
* `./hero.png?width=400` is an image, and the query is how an import says what
|
|
55
|
+
* it wants.
|
|
56
|
+
*/
|
|
57
|
+
export function assetKind(id) {
|
|
58
|
+
const clean = stripQuery(id);
|
|
59
|
+
const lower = clean.toLowerCase();
|
|
60
|
+
if (IMAGE_EXTENSIONS.some((extension) => lower.endsWith(extension))) return "image";
|
|
61
|
+
if (FONT_EXTENSIONS.some((extension) => lower.endsWith(extension))) return "font";
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function stripQuery(id) {
|
|
66
|
+
const at = id.indexOf("?");
|
|
67
|
+
return at === -1 ? id : id.slice(0, at);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** An asset the pipeline could not process. */
|
|
71
|
+
export class AssetError extends Error {
|
|
72
|
+
constructor(id, message) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.name = "AssetError";
|
|
75
|
+
this.id = id;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* One `uf assets` process, with requests answered in the order they were sent.
|
|
81
|
+
*
|
|
82
|
+
* The same arrangement as `TransformService` next door, including the reason
|
|
83
|
+
* there are no correlation ids: the service replies once per request and in
|
|
84
|
+
* order, so a plain queue of resolvers pairs a reply with its caller. Any exit
|
|
85
|
+
* is final and every outstanding request is rejected at once.
|
|
86
|
+
*/
|
|
87
|
+
export class AssetService {
|
|
88
|
+
#child;
|
|
89
|
+
#pending = [];
|
|
90
|
+
#identity;
|
|
91
|
+
#failure = null;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {object} [options]
|
|
95
|
+
* @param {string} [options.command] the `uf` binary; `ufBinary()` by default
|
|
96
|
+
* @param {string} [options.root] project root, so `uf.config.js` is found
|
|
97
|
+
*/
|
|
98
|
+
constructor(options = {}) {
|
|
99
|
+
const command = options.command ?? ufBinary();
|
|
100
|
+
const root = options.root ?? process.cwd();
|
|
101
|
+
// Read before the spawn and kept, for the reason `TransformService` gives:
|
|
102
|
+
// the child goes on executing the binary it started from however many
|
|
103
|
+
// times that file is rewritten underneath it, and anything cached from an
|
|
104
|
+
// answer of this service belongs under *this* identity.
|
|
105
|
+
this.#identity = ufBinaryIdentity(command);
|
|
106
|
+
this.#child = spawn(command, ["--cwd", root, "assets"], {
|
|
107
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
createInterface({ input: this.#child.stdout }).on("line", (line) => {
|
|
111
|
+
const waiting = this.#pending.shift();
|
|
112
|
+
if (!waiting) return;
|
|
113
|
+
let reply;
|
|
114
|
+
try {
|
|
115
|
+
reply = JSON.parse(line);
|
|
116
|
+
} catch {
|
|
117
|
+
waiting.reject(new Error(`uf assets sent a malformed reply: ${line}`));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (reply.error != null) {
|
|
121
|
+
waiting.reject(new AssetError(waiting.id, reply.error));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
waiting.resolve(reply);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
this.#child.on("error", (error) => {
|
|
128
|
+
this.#settleAll(new Error(`could not run \`${command} assets\`: ${error.message}`));
|
|
129
|
+
});
|
|
130
|
+
this.#child.on("close", (code) => {
|
|
131
|
+
this.#settleAll(new Error(`uf assets exited (${code})`));
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#send(request) {
|
|
136
|
+
if (this.#failure) return Promise.reject(this.#failure);
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
this.#pending.push({ id: request.id, resolve, reject });
|
|
139
|
+
this.#child.stdin.write(`${JSON.stringify(request)}\n`);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#settleAll(error) {
|
|
144
|
+
this.#failure = error;
|
|
145
|
+
while (this.#pending.length > 0) this.#pending.shift().reject(error);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Resize and re-encode one image.
|
|
150
|
+
*
|
|
151
|
+
* Resolves to the manifest the component reads: `{ width, height, format,
|
|
152
|
+
* variants, blur, declined, note }`. Every field is named here rather than
|
|
153
|
+
* passed through, which is deliberate and is the bug `transform.js` records
|
|
154
|
+
* next door: a shim that copies three of four fields drops the fourth
|
|
155
|
+
* silently, and the caller sees `undefined` rather than an error.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} id absolute path to the source image
|
|
158
|
+
* @param {object} options
|
|
159
|
+
* @param {string} options.outDir where variants are written
|
|
160
|
+
* @param {number[]} [options.widths] overriding the project's
|
|
161
|
+
* @param {number} [options.quality]
|
|
162
|
+
* @param {boolean} [options.blur]
|
|
163
|
+
*/
|
|
164
|
+
async image(id, options) {
|
|
165
|
+
const reply = await this.#send({
|
|
166
|
+
kind: "image",
|
|
167
|
+
id,
|
|
168
|
+
outDir: options.outDir,
|
|
169
|
+
widths: options.widths,
|
|
170
|
+
quality: options.quality,
|
|
171
|
+
blur: options.blur,
|
|
172
|
+
});
|
|
173
|
+
const image = reply.image;
|
|
174
|
+
if (image == null) throw new AssetError(id, "uf assets returned no image");
|
|
175
|
+
return {
|
|
176
|
+
width: image.width ?? null,
|
|
177
|
+
height: image.height ?? null,
|
|
178
|
+
format: image.format,
|
|
179
|
+
variants: image.variants ?? [],
|
|
180
|
+
blur: image.blur ?? null,
|
|
181
|
+
declined: image.declined ?? [],
|
|
182
|
+
note: image.note ?? null,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Self-host one font and describe it.
|
|
188
|
+
*
|
|
189
|
+
* Resolves to `{ file, mime, bytes, family, fallbackFamily, container,
|
|
190
|
+
* metrics, fallback, fallbackDeclined, css }`.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} id absolute path to the source font
|
|
193
|
+
* @param {object} options
|
|
194
|
+
* @param {string} options.outDir where the copy is written
|
|
195
|
+
* @param {string} [options.family]
|
|
196
|
+
* @param {string} [options.weight]
|
|
197
|
+
* @param {string} [options.style]
|
|
198
|
+
* @param {string} [options.display]
|
|
199
|
+
* @param {string} [options.baseUrl] prefixed to the file name in `src: url()`
|
|
200
|
+
* @param {string | null} [options.fallback] `null` for no fallback face
|
|
201
|
+
*/
|
|
202
|
+
async font(id, options) {
|
|
203
|
+
const request = {
|
|
204
|
+
kind: "font",
|
|
205
|
+
id,
|
|
206
|
+
outDir: options.outDir,
|
|
207
|
+
family: options.family,
|
|
208
|
+
weight: options.weight,
|
|
209
|
+
style: options.style,
|
|
210
|
+
display: options.display,
|
|
211
|
+
baseUrl: options.baseUrl,
|
|
212
|
+
};
|
|
213
|
+
// Only sent when the caller had an opinion. The service distinguishes "not
|
|
214
|
+
// mentioned, use the project's" from "explicitly none", and a key that is
|
|
215
|
+
// always present collapses the two.
|
|
216
|
+
if ("fallback" in options) request.fallback = options.fallback;
|
|
217
|
+
const reply = await this.#send(request);
|
|
218
|
+
const font = reply.font;
|
|
219
|
+
if (font == null) throw new AssetError(id, "uf assets returned no font");
|
|
220
|
+
return {
|
|
221
|
+
file: font.file,
|
|
222
|
+
mime: font.mime,
|
|
223
|
+
bytes: font.bytes,
|
|
224
|
+
family: font.family,
|
|
225
|
+
fallbackFamily: font.fallbackFamily ?? null,
|
|
226
|
+
container: font.container,
|
|
227
|
+
metrics: font.metrics,
|
|
228
|
+
fallback: font.fallback ?? null,
|
|
229
|
+
fallbackDeclined: font.fallbackDeclined ?? null,
|
|
230
|
+
css: font.css,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The build of `uf` this service's child is executing, or `null`.
|
|
236
|
+
*
|
|
237
|
+
* @returns {string | null}
|
|
238
|
+
*/
|
|
239
|
+
get identity() {
|
|
240
|
+
return this.#identity;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Stop the process. Outstanding requests are rejected. */
|
|
244
|
+
close() {
|
|
245
|
+
this.#child.stdin.end();
|
|
246
|
+
this.#child.kill();
|
|
247
|
+
}
|
|
248
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/host",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.9",
|
|
4
4
|
"description": "Running Flow on a Capability JS Host, with no bundler in the way.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"directory": "packages/host"
|
|
12
12
|
},
|
|
13
13
|
"exports": {
|
|
14
|
+
"./assets": "./assets.js",
|
|
14
15
|
"./bun-preload": "./bun-preload.js",
|
|
15
16
|
"./internal/node-hooks.js": "./internal/node-hooks.js",
|
|
16
17
|
"./module-mocks": "./module-mocks.js",
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
},
|
|
21
22
|
"files": [
|
|
22
23
|
"register.js",
|
|
24
|
+
"assets.js",
|
|
23
25
|
"bun-preload.js",
|
|
24
26
|
"module-mocks.js",
|
|
25
27
|
"transform.js",
|