@kaptive/cli 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/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/bundle-io.d.ts +19 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +97 -0
- package/dist/codegen.d.ts +18 -0
- package/dist/dev-env.d.ts +70 -0
- package/dist/errors.d.ts +9 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +204 -0
- package/dist/manifest-io.d.ts +13 -0
- package/dist/pack.d.ts +23 -0
- package/dist/plugin.d.ts +28 -0
- package/dist/validate-Cf0ODuMg.js +287 -0
- package/dist/validate.d.ts +9 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 state systems gmbh
|
|
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
|
|
13
|
+
all 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,47 @@
|
|
|
1
|
+
# @kaptive/cli
|
|
2
|
+
|
|
3
|
+
The `kaptive` CLI: a Vite plugin, typed parameters, and bundle packaging for [Kaptive](https://kaptive.ch) custom widgets.
|
|
4
|
+
|
|
5
|
+
Most widget authors don't install this directly — `npm create @kaptive/widget` scaffolds a project that already depends on it and wires up its commands as npm scripts. This package is documented here for anyone customizing that setup or building tooling around it.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install --save-dev @kaptive/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Vite 8 as a peer — a scaffolded widget already has it.
|
|
14
|
+
|
|
15
|
+
## The Vite plugin
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// vite.config.ts
|
|
19
|
+
import { defineConfig } from "vite";
|
|
20
|
+
import { kaptiveWidget } from "@kaptive/cli";
|
|
21
|
+
|
|
22
|
+
export default defineConfig({
|
|
23
|
+
plugins: [kaptiveWidget()],
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
It validates `kaptive.manifest.json` before every build, generates `src/kaptive-env.d.ts` so `parameters.yourKey` is typed from the manifest, and injects the right context into the page depending on whether you're running `vite dev` (from `kaptive.dev.json`) or building for production (`kaptive.prod.json`, if you have one).
|
|
28
|
+
|
|
29
|
+
## Commands
|
|
30
|
+
|
|
31
|
+
Run these via `npx kaptive widget <command>`, or the npm scripts a scaffolded project already has:
|
|
32
|
+
|
|
33
|
+
| Command | What it does |
|
|
34
|
+
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
35
|
+
| `dev` | Starts the Vite dev server, with parameters from `kaptive.dev.json`. |
|
|
36
|
+
| `build` | Builds the widget and packages it into `<id>-<version>.zip`. |
|
|
37
|
+
| `pack` | Packages an existing `dist/` build without rebuilding it. |
|
|
38
|
+
| `validate [bundle]` | Validates the project (manifest, and build output if present), or an already-packaged `.zip`, against the same rules the Kaptive manager enforces at upload. |
|
|
39
|
+
| `types` | Regenerates `src/kaptive-env.d.ts` from the manifest. |
|
|
40
|
+
|
|
41
|
+
## Bundle limits
|
|
42
|
+
|
|
43
|
+
Every workspace starts with a default upload limit; a Kaptive admin can raise it per workspace. `pack`/`build` always enforce the hard ceiling no workspace can exceed, and warn (without failing) when a bundle is over the default, so you know ahead of time whether an upload might need that limit raised.
|
|
44
|
+
|
|
45
|
+
## Learn more
|
|
46
|
+
|
|
47
|
+
See the [`@kaptive/create-widget`](https://www.npmjs.com/package/@kaptive/create-widget) README for a full walkthrough of building and shipping a widget.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type BundleEntry } from "@kaptive/widget-api/bundle";
|
|
2
|
+
export interface DiskFile extends BundleEntry {
|
|
3
|
+
absolutePath: string;
|
|
4
|
+
}
|
|
5
|
+
/** Lists every file under `directory`, with bundle-relative `/`-joined paths. */
|
|
6
|
+
export declare function listBundleFiles(directory: string): Promise<DiskFile[]>;
|
|
7
|
+
/**
|
|
8
|
+
* Checks a listing against the shared bundle rules plus the two things only a
|
|
9
|
+
* complete bundle can be checked for: the manifest is present and valid, and
|
|
10
|
+
* the entry HTML it names actually exists.
|
|
11
|
+
*/
|
|
12
|
+
export declare function checkBundleContents(entries: readonly BundleEntry[], manifestSource: string | null): string[];
|
|
13
|
+
export interface ReadBundleResult {
|
|
14
|
+
entries: BundleEntry[];
|
|
15
|
+
manifestSource: string | null;
|
|
16
|
+
compressedBytes: number;
|
|
17
|
+
}
|
|
18
|
+
/** Reads a `.zip` bundle from disk without trusting anything inside it. */
|
|
19
|
+
export declare function readBundleFile(zipPath: string): Promise<ReadBundleResult>;
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { c as loadManifest, f as WidgetCliError, h as writeParametersDts, n as validateProject, r as packBundle, t as validateBundle } from "./validate-Cf0ODuMg.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { formatBytes } from "@kaptive/widget-api/bundle";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { build, createServer } from "vite";
|
|
9
|
+
//#region src/cli.ts
|
|
10
|
+
async function readCliVersion() {
|
|
11
|
+
try {
|
|
12
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const source = await readFile(path.join(here, "..", "package.json"), "utf8");
|
|
14
|
+
const parsed = JSON.parse(source);
|
|
15
|
+
if (typeof parsed === "object" && parsed !== null && "version" in parsed && typeof parsed.version === "string") return parsed.version;
|
|
16
|
+
} catch {}
|
|
17
|
+
return "unknown";
|
|
18
|
+
}
|
|
19
|
+
function resolveRoot(opts) {
|
|
20
|
+
return path.resolve(opts.root ?? process.cwd());
|
|
21
|
+
}
|
|
22
|
+
function resolveOptional(value) {
|
|
23
|
+
return value ? path.resolve(value) : void 0;
|
|
24
|
+
}
|
|
25
|
+
async function runDev(root) {
|
|
26
|
+
const server = await createServer({ root });
|
|
27
|
+
await server.listen();
|
|
28
|
+
server.printUrls();
|
|
29
|
+
}
|
|
30
|
+
async function runBuild(root, distDir, outDir) {
|
|
31
|
+
await loadManifest(root);
|
|
32
|
+
await build({ root });
|
|
33
|
+
const result = await packBundle({
|
|
34
|
+
root,
|
|
35
|
+
distDir,
|
|
36
|
+
outDir
|
|
37
|
+
});
|
|
38
|
+
reportPack(result.zipPath, result);
|
|
39
|
+
}
|
|
40
|
+
function reportPack(zipPath, result) {
|
|
41
|
+
console.log(`\nBundle written to ${zipPath}\n ${result.fileCount} files, ${formatBytes(result.uncompressedBytes)} uncompressed, ${formatBytes(result.compressedBytes)} zipped\n\nUpload it in Kaptive under Settings → Widgets.`);
|
|
42
|
+
for (const warning of result.warnings) console.warn(`\nWarning: ${warning}`);
|
|
43
|
+
}
|
|
44
|
+
async function runValidate(root, distDir, bundlePath) {
|
|
45
|
+
if (bundlePath) {
|
|
46
|
+
await validateBundle(path.resolve(bundlePath));
|
|
47
|
+
console.log(`${bundlePath} is a valid widget bundle.`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const { checkedBuild } = await validateProject(root, distDir);
|
|
51
|
+
console.log(checkedBuild ? "Manifest and build output are valid." : "Manifest is valid. No build output found, so only the manifest was checked.");
|
|
52
|
+
}
|
|
53
|
+
async function runTypes(root) {
|
|
54
|
+
const manifest = await loadManifest(root);
|
|
55
|
+
const { path: file, changed } = await writeParametersDts(root, manifest);
|
|
56
|
+
console.log(changed ? `Wrote ${file}` : `${file} is already up to date`);
|
|
57
|
+
}
|
|
58
|
+
var ROOT_OPTION = ["--root <dir>", "Widget project directory (default: current directory)"];
|
|
59
|
+
var DIST_OPTION = ["--dist <dir>", "Build output directory (default: <root>/dist)"];
|
|
60
|
+
var OUT_OPTION = ["--out <dir>", "Directory to write the bundle into (default: <root>)"];
|
|
61
|
+
var program = new Command("kaptive").description("The Kaptive CLI").version(await readCliVersion(), "-v, --version", "Show the CLI version");
|
|
62
|
+
program.action(() => program.help({ error: true }));
|
|
63
|
+
var widget = program.command("widget").description("Build and package Kaptive custom widgets");
|
|
64
|
+
widget.action(() => widget.help({ error: true }));
|
|
65
|
+
widget.command("dev").description("Start the Vite dev server with parameters from kaptive.dev.json").option(...ROOT_OPTION).action(async (opts) => {
|
|
66
|
+
await runDev(resolveRoot(opts));
|
|
67
|
+
});
|
|
68
|
+
widget.command("build").description("Build the widget and package it into an uploadable bundle").option(...ROOT_OPTION).option(...DIST_OPTION).option(...OUT_OPTION).action(async (opts) => {
|
|
69
|
+
await runBuild(resolveRoot(opts), resolveOptional(opts.dist), resolveOptional(opts.out));
|
|
70
|
+
});
|
|
71
|
+
widget.command("pack").description("Package an existing build into an uploadable bundle").option(...ROOT_OPTION).option(...DIST_OPTION).option(...OUT_OPTION).action(async (opts) => {
|
|
72
|
+
const root = resolveRoot(opts);
|
|
73
|
+
const result = await packBundle({
|
|
74
|
+
root,
|
|
75
|
+
distDir: resolveOptional(opts.dist),
|
|
76
|
+
outDir: resolveOptional(opts.out)
|
|
77
|
+
});
|
|
78
|
+
reportPack(result.zipPath, result);
|
|
79
|
+
});
|
|
80
|
+
widget.command("validate [bundle]").description("Validate this project, or a packaged .zip bundle").option(...ROOT_OPTION).option(...DIST_OPTION).action(async (bundlePath, opts) => {
|
|
81
|
+
await runValidate(resolveRoot(opts), resolveOptional(opts.dist), bundlePath);
|
|
82
|
+
});
|
|
83
|
+
widget.command("types").description("Regenerate src/kaptive-env.d.ts from the manifest").option(...ROOT_OPTION).action(async (opts) => {
|
|
84
|
+
await runTypes(resolveRoot(opts));
|
|
85
|
+
});
|
|
86
|
+
try {
|
|
87
|
+
await program.parseAsync(process.argv);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error instanceof WidgetCliError) {
|
|
90
|
+
console.error(`\n${error.message}`);
|
|
91
|
+
for (const detail of error.details) console.error(` • ${detail}`);
|
|
92
|
+
console.error("");
|
|
93
|
+
} else console.error(error);
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { WidgetManifest } from "@kaptive/widget-api/manifest";
|
|
2
|
+
/** File the generated parameter types are written to, relative to `src/`. */
|
|
3
|
+
export declare const GENERATED_TYPES_FILENAME = "kaptive-env.d.ts";
|
|
4
|
+
/**
|
|
5
|
+
* Renders the module augmentation that turns `kaptive.parameters` from an empty
|
|
6
|
+
* object into the manifest's actual shape. The leading `export {}` is what makes
|
|
7
|
+
* this file a module, which is what makes `declare module` an augmentation of
|
|
8
|
+
* `@kaptive/widget-api` rather than a replacement of it.
|
|
9
|
+
*/
|
|
10
|
+
export declare function renderParametersDts(manifest: WidgetManifest): string;
|
|
11
|
+
/**
|
|
12
|
+
* Writes the generated types, skipping the write when nothing changed — a dev
|
|
13
|
+
* server watching `src/` would otherwise reload itself in a loop.
|
|
14
|
+
*/
|
|
15
|
+
export declare function writeParametersDts(root: string, manifest: WidgetManifest): Promise<{
|
|
16
|
+
path: string;
|
|
17
|
+
changed: boolean;
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type ResolvedParameterValues, type WidgetManifest } from "@kaptive/widget-api/manifest";
|
|
2
|
+
/**
|
|
3
|
+
* `kaptive.dev.json`: stands in for the Kaptive editor and player agent while
|
|
4
|
+
* running `kaptive-widget dev`.
|
|
5
|
+
*
|
|
6
|
+
* `kaptive.prod.json`: the same shape, read once at build time. When present,
|
|
7
|
+
* its values are baked into the built bundle and used by a widget that finds
|
|
8
|
+
* no player agent to talk to — i.e. one deployed and opened outside Kaptive.
|
|
9
|
+
* Absent one, a standalone widget falls back to its manifest defaults instead.
|
|
10
|
+
*
|
|
11
|
+
* ```json
|
|
12
|
+
* {
|
|
13
|
+
* "parameters": {
|
|
14
|
+
* "city": "Bern",
|
|
15
|
+
* "refreshSeconds": 30,
|
|
16
|
+
* "showIcon": true,
|
|
17
|
+
* "accent": "#f06a22",
|
|
18
|
+
* "logo": "/logo.png",
|
|
19
|
+
* "poster": { "source": "local", "localFilePath": "assets/poster.png" }
|
|
20
|
+
* },
|
|
21
|
+
* "player": {
|
|
22
|
+
* "deviceName": "Lobby screen",
|
|
23
|
+
* "orientation": 90,
|
|
24
|
+
* "timezone": "Europe/Zurich",
|
|
25
|
+
* "resolution": { "width": 1080, "height": 1920 }
|
|
26
|
+
* },
|
|
27
|
+
* "block": { "width": 600, "height": 400 }
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* An `asset` parameter takes either a plain string (shorthand for a URL, e.g.
|
|
32
|
+
* a file under `public/`) or the same object a real content block would store,
|
|
33
|
+
* for simulating a local-player file.
|
|
34
|
+
*/
|
|
35
|
+
export declare const DEV_CONFIG_FILENAME = "kaptive.dev.json";
|
|
36
|
+
export declare const PROD_CONFIG_FILENAME = "kaptive.prod.json";
|
|
37
|
+
export interface FileContextPlayer {
|
|
38
|
+
device_id: string;
|
|
39
|
+
device_name: string;
|
|
40
|
+
orientation: number;
|
|
41
|
+
timezone: string;
|
|
42
|
+
resolution: {
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** The context built from a `kaptive.dev.json`/`kaptive.prod.json` file. */
|
|
48
|
+
export interface FileContext {
|
|
49
|
+
parameters: ResolvedParameterValues;
|
|
50
|
+
player: FileContextPlayer;
|
|
51
|
+
block: {
|
|
52
|
+
id: string;
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Builds the context the SDK uses while `kaptive-widget dev` is running, from
|
|
59
|
+
* `kaptive.dev.json`. Always returns a full context — manifest defaults fill
|
|
60
|
+
* in anything the file doesn't set, and a missing file is the same as an empty
|
|
61
|
+
* one, so a widget with no config file at all still runs.
|
|
62
|
+
*/
|
|
63
|
+
export declare function loadDevContext(root: string, manifest: WidgetManifest): Promise<FileContext>;
|
|
64
|
+
/**
|
|
65
|
+
* Reads `kaptive.prod.json` for baking into a production build. `null` when
|
|
66
|
+
* the file isn't there — unlike dev, its absence means "inject nothing", not
|
|
67
|
+
* "inject defaults": whether a standalone deployment gets manifest defaults or
|
|
68
|
+
* this file's values is exactly the presence of the file.
|
|
69
|
+
*/
|
|
70
|
+
export declare function loadProdContext(root: string, manifest: WidgetManifest): Promise<FileContext | null>;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An error the CLI is expected to produce — a bad manifest, a bundle that
|
|
3
|
+
* breaks the limits. These print as a clean message plus a bullet list, with no
|
|
4
|
+
* stack trace; anything else that escapes is a real crash and prints in full.
|
|
5
|
+
*/
|
|
6
|
+
export declare class WidgetCliError extends Error {
|
|
7
|
+
readonly details: readonly string[];
|
|
8
|
+
constructor(message: string, details?: readonly string[]);
|
|
9
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { kaptiveWidget, type KaptiveWidgetOptions } from "./plugin.js";
|
|
2
|
+
export { packBundle, type PackOptions, type PackResult } from "./pack.js";
|
|
3
|
+
export { validateBundle, validateProject } from "./validate.js";
|
|
4
|
+
export { checkBundleContents, listBundleFiles, readBundleFile, type ReadBundleResult, } from "./bundle-io.js";
|
|
5
|
+
export { loadManifest, manifestPath, parseManifestSource, serializeManifest, WIDGET_MANIFEST_FILENAME, } from "./manifest-io.js";
|
|
6
|
+
export { DEV_CONFIG_FILENAME, PROD_CONFIG_FILENAME, loadDevContext, loadProdContext, type FileContext, } from "./dev-env.js";
|
|
7
|
+
export { GENERATED_TYPES_FILENAME, renderParametersDts, writeParametersDts, } from "./codegen.js";
|
|
8
|
+
export { WidgetCliError } from "./errors.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { a as listBundleFiles, c as loadManifest, d as serializeManifest, f as WidgetCliError, h as writeParametersDts, i as checkBundleContents, l as manifestPath, m as renderParametersDts, n as validateProject, o as readBundleFile, p as GENERATED_TYPES_FILENAME, r as packBundle, s as WIDGET_MANIFEST_FILENAME, t as validateBundle, u as parseManifestSource } from "./validate-Cf0ODuMg.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { isWidgetAssetValue, resolveParameterValues } from "@kaptive/widget-api/manifest";
|
|
5
|
+
//#region src/dev-env.ts
|
|
6
|
+
/**
|
|
7
|
+
* `kaptive.dev.json`: stands in for the Kaptive editor and player agent while
|
|
8
|
+
* running `kaptive-widget dev`.
|
|
9
|
+
*
|
|
10
|
+
* `kaptive.prod.json`: the same shape, read once at build time. When present,
|
|
11
|
+
* its values are baked into the built bundle and used by a widget that finds
|
|
12
|
+
* no player agent to talk to — i.e. one deployed and opened outside Kaptive.
|
|
13
|
+
* Absent one, a standalone widget falls back to its manifest defaults instead.
|
|
14
|
+
*
|
|
15
|
+
* ```json
|
|
16
|
+
* {
|
|
17
|
+
* "parameters": {
|
|
18
|
+
* "city": "Bern",
|
|
19
|
+
* "refreshSeconds": 30,
|
|
20
|
+
* "showIcon": true,
|
|
21
|
+
* "accent": "#f06a22",
|
|
22
|
+
* "logo": "/logo.png",
|
|
23
|
+
* "poster": { "source": "local", "localFilePath": "assets/poster.png" }
|
|
24
|
+
* },
|
|
25
|
+
* "player": {
|
|
26
|
+
* "deviceName": "Lobby screen",
|
|
27
|
+
* "orientation": 90,
|
|
28
|
+
* "timezone": "Europe/Zurich",
|
|
29
|
+
* "resolution": { "width": 1080, "height": 1920 }
|
|
30
|
+
* },
|
|
31
|
+
* "block": { "width": 600, "height": 400 }
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* An `asset` parameter takes either a plain string (shorthand for a URL, e.g.
|
|
36
|
+
* a file under `public/`) or the same object a real content block would store,
|
|
37
|
+
* for simulating a local-player file.
|
|
38
|
+
*/
|
|
39
|
+
var DEV_CONFIG_FILENAME = "kaptive.dev.json";
|
|
40
|
+
var PROD_CONFIG_FILENAME = "kaptive.prod.json";
|
|
41
|
+
/** Reads and JSON-parses a config file. `null` if it doesn't exist, or isn't valid JSON. */
|
|
42
|
+
async function readConfigFile(root, filename) {
|
|
43
|
+
let raw;
|
|
44
|
+
try {
|
|
45
|
+
raw = await readFile(path.join(root, filename), "utf8");
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(raw);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.warn(`[kaptive-widget] ${filename} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function coerceValue(parameter, raw) {
|
|
57
|
+
if (raw === void 0) return void 0;
|
|
58
|
+
switch (parameter.type) {
|
|
59
|
+
case "text":
|
|
60
|
+
case "color": return typeof raw === "string" ? raw : void 0;
|
|
61
|
+
case "number": return typeof raw === "number" && Number.isFinite(raw) ? raw : void 0;
|
|
62
|
+
case "boolean": return typeof raw === "boolean" ? raw : void 0;
|
|
63
|
+
case "asset":
|
|
64
|
+
if (typeof raw === "string") return {
|
|
65
|
+
assetId: "dev-asset",
|
|
66
|
+
assetUrl: raw
|
|
67
|
+
};
|
|
68
|
+
return isWidgetAssetValue(raw) ? raw : void 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function toOrientation(value) {
|
|
72
|
+
return value === 90 || value === 180 || value === 270 ? value : 0;
|
|
73
|
+
}
|
|
74
|
+
/** Builds a `FileContext`, filling in manifest defaults for anything unset. */
|
|
75
|
+
function buildFileContext(parsed, manifest) {
|
|
76
|
+
const stored = {};
|
|
77
|
+
for (const parameter of manifest.parameters) {
|
|
78
|
+
const value = coerceValue(parameter, parsed?.parameters?.[parameter.key]);
|
|
79
|
+
if (value !== void 0) stored[parameter.key] = value;
|
|
80
|
+
}
|
|
81
|
+
const resolution = parsed?.player?.resolution ?? {
|
|
82
|
+
width: 1920,
|
|
83
|
+
height: 1080
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
parameters: resolveParameterValues(manifest, stored),
|
|
87
|
+
player: {
|
|
88
|
+
device_id: parsed?.player?.deviceId ?? "dev-device",
|
|
89
|
+
device_name: parsed?.player?.deviceName ?? "Development",
|
|
90
|
+
orientation: toOrientation(parsed?.player?.orientation),
|
|
91
|
+
timezone: parsed?.player?.timezone ?? "Europe/Zurich",
|
|
92
|
+
resolution
|
|
93
|
+
},
|
|
94
|
+
block: {
|
|
95
|
+
id: parsed?.block?.id ?? "dev-block",
|
|
96
|
+
width: parsed?.block?.width ?? resolution.width,
|
|
97
|
+
height: parsed?.block?.height ?? resolution.height
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Builds the context the SDK uses while `kaptive-widget dev` is running, from
|
|
103
|
+
* `kaptive.dev.json`. Always returns a full context — manifest defaults fill
|
|
104
|
+
* in anything the file doesn't set, and a missing file is the same as an empty
|
|
105
|
+
* one, so a widget with no config file at all still runs.
|
|
106
|
+
*/
|
|
107
|
+
async function loadDevContext(root, manifest) {
|
|
108
|
+
return buildFileContext(await readConfigFile(root, DEV_CONFIG_FILENAME), manifest);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Reads `kaptive.prod.json` for baking into a production build. `null` when
|
|
112
|
+
* the file isn't there — unlike dev, its absence means "inject nothing", not
|
|
113
|
+
* "inject defaults": whether a standalone deployment gets manifest defaults or
|
|
114
|
+
* this file's values is exactly the presence of the file.
|
|
115
|
+
*/
|
|
116
|
+
async function loadProdContext(root, manifest) {
|
|
117
|
+
const parsed = await readConfigFile(root, PROD_CONFIG_FILENAME);
|
|
118
|
+
if (parsed === null) return null;
|
|
119
|
+
return buildFileContext(parsed, manifest);
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/plugin.ts
|
|
123
|
+
/**
|
|
124
|
+
* Serializes a value for embedding in an inline `<script>`. Escaping `<` is
|
|
125
|
+
* what stops a manifest string containing `<\/script>` from closing the tag.
|
|
126
|
+
*/
|
|
127
|
+
function toInlineJson(value) {
|
|
128
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Vite plugin every Kaptive widget uses. It:
|
|
132
|
+
*
|
|
133
|
+
* - validates `kaptive.manifest.json` before anything is built;
|
|
134
|
+
* - generates the typed `KaptiveParameters` augmentation;
|
|
135
|
+
* - injects the manifest into the page so the SDK can reconcile parameter
|
|
136
|
+
* values against the version the widget was built with;
|
|
137
|
+
* - injects a development context from `kaptive.dev.json` so `vite dev`
|
|
138
|
+
* behaves like a player without one being present;
|
|
139
|
+
* - on a production build, bakes in `kaptive.prod.json` if present, so a
|
|
140
|
+
* widget deployed outside Kaptive still has something to run on;
|
|
141
|
+
* - copies the manifest into the build output, where `kaptive-widget pack`
|
|
142
|
+
* and the Kaptive manager expect to find it.
|
|
143
|
+
*/
|
|
144
|
+
function kaptiveWidget(options = {}) {
|
|
145
|
+
let projectRoot = options.root ?? process.cwd();
|
|
146
|
+
let isDev = false;
|
|
147
|
+
let manifest = null;
|
|
148
|
+
async function refreshManifest() {
|
|
149
|
+
manifest = await loadManifest(projectRoot);
|
|
150
|
+
if (options.generateTypes !== false) await writeParametersDts(projectRoot, manifest);
|
|
151
|
+
return manifest;
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
name: "kaptive-widget",
|
|
155
|
+
configResolved(config) {
|
|
156
|
+
projectRoot = options.root ?? config.root;
|
|
157
|
+
isDev = config.command === "serve";
|
|
158
|
+
},
|
|
159
|
+
async buildStart() {
|
|
160
|
+
await refreshManifest();
|
|
161
|
+
},
|
|
162
|
+
configureServer(server) {
|
|
163
|
+
const watched = [manifestPath(projectRoot), path.join(projectRoot, DEV_CONFIG_FILENAME)];
|
|
164
|
+
server.watcher.add(watched);
|
|
165
|
+
const onChange = (file) => {
|
|
166
|
+
if (!watched.includes(file)) return;
|
|
167
|
+
refreshManifest().then(() => {
|
|
168
|
+
server.hot.send({ type: "full-reload" });
|
|
169
|
+
}).catch((error) => {
|
|
170
|
+
server.config.logger.error(`[kaptive-widget] ${error instanceof Error ? error.message : String(error)}`);
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
server.watcher.on("change", onChange);
|
|
174
|
+
server.watcher.on("add", onChange);
|
|
175
|
+
},
|
|
176
|
+
transformIndexHtml: {
|
|
177
|
+
order: "pre",
|
|
178
|
+
async handler() {
|
|
179
|
+
const current = manifest ?? await refreshManifest();
|
|
180
|
+
const injected = { manifest: current };
|
|
181
|
+
if (isDev) injected.devContext = await loadDevContext(projectRoot, current);
|
|
182
|
+
else {
|
|
183
|
+
const prodContext = await loadProdContext(projectRoot, current);
|
|
184
|
+
if (prodContext) injected.prodContext = prodContext;
|
|
185
|
+
}
|
|
186
|
+
return [{
|
|
187
|
+
tag: "script",
|
|
188
|
+
injectTo: "head-prepend",
|
|
189
|
+
children: `window.__KAPTIVE__=${toInlineJson(injected)};`
|
|
190
|
+
}];
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
generateBundle() {
|
|
194
|
+
if (!manifest) return;
|
|
195
|
+
this.emitFile({
|
|
196
|
+
type: "asset",
|
|
197
|
+
fileName: WIDGET_MANIFEST_FILENAME,
|
|
198
|
+
source: serializeManifest(manifest)
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
export { DEV_CONFIG_FILENAME, GENERATED_TYPES_FILENAME, PROD_CONFIG_FILENAME, WIDGET_MANIFEST_FILENAME, WidgetCliError, checkBundleContents, kaptiveWidget, listBundleFiles, loadDevContext, loadManifest, loadProdContext, manifestPath, packBundle, parseManifestSource, readBundleFile, renderParametersDts, serializeManifest, validateBundle, validateProject, writeParametersDts };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { WIDGET_MANIFEST_FILENAME, type WidgetManifest } from "@kaptive/widget-api/manifest";
|
|
2
|
+
export { WIDGET_MANIFEST_FILENAME };
|
|
3
|
+
export declare function manifestPath(root: string): string;
|
|
4
|
+
/** Parses manifest JSON, reporting `origin` in any error message. */
|
|
5
|
+
export declare function parseManifestSource(source: string, origin: string): WidgetManifest;
|
|
6
|
+
/** Reads and validates `kaptive.manifest.json` from a widget project. */
|
|
7
|
+
export declare function loadManifest(root: string): Promise<WidgetManifest>;
|
|
8
|
+
/**
|
|
9
|
+
* The canonical on-disk form of a manifest. Everything that writes a manifest
|
|
10
|
+
* — the Vite plugin into `dist/`, the packer into the zip — goes through this,
|
|
11
|
+
* so the bytes the manager validates are the bytes the schema produced.
|
|
12
|
+
*/
|
|
13
|
+
export declare function serializeManifest(manifest: WidgetManifest): string;
|
package/dist/pack.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface PackOptions {
|
|
2
|
+
root: string;
|
|
3
|
+
/** Build output to package. Defaults to `<root>/dist`. */
|
|
4
|
+
distDir?: string;
|
|
5
|
+
/** Where to write the zip. Defaults to `<root>`. */
|
|
6
|
+
outDir?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface PackResult {
|
|
9
|
+
zipPath: string;
|
|
10
|
+
fileCount: number;
|
|
11
|
+
uncompressedBytes: number;
|
|
12
|
+
compressedBytes: number;
|
|
13
|
+
/**
|
|
14
|
+
* Non-fatal notices — currently just the bundle exceeding the *default*
|
|
15
|
+
* per-workspace upload limit. A Kaptive workspace admin can raise that
|
|
16
|
+
* limit, so this is a heads-up to print, never a reason to fail the build:
|
|
17
|
+
* only exceeding {@link WIDGET_BUNDLE_HARD_LIMITS} (which no workspace can
|
|
18
|
+
* exceed) does that.
|
|
19
|
+
*/
|
|
20
|
+
warnings: string[];
|
|
21
|
+
}
|
|
22
|
+
/** Packages a built widget into the zip an admin uploads to Kaptive. */
|
|
23
|
+
export declare function packBundle(options: PackOptions): Promise<PackResult>;
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
export interface KaptiveWidgetOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Directory holding `kaptive.manifest.json`. Defaults to the Vite root, which
|
|
5
|
+
* is what a scaffolded widget wants.
|
|
6
|
+
*/
|
|
7
|
+
root?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Write `src/kaptive-env.d.ts` on every dev/build run so `kaptive.parameters`
|
|
10
|
+
* is typed from the manifest. On by default.
|
|
11
|
+
*/
|
|
12
|
+
generateTypes?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Vite plugin every Kaptive widget uses. It:
|
|
16
|
+
*
|
|
17
|
+
* - validates `kaptive.manifest.json` before anything is built;
|
|
18
|
+
* - generates the typed `KaptiveParameters` augmentation;
|
|
19
|
+
* - injects the manifest into the page so the SDK can reconcile parameter
|
|
20
|
+
* values against the version the widget was built with;
|
|
21
|
+
* - injects a development context from `kaptive.dev.json` so `vite dev`
|
|
22
|
+
* behaves like a player without one being present;
|
|
23
|
+
* - on a production build, bakes in `kaptive.prod.json` if present, so a
|
|
24
|
+
* widget deployed outside Kaptive still has something to run on;
|
|
25
|
+
* - copies the manifest into the build output, where `kaptive-widget pack`
|
|
26
|
+
* and the Kaptive manager expect to find it.
|
|
27
|
+
*/
|
|
28
|
+
export declare function kaptiveWidget(options?: KaptiveWidgetOptions): Plugin;
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { WIDGET_MANIFEST_FILENAME, WIDGET_MANIFEST_FILENAME as WIDGET_MANIFEST_FILENAME$1, parseWidgetManifest } from "@kaptive/widget-api/manifest";
|
|
4
|
+
import JSZip from "jszip";
|
|
5
|
+
import { DEFAULT_WIDGET_BUNDLE_COMPRESSED_BYTES, WIDGET_BUNDLE_HARD_LIMITS, checkBundleEntries, formatBytes, isIgnoredBundleEntry } from "@kaptive/widget-api/bundle";
|
|
6
|
+
//#region src/codegen.ts
|
|
7
|
+
/** File the generated parameter types are written to, relative to `src/`. */
|
|
8
|
+
var GENERATED_TYPES_FILENAME = "kaptive-env.d.ts";
|
|
9
|
+
var HEADER = `// Generated by kaptive-widget from kaptive.manifest.json.
|
|
10
|
+
// Do not edit — your changes will be overwritten on the next dev or build run.
|
|
11
|
+
`;
|
|
12
|
+
function tsType(parameter) {
|
|
13
|
+
switch (parameter.type) {
|
|
14
|
+
case "text":
|
|
15
|
+
case "color": return "string";
|
|
16
|
+
case "number": return "number";
|
|
17
|
+
case "boolean": return "boolean";
|
|
18
|
+
case "asset": return "string | null";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function docComment(parameter, indent) {
|
|
22
|
+
const lines = [parameter.label];
|
|
23
|
+
if (parameter.description) lines.push(parameter.description);
|
|
24
|
+
if (parameter.type === "asset") lines.push(`URL of the chosen ${parameter.accept.join(" or ")}, or null.`);
|
|
25
|
+
else if (parameter.default !== void 0) lines.push(`@default ${JSON.stringify(parameter.default)}`);
|
|
26
|
+
if (lines.length === 1) return `${indent}/** ${lines[0]} */\n`;
|
|
27
|
+
return `${indent}/**\n${lines.map((line) => `${indent} * ${line}`).join("\n")}\n${indent} */\n`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Renders the module augmentation that turns `kaptive.parameters` from an empty
|
|
31
|
+
* object into the manifest's actual shape. The leading `export {}` is what makes
|
|
32
|
+
* this file a module, which is what makes `declare module` an augmentation of
|
|
33
|
+
* `@kaptive/widget-api` rather than a replacement of it.
|
|
34
|
+
*/
|
|
35
|
+
function renderParametersDts(manifest) {
|
|
36
|
+
const body = manifest.parameters.map((parameter) => `${docComment(parameter, " ")} ${parameter.key}: ${tsType(parameter)};`).join("\n");
|
|
37
|
+
return `${HEADER}
|
|
38
|
+
export {};
|
|
39
|
+
|
|
40
|
+
declare module "@kaptive/widget-api" {
|
|
41
|
+
interface KaptiveParameters {${manifest.parameters.length ? `\n${body}\n ` : "\n // This widget declares no parameters.\n "}}
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Writes the generated types, skipping the write when nothing changed — a dev
|
|
47
|
+
* server watching `src/` would otherwise reload itself in a loop.
|
|
48
|
+
*/
|
|
49
|
+
async function writeParametersDts(root, manifest) {
|
|
50
|
+
const directory = path.join(root, "src");
|
|
51
|
+
const file = path.join(directory, GENERATED_TYPES_FILENAME);
|
|
52
|
+
const next = renderParametersDts(manifest);
|
|
53
|
+
let current = null;
|
|
54
|
+
try {
|
|
55
|
+
current = await readFile(file, "utf8");
|
|
56
|
+
} catch {}
|
|
57
|
+
if (current === next) return {
|
|
58
|
+
path: file,
|
|
59
|
+
changed: false
|
|
60
|
+
};
|
|
61
|
+
await mkdir(directory, { recursive: true });
|
|
62
|
+
await writeFile(file, next, "utf8");
|
|
63
|
+
return {
|
|
64
|
+
path: file,
|
|
65
|
+
changed: true
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/errors.ts
|
|
70
|
+
/**
|
|
71
|
+
* An error the CLI is expected to produce — a bad manifest, a bundle that
|
|
72
|
+
* breaks the limits. These print as a clean message plus a bullet list, with no
|
|
73
|
+
* stack trace; anything else that escapes is a real crash and prints in full.
|
|
74
|
+
*/
|
|
75
|
+
var WidgetCliError = class extends Error {
|
|
76
|
+
details;
|
|
77
|
+
constructor(message, details = []) {
|
|
78
|
+
super(message);
|
|
79
|
+
this.name = "WidgetCliError";
|
|
80
|
+
this.details = details;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/manifest-io.ts
|
|
85
|
+
function manifestPath(root) {
|
|
86
|
+
return path.join(root, WIDGET_MANIFEST_FILENAME$1);
|
|
87
|
+
}
|
|
88
|
+
/** Parses manifest JSON, reporting `origin` in any error message. */
|
|
89
|
+
function parseManifestSource(source, origin) {
|
|
90
|
+
let json;
|
|
91
|
+
try {
|
|
92
|
+
json = JSON.parse(source);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
throw new WidgetCliError(`${origin} is not valid JSON`, [error instanceof Error ? error.message : String(error)]);
|
|
95
|
+
}
|
|
96
|
+
const result = parseWidgetManifest(json);
|
|
97
|
+
if (!result.ok) throw new WidgetCliError(`${origin} is not a valid widget manifest`, result.errors);
|
|
98
|
+
return result.manifest;
|
|
99
|
+
}
|
|
100
|
+
/** Reads and validates `kaptive.manifest.json` from a widget project. */
|
|
101
|
+
async function loadManifest(root) {
|
|
102
|
+
const file = manifestPath(root);
|
|
103
|
+
let source;
|
|
104
|
+
try {
|
|
105
|
+
source = await readFile(file, "utf8");
|
|
106
|
+
} catch {
|
|
107
|
+
throw new WidgetCliError(`No ${WIDGET_MANIFEST_FILENAME$1} found in ${root}`, ["Run this command from the root of a widget project, or pass --root."]);
|
|
108
|
+
}
|
|
109
|
+
return parseManifestSource(source, WIDGET_MANIFEST_FILENAME$1);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The canonical on-disk form of a manifest. Everything that writes a manifest
|
|
113
|
+
* — the Vite plugin into `dist/`, the packer into the zip — goes through this,
|
|
114
|
+
* so the bytes the manager validates are the bytes the schema produced.
|
|
115
|
+
*/
|
|
116
|
+
function serializeManifest(manifest) {
|
|
117
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/bundle-io.ts
|
|
121
|
+
/** Lists every file under `directory`, with bundle-relative `/`-joined paths. */
|
|
122
|
+
async function listBundleFiles(directory) {
|
|
123
|
+
const files = [];
|
|
124
|
+
async function walk(current, prefix) {
|
|
125
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
if (isIgnoredBundleEntry(entry.name)) continue;
|
|
128
|
+
const absolutePath = path.join(current, entry.name);
|
|
129
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
130
|
+
if (entry.isDirectory()) await walk(absolutePath, relative);
|
|
131
|
+
else if (entry.isFile()) {
|
|
132
|
+
const stats = await stat(absolutePath);
|
|
133
|
+
files.push({
|
|
134
|
+
path: relative,
|
|
135
|
+
size: stats.size,
|
|
136
|
+
absolutePath
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
await walk(directory, "");
|
|
142
|
+
return files;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Checks a listing against the shared bundle rules plus the two things only a
|
|
146
|
+
* complete bundle can be checked for: the manifest is present and valid, and
|
|
147
|
+
* the entry HTML it names actually exists.
|
|
148
|
+
*/
|
|
149
|
+
function checkBundleContents(entries, manifestSource) {
|
|
150
|
+
const problems = [...checkBundleEntries(entries)];
|
|
151
|
+
if (manifestSource === null) {
|
|
152
|
+
problems.push(`Bundle is missing ${WIDGET_MANIFEST_FILENAME} at its root`);
|
|
153
|
+
return problems;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const manifest = parseManifestSource(manifestSource, WIDGET_MANIFEST_FILENAME);
|
|
157
|
+
if (!entries.some((entry) => entry.path === manifest.entry)) problems.push(`Manifest entry "${manifest.entry}" is not in the bundle`);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
problems.push(error instanceof Error ? error.message : String(error));
|
|
160
|
+
if (error instanceof Error && "details" in error) problems.push(...error.details);
|
|
161
|
+
}
|
|
162
|
+
return problems;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* JSZip exposes the sizes recorded in the zip's own headers only on a private
|
|
166
|
+
* field. Reading them lets a bomb be rejected *before* it is decompressed,
|
|
167
|
+
* which is the whole point of the check — so it is worth the reach-in, with a
|
|
168
|
+
* decompressing fallback if the shape ever changes.
|
|
169
|
+
*/
|
|
170
|
+
function declaredSizes(file) {
|
|
171
|
+
const data = file._data;
|
|
172
|
+
return {
|
|
173
|
+
size: typeof data?.uncompressedSize === "number" ? data.uncompressedSize : void 0,
|
|
174
|
+
compressedSize: typeof data?.compressedSize === "number" ? data.compressedSize : void 0
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** Reads a `.zip` bundle from disk without trusting anything inside it. */
|
|
178
|
+
async function readBundleFile(zipPath) {
|
|
179
|
+
const buffer = await readFile(zipPath);
|
|
180
|
+
const compressedBytes = buffer.byteLength;
|
|
181
|
+
const zip = await JSZip.loadAsync(buffer);
|
|
182
|
+
const entries = [];
|
|
183
|
+
let manifestSource = null;
|
|
184
|
+
let total = 0;
|
|
185
|
+
for (const file of Object.values(zip.files)) {
|
|
186
|
+
if (file.dir) continue;
|
|
187
|
+
const declared = declaredSizes(file);
|
|
188
|
+
let size = declared.size;
|
|
189
|
+
if (size === void 0) size = (await file.async("uint8array")).length;
|
|
190
|
+
total += size;
|
|
191
|
+
if (total > WIDGET_BUNDLE_HARD_LIMITS.maxUncompressedBytes) throw new Error(`Bundle expands to more than ${formatBytes(WIDGET_BUNDLE_HARD_LIMITS.maxUncompressedBytes)}`);
|
|
192
|
+
entries.push({
|
|
193
|
+
path: file.name,
|
|
194
|
+
size,
|
|
195
|
+
compressedSize: declared.compressedSize
|
|
196
|
+
});
|
|
197
|
+
if (file.name === WIDGET_MANIFEST_FILENAME) manifestSource = await file.async("string");
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
entries,
|
|
201
|
+
manifestSource,
|
|
202
|
+
compressedBytes
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/pack.ts
|
|
207
|
+
/** Packages a built widget into the zip an admin uploads to Kaptive. */
|
|
208
|
+
async function packBundle(options) {
|
|
209
|
+
const { root } = options;
|
|
210
|
+
const distDir = options.distDir ?? path.join(root, "dist");
|
|
211
|
+
const outDir = options.outDir ?? root;
|
|
212
|
+
const manifest = await loadManifest(root);
|
|
213
|
+
let files;
|
|
214
|
+
try {
|
|
215
|
+
files = await listBundleFiles(distDir);
|
|
216
|
+
} catch {
|
|
217
|
+
throw new WidgetCliError(`No build output found at ${distDir}`, ["Run the build first, or pass --dist to point at the output directory."]);
|
|
218
|
+
}
|
|
219
|
+
if (files.length === 0) throw new WidgetCliError(`Build output at ${distDir} is empty`);
|
|
220
|
+
const manifestSource = serializeManifest(manifest);
|
|
221
|
+
const manifestBytes = Buffer.byteLength(manifestSource, "utf8");
|
|
222
|
+
const payload = files.filter((file) => file.path !== WIDGET_MANIFEST_FILENAME);
|
|
223
|
+
const entries = [...payload, {
|
|
224
|
+
path: WIDGET_MANIFEST_FILENAME,
|
|
225
|
+
size: manifestBytes
|
|
226
|
+
}];
|
|
227
|
+
const problems = checkBundleContents(entries, manifestSource);
|
|
228
|
+
if (problems.length > 0) throw new WidgetCliError("This build cannot be packaged", problems);
|
|
229
|
+
const zip = new JSZip();
|
|
230
|
+
zip.file(WIDGET_MANIFEST_FILENAME, manifestSource);
|
|
231
|
+
for (const file of payload) zip.file(file.path, await readFile(file.absolutePath));
|
|
232
|
+
const archive = await zip.generateAsync({
|
|
233
|
+
type: "nodebuffer",
|
|
234
|
+
compression: "DEFLATE",
|
|
235
|
+
compressionOptions: { level: 9 }
|
|
236
|
+
});
|
|
237
|
+
if (archive.byteLength > WIDGET_BUNDLE_HARD_LIMITS.maxCompressedBytes) throw new WidgetCliError(`Bundle is ${formatBytes(archive.byteLength)}, the limit is ${formatBytes(WIDGET_BUNDLE_HARD_LIMITS.maxCompressedBytes)}`);
|
|
238
|
+
const warnings = [];
|
|
239
|
+
if (archive.byteLength > DEFAULT_WIDGET_BUNDLE_COMPRESSED_BYTES) warnings.push(`Bundle is ${formatBytes(archive.byteLength)}; the default workspace upload limit is ${formatBytes(DEFAULT_WIDGET_BUNDLE_COMPRESSED_BYTES)}. A Kaptive admin can raise it for your workspace under Settings → Widgets.`);
|
|
240
|
+
const zipPath = path.join(outDir, `${manifest.id}-${manifest.version}.zip`);
|
|
241
|
+
await writeFile(zipPath, archive);
|
|
242
|
+
return {
|
|
243
|
+
zipPath,
|
|
244
|
+
fileCount: entries.length,
|
|
245
|
+
uncompressedBytes: entries.reduce((total, entry) => total + entry.size, 0),
|
|
246
|
+
compressedBytes: archive.byteLength,
|
|
247
|
+
warnings
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
//#endregion
|
|
251
|
+
//#region src/validate.ts
|
|
252
|
+
/**
|
|
253
|
+
* Validates a widget project: the manifest always, and the build output too
|
|
254
|
+
* when it is there. Throws {@link WidgetCliError} listing everything wrong.
|
|
255
|
+
*/
|
|
256
|
+
async function validateProject(root, distDir) {
|
|
257
|
+
const manifest = await loadManifest(root);
|
|
258
|
+
const outputDir = distDir ?? path.join(root, "dist");
|
|
259
|
+
let files;
|
|
260
|
+
try {
|
|
261
|
+
files = await listBundleFiles(outputDir);
|
|
262
|
+
} catch {
|
|
263
|
+
return { checkedBuild: false };
|
|
264
|
+
}
|
|
265
|
+
if (files.length === 0) return { checkedBuild: false };
|
|
266
|
+
const manifestSource = serializeManifest(manifest);
|
|
267
|
+
const problems = checkBundleContents([...files.filter((file) => file.path !== WIDGET_MANIFEST_FILENAME), {
|
|
268
|
+
path: WIDGET_MANIFEST_FILENAME,
|
|
269
|
+
size: Buffer.byteLength(manifestSource, "utf8")
|
|
270
|
+
}], manifestSource);
|
|
271
|
+
if (problems.length > 0) throw new WidgetCliError(`Build output at ${outputDir} is not packageable`, problems);
|
|
272
|
+
return { checkedBuild: true };
|
|
273
|
+
}
|
|
274
|
+
/** Validates an already-packaged `.zip` exactly as the Kaptive manager will. */
|
|
275
|
+
async function validateBundle(zipPath) {
|
|
276
|
+
let bundle;
|
|
277
|
+
try {
|
|
278
|
+
bundle = await readBundleFile(zipPath);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
throw new WidgetCliError(`${zipPath} could not be read as a widget bundle`, [error instanceof Error ? error.message : String(error)]);
|
|
281
|
+
}
|
|
282
|
+
const problems = checkBundleContents(bundle.entries, bundle.manifestSource);
|
|
283
|
+
if (bundle.compressedBytes > WIDGET_BUNDLE_HARD_LIMITS.maxCompressedBytes) problems.push(`Bundle is ${formatBytes(bundle.compressedBytes)}, the limit is ${formatBytes(WIDGET_BUNDLE_HARD_LIMITS.maxCompressedBytes)}`);
|
|
284
|
+
if (problems.length > 0) throw new WidgetCliError(`${zipPath} is not a valid widget bundle`, problems);
|
|
285
|
+
}
|
|
286
|
+
//#endregion
|
|
287
|
+
export { listBundleFiles as a, loadManifest as c, serializeManifest as d, WidgetCliError as f, writeParametersDts as h, checkBundleContents as i, manifestPath as l, renderParametersDts as m, validateProject as n, readBundleFile as o, GENERATED_TYPES_FILENAME as p, packBundle as r, WIDGET_MANIFEST_FILENAME$1 as s, validateBundle as t, parseManifestSource as u };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates a widget project: the manifest always, and the build output too
|
|
3
|
+
* when it is there. Throws {@link WidgetCliError} listing everything wrong.
|
|
4
|
+
*/
|
|
5
|
+
export declare function validateProject(root: string, distDir?: string): Promise<{
|
|
6
|
+
checkedBuild: boolean;
|
|
7
|
+
}>;
|
|
8
|
+
/** Validates an already-packaged `.zip` exactly as the Kaptive manager will. */
|
|
9
|
+
export declare function validateBundle(zipPath: string): Promise<void>;
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kaptive/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The kaptive CLI: Vite plugin, typed parameters and bundle packaging for Kaptive custom widgets, under `kaptive widget`.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"kaptive",
|
|
7
|
+
"widget",
|
|
8
|
+
"cli",
|
|
9
|
+
"vite-plugin",
|
|
10
|
+
"digital-signage"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"homepage": "https://kaptive.ch",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"kaptive": "./dist/cli.js"
|
|
26
|
+
},
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"vite": ">=8"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"commander": "^14.0.0",
|
|
40
|
+
"jszip": "^3.10.1",
|
|
41
|
+
"@kaptive/widget-api": "^0.1.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^24.0.0",
|
|
45
|
+
"eslint": "^10.0.0",
|
|
46
|
+
"prettier": "^3.8.1",
|
|
47
|
+
"typescript": "6.0.3",
|
|
48
|
+
"vite": "^8.0.0",
|
|
49
|
+
"vitest": "^4.0.0",
|
|
50
|
+
"@repo/typescript-config": "0.0.0",
|
|
51
|
+
"@repo/eslint-config": "0.0.0"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "vite build && tsc -p tsconfig.build.json",
|
|
55
|
+
"lint": "eslint . --max-warnings 0 --cache --cache-location node_modules/.cache/eslint/",
|
|
56
|
+
"format": "prettier --write .",
|
|
57
|
+
"check-format": "prettier --check .",
|
|
58
|
+
"check-types": "tsc --noEmit",
|
|
59
|
+
"test": "vitest run"
|
|
60
|
+
}
|
|
61
|
+
}
|