@astralarium/repo-config-vitest 0.1.0 → 0.2.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/README.md +16 -0
- package/dist/pages/head.d.ts +38 -0
- package/dist/pages/head.js +111 -0
- package/dist/pages/index.d.ts +3 -1
- package/dist/pages/index.js +3 -1
- package/dist/start/client-compiler.d.ts +4 -0
- package/dist/start/client-compiler.js +16 -0
- package/dist/start/index.d.ts +3 -0
- package/dist/start/index.js +3 -0
- package/dist/start/library-build.d.ts +20 -0
- package/dist/start/library-build.js +26 -0
- package/package.json +13 -7
package/README.md
CHANGED
|
@@ -33,3 +33,19 @@ export default defineConfig({
|
|
|
33
33
|
},
|
|
34
34
|
});
|
|
35
35
|
```
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
testPages(pages, [
|
|
39
|
+
expectHead(
|
|
40
|
+
{ title: "About - App", property: { "og:url": "/about" } },
|
|
41
|
+
{ origin },
|
|
42
|
+
),
|
|
43
|
+
]);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// ./start: client-bundle view of a Start module graph
|
|
48
|
+
const { code, modules, imports } = await libraryBuild(entry, {
|
|
49
|
+
plugins: startClientCompiler(),
|
|
50
|
+
});
|
|
51
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { PageCheck } from "./types.ts";
|
|
2
|
+
/** A page's head metadata, character references decoded. */
|
|
3
|
+
export interface PageHead {
|
|
4
|
+
/** First `<title>` in `<head>`. */
|
|
5
|
+
title?: string;
|
|
6
|
+
/** `<meta name>` contents in `<head>` by lowercase name (HTML matches
|
|
7
|
+
* names case-insensitively); the first tag wins. */
|
|
8
|
+
name: Record<string, string>;
|
|
9
|
+
/** `<meta property>` contents in `<head>` by lowercase property
|
|
10
|
+
* (OpenGraph); the first tag wins. */
|
|
11
|
+
property: Record<string, string>;
|
|
12
|
+
/** Target of the first `<meta http-equiv="refresh">`, anywhere in the
|
|
13
|
+
* document. */
|
|
14
|
+
refresh?: string;
|
|
15
|
+
}
|
|
16
|
+
/** The head fields an `expectHead` check asserts; each named field must
|
|
17
|
+
* match, `undefined` asserting absence. */
|
|
18
|
+
export interface HeadExpectation {
|
|
19
|
+
title?: string;
|
|
20
|
+
/** `<meta name>` contents by name, any case. */
|
|
21
|
+
name?: Record<string, string | undefined>;
|
|
22
|
+
/** `<meta property>` contents by property, any case. */
|
|
23
|
+
property?: Record<string, string | undefined>;
|
|
24
|
+
refresh?: string;
|
|
25
|
+
}
|
|
26
|
+
/** `headOf` settings. */
|
|
27
|
+
export interface HeadOptions {
|
|
28
|
+
/** Origin the page serves from: same-origin absolute URLs in `property`
|
|
29
|
+
* contents and the `refresh` target read origin-relative. */
|
|
30
|
+
origin?: string;
|
|
31
|
+
}
|
|
32
|
+
/** `html`'s head metadata (`PageHead`). */
|
|
33
|
+
export declare function headOf(html: string, { origin }?: HeadOptions): PageHead;
|
|
34
|
+
/** The page's head carries each field `expected` names (`headOf`). */
|
|
35
|
+
export declare function expectHead(expected: HeadExpectation, options?: HeadOptions): PageCheck;
|
|
36
|
+
/** `url` resolved against `origin`: its path, query and hash when it
|
|
37
|
+
* shares `origin`, else absolute. */
|
|
38
|
+
export declare function originRelative(origin: string, url: string): string;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { expect } from "vitest";
|
|
2
|
+
/** Every `<meta>` start tag of `html`, as attributes; a `>` inside a
|
|
3
|
+
* quoted value stays in the tag. */
|
|
4
|
+
function metaTags(html) {
|
|
5
|
+
return Array.from(html.matchAll(/<meta\b((?:[^>"']|"[^"]*"|'[^']*')*)>/gi), ([, attrs]) => attributes(attrs ?? ""));
|
|
6
|
+
}
|
|
7
|
+
/** `html`'s head metadata (`PageHead`). */
|
|
8
|
+
export function headOf(html, { origin } = {}) {
|
|
9
|
+
const head = html.split(/<\/head>/i)[0] ?? html;
|
|
10
|
+
const name = {};
|
|
11
|
+
const property = {};
|
|
12
|
+
for (const attrs of metaTags(head)) {
|
|
13
|
+
if (attrs.content === undefined)
|
|
14
|
+
continue;
|
|
15
|
+
const content = decodeEntities(attrs.content);
|
|
16
|
+
if (attrs.name !== undefined)
|
|
17
|
+
name[attrs.name.toLowerCase()] ??= content;
|
|
18
|
+
if (attrs.property !== undefined)
|
|
19
|
+
property[attrs.property.toLowerCase()] ??=
|
|
20
|
+
origin && /^https?:\/\//i.test(content)
|
|
21
|
+
? originRelative(origin, content)
|
|
22
|
+
: content;
|
|
23
|
+
}
|
|
24
|
+
const result = { name, property };
|
|
25
|
+
const title = /<title\b[^>]*>([\s\S]*?)<\/title>/i.exec(head)?.[1];
|
|
26
|
+
if (title !== undefined)
|
|
27
|
+
result.title = decodeEntities(title);
|
|
28
|
+
const refresh = metaTags(html).find((attrs) => attrs["http-equiv"]?.toLowerCase() === "refresh")?.content;
|
|
29
|
+
const target = refresh === undefined ? undefined : refreshUrl(decodeEntities(refresh));
|
|
30
|
+
if (target)
|
|
31
|
+
result.refresh = origin ? originRelative(origin, target) : target;
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
/** The page's head carries each field `expected` names (`headOf`). */
|
|
35
|
+
export function expectHead(expected, options = {}) {
|
|
36
|
+
return {
|
|
37
|
+
name: "carries the expected head metadata",
|
|
38
|
+
check: ({ html }) => {
|
|
39
|
+
const head = headOf(html, options);
|
|
40
|
+
const actual = {};
|
|
41
|
+
if ("title" in expected)
|
|
42
|
+
actual.title = head.title;
|
|
43
|
+
if ("refresh" in expected)
|
|
44
|
+
actual.refresh = head.refresh;
|
|
45
|
+
if (expected.name)
|
|
46
|
+
actual.name = Object.fromEntries(Object.keys(expected.name).map((key) => [
|
|
47
|
+
key,
|
|
48
|
+
head.name[key.toLowerCase()],
|
|
49
|
+
]));
|
|
50
|
+
if (expected.property)
|
|
51
|
+
actual.property = Object.fromEntries(Object.keys(expected.property).map((key) => [
|
|
52
|
+
key,
|
|
53
|
+
head.property[key.toLowerCase()],
|
|
54
|
+
]));
|
|
55
|
+
expect(actual).toEqual(expected);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** `url` resolved against `origin`: its path, query and hash when it
|
|
60
|
+
* shares `origin`, else absolute. */
|
|
61
|
+
export function originRelative(origin, url) {
|
|
62
|
+
const resolved = new URL(url, origin);
|
|
63
|
+
return resolved.origin === new URL(origin).origin
|
|
64
|
+
? `${resolved.pathname}${resolved.search}${resolved.hash}`
|
|
65
|
+
: resolved.href;
|
|
66
|
+
}
|
|
67
|
+
/** The URL of a refresh `content` (`0; url=…`), its quotes stripped. */
|
|
68
|
+
function refreshUrl(content) {
|
|
69
|
+
const url = /url\s*=\s*(.*)$/i.exec(content)?.[1]?.trim();
|
|
70
|
+
const quote = url?.[0];
|
|
71
|
+
if (url === undefined || (quote !== '"' && quote !== "'"))
|
|
72
|
+
return url;
|
|
73
|
+
const end = url.indexOf(quote, 1);
|
|
74
|
+
return url.slice(1, end === -1 ? undefined : end);
|
|
75
|
+
}
|
|
76
|
+
/** One attribute of a start tag: name, then a double-quoted,
|
|
77
|
+
* single-quoted or unquoted value, or none. */
|
|
78
|
+
const ATTRIBUTE = /([^\s"'<>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
79
|
+
/** Attributes of a start tag's attribute text, by lowercase name; the first
|
|
80
|
+
* of a name wins, and a valueless attribute reads `""`. */
|
|
81
|
+
function attributes(text) {
|
|
82
|
+
const attrs = {};
|
|
83
|
+
for (const [, name, double, single, unquoted] of text.matchAll(ATTRIBUTE))
|
|
84
|
+
attrs[name.toLowerCase()] ??= double ?? single ?? unquoted ?? "";
|
|
85
|
+
return attrs;
|
|
86
|
+
}
|
|
87
|
+
/** Named character references `decodeEntities` resolves. */
|
|
88
|
+
const NAMED_ENTITIES = {
|
|
89
|
+
amp: "&",
|
|
90
|
+
lt: "<",
|
|
91
|
+
gt: ">",
|
|
92
|
+
quot: '"',
|
|
93
|
+
apos: "'",
|
|
94
|
+
nbsp: " ",
|
|
95
|
+
};
|
|
96
|
+
/** Highest Unicode code point. */
|
|
97
|
+
const MAX_CODE_POINT = 0x10ffff;
|
|
98
|
+
/** One pass of HTML character reference decoding: numeric references to
|
|
99
|
+
* code points and `NAMED_ENTITIES`; others stay as written. */
|
|
100
|
+
function decodeEntities(text) {
|
|
101
|
+
return text.replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, (reference, body) => {
|
|
102
|
+
if (!body.startsWith("#"))
|
|
103
|
+
return NAMED_ENTITIES[body.toLowerCase()] ?? reference;
|
|
104
|
+
const codePoint = /^#x/i.test(body)
|
|
105
|
+
? parseInt(body.slice(2), 16)
|
|
106
|
+
: parseInt(body.slice(1), 10);
|
|
107
|
+
return codePoint <= MAX_CODE_POINT
|
|
108
|
+
? String.fromCodePoint(codePoint)
|
|
109
|
+
: reference;
|
|
110
|
+
});
|
|
111
|
+
}
|
package/dist/pages/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
/** Served-page invariants: the runner, and checks of server rendering
|
|
1
|
+
/** Served-page invariants: the runner, and checks of server rendering
|
|
2
|
+
* and head metadata. */
|
|
2
3
|
export * from "./checks.ts";
|
|
3
4
|
export * from "./classes.ts";
|
|
4
5
|
export { dehydratedContext, type DehydratedContextOptions, type DehydratedMatch, type DehydratedRouter, dehydratedRouter, } from "./dehydrated.ts";
|
|
6
|
+
export * from "./head.ts";
|
|
5
7
|
export * from "./runner.ts";
|
|
6
8
|
export * from "./types.ts";
|
|
7
9
|
export * from "./viewer.ts";
|
package/dist/pages/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
/** Served-page invariants: the runner, and checks of server rendering
|
|
1
|
+
/** Served-page invariants: the runner, and checks of server rendering
|
|
2
|
+
* and head metadata. */
|
|
2
3
|
export * from "./checks.js";
|
|
3
4
|
export * from "./classes.js";
|
|
4
5
|
export { dehydratedContext, dehydratedRouter, } from "./dehydrated.js";
|
|
6
|
+
export * from "./head.js";
|
|
5
7
|
export * from "./runner.js";
|
|
6
8
|
export * from "./types.js";
|
|
7
9
|
export * from "./viewer.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
|
2
|
+
/** Names of the client environment's compiler plugins in `tanstackStart()`. */
|
|
3
|
+
const CLIENT_COMPILER = new Set([
|
|
4
|
+
"tanstack-start-core::server-fn:client",
|
|
5
|
+
"tanstack-start-core:compiler-virtual-module",
|
|
6
|
+
]);
|
|
7
|
+
/** Start's compiler for the client environment, as an app's
|
|
8
|
+
* `tanstackStart()` configures it. */
|
|
9
|
+
export function startClientCompiler() {
|
|
10
|
+
return tanstackStart()
|
|
11
|
+
.flat(Infinity)
|
|
12
|
+
.filter((plugin) => typeof plugin === "object" &&
|
|
13
|
+
plugin !== null &&
|
|
14
|
+
"name" in plugin &&
|
|
15
|
+
CLIENT_COMPILER.has(plugin.name));
|
|
16
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Plugin } from "vite";
|
|
2
|
+
/** What `libraryBuild` builds with. */
|
|
3
|
+
export interface LibraryBuildOptions {
|
|
4
|
+
/** Plugins the build runs, as an app's config adds them. */
|
|
5
|
+
plugins?: Plugin[];
|
|
6
|
+
/** Compile-time replacements, e.g. the dev build flag. */
|
|
7
|
+
define?: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
/** A library build's output. */
|
|
10
|
+
export interface LibraryBuild {
|
|
11
|
+
/** The chunks' code, joined. */
|
|
12
|
+
code: string;
|
|
13
|
+
/** Files the chunks bundle. */
|
|
14
|
+
modules: string[];
|
|
15
|
+
/** Bare specifiers the chunks import. */
|
|
16
|
+
imports: string[];
|
|
17
|
+
}
|
|
18
|
+
/** The ES build of the module at `entry`, a file path: unminified,
|
|
19
|
+
* comment-free, bare imports external. */
|
|
20
|
+
export declare function libraryBuild(entry: string, { plugins, define }?: LibraryBuildOptions): Promise<LibraryBuild>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { build } from "vite";
|
|
2
|
+
/** The ES build of the module at `entry`, a file path: unminified,
|
|
3
|
+
* comment-free, bare imports external. */
|
|
4
|
+
export async function libraryBuild(entry, { plugins = [], define } = {}) {
|
|
5
|
+
const [output] = (await build({
|
|
6
|
+
configFile: false,
|
|
7
|
+
logLevel: "silent",
|
|
8
|
+
plugins,
|
|
9
|
+
define,
|
|
10
|
+
build: {
|
|
11
|
+
write: false,
|
|
12
|
+
minify: false,
|
|
13
|
+
lib: { entry, formats: ["es"] },
|
|
14
|
+
rolldownOptions: {
|
|
15
|
+
external: (id) => !id.startsWith(".") && !id.startsWith("/"),
|
|
16
|
+
output: { comments: false },
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
const chunks = output.output.flatMap((chunk) => chunk.type === "chunk" ? [chunk] : []);
|
|
21
|
+
return {
|
|
22
|
+
code: chunks.map((chunk) => chunk.code).join("\n"),
|
|
23
|
+
modules: chunks.flatMap((chunk) => chunk.moduleIds),
|
|
24
|
+
imports: chunks.flatMap((chunk) => chunk.imports),
|
|
25
|
+
};
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astralarium/repo-config-vitest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://github.com/astralarium/repo/tree/main/packages/repo-config-vitest#readme",
|
|
6
6
|
"repository": {
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"./harness/app-process": "./dist/harness/app-process.js",
|
|
22
22
|
"./harness/worker-process": "./dist/harness/worker-process.js",
|
|
23
23
|
"./pages": "./dist/pages/index.js",
|
|
24
|
-
"./react": "./dist/react.js"
|
|
24
|
+
"./react": "./dist/react.js",
|
|
25
|
+
"./start": "./dist/start/index.js"
|
|
25
26
|
},
|
|
26
27
|
"publishConfig": {
|
|
27
28
|
"access": "public"
|
|
@@ -38,10 +39,11 @@
|
|
|
38
39
|
"seroval": "^1.7.2",
|
|
39
40
|
"undici": "^7.29.0",
|
|
40
41
|
"zod": "^4.6.7",
|
|
41
|
-
"@astralarium/repo-
|
|
42
|
-
"@astralarium/repo-
|
|
42
|
+
"@astralarium/repo-dev": "^0.1.1",
|
|
43
|
+
"@astralarium/repo-telemetry": "^0.2.0"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
46
|
+
"@tanstack/react-start": "^1.169.4",
|
|
45
47
|
"@types/node": "^24.2.0",
|
|
46
48
|
"eslint": "^10.2.1",
|
|
47
49
|
"jsdom": "^29.1.1",
|
|
@@ -49,17 +51,21 @@
|
|
|
49
51
|
"vite": "^8.3.4",
|
|
50
52
|
"vitest": "^5.0.3",
|
|
51
53
|
"wrangler": "^4.132.0",
|
|
52
|
-
"@astralarium/repo-
|
|
53
|
-
"@astralarium/repo-config-eslint": "^0.
|
|
54
|
-
"@astralarium/repo-
|
|
54
|
+
"@astralarium/repo-config-typescript": "^0.1.0",
|
|
55
|
+
"@astralarium/repo-config-eslint": "^0.3.0",
|
|
56
|
+
"@astralarium/repo-package-files": "^0.1.0"
|
|
55
57
|
},
|
|
56
58
|
"peerDependencies": {
|
|
59
|
+
"@tanstack/react-start": "^1.169.4",
|
|
57
60
|
"jsdom": "^29.1.1",
|
|
58
61
|
"vite": "^8.3.4",
|
|
59
62
|
"vitest": "^5.0.3",
|
|
60
63
|
"wrangler": "^4.132.0"
|
|
61
64
|
},
|
|
62
65
|
"peerDependenciesMeta": {
|
|
66
|
+
"@tanstack/react-start": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
63
69
|
"jsdom": {
|
|
64
70
|
"optional": true
|
|
65
71
|
},
|