@mh-gg/vite 0.1.1-alpha.20260626T104441232Z
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/LICENSE +21 -0
- package/README.md +25 -0
- package/package.json +19 -0
- package/src/index.js +321 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matterhorn contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# matterhorn-sdk/vite
|
|
2
|
+
|
|
3
|
+
Vite plugin for Matterhorn frontend bundle publishing.
|
|
4
|
+
|
|
5
|
+
This package keeps the standalone `@mh-gg/vite` package available, but new code should import the plugin through `matterhorn-sdk/vite`.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { defineConfig } from "vite";
|
|
9
|
+
import { matterhorn } from "matterhorn-sdk/vite";
|
|
10
|
+
|
|
11
|
+
export default defineConfig({
|
|
12
|
+
plugins: [matterhorn()]
|
|
13
|
+
});
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The plugin emits:
|
|
17
|
+
|
|
18
|
+
- `matterhorn-frontend-manifest.json`, listing every built file with SHA-256 integrity and byte length.
|
|
19
|
+
- `matterhorn-frontend-bundle.zip`, containing the built files plus the manifest.
|
|
20
|
+
|
|
21
|
+
By default these files are written to the Vite root, not `dist`, so the generated archive is not included in the files it describes. Pass a relative directory when you want a separate deploy folder:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
plugins: [matterhorn("./deploy/")]
|
|
25
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mh-gg/vite",
|
|
3
|
+
"version": "0.1.1-alpha.20260626T104441232Z",
|
|
4
|
+
"description": "Vite plugin for Matterhorn frontend bundle manifests and archives.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.js",
|
|
8
|
+
"./package.json": "./package.json"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"package.json"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22.12"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT"
|
|
19
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MANIFEST_FILE = "matterhorn-frontend-manifest.json";
|
|
7
|
+
const DEFAULT_ARCHIVE_FILE = "matterhorn-frontend-bundle.zip";
|
|
8
|
+
const DOCUMENT_ENTRYPOINT = "index.html";
|
|
9
|
+
|
|
10
|
+
function pluginOptions(value) {
|
|
11
|
+
if (typeof value === "string") return { path: value };
|
|
12
|
+
if (value && typeof value === "object") return value;
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
function bytes(value) {
|
|
16
|
+
if (value instanceof Uint8Array) return Buffer.from(value);
|
|
17
|
+
return Buffer.from(String(value));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function crc32(buffer) {
|
|
21
|
+
let crc = 0xffffffff;
|
|
22
|
+
for (const byte of buffer) {
|
|
23
|
+
crc ^= byte;
|
|
24
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
25
|
+
crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function dosTime(date) {
|
|
32
|
+
return ((date.getHours() & 31) << 11) | ((date.getMinutes() & 63) << 5) | Math.floor(date.getSeconds() / 2);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function dosDate(date) {
|
|
36
|
+
return (((date.getFullYear() - 1980) & 127) << 9) | (((date.getMonth() + 1) & 15) << 5) | (date.getDate() & 31);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function u16(value) {
|
|
40
|
+
const buffer = Buffer.allocUnsafe(2);
|
|
41
|
+
buffer.writeUInt16LE(value);
|
|
42
|
+
return buffer;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function u32(value) {
|
|
46
|
+
const buffer = Buffer.allocUnsafe(4);
|
|
47
|
+
buffer.writeUInt32LE(value);
|
|
48
|
+
return buffer;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function zipStore(files) {
|
|
52
|
+
const date = new Date("2020-01-01T00:00:00Z");
|
|
53
|
+
const localParts = [];
|
|
54
|
+
const centralParts = [];
|
|
55
|
+
let offset = 0;
|
|
56
|
+
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
const name = bytes(file.path);
|
|
59
|
+
const body = bytes(file.body);
|
|
60
|
+
const checksum = crc32(body);
|
|
61
|
+
const localHeader = Buffer.concat([
|
|
62
|
+
u32(0x04034b50), u16(20), u16(0x0800), u16(0), u16(dosTime(date)), u16(dosDate(date)),
|
|
63
|
+
u32(checksum), u32(body.byteLength), u32(body.byteLength), u16(name.byteLength), u16(0), name
|
|
64
|
+
]);
|
|
65
|
+
localParts.push(localHeader, body);
|
|
66
|
+
centralParts.push(Buffer.concat([
|
|
67
|
+
u32(0x02014b50), u16(20), u16(20), u16(0x0800), u16(0), u16(dosTime(date)), u16(dosDate(date)),
|
|
68
|
+
u32(checksum), u32(body.byteLength), u32(body.byteLength), u16(name.byteLength), u16(0), u16(0),
|
|
69
|
+
u16(0), u16(0), u32(0), u32(offset), name
|
|
70
|
+
]));
|
|
71
|
+
offset += localHeader.byteLength + body.byteLength;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const central = Buffer.concat(centralParts);
|
|
75
|
+
return Buffer.concat([
|
|
76
|
+
...localParts,
|
|
77
|
+
central,
|
|
78
|
+
u32(0x06054b50), u16(0), u16(0), u16(files.length), u16(files.length),
|
|
79
|
+
u32(central.byteLength), u32(offset), u16(0)
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function integrity(buffer) {
|
|
84
|
+
return `sha256-${createHash("sha256").update(buffer).digest("hex")}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function sha256FromIntegrity(value) {
|
|
88
|
+
return value.slice("sha256-".length);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function contentType(fileName) {
|
|
92
|
+
if (fileName.endsWith(".js") || fileName.endsWith(".mjs")) return "text/javascript";
|
|
93
|
+
if (fileName.endsWith(".css")) return "text/css";
|
|
94
|
+
if (fileName.endsWith(".html")) return "text/html";
|
|
95
|
+
if (fileName.endsWith(".json")) return "application/json";
|
|
96
|
+
if (fileName.endsWith(".svg")) return "image/svg+xml";
|
|
97
|
+
if (fileName.endsWith(".wasm")) return "application/wasm";
|
|
98
|
+
return "application/octet-stream";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function chunkKind(fileName, type) {
|
|
102
|
+
const lower = fileName.toLowerCase();
|
|
103
|
+
if (lower.endsWith(".map")) return "source-map";
|
|
104
|
+
if (lower.endsWith(".wasm") || type === "application/wasm") return "wasm";
|
|
105
|
+
if (lower.endsWith(".worker.js") || lower.endsWith(".worker.mjs") || lower.includes("/workers/")) return "worker";
|
|
106
|
+
if (lower.endsWith(".js") || lower.endsWith(".mjs") || type === "text/javascript") return "script";
|
|
107
|
+
if (lower.endsWith(".css") || type === "text/css") return "style";
|
|
108
|
+
return "asset";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function chunkExecutable(kind) {
|
|
112
|
+
return kind === "script" || kind === "style" || kind === "wasm" || kind === "worker";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function frontendChunksFromFiles(files, entrypoint) {
|
|
116
|
+
const chunks = files
|
|
117
|
+
.map((file) => {
|
|
118
|
+
const kind = chunkKind(file.path, file.type);
|
|
119
|
+
if (kind === "asset") return undefined;
|
|
120
|
+
return {
|
|
121
|
+
id: file.path,
|
|
122
|
+
url: file.path,
|
|
123
|
+
sha256: sha256FromIntegrity(file.integrity),
|
|
124
|
+
bytes: file.byteLength,
|
|
125
|
+
contentType: file.type,
|
|
126
|
+
kind,
|
|
127
|
+
executable: chunkExecutable(kind),
|
|
128
|
+
entry: file.path === entrypoint
|
|
129
|
+
};
|
|
130
|
+
})
|
|
131
|
+
.filter(Boolean);
|
|
132
|
+
return {
|
|
133
|
+
schemaVersion: 1,
|
|
134
|
+
algorithm: "sha256",
|
|
135
|
+
hashEncoding: "hex",
|
|
136
|
+
sourceMapPolicy: chunks.some((chunk) => chunk.kind === "source-map") ? "included" : "excluded",
|
|
137
|
+
chunks
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function bundleOutputFiles(bundle) {
|
|
142
|
+
return new Set(Object.values(bundle)
|
|
143
|
+
.map((item) => normalizePath(item.fileName))
|
|
144
|
+
.filter(Boolean));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function assertBundleFilesIncluded(files, bundle) {
|
|
148
|
+
const manifestPaths = new Set(files.map((file) => file.path));
|
|
149
|
+
for (const fileName of bundleOutputFiles(bundle)) {
|
|
150
|
+
if (!manifestPaths.has(fileName)) {
|
|
151
|
+
throw new Error(`Matterhorn Vite manifest is missing emitted bundle file: ${fileName}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function normalizePath(value) {
|
|
157
|
+
return String(value).replaceAll("\\", "/");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function outputDir(outputOptions, config) {
|
|
161
|
+
if (outputOptions?.dir) return path.resolve(outputOptions.dir);
|
|
162
|
+
if (outputOptions?.file) return path.dirname(path.resolve(outputOptions.file));
|
|
163
|
+
return path.resolve(config?.root || process.cwd(), config?.build?.outDir || "dist");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isInside(base, target) {
|
|
167
|
+
const relative = path.relative(path.resolve(base), path.resolve(target));
|
|
168
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function artifactPath(options) {
|
|
172
|
+
return options.path || options.bundlePath || options.outDir || options.dir || ".";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function artifactDir(outputOptions, config, options) {
|
|
176
|
+
const root = path.resolve(config?.root || path.dirname(outputDir(outputOptions, config)));
|
|
177
|
+
const requested = String(artifactPath(options) || ".");
|
|
178
|
+
const resolved = path.resolve(root, requested);
|
|
179
|
+
if (!path.isAbsolute(requested) && !isInside(root, resolved)) {
|
|
180
|
+
throw new Error(`Matterhorn Vite bundle path must stay inside the Vite root: ${requested}`);
|
|
181
|
+
}
|
|
182
|
+
return resolved;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function entrypointsFromBundle(bundle) {
|
|
186
|
+
return new Set(Object.values(bundle)
|
|
187
|
+
.filter((item) => item.type === "chunk" && item.isEntry === true)
|
|
188
|
+
.map((item) => normalizePath(item.fileName)));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function walkFiles(root, current, files) {
|
|
192
|
+
const entries = await fs.readdir(current, { withFileTypes: true });
|
|
193
|
+
for (const entry of entries) {
|
|
194
|
+
const file = path.join(current, entry.name);
|
|
195
|
+
if (entry.isDirectory()) {
|
|
196
|
+
await walkFiles(root, file, files);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (!entry.isFile()) continue;
|
|
200
|
+
files.push({
|
|
201
|
+
path: normalizePath(path.relative(root, file)),
|
|
202
|
+
file
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function distFiles(dir, artifactRoot, manifestFile, archiveFile, entrypoints) {
|
|
208
|
+
const discovered = [];
|
|
209
|
+
const skip = new Set([normalizePath(manifestFile), normalizePath(archiveFile)]);
|
|
210
|
+
await walkFiles(dir, dir, discovered);
|
|
211
|
+
const files = [];
|
|
212
|
+
const artifactRelative = isInside(dir, artifactRoot) ? normalizePath(path.relative(dir, artifactRoot)) : "";
|
|
213
|
+
const artifactPrefix = artifactRelative ? `${artifactRelative}/` : "";
|
|
214
|
+
for (const item of discovered) {
|
|
215
|
+
if (skip.has(item.path)) continue;
|
|
216
|
+
if (artifactPrefix && item.path.startsWith(artifactPrefix)) continue;
|
|
217
|
+
const body = await fs.readFile(item.file);
|
|
218
|
+
files.push({
|
|
219
|
+
path: item.path,
|
|
220
|
+
body,
|
|
221
|
+
entrypoint: entrypoints.has(item.path),
|
|
222
|
+
type: contentType(item.path),
|
|
223
|
+
integrity: integrity(body),
|
|
224
|
+
byteLength: body.byteLength
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function manifestFromFiles(files, options) {
|
|
231
|
+
const entrypoint = options.entrypoint || files.find((file) => file.entrypoint)?.path || files.find((file) => file.path.endsWith(".js"))?.path;
|
|
232
|
+
if (!entrypoint) throw new Error("Matterhorn Vite manifest could not determine an entrypoint.");
|
|
233
|
+
if (!files.some((file) => file.path === entrypoint)) {
|
|
234
|
+
throw new Error(`Matterhorn Vite manifest entrypoint is missing from emitted files: ${entrypoint}`);
|
|
235
|
+
}
|
|
236
|
+
if (!files.some((file) => file.path === DOCUMENT_ENTRYPOINT)) {
|
|
237
|
+
throw new Error(`Matterhorn Vite manifest requires ${DOCUMENT_ENTRYPOINT} in the build output.`);
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
kind: "matterhorn.frontend.manifest",
|
|
241
|
+
version: 1,
|
|
242
|
+
entrypoint,
|
|
243
|
+
files: files.map((file) => ({
|
|
244
|
+
path: file.path,
|
|
245
|
+
type: file.type,
|
|
246
|
+
integrity: file.integrity,
|
|
247
|
+
byteLength: file.byteLength
|
|
248
|
+
})),
|
|
249
|
+
frontendChunks: frontendChunksFromFiles(files, entrypoint)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function rewriteDocumentEntrypoint(html, entrypoint) {
|
|
254
|
+
return html.replace(
|
|
255
|
+
/(<script\b[^>]*\btype=["']module["'][^>]*\bsrc=["'])(?:\/?\.?\/?src\/[^"']+)(["'][^>]*>\s*<\/script>)/i,
|
|
256
|
+
`$1./${entrypoint}$2`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function includeRootDocument(files, config, outDir, entrypoint) {
|
|
261
|
+
if (files.some((file) => file.path === DOCUMENT_ENTRYPOINT)) return files;
|
|
262
|
+
const root = path.resolve(config?.root || process.cwd());
|
|
263
|
+
const file = path.join(root, DOCUMENT_ENTRYPOINT);
|
|
264
|
+
let body;
|
|
265
|
+
try {
|
|
266
|
+
body = Buffer.from(rewriteDocumentEntrypoint(await fs.readFile(file, "utf8"), entrypoint));
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (error && (error.code === "ENOENT" || error.code === "ENOTDIR")) return files;
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
await fs.writeFile(path.join(outDir, DOCUMENT_ENTRYPOINT), body);
|
|
272
|
+
return [...files, {
|
|
273
|
+
path: DOCUMENT_ENTRYPOINT,
|
|
274
|
+
body,
|
|
275
|
+
entrypoint: false,
|
|
276
|
+
type: contentType(DOCUMENT_ENTRYPOINT),
|
|
277
|
+
integrity: integrity(body),
|
|
278
|
+
byteLength: body.byteLength
|
|
279
|
+
}].sort((left, right) => left.path.localeCompare(right.path));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function matterhorn(rawOptions = {}) {
|
|
283
|
+
const options = pluginOptions(rawOptions);
|
|
284
|
+
const manifestFile = options.manifestFile || DEFAULT_MANIFEST_FILE;
|
|
285
|
+
const archiveFile = options.archiveFile || DEFAULT_ARCHIVE_FILE;
|
|
286
|
+
let config;
|
|
287
|
+
return {
|
|
288
|
+
name: "matterhorn-frontend-bundle",
|
|
289
|
+
apply: "build",
|
|
290
|
+
configResolved(resolvedConfig) {
|
|
291
|
+
config = resolvedConfig;
|
|
292
|
+
},
|
|
293
|
+
async writeBundle(outputOptions, bundle) {
|
|
294
|
+
const outDir = outputDir(outputOptions, config);
|
|
295
|
+
const bundleDir = artifactDir(outputOptions, config, options);
|
|
296
|
+
const emittedFiles = await distFiles(outDir, bundleDir, manifestFile, archiveFile, entrypointsFromBundle(bundle));
|
|
297
|
+
const entrypoint = options.entrypoint || emittedFiles.find((file) => file.entrypoint)?.path || emittedFiles.find((file) => file.path.endsWith(".js"))?.path;
|
|
298
|
+
const files = entrypoint ? await includeRootDocument(emittedFiles, config, outDir, entrypoint) : emittedFiles;
|
|
299
|
+
assertBundleFilesIncluded(files, bundle);
|
|
300
|
+
const manifest = manifestFromFiles(files, options);
|
|
301
|
+
const manifestBody = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
302
|
+
const archive = zipStore([
|
|
303
|
+
...files.map((file) => ({ path: file.path, body: file.body })),
|
|
304
|
+
{ path: manifestFile, body: manifestBody }
|
|
305
|
+
]);
|
|
306
|
+
const archiveMetadata = {
|
|
307
|
+
path: archiveFile,
|
|
308
|
+
delivery: "relay",
|
|
309
|
+
type: "application/zip",
|
|
310
|
+
integrity: integrity(archive),
|
|
311
|
+
byteLength: archive.byteLength
|
|
312
|
+
};
|
|
313
|
+
const finalManifest = { ...manifest, archive: archiveMetadata };
|
|
314
|
+
await fs.mkdir(bundleDir, { recursive: true });
|
|
315
|
+
await fs.writeFile(path.join(bundleDir, manifestFile), `${JSON.stringify(finalManifest, null, 2)}\n`);
|
|
316
|
+
await fs.writeFile(path.join(bundleDir, archiveFile), archive);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export const matterhornVitePlugin = matterhorn;
|