@uniflowed/vite 0.0.0-alpha.4 → 0.0.0-alpha.40

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.
@@ -435,7 +435,7 @@ export function markLines(lines) {
435
435
  *
436
436
  * Exported because it is where the decision is visible without starting Shiki:
437
437
  * give it the token split a grammar would produce and it says which words it
438
- * marked. `tests/library/highlight.test.js` uses exactly that, and the splits
438
+ * marked. `packages/vite/highlight.test.js` uses exactly that, and the splits
439
439
  * in it are the ones that had bugs.
440
440
  */
441
441
  export function markLine(line) {
@@ -0,0 +1,33 @@
1
+ // @noflow
2
+ //
3
+ // A document's YAML front matter, as `export const frontmatter`.
4
+ //
5
+ // Plain JavaScript, for the reason `index.js` gives: Vite imports this before
6
+ // any transform runs.
7
+ //
8
+ // This is `remark-mdx-frontmatter` for the one format uf reads, and it replaces
9
+ // that package for one reason. The package imported `toml` 3.0.0 at the top of
10
+ // its module whether or not a document held any TOML, so every project uf
11
+ // scaffolded installed a parser with two high advisories and failed its first
12
+ // `uf audit` (#1009). The TOML half was never reachable from here either:
13
+ // `remark-frontmatter` is given its default, which recognises YAML and nothing
14
+ // else, so a `+++` block was never a front-matter node to parse.
15
+ //
16
+ // For YAML the output is the package's, through the same two helpers it used:
17
+ // the first `yaml` node is parsed and defined as `frontmatter`, and a document
18
+ // with none exports `undefined`.
19
+
20
+ import { valueToEstree } from "estree-util-value-to-estree";
21
+ import { define } from "unist-util-mdx-define";
22
+ import { parse } from "yaml";
23
+
24
+ /** The remark plugin: `export const frontmatter` from a document's YAML. */
25
+ export default function remarkFrontmatterExport() {
26
+ return (tree, file) => {
27
+ const node = tree.children.find((child) => child.type === "yaml");
28
+ const data = node == null ? undefined : parse(node.value);
29
+ define(tree, file, {
30
+ frontmatter: valueToEstree(data, { preserveReferences: true }),
31
+ });
32
+ };
33
+ }
@@ -0,0 +1,98 @@
1
+ // @noflow
2
+ //
3
+ // Plain JavaScript: executed by the host that runs Vite, before any transform.
4
+ //
5
+ // Node's request and response objects on one side, the platform's `Request`
6
+ // and `Response` on the other.
7
+ //
8
+ // uf's server contracts are the platform's — a route handler and a middleware
9
+ // both take a `Request` and return a `Response`, because that is what runs
10
+ // unchanged on Node.js, Bun, Deno and a Cloudflare Worker. Node's dev server
11
+ // speaks `IncomingMessage` and `ServerResponse`, so exactly one place has to
12
+ // translate.
13
+ //
14
+ // It is a module rather than two functions in `driver.js` because there are
15
+ // two dev servers: `driver.js` is what `uf dev` spawns, and the `uf:flow`
16
+ // plugin's own `configureServer` is what a project using Vite directly gets.
17
+ // Both have to run the same middleware before the same request, and a second
18
+ // copy of this translation is how the two would come to disagree about, say,
19
+ // whether a repeated header is joined or appended.
20
+
21
+ /**
22
+ * A Node request as a `Request`.
23
+ *
24
+ * The body is read as a stream where the host supports it, because a handler
25
+ * that accepts an upload should not need the whole thing buffered before it
26
+ * starts.
27
+ *
28
+ * `path` names the path and query to build it at, when that is not the one the
29
+ * request line carried — `uf dev` passes the one Vite's base middleware has
30
+ * already taken `app.router.basePath` off.
31
+ *
32
+ * @param {import("node:http").IncomingMessage} incoming
33
+ * @param {{server?: {https?: unknown}} | undefined} config the resolved Vite config
34
+ * @param {string} [path]
35
+ */
36
+ export async function toRequest(incoming, config, path) {
37
+ const host = incoming.headers.host ?? "localhost";
38
+ const protocol = config?.server?.https == null ? "http" : "https";
39
+ const url = new URL(path ?? incoming.originalUrl ?? incoming.url ?? "/", `${protocol}://${host}`);
40
+
41
+ const headers = new Headers();
42
+ for (const [name, value] of Object.entries(incoming.headers)) {
43
+ if (value == null) continue;
44
+ for (const entry of Array.isArray(value) ? value : [value]) {
45
+ headers.append(name, entry);
46
+ }
47
+ }
48
+
49
+ const method = (incoming.method ?? "GET").toUpperCase();
50
+ const init = { method, headers };
51
+ if (method !== "GET" && method !== "HEAD") {
52
+ // `duplex` is required by the specification whenever a body is a stream,
53
+ // and Node throws without it.
54
+ init.body = incoming;
55
+ init.duplex = "half";
56
+ }
57
+ return new Request(url, init);
58
+ }
59
+
60
+ /**
61
+ * A Node request as a `Request` that carries its address and nothing else.
62
+ *
63
+ * For the questions asked about a URL alone — `app.router`'s redirects and
64
+ * headers, in front of Vite's own middleware — which must not wrap the body a
65
+ * later middleware turns into the `Request` the application reads.
66
+ *
67
+ * @param {import("node:http").IncomingMessage} incoming
68
+ */
69
+ export function toAddressRequest(incoming) {
70
+ const host = incoming.headers.host ?? "localhost";
71
+ return new Request(new URL(incoming.originalUrl ?? incoming.url ?? "/", `http://${host}`));
72
+ }
73
+
74
+ /**
75
+ * Write a `Response` to a Node response.
76
+ *
77
+ * One implementation, reached late. This was a second copy of the loop in
78
+ * `@uniflowed/server`'s `node.js`, and the two drifted the moment the shared
79
+ * one moved: `uf start` and every adapter lost the socket pacing and the
80
+ * hang-up cancel while `uf dev` and `uf preview` kept them, which is a
81
+ * deployment whose memory profile differs from the one that was checked. See
82
+ * ubugeeei-prod/uf#400.
83
+ *
84
+ * The import is inside the function, and that is not a style choice.
85
+ * `driver.js` imports this module *statically* and registers the Flow loader
86
+ * hooks in its own body, so anything reachable from a static import here is
87
+ * read by Node before there is anything to compile Flow with —
88
+ * `@uniflowed/server/node` is Flow source, and a static re-export of it makes
89
+ * every `uf build` die on `import type` with a `SyntaxError`. `loadBuild` in
90
+ * `internal/serve.js` defers for the same reason and says so.
91
+ *
92
+ * @param {import("node:http").ServerResponse} outgoing
93
+ * @param {Response} result
94
+ */
95
+ export async function send(outgoing, result) {
96
+ const { send: write } = await import("@uniflowed/server/node");
97
+ await write(outgoing, result);
98
+ }
@@ -0,0 +1,134 @@
1
+ // @noflow
2
+ //
3
+ // Plain JavaScript: the driver imports it, and the driver runs before any Flow
4
+ // loader exists.
5
+ //
6
+ // The module graph `uf build --analyze` reads: every module each bundle was
7
+ // built from, what it imports, and which chunk its rendered code went into.
8
+ //
9
+ // What the graph means — which route a module belongs to, the chain of imports
10
+ // behind it, what it weighs — is uf's, in `crates/uf_bundle/src/analysis.rs`.
11
+ // This file writes down only what the bundler knows and uf cannot, and that
12
+ // split is the reason the graph is a file at all: a builder other than this
13
+ // one that writes the same file gets the same analysis.
14
+ //
15
+ // {
16
+ // "version": 1,
17
+ // "builds": [{
18
+ // "environment": "client",
19
+ // "entries": ["virtual:uf/client"],
20
+ // "modules": [{ "id": "app/$page.js", "imports": ["app/Counter.js"], "dynamicImports": [] }],
21
+ // "chunks": [{
22
+ // "file": "assets/index-3f2a.js",
23
+ // "facade": "virtual:uf/client",
24
+ // "modules": [{ "id": "virtual:uf/client", "code": "…" }]
25
+ // }]
26
+ // }]
27
+ // }
28
+
29
+ import { mkdirSync, writeFileSync } from "node:fs";
30
+ import path from "node:path";
31
+
32
+ /** Where the driver writes the graph, in the build's metadata directory. */
33
+ export const MODULE_GRAPH_FILE = "uf-module-graph.json";
34
+
35
+ const VERSION = 1;
36
+
37
+ /**
38
+ * A plugin that records the graph of every bundle it takes part in, and the
39
+ * means to write what it recorded.
40
+ *
41
+ * `isReference(file)` says whether an absolute file is a client reference: an
42
+ * entry the browser loads because a server component named it, rather than on
43
+ * every page. Its chunk still carries it as the facade, and it is left out of
44
+ * `entries`, which is the list of what every page of a bundle loads.
45
+ */
46
+ export function createModuleGraphCollector(root, { isReference = () => false } = {}) {
47
+ const builds = [];
48
+ const graph = () => ({ version: VERSION, builds });
49
+ return {
50
+ plugin: {
51
+ name: "uf:module-graph",
52
+ // Last, so the bundle it reads is one every other plugin has finished.
53
+ enforce: "post",
54
+ generateBundle(_options, bundle) {
55
+ builds.push(describeBundle(this, root, bundle, isReference));
56
+ },
57
+ },
58
+ graph,
59
+ write(file) {
60
+ mkdirSync(path.dirname(file), { recursive: true });
61
+ writeFileSync(file, `${JSON.stringify(graph())}\n`);
62
+ },
63
+ };
64
+ }
65
+
66
+ /** One bundle, as `generateBundle` sees it. */
67
+ function describeBundle(context, root, bundle, isReference) {
68
+ const ids = typeof context.getModuleIds === "function" ? [...context.getModuleIds()] : [];
69
+ const modules = ids
70
+ .map((id) => {
71
+ const info = context.getModuleInfo(id);
72
+ return {
73
+ id: moduleId(root, id),
74
+ imports: (info?.importedIds ?? []).map((imported) => moduleId(root, imported)),
75
+ dynamicImports: (info?.dynamicallyImportedIds ?? []).map((imported) =>
76
+ moduleId(root, imported),
77
+ ),
78
+ };
79
+ })
80
+ .sort(byKey("id"));
81
+ const entries = new Set();
82
+ const chunks = [];
83
+ for (const output of Object.values(bundle)) {
84
+ if (output.type !== "chunk") {
85
+ continue;
86
+ }
87
+ const facade = output.isEntry ? (output.facadeModuleId ?? null) : null;
88
+ if (facade != null && !isReference(facade.split("?")[0])) {
89
+ entries.add(moduleId(root, facade));
90
+ }
91
+ chunks.push({
92
+ file: output.fileName,
93
+ facade: facade == null ? null : moduleId(root, facade),
94
+ // A module the bundler left no code for was still walked through to
95
+ // reach what it imports, so it stays in `modules` above; it has nothing
96
+ // to weigh, so it is not in the chunk.
97
+ modules: Object.entries(output.modules ?? {})
98
+ .map(([id, rendered]) => ({ id: moduleId(root, id), code: rendered?.code ?? "" }))
99
+ .filter((module) => module.code !== ""),
100
+ });
101
+ }
102
+ return {
103
+ environment: context.environment?.name ?? "client",
104
+ entries: [...entries].sort(),
105
+ modules,
106
+ chunks: chunks.sort(byKey("file")),
107
+ };
108
+ }
109
+
110
+ /**
111
+ * A module id as the analysis spells it: relative to the project root with
112
+ * `/`, without the `\0` a resolved virtual module carries, and with its query.
113
+ *
114
+ * Relative even when it climbs out of the root, as a workspace package does,
115
+ * because an absolute path would make two machines' reports of the same build
116
+ * disagree.
117
+ */
118
+ export function moduleId(root, id) {
119
+ const bare = id.startsWith("\0") ? id.slice(1) : id;
120
+ const at = bare.indexOf("?");
121
+ const file = at === -1 ? bare : bare.slice(0, at);
122
+ if (!path.isAbsolute(file)) {
123
+ return bare;
124
+ }
125
+ const relative = path.relative(root, file);
126
+ if (path.isAbsolute(relative)) {
127
+ return bare;
128
+ }
129
+ return `${relative.split(path.sep).join("/")}${at === -1 ? "" : bare.slice(at)}`;
130
+ }
131
+
132
+ function byKey(key) {
133
+ return (a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0);
134
+ }
@@ -0,0 +1,218 @@
1
+ // @noflow
2
+ //
3
+ // Route-handler OpenAPI output.
4
+ //
5
+ // The build driver imports this module before it registers the Node Flow
6
+ // hooks, so it must stay plain JavaScript and must not statically import
7
+ // Flow-authored packages. The schema and method vocabulary are loaded inside
8
+ // `createOpenApiDocument`, after the driver has installed the hooks.
9
+
10
+ const JSON = "application/json";
11
+ const QUERY_EXTENSION = "x-uf-query";
12
+ const STANDARD_METHODS = new Set(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]);
13
+
14
+ export async function createOpenApiDocument(handlers, options = {}) {
15
+ const [{ HANDLER_METHODS }, { toJsonSchema }] = await Promise.all([
16
+ import("@uniflowed/router/handler"),
17
+ import("@uniflowed/validator/json-schema"),
18
+ ]);
19
+ const paths = {};
20
+ for (const record of handlers ?? []) {
21
+ const routePath = openApiPath(record.path);
22
+ const pathItem = paths[routePath] ?? {};
23
+ paths[routePath] = pathItem;
24
+ const module = await loadHandlerModule(record, pathItem);
25
+ if (module == null) continue;
26
+ for (const method of implementedMethods(module, HANDLER_METHODS)) {
27
+ const operation = createOperation(record, method, schemaFor(module, method), toJsonSchema);
28
+ if (STANDARD_METHODS.has(method)) {
29
+ pathItem[method.toLowerCase()] = operation;
30
+ } else if (method === "QUERY") {
31
+ pathItem[QUERY_EXTENSION] = operation;
32
+ }
33
+ }
34
+ }
35
+ return {
36
+ openapi: "3.1.0",
37
+ info: {
38
+ title: options.title ?? "uf application",
39
+ version: options.version ?? "0.0.0",
40
+ },
41
+ paths,
42
+ };
43
+ }
44
+
45
+ async function loadHandlerModule(record, pathItem) {
46
+ try {
47
+ return await record.load();
48
+ } catch (error) {
49
+ if (record.file != null) pathItem["x-uf-source"] = record.file;
50
+ pathItem["x-uf-schema-unavailable"] = errorMessage(error);
51
+ return null;
52
+ }
53
+ }
54
+
55
+ function implementedMethods(module, methods) {
56
+ return methods.filter(
57
+ (method) =>
58
+ typeof module[method] === "function" ||
59
+ (method === "HEAD" && typeof module.GET === "function"),
60
+ );
61
+ }
62
+
63
+ function schemaFor(module, method) {
64
+ const table = module.schemas;
65
+ if (!isRecord(table)) return null;
66
+ const schema = table[method];
67
+ if (schema == null) return null;
68
+ if (!isRecord(schema)) {
69
+ throw new Error(`route handler schemas.${method} must be an object`);
70
+ }
71
+ return schema;
72
+ }
73
+
74
+ function createOperation(record, method, schema, toJsonSchema) {
75
+ const parameters = pathParameters(record);
76
+ const operation = {
77
+ operationId: operationId(record, method),
78
+ responses: untypedResponses(method),
79
+ };
80
+ if (record.file != null) operation["x-uf-source"] = record.file;
81
+ if (parameters.length > 0) operation.parameters = parameters;
82
+ if (schema == null) {
83
+ operation["x-uf-untyped"] = true;
84
+ return operation;
85
+ }
86
+
87
+ const unrepresentable = [];
88
+ if (schema.query != null) {
89
+ addQuerySchema(operation, exportSchema(toJsonSchema, schema.query, "query", unrepresentable));
90
+ }
91
+ if (schema.body != null) {
92
+ operation.requestBody = {
93
+ required: true,
94
+ content: {
95
+ [JSON]: { schema: exportSchema(toJsonSchema, schema.body, "body", unrepresentable) },
96
+ },
97
+ };
98
+ }
99
+ if (schema.response != null) {
100
+ operation.responses = {
101
+ "200": {
102
+ description: method === "HEAD" ? "Typed response headers" : "Typed response",
103
+ content:
104
+ method === "HEAD"
105
+ ? undefined
106
+ : {
107
+ [JSON]: {
108
+ schema: exportSchema(toJsonSchema, schema.response, "response", unrepresentable),
109
+ },
110
+ },
111
+ },
112
+ };
113
+ if (method === "HEAD") delete operation.responses["200"].content;
114
+ }
115
+ if (unrepresentable.length > 0) {
116
+ operation["x-uf-unrepresentable"] = unrepresentable;
117
+ }
118
+ return operation;
119
+ }
120
+
121
+ function untypedResponses(method) {
122
+ return {
123
+ "200": {
124
+ description: method === "HEAD" ? "Untyped response headers" : "Untyped response",
125
+ },
126
+ };
127
+ }
128
+
129
+ function exportSchema(toJsonSchema, schema, part, unrepresentable) {
130
+ const exported = toJsonSchema(schema);
131
+ for (const item of exported.unrepresentable) {
132
+ unrepresentable.push({ part, path: item.path, kind: item.kind });
133
+ }
134
+ return stripDialect(exported.schema);
135
+ }
136
+
137
+ function addQuerySchema(operation, schema) {
138
+ if (schema.type !== "object" || !isRecord(schema.properties) || hasOwn(schema, "$defs")) {
139
+ operation["x-uf-query-schema"] = schema;
140
+ return;
141
+ }
142
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
143
+ const parameters = Object.keys(schema.properties).map((name) => ({
144
+ name,
145
+ in: "query",
146
+ required: required.has(name),
147
+ schema: schema.properties[name],
148
+ }));
149
+ operation.parameters = [...(operation.parameters ?? []), ...parameters];
150
+ }
151
+
152
+ function stripDialect(value) {
153
+ if (Array.isArray(value)) return value.map(stripDialect);
154
+ if (!isRecord(value)) return value;
155
+ const out = {};
156
+ for (const key of Object.keys(value)) {
157
+ if (key === "$schema") continue;
158
+ out[key] = stripDialect(value[key]);
159
+ }
160
+ return out;
161
+ }
162
+
163
+ function openApiPath(routePath) {
164
+ return routePath
165
+ .split("/")
166
+ .map((segment) => {
167
+ if (!segment.startsWith(":")) return segment;
168
+ return `{${segment.endsWith("*") ? segment.slice(1, -1) : segment.slice(1)}}`;
169
+ })
170
+ .join("/");
171
+ }
172
+
173
+ function pathParameters(record) {
174
+ const params = record.params ?? paramsFromPath(record.path);
175
+ return params.map((param) => {
176
+ const parameter = {
177
+ name: param.name,
178
+ in: "path",
179
+ required: true,
180
+ schema: { type: "string" },
181
+ };
182
+ if (param.catchAll) {
183
+ parameter.description = "Catch-all route segment, slash-separated in the URL.";
184
+ }
185
+ return parameter;
186
+ });
187
+ }
188
+
189
+ function paramsFromPath(routePath) {
190
+ return routePath
191
+ .split("/")
192
+ .filter((segment) => segment.startsWith(":"))
193
+ .map((segment) => ({
194
+ name: segment.endsWith("*") ? segment.slice(1, -1) : segment.slice(1),
195
+ catchAll: segment.endsWith("*"),
196
+ }));
197
+ }
198
+
199
+ function operationId(record, method) {
200
+ const name = `${method.toLowerCase()} ${record.path}`;
201
+ return name
202
+ .replaceAll(/[^a-zA-Z0-9]+/g, " ")
203
+ .trim()
204
+ .replaceAll(/ ([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
205
+ }
206
+
207
+ function isRecord(value) {
208
+ return value != null && typeof value === "object" && !Array.isArray(value);
209
+ }
210
+
211
+ function hasOwn(value, key) {
212
+ return Object.prototype.hasOwnProperty.call(value, key);
213
+ }
214
+
215
+ function errorMessage(error) {
216
+ if (error instanceof Error) return error.message;
217
+ return String(error);
218
+ }