@atelic-action/ui 0.15.1 → 0.16.1
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 +58 -8
- package/bin/atelic-check-access.ts +5 -0
- package/bin/atelic-check-links.ts +5 -0
- package/bin/atelic-check-wall.ts +5 -0
- package/bin/atelic-print-sheets.sh +69 -0
- package/bin/atelic-reader-link.ts +5 -0
- package/bin/atelic-seal-document.mjs +6 -0
- package/bin/atelic-serve-static.ts +5 -0
- package/bin/atelic-stamp-health.ts +5 -0
- package/dist/vite/artifact-config.d.ts +24 -0
- package/dist/vite/artifact-config.js +83 -0
- package/dist/vite/index.d.ts +7 -0
- package/dist/vite/index.js +7 -0
- package/package.json +28 -5
- package/src/scripts/build-gate-payload.mjs +177 -0
- package/src/scripts/check-access.ts +93 -0
- package/src/scripts/check-links.ts +104 -0
- package/src/scripts/check-wall.ts +357 -0
- package/src/scripts/reader-link.ts +108 -0
- package/src/scripts/serve-static.ts +47 -0
- package/src/scripts/site-config.ts +34 -0
- package/src/scripts/stamp-health.ts +29 -0
- package/src/vite/artifact-config.ts +105 -0
- package/src/vite/index.ts +12 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serves dist/client the way Vercel does for these sites: a file as is, a
|
|
3
|
+
* clean URL from its .html file or folder index, and anything else as
|
|
4
|
+
* 404.html with a real 404 status. Only the e2e suite uses it; production
|
|
5
|
+
* never runs a server. The build it serves is the site's, found from the
|
|
6
|
+
* working directory.
|
|
7
|
+
*
|
|
8
|
+
* Usage: atelic-serve-static (PORT sets the port, 4173 unless given)
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, statSync } from "node:fs";
|
|
11
|
+
import { join, normalize } from "node:path";
|
|
12
|
+
|
|
13
|
+
/** The file a request path names under `root`, by Vercel's clean URL rules; none on a miss. */
|
|
14
|
+
export function resolveFile(root: string, pathname: string): string | undefined {
|
|
15
|
+
const clean = normalize(decodeURIComponent(pathname)).replace(/^(\.\.[/\\])+/, "");
|
|
16
|
+
const candidates = [
|
|
17
|
+
join(root, clean),
|
|
18
|
+
join(root, `${clean}.html`),
|
|
19
|
+
join(root, clean, "index.html"),
|
|
20
|
+
];
|
|
21
|
+
return candidates.find(
|
|
22
|
+
(file) => file.startsWith(root) && existsSync(file) && statSync(file).isFile(),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The command: serves dist/client of the working directory until stopped. */
|
|
27
|
+
export function main(cwd: string = process.cwd()): void {
|
|
28
|
+
const root = join(cwd, "dist", "client");
|
|
29
|
+
const port = Number(process.env.PORT ?? 4173);
|
|
30
|
+
|
|
31
|
+
Bun.serve({
|
|
32
|
+
// Loopback only: this serves the build to the local browser under test and
|
|
33
|
+
// has no business answering anyone else on the network.
|
|
34
|
+
hostname: "127.0.0.1",
|
|
35
|
+
port,
|
|
36
|
+
fetch(request) {
|
|
37
|
+
const file = resolveFile(root, new URL(request.url).pathname);
|
|
38
|
+
if (file) return new Response(Bun.file(file));
|
|
39
|
+
return new Response(Bun.file(join(root, "404.html")), {
|
|
40
|
+
status: 404,
|
|
41
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
console.log(`serving ${root} on http://127.0.0.1:${port}`);
|
|
47
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finds the site a command was run in. A command lives in the package, so
|
|
3
|
+
* it cannot import the site's config by a relative path as a script in the
|
|
4
|
+
* site once did: it resolves src/site.config.ts against the directory it
|
|
5
|
+
* was run from and imports that.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
import type { ArtifactConfig } from "../artifact/artifactConfig";
|
|
11
|
+
|
|
12
|
+
/** Where a site keeps its config, from the site's root. */
|
|
13
|
+
export const SITE_CONFIG = "src/site.config.ts";
|
|
14
|
+
|
|
15
|
+
/** The absolute path of the config for the site rooted at `cwd`. */
|
|
16
|
+
export function siteConfigPath(cwd: string = process.cwd()): string {
|
|
17
|
+
return resolve(cwd, SITE_CONFIG);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The `site` export of the config in `cwd`. Throws one line naming the
|
|
22
|
+
* directory when there is no config there, or when it exports no `site`.
|
|
23
|
+
*/
|
|
24
|
+
export async function loadSite(cwd: string = process.cwd()): Promise<ArtifactConfig> {
|
|
25
|
+
const path = siteConfigPath(cwd);
|
|
26
|
+
if (!existsSync(path)) {
|
|
27
|
+
throw new Error(`no ${SITE_CONFIG} in ${cwd}. Run this from the root of an artifact site.`);
|
|
28
|
+
}
|
|
29
|
+
const module = (await import(pathToFileURL(path).href)) as { site?: ArtifactConfig };
|
|
30
|
+
if (!module.site) {
|
|
31
|
+
throw new Error(`${SITE_CONFIG} in ${cwd} exports no \`site\`.`);
|
|
32
|
+
}
|
|
33
|
+
return module.site;
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes dist/client/health.json after the build: a shallow health endpoint
|
|
3
|
+
* for uptime monitors that also answers "which build is live?".
|
|
4
|
+
* Runs as the last step of `bun build`, from the site's root.
|
|
5
|
+
*
|
|
6
|
+
* Usage: atelic-stamp-health
|
|
7
|
+
*/
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { writeFileSync } from "node:fs";
|
|
10
|
+
|
|
11
|
+
function commitSha(): string {
|
|
12
|
+
// Vercel builds have no .git directory; the CLI/CI export the sha instead.
|
|
13
|
+
const fromEnv = process.env.VERCEL_GIT_COMMIT_SHA ?? process.env.GITHUB_SHA;
|
|
14
|
+
if (fromEnv) return fromEnv.slice(0, 7);
|
|
15
|
+
const git = spawnSync("git", ["rev-parse", "--short", "HEAD"], { encoding: "utf8" });
|
|
16
|
+
return git.status === 0 ? git.stdout.trim() : "unknown";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The command: stamps the build in the working directory. */
|
|
20
|
+
export function main(): void {
|
|
21
|
+
const health = {
|
|
22
|
+
status: "ok",
|
|
23
|
+
commit: commitSha(),
|
|
24
|
+
builtAt: new Date().toISOString(),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
writeFileSync("dist/client/health.json", `${JSON.stringify(health, null, "\t")}\n`);
|
|
28
|
+
console.log(`[health] dist/client/health.json (${health.commit})`);
|
|
29
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The whole Vite configuration of an artifact site, as one call. Every site
|
|
3
|
+
* cut from the artifact template carried its own copy of this configuration;
|
|
4
|
+
* here it is written once, and a site's vite.config.ts names its pages:
|
|
5
|
+
*
|
|
6
|
+
* import { artifactConfig } from "@atelic-action/ui/vite";
|
|
7
|
+
*
|
|
8
|
+
* export default artifactConfig({ pages: ["/writeup", "/proposal", "/report"] });
|
|
9
|
+
*
|
|
10
|
+
* The site's own paths (the `@` alias to its src) are found from the
|
|
11
|
+
* directory Vite is run in, which is the site's root.
|
|
12
|
+
*/
|
|
13
|
+
import { resolve } from "node:path";
|
|
14
|
+
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
|
15
|
+
import react from "@vitejs/plugin-react";
|
|
16
|
+
import { mergeConfig, type UserConfig } from "vite";
|
|
17
|
+
import { privateChunks } from "./private-chunks.js";
|
|
18
|
+
|
|
19
|
+
export interface ArtifactConfigOptions {
|
|
20
|
+
/**
|
|
21
|
+
* The path of each artifact page the site carries, such as "/writeup".
|
|
22
|
+
* The root and the 404 page are always prerendered and need no entry.
|
|
23
|
+
*/
|
|
24
|
+
pages: readonly string[];
|
|
25
|
+
/**
|
|
26
|
+
* The escape hatch for a site with a real need of its own: merged over
|
|
27
|
+
* the result with Vite's `mergeConfig`, so a site adds a setting without
|
|
28
|
+
* going back to a configuration written by hand.
|
|
29
|
+
*/
|
|
30
|
+
extend?: UserConfig;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** One entry of the prerender list. */
|
|
34
|
+
export interface ArtifactPage {
|
|
35
|
+
path: string;
|
|
36
|
+
prerender: { enabled: true };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The prerender list for a site: the root, each page it names, then the 404. */
|
|
40
|
+
export function artifactPages(pages: readonly string[]): ArtifactPage[] {
|
|
41
|
+
const named = pages.filter((path) => path !== "/" && path !== "/404");
|
|
42
|
+
return [
|
|
43
|
+
// The root holds no document: it sends the reader to /writeup
|
|
44
|
+
// (vercel.json does the same on the host), and its prerendered
|
|
45
|
+
// index.html is what a static preview serves at the bare host.
|
|
46
|
+
{ path: "/", prerender: { enabled: true } },
|
|
47
|
+
...named.map((path): ArtifactPage => ({ path, prerender: { enabled: true } })),
|
|
48
|
+
// The 404 page, rendered by the catch all route and copied to
|
|
49
|
+
// dist/client/404.html by the build for the host to serve on any miss.
|
|
50
|
+
{ path: "/404", prerender: { enabled: true } },
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function artifactConfig({ pages, extend }: ArtifactConfigOptions): UserConfig {
|
|
55
|
+
const config: UserConfig = {
|
|
56
|
+
// Every stylesheet rides in the one base sheet the root route links
|
|
57
|
+
// (src/styles/base.css), the documents' own included, since they come from
|
|
58
|
+
// @atelic-action/ui. Nothing imports CSS from a component, so there is no
|
|
59
|
+
// component bundle to fold: `build.cssCodeSplit: false`, which sat here to
|
|
60
|
+
// fold it, now fails the build, because TanStack Start looks for the
|
|
61
|
+
// `style.css` that setting emits and none is written. A site that gives a
|
|
62
|
+
// component its own stylesheet again restores the setting through `extend`.
|
|
63
|
+
plugins: [
|
|
64
|
+
tanstackStart({
|
|
65
|
+
prerender: {
|
|
66
|
+
enabled: true,
|
|
67
|
+
crawlLinks: true,
|
|
68
|
+
// The writeup links each screenshot crop to its full image, and
|
|
69
|
+
// the crawler follows every href it finds: without this filter it
|
|
70
|
+
// prerenders /images/<shot>.png as an HTML page and writes that
|
|
71
|
+
// over the real file in dist/client (found on a client's writeup,
|
|
72
|
+
// 2026-09-24).
|
|
73
|
+
filter: ({ path }) => !path.startsWith("/images/"),
|
|
74
|
+
},
|
|
75
|
+
// Nothing links to an artifact page, so the crawler would miss every
|
|
76
|
+
// one of them; each is listed by the site to be prerendered at all. A
|
|
77
|
+
// site names the pages it uses and no others.
|
|
78
|
+
pages: artifactPages(pages),
|
|
79
|
+
// No sitemap. A sitemap is a page asking to be crawled, and every
|
|
80
|
+
// page here is a document prepared for one business; public/robots.txt
|
|
81
|
+
// disallows the whole host to match.
|
|
82
|
+
sitemap: { enabled: false },
|
|
83
|
+
}),
|
|
84
|
+
react(),
|
|
85
|
+
// A document's content builds into /assets/doc/, which the access wall
|
|
86
|
+
// holds back, and the build fails if any lands where the login page
|
|
87
|
+
// can load it (privateChunks beside this file, needsSession in
|
|
88
|
+
// @atelic-action/ui/gate).
|
|
89
|
+
privateChunks(),
|
|
90
|
+
],
|
|
91
|
+
resolve: {
|
|
92
|
+
alias: {
|
|
93
|
+
// The site's src, from the directory Vite is run in: this file lives
|
|
94
|
+
// in the package, so nothing here can be found relative to it.
|
|
95
|
+
"@": resolve(process.cwd(), "./src"),
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
// @atelic-action/ui ships TSX and CSS as written, so the prerender's server build
|
|
99
|
+
// has to compile it rather than hand it to Node as an external.
|
|
100
|
+
ssr: {
|
|
101
|
+
noExternal: ["@atelic-action/ui"],
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
return extend ? mergeConfig(config, extend) : config;
|
|
105
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a site's vite.config.ts imports (`@atelic-action/ui/vite`): the
|
|
3
|
+
* configuration of an artifact site as one call, and the plugin inside it
|
|
4
|
+
* for a site that writes its own.
|
|
5
|
+
*/
|
|
6
|
+
export {
|
|
7
|
+
type ArtifactConfigOptions,
|
|
8
|
+
type ArtifactPage,
|
|
9
|
+
artifactConfig,
|
|
10
|
+
artifactPages,
|
|
11
|
+
} from "./artifact-config.js";
|
|
12
|
+
export { DOC_DIR, isPrivateContent, privateChunks } from "./private-chunks.js";
|