@txco/vite-plugin 0.0.0-stage → 0.1.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 +107 -2
- package/dist/build.d.ts +14 -0
- package/dist/build.js +16 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +124 -0
- package/package.json +56 -4
- package/src/build.ts +27 -0
- package/src/index.ts +159 -0
package/README.md
CHANGED
|
@@ -1,3 +1,108 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @txco/vite-plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A [Vite](https://vite.dev) plugin for [Thanks, Computer](https://www.thanks.computer)
|
|
4
|
+
(txco). It's for plain Vite apps: React, Vue, Solid, Preact or vanilla, with no
|
|
5
|
+
meta-framework. After `vite build`, it writes a **Web ABI build** beside your
|
|
6
|
+
`dist/`:
|
|
7
|
+
|
|
8
|
+
- a copy of `dist/` as `public/`;
|
|
9
|
+
- the ops a browser-routed app needs, as `ops/`;
|
|
10
|
+
- a `txco-web.json` manifest.
|
|
11
|
+
|
|
12
|
+
`txco apply` installs the build into a stack.
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install -D @txco/vite-plugin
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Vite 6, 7 or 8.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// vite.config.ts
|
|
24
|
+
import { defineConfig } from "vite";
|
|
25
|
+
import react from "@vitejs/plugin-react";
|
|
26
|
+
import txco from "@txco/vite-plugin";
|
|
27
|
+
|
|
28
|
+
export default defineConfig({
|
|
29
|
+
plugins: [react(), txco()],
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Bind the build to a stack in your workspace's `txco.yaml`. The path is
|
|
34
|
+
relative to the workspace root, and must lie outside `OPS/`:
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
stacks:
|
|
38
|
+
web:
|
|
39
|
+
abi: txco-web
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Then build, check and deploy:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
vite build # writes dist/ as usual, and txco-web/
|
|
46
|
+
txco web check txco-web # installs it on a scratch chassis and probes it
|
|
47
|
+
txco apply # or: txco push web
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Gitignore `txco-web/`. It's all build output, and the fallback op embeds
|
|
51
|
+
`index.html`, asset hashes included, so it changes on every build.
|
|
52
|
+
|
|
53
|
+
## What it writes
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
txco-web/
|
|
57
|
+
txco-web.json { "abi": 1, "immutable": ["assets/"], … }
|
|
58
|
+
public/ a copy of dist/
|
|
59
|
+
ops/900000/spa-fallback.txcl 200 + index.html, for any page navigation
|
|
60
|
+
ops/900900/not-found.txcl a plain 404 for everything else (an asset miss, a POST)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`dist/` is left as Vite wrote it, so `vite preview` and anything else that
|
|
64
|
+
reads it still works.
|
|
65
|
+
|
|
66
|
+
**A single-page app** (one `index.html`) routes in the browser. Every page
|
|
67
|
+
path that no file or op of yours answers gets `index.html` with status 200.
|
|
68
|
+
The client router renders its own not-found view for a path it doesn't know.
|
|
69
|
+
There's no route table to check, so this is the Web ABI's plain fallback.
|
|
70
|
+
|
|
71
|
+
**A multi-page app** (several HTML inputs in `build.rollupOptions.input`)
|
|
72
|
+
writes `ops/900000/page-404.txcl` instead. A path with no page gets
|
|
73
|
+
`404.html` with status 404 if the build has one, from `public/404.html` or an
|
|
74
|
+
input page, and a plain 404 page if not.
|
|
75
|
+
|
|
76
|
+
**`base`.** With `base: "/app/"` the site lands under `public/app/`, and is
|
|
77
|
+
served at `/app/`. A full-URL `base` (a CDN) loads its assets from there; the
|
|
78
|
+
plugin warns.
|
|
79
|
+
|
|
80
|
+
**Caching.** Vite's hashed output under `assets/` is cached for a year. A
|
|
81
|
+
file you put in `public/assets/` yourself isn't hashed, and the plugin warns
|
|
82
|
+
about it.
|
|
83
|
+
|
|
84
|
+
**Dot paths** (`public/.well-known/…`) never deploy; the plugin warns and
|
|
85
|
+
leaves them out. Library and SSR builds write nothing.
|
|
86
|
+
|
|
87
|
+
## Options
|
|
88
|
+
|
|
89
|
+
| Option | Default | Meaning |
|
|
90
|
+
| ----------- | -------------- | ------- |
|
|
91
|
+
| `out` | `"txco-web"` | The Web ABI directory to write, relative to the project root. It must be a directory of its own, outside `OPS/` and `dist/`. |
|
|
92
|
+
| `spa` | detected | `true` answers every unknown page path with `index.html` and 200; `false` with the 404 page. |
|
|
93
|
+
| `immutable` | `["assets/"]` | The `public/` prefixes cached for a year. |
|
|
94
|
+
| `scope` | `900000` | The navigation op's scope. The catch-all goes 900 above it. |
|
|
95
|
+
|
|
96
|
+
## Safety
|
|
97
|
+
|
|
98
|
+
The plugin wipes and rewrites `out` on every build. Before the build starts,
|
|
99
|
+
it refuses an `out` that:
|
|
100
|
+
|
|
101
|
+
- is inside `OPS/`;
|
|
102
|
+
- is inside `dist/`;
|
|
103
|
+
- holds anything that isn't part of a Web ABI build;
|
|
104
|
+
- holds your project.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
package/dist/build.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a page navigation nothing else answered is handled:
|
|
3
|
+
*
|
|
4
|
+
* - "spa": one page (index.html), which routes in the browser: every page
|
|
5
|
+
* path gets the shell with 200;
|
|
6
|
+
* - "mpa": several pages, each a file: a path with no page gets 404.html
|
|
7
|
+
* (or a built-in 404 page) with 404.
|
|
8
|
+
*/
|
|
9
|
+
export type Mode = "spa" | "mpa";
|
|
10
|
+
/** The mode for a build's HTML pages, unless the `spa` option forces it. */
|
|
11
|
+
export declare function chooseMode(pages: readonly string[], spa?: boolean): Mode;
|
|
12
|
+
/** The page an spa build's fallback serves: index.html, or its only page. */
|
|
13
|
+
export declare function shellPage(pages: readonly string[]): string | null;
|
|
14
|
+
export { isDotPath, publicPrefix } from "@txco/web-abi/producer";
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The plugin's decisions about a finished build. Pure, so they're unit-tested.
|
|
2
|
+
/** The mode for a build's HTML pages, unless the `spa` option forces it. */
|
|
3
|
+
export function chooseMode(pages, spa) {
|
|
4
|
+
if (typeof spa === "boolean")
|
|
5
|
+
return spa ? "spa" : "mpa";
|
|
6
|
+
return pages.length > 1 ? "mpa" : "spa";
|
|
7
|
+
}
|
|
8
|
+
/** The page an spa build's fallback serves: index.html, or its only page. */
|
|
9
|
+
export function shellPage(pages) {
|
|
10
|
+
if (pages.includes("index.html"))
|
|
11
|
+
return "index.html";
|
|
12
|
+
return pages.length === 1 ? pages[0] : null;
|
|
13
|
+
}
|
|
14
|
+
// Where the build lands under public/, from `base`, and which paths never
|
|
15
|
+
// deploy: shared with every producer.
|
|
16
|
+
export { isDotPath, publicPrefix } from "@txco/web-abi/producer";
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
export declare const NAME = "@txco/vite-plugin";
|
|
3
|
+
export declare const VERSION = "0.1.0";
|
|
4
|
+
export interface TxcoOptions {
|
|
5
|
+
/** The Web ABI directory to write, relative to the project root. Default "txco-web". */
|
|
6
|
+
out?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Answer every unknown page path with the shell (index.html) and 200. The
|
|
9
|
+
* default: on for a one-page build, off for a multi-page one.
|
|
10
|
+
*/
|
|
11
|
+
spa?: boolean;
|
|
12
|
+
/** The public/ prefixes cached for a year. Default: Vite's assetsDir (`assets/`). */
|
|
13
|
+
immutable?: string[];
|
|
14
|
+
/** The navigation op's scope; the catch-all goes 900 above it. Default 900000. */
|
|
15
|
+
scope?: number;
|
|
16
|
+
}
|
|
17
|
+
export default function txco(options?: TxcoOptions): Plugin;
|
|
18
|
+
export { chooseMode, publicPrefix } from "./build.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// @txco/vite-plugin: after `vite build`, writes a Web ABI build for Thanks,
|
|
2
|
+
// Computer (txco) beside your dist/:
|
|
3
|
+
//
|
|
4
|
+
// txco-web/
|
|
5
|
+
// txco-web.json the manifest
|
|
6
|
+
// public/ a copy of dist/
|
|
7
|
+
// ops/ the navigation op and the catch-all
|
|
8
|
+
//
|
|
9
|
+
// // vite.config.ts
|
|
10
|
+
// import txco from "@txco/vite-plugin";
|
|
11
|
+
// export default defineConfig({ plugins: [txco()] });
|
|
12
|
+
//
|
|
13
|
+
// `txco apply` installs it into the stack txco.yaml binds it to.
|
|
14
|
+
import { readdir, readFile, rm } from "node:fs/promises";
|
|
15
|
+
import { join, relative, resolve } from "node:path";
|
|
16
|
+
import { checkOutDir, copyPublic, listFiles, looksHashed, publicPrefix, renderOps, writeManifest, writeOps, } from "@txco/web-abi/producer";
|
|
17
|
+
import { chooseMode, shellPage } from "./build.js";
|
|
18
|
+
export const NAME = "@txco/vite-plugin";
|
|
19
|
+
export const VERSION = "0.1.0";
|
|
20
|
+
/** An op answer is capped at 4 MiB (--op-payload-max), base64 included. */
|
|
21
|
+
const PAGE_WARN_BYTES = 1 << 20;
|
|
22
|
+
export default function txco(options = {}) {
|
|
23
|
+
let config;
|
|
24
|
+
let out = "";
|
|
25
|
+
let skip = false;
|
|
26
|
+
return {
|
|
27
|
+
name: "txco",
|
|
28
|
+
apply: "build",
|
|
29
|
+
enforce: "post",
|
|
30
|
+
// One Web ABI build, from the client build (Vite ≥ 6 asks per environment).
|
|
31
|
+
applyToEnvironment: (env) => env.name === "client",
|
|
32
|
+
async configResolved(c) {
|
|
33
|
+
config = c;
|
|
34
|
+
if (c.build.lib || c.build.ssr) {
|
|
35
|
+
skip = true;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
out = resolve(c.root, options.out ?? "txco-web");
|
|
39
|
+
const outDir = resolve(c.root, c.build.outDir);
|
|
40
|
+
// Before anything is written: the build wipes and rewrites `out`.
|
|
41
|
+
const problems = checkOutDir({
|
|
42
|
+
dir: out,
|
|
43
|
+
protect: [c.root, outDir, c.publicDir].filter(Boolean),
|
|
44
|
+
notInside: [outDir],
|
|
45
|
+
entries: await readdir(out).catch(() => null),
|
|
46
|
+
});
|
|
47
|
+
if (problems.length > 0)
|
|
48
|
+
throw new Error(`[txco] ${problems.join("\n")}`);
|
|
49
|
+
},
|
|
50
|
+
writeBundle: {
|
|
51
|
+
order: "post",
|
|
52
|
+
sequential: true,
|
|
53
|
+
async handler(_output, bundle) {
|
|
54
|
+
if (skip)
|
|
55
|
+
return;
|
|
56
|
+
const log = config.logger;
|
|
57
|
+
const warn = (msg) => log.warn(`[txco] ${msg}`);
|
|
58
|
+
const outDir = resolve(config.root, config.build.outDir);
|
|
59
|
+
const pages = Object.values(bundle)
|
|
60
|
+
.filter((o) => o.type === "asset" && o.fileName.endsWith(".html"))
|
|
61
|
+
.map((o) => o.fileName)
|
|
62
|
+
.sort();
|
|
63
|
+
const mode = chooseMode(pages, options.spa);
|
|
64
|
+
const { prefix, external } = publicPrefix(config.base);
|
|
65
|
+
if (external)
|
|
66
|
+
warn(`base is ${config.base}: the assets load from there, and the pages are deployed at the root`);
|
|
67
|
+
// dist/ as public/, without what never deploys: .vite/ (build
|
|
68
|
+
// metadata) quietly, any other dot path with a warning.
|
|
69
|
+
await rm(out, { recursive: true, force: true });
|
|
70
|
+
const { skipped: dots } = await copyPublic(outDir, out, prefix);
|
|
71
|
+
if (dots.length > 0)
|
|
72
|
+
warn(`dist/ has ${dots.length} dot path(s) (${dots.slice(0, 3).join(", ")}); they never deploy`);
|
|
73
|
+
const readPage = async (name) => {
|
|
74
|
+
const html = await readFile(join(outDir, name), "utf8").catch(() => null);
|
|
75
|
+
return html === null ? null : { name, html };
|
|
76
|
+
};
|
|
77
|
+
let page;
|
|
78
|
+
if (mode === "spa") {
|
|
79
|
+
const name = shellPage(pages);
|
|
80
|
+
page = name ? await readPage(name) : null;
|
|
81
|
+
if (!page)
|
|
82
|
+
throw new Error(`[txco] a single-page build needs its shell (index.html), and ${outDir} has none`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
page = await readPage("404.html");
|
|
86
|
+
}
|
|
87
|
+
if (page && Buffer.byteLength(page.html) > PAGE_WARN_BYTES) {
|
|
88
|
+
warn(`${page.name} is ${Buffer.byteLength(page.html)} bytes; the op that serves it is capped at 4 MiB, base64 included`);
|
|
89
|
+
}
|
|
90
|
+
const ops = renderOps({
|
|
91
|
+
mode: mode === "spa" ? "spa" : "404",
|
|
92
|
+
scope: options.scope,
|
|
93
|
+
producer: NAME,
|
|
94
|
+
page,
|
|
95
|
+
notes: [
|
|
96
|
+
mode === "spa"
|
|
97
|
+
? "A Vite app routes in the browser and has no route table to check."
|
|
98
|
+
: `This build has ${pages.length} pages, each a file in public/.`,
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
await writeOps(out, ops);
|
|
102
|
+
const files = await listFiles(join(out, "public"));
|
|
103
|
+
const assets = config.build.assetsDir.replace(/^\/+|\/+$/g, "");
|
|
104
|
+
const immutable = options.immutable ?? (assets && files.some((f) => f.startsWith(`${prefix}${assets}/`)) ? [`${prefix}${assets}/`] : []);
|
|
105
|
+
for (const p of immutable) {
|
|
106
|
+
const unhashed = files.filter((f) => f.startsWith(p) && !looksHashed(f.split("/").pop()));
|
|
107
|
+
if (unhashed.length > 0) {
|
|
108
|
+
warn(`${unhashed.length} file(s) under the immutable prefix ${p} carry no content hash (${unhashed.slice(0, 3).join(", ")}), so a change to them wouldn't reach a browser for a year`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
await writeManifest(out, {
|
|
112
|
+
abi: 1,
|
|
113
|
+
...(immutable.length > 0 ? { immutable } : {}),
|
|
114
|
+
"x-producer": { name: NAME, version: VERSION },
|
|
115
|
+
"x-vite": { version: this.meta.viteVersion, mode, pages: pages.length },
|
|
116
|
+
});
|
|
117
|
+
const shown = relative(process.cwd(), out) || ".";
|
|
118
|
+
log.info(`[txco] Web ABI build (${mode}) at ${shown}: ${files.length} public file(s), ${Object.keys(ops).length} op(s). ` +
|
|
119
|
+
`Next: \`txco web check ${shown}\`, bind it in txco.yaml (stacks: <name>: abi: ${shown}), then \`txco apply\`.`);
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export { chooseMode, publicPrefix } from "./build.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@txco/vite-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Vite plugin for Thanks, Computer (txco): after `vite build`, writes a Web ABI build — your dist/ as public/, the ops a browser-routed app needs as ops/, and txco-web.json — for `txco apply` to install.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"keywords": [
|
|
9
|
+
"vite",
|
|
10
|
+
"vite-plugin",
|
|
11
|
+
"spa",
|
|
12
|
+
"txco",
|
|
13
|
+
"thanks-computer",
|
|
14
|
+
"static",
|
|
15
|
+
"web-abi"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/LoremLabs/thanks-computer.git",
|
|
20
|
+
"directory": "sdk/vite-plugin"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/LoremLabs/thanks-computer/tree/master/sdk/vite-plugin#readme",
|
|
23
|
+
"bugs": "https://github.com/LoremLabs/thanks-computer/issues",
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=20"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"clean": "rm -rf dist",
|
|
43
|
+
"build": "tsc -p tsconfig.json",
|
|
44
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
45
|
+
"test": "npm run build && node --test test/*.test.mjs"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@txco/web-abi": "^0.2.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"vite": "^6.1.0 || ^7.0.0 || ^8.0.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^20.0.0",
|
|
55
|
+
"typescript": "^5.4.0",
|
|
56
|
+
"vite": "^8.3.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/build.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The plugin's decisions about a finished build. Pure, so they're unit-tested.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How a page navigation nothing else answered is handled:
|
|
5
|
+
*
|
|
6
|
+
* - "spa": one page (index.html), which routes in the browser: every page
|
|
7
|
+
* path gets the shell with 200;
|
|
8
|
+
* - "mpa": several pages, each a file: a path with no page gets 404.html
|
|
9
|
+
* (or a built-in 404 page) with 404.
|
|
10
|
+
*/
|
|
11
|
+
export type Mode = "spa" | "mpa";
|
|
12
|
+
|
|
13
|
+
/** The mode for a build's HTML pages, unless the `spa` option forces it. */
|
|
14
|
+
export function chooseMode(pages: readonly string[], spa?: boolean): Mode {
|
|
15
|
+
if (typeof spa === "boolean") return spa ? "spa" : "mpa";
|
|
16
|
+
return pages.length > 1 ? "mpa" : "spa";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The page an spa build's fallback serves: index.html, or its only page. */
|
|
20
|
+
export function shellPage(pages: readonly string[]): string | null {
|
|
21
|
+
if (pages.includes("index.html")) return "index.html";
|
|
22
|
+
return pages.length === 1 ? pages[0] : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Where the build lands under public/, from `base`, and which paths never
|
|
26
|
+
// deploy: shared with every producer.
|
|
27
|
+
export { isDotPath, publicPrefix } from "@txco/web-abi/producer";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// @txco/vite-plugin: after `vite build`, writes a Web ABI build for Thanks,
|
|
2
|
+
// Computer (txco) beside your dist/:
|
|
3
|
+
//
|
|
4
|
+
// txco-web/
|
|
5
|
+
// txco-web.json the manifest
|
|
6
|
+
// public/ a copy of dist/
|
|
7
|
+
// ops/ the navigation op and the catch-all
|
|
8
|
+
//
|
|
9
|
+
// // vite.config.ts
|
|
10
|
+
// import txco from "@txco/vite-plugin";
|
|
11
|
+
// export default defineConfig({ plugins: [txco()] });
|
|
12
|
+
//
|
|
13
|
+
// `txco apply` installs it into the stack txco.yaml binds it to.
|
|
14
|
+
import { readdir, readFile, rm } from "node:fs/promises";
|
|
15
|
+
import { join, relative, resolve } from "node:path";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
checkOutDir,
|
|
19
|
+
copyPublic,
|
|
20
|
+
listFiles,
|
|
21
|
+
looksHashed,
|
|
22
|
+
publicPrefix,
|
|
23
|
+
renderOps,
|
|
24
|
+
writeManifest,
|
|
25
|
+
writeOps,
|
|
26
|
+
} from "@txco/web-abi/producer";
|
|
27
|
+
import type { Plugin, ResolvedConfig } from "vite";
|
|
28
|
+
|
|
29
|
+
import { chooseMode, shellPage } from "./build.js";
|
|
30
|
+
|
|
31
|
+
export const NAME = "@txco/vite-plugin";
|
|
32
|
+
export const VERSION = "0.1.0";
|
|
33
|
+
/** An op answer is capped at 4 MiB (--op-payload-max), base64 included. */
|
|
34
|
+
const PAGE_WARN_BYTES = 1 << 20;
|
|
35
|
+
|
|
36
|
+
export interface TxcoOptions {
|
|
37
|
+
/** The Web ABI directory to write, relative to the project root. Default "txco-web". */
|
|
38
|
+
out?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Answer every unknown page path with the shell (index.html) and 200. The
|
|
41
|
+
* default: on for a one-page build, off for a multi-page one.
|
|
42
|
+
*/
|
|
43
|
+
spa?: boolean;
|
|
44
|
+
/** The public/ prefixes cached for a year. Default: Vite's assetsDir (`assets/`). */
|
|
45
|
+
immutable?: string[];
|
|
46
|
+
/** The navigation op's scope; the catch-all goes 900 above it. Default 900000. */
|
|
47
|
+
scope?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default function txco(options: TxcoOptions = {}): Plugin {
|
|
51
|
+
let config: ResolvedConfig;
|
|
52
|
+
let out = "";
|
|
53
|
+
let skip = false;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
name: "txco",
|
|
57
|
+
apply: "build",
|
|
58
|
+
enforce: "post",
|
|
59
|
+
// One Web ABI build, from the client build (Vite ≥ 6 asks per environment).
|
|
60
|
+
applyToEnvironment: (env) => env.name === "client",
|
|
61
|
+
|
|
62
|
+
async configResolved(c) {
|
|
63
|
+
config = c;
|
|
64
|
+
if (c.build.lib || c.build.ssr) {
|
|
65
|
+
skip = true;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
out = resolve(c.root, options.out ?? "txco-web");
|
|
69
|
+
const outDir = resolve(c.root, c.build.outDir);
|
|
70
|
+
// Before anything is written: the build wipes and rewrites `out`.
|
|
71
|
+
const problems = checkOutDir({
|
|
72
|
+
dir: out,
|
|
73
|
+
protect: [c.root, outDir, c.publicDir].filter(Boolean),
|
|
74
|
+
notInside: [outDir],
|
|
75
|
+
entries: await readdir(out).catch(() => null),
|
|
76
|
+
});
|
|
77
|
+
if (problems.length > 0) throw new Error(`[txco] ${problems.join("\n")}`);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
writeBundle: {
|
|
81
|
+
order: "post",
|
|
82
|
+
sequential: true,
|
|
83
|
+
async handler(_output, bundle) {
|
|
84
|
+
if (skip) return;
|
|
85
|
+
const log = config.logger;
|
|
86
|
+
const warn = (msg: string) => log.warn(`[txco] ${msg}`);
|
|
87
|
+
const outDir = resolve(config.root, config.build.outDir);
|
|
88
|
+
|
|
89
|
+
const pages = Object.values(bundle)
|
|
90
|
+
.filter((o) => o.type === "asset" && o.fileName.endsWith(".html"))
|
|
91
|
+
.map((o) => o.fileName)
|
|
92
|
+
.sort();
|
|
93
|
+
const mode = chooseMode(pages, options.spa);
|
|
94
|
+
const { prefix, external } = publicPrefix(config.base);
|
|
95
|
+
if (external) warn(`base is ${config.base}: the assets load from there, and the pages are deployed at the root`);
|
|
96
|
+
|
|
97
|
+
// dist/ as public/, without what never deploys: .vite/ (build
|
|
98
|
+
// metadata) quietly, any other dot path with a warning.
|
|
99
|
+
await rm(out, { recursive: true, force: true });
|
|
100
|
+
const { skipped: dots } = await copyPublic(outDir, out, prefix);
|
|
101
|
+
if (dots.length > 0) warn(`dist/ has ${dots.length} dot path(s) (${dots.slice(0, 3).join(", ")}); they never deploy`);
|
|
102
|
+
|
|
103
|
+
const readPage = async (name: string) => {
|
|
104
|
+
const html = await readFile(join(outDir, name), "utf8").catch(() => null);
|
|
105
|
+
return html === null ? null : { name, html };
|
|
106
|
+
};
|
|
107
|
+
let page;
|
|
108
|
+
if (mode === "spa") {
|
|
109
|
+
const name = shellPage(pages);
|
|
110
|
+
page = name ? await readPage(name) : null;
|
|
111
|
+
if (!page) throw new Error(`[txco] a single-page build needs its shell (index.html), and ${outDir} has none`);
|
|
112
|
+
} else {
|
|
113
|
+
page = await readPage("404.html");
|
|
114
|
+
}
|
|
115
|
+
if (page && Buffer.byteLength(page.html) > PAGE_WARN_BYTES) {
|
|
116
|
+
warn(`${page.name} is ${Buffer.byteLength(page.html)} bytes; the op that serves it is capped at 4 MiB, base64 included`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const ops = renderOps({
|
|
120
|
+
mode: mode === "spa" ? "spa" : "404",
|
|
121
|
+
scope: options.scope,
|
|
122
|
+
producer: NAME,
|
|
123
|
+
page,
|
|
124
|
+
notes: [
|
|
125
|
+
mode === "spa"
|
|
126
|
+
? "A Vite app routes in the browser and has no route table to check."
|
|
127
|
+
: `This build has ${pages.length} pages, each a file in public/.`,
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
await writeOps(out, ops);
|
|
131
|
+
|
|
132
|
+
const files = await listFiles(join(out, "public"));
|
|
133
|
+
const assets = config.build.assetsDir.replace(/^\/+|\/+$/g, "");
|
|
134
|
+
const immutable = options.immutable ?? (assets && files.some((f) => f.startsWith(`${prefix}${assets}/`)) ? [`${prefix}${assets}/`] : []);
|
|
135
|
+
for (const p of immutable) {
|
|
136
|
+
const unhashed = files.filter((f) => f.startsWith(p) && !looksHashed(f.split("/").pop()!));
|
|
137
|
+
if (unhashed.length > 0) {
|
|
138
|
+
warn(`${unhashed.length} file(s) under the immutable prefix ${p} carry no content hash (${unhashed.slice(0, 3).join(", ")}), so a change to them wouldn't reach a browser for a year`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await writeManifest(out, {
|
|
143
|
+
abi: 1,
|
|
144
|
+
...(immutable.length > 0 ? { immutable } : {}),
|
|
145
|
+
"x-producer": { name: NAME, version: VERSION },
|
|
146
|
+
"x-vite": { version: this.meta.viteVersion, mode, pages: pages.length },
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const shown = relative(process.cwd(), out) || ".";
|
|
150
|
+
log.info(
|
|
151
|
+
`[txco] Web ABI build (${mode}) at ${shown}: ${files.length} public file(s), ${Object.keys(ops).length} op(s). ` +
|
|
152
|
+
`Next: \`txco web check ${shown}\`, bind it in txco.yaml (stacks: <name>: abi: ${shown}), then \`txco apply\`.`,
|
|
153
|
+
);
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export { chooseMode, publicPrefix } from "./build.js";
|