@allior/wmake-cli 0.0.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 ADDED
@@ -0,0 +1,53 @@
1
+ # @allior/wmake-cli
2
+
3
+ CLI for streamiby / wmake: widget build, base64 encoding, field generation.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add -g @allior/wmake-cli
9
+ # or from the monorepo:
10
+ cd cli && pnpm install && pnpm run build
11
+ ```
12
+
13
+ ## Commands
14
+
15
+ ### `wmake widget`
16
+
17
+ Builds chat-demo into widget files (html.txt, js.txt, css.txt, fields.txt, data.txt) and a zip archive in `examples/chat-demo/dist`.
18
+
19
+ ```bash
20
+ wmake widget
21
+ wmake widget --full # full bundle without CDN (legacy build style)
22
+ ```
23
+
24
+ ### `wmake base64`
25
+
26
+ Converts images, videos, and SVG to base64 for use in the browser.
27
+
28
+ ```bash
29
+ wmake base64 <path> # file or directory
30
+ wmake base64 ./assets -o out # output to file or directory
31
+ wmake base64 ./img -f # output with extra info
32
+ ```
33
+
34
+ ### `wmake generate-fields`
35
+
36
+ Generates `fields.json` from `fields.base.json` and test data. Requires the `WMAKE_FIELDS_DIR` environment variable.
37
+
38
+ ```bash
39
+ WMAKE_FIELDS_DIR=/path/to/fields wmake generate-fields
40
+ ```
41
+
42
+ ### `wmake extract-test-data`
43
+
44
+ Test data lives in `streamelements/src/assets` as TS objects; this command is kept for compatibility.
45
+
46
+ ## Development
47
+
48
+ ```bash
49
+ pnpm install
50
+ pnpm run build # compile
51
+ pnpm run dev # run via tsx (src/index.ts)
52
+ pnpm start # run compiled dist/index.js
53
+ ```
package/dist/base64.js ADDED
@@ -0,0 +1,85 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as mime from "mime-types";
4
+ const SUPPORTED_IMAGE = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"];
5
+ const SUPPORTED_VIDEO = [".mp4", ".webm", ".ogg", ".mov", ".avi"];
6
+ const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
7
+ export function isSupportedFile(filePath) {
8
+ const ext = path.extname(filePath).toLowerCase();
9
+ return [...SUPPORTED_IMAGE, ...SUPPORTED_VIDEO].includes(ext);
10
+ }
11
+ export function fileToBase64(filePath) {
12
+ return new Promise((resolve, reject) => {
13
+ fs.stat(filePath, (err, stats) => {
14
+ if (err) {
15
+ reject(new Error(`Cannot access file: ${err.message}`));
16
+ return;
17
+ }
18
+ if (!stats.isFile()) {
19
+ reject(new Error("Path is not a file"));
20
+ return;
21
+ }
22
+ if (stats.size > MAX_FILE_SIZE) {
23
+ reject(new Error(`File too large: ${stats.size} bytes (max: ${MAX_FILE_SIZE} bytes)`));
24
+ return;
25
+ }
26
+ if (!isSupportedFile(filePath)) {
27
+ reject(new Error("Unsupported file type"));
28
+ return;
29
+ }
30
+ fs.readFile(filePath, (err, data) => {
31
+ if (err) {
32
+ reject(new Error(`Error reading file: ${err.message}`));
33
+ return;
34
+ }
35
+ const mimeType = mime.lookup(filePath) || "application/octet-stream";
36
+ resolve({
37
+ filename: path.basename(filePath),
38
+ base64: data.toString("base64"),
39
+ mimeType: String(mimeType),
40
+ size: stats.size,
41
+ });
42
+ });
43
+ });
44
+ });
45
+ }
46
+ export function getOutputPath(output, originalFilename) {
47
+ if (fs.existsSync(output) && fs.statSync(output).isDirectory()) {
48
+ const name = path.basename(originalFilename, path.extname(originalFilename));
49
+ return path.join(output, `${name}_base64.txt`);
50
+ }
51
+ return output;
52
+ }
53
+ export async function processPath(inputPath, output, full) {
54
+ const stats = fs.statSync(inputPath);
55
+ if (stats.isFile()) {
56
+ const fileInfo = await fileToBase64(inputPath);
57
+ const dataUrl = `data:${fileInfo.mimeType};base64,${fileInfo.base64}`;
58
+ const content = full
59
+ ? `\nFile: ${fileInfo.filename}\nMIME Type: ${fileInfo.mimeType}\nSize: ${fileInfo.size} bytes\nBase64 Data URL:\n${dataUrl}\n${"-".repeat(50)}`
60
+ : dataUrl;
61
+ if (output) {
62
+ const outPath = getOutputPath(output, fileInfo.filename);
63
+ fs.writeFileSync(outPath, content);
64
+ console.log(`Output written to: ${outPath}`);
65
+ }
66
+ else {
67
+ console.log(content);
68
+ }
69
+ }
70
+ else if (stats.isDirectory()) {
71
+ const files = fs.readdirSync(inputPath);
72
+ let count = 0;
73
+ for (const file of files) {
74
+ const fullPath = path.join(inputPath, file);
75
+ if (fs.statSync(fullPath).isFile() && isSupportedFile(fullPath)) {
76
+ await processPath(fullPath, output, full);
77
+ count++;
78
+ }
79
+ }
80
+ if (count === 0)
81
+ console.log("No supported files found in directory");
82
+ else
83
+ console.log(`\nProcessed ${count} files`);
84
+ }
85
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Раньше создавал test-messages.json и test-alerts.json в streamelements/src/assets.
3
+ * Теперь тестовые данные — объекты в скриптах: streamelements/src/assets/test-messages.ts и test-alerts.ts.
4
+ * Команда оставлена для совместимости (no-op).
5
+ */
6
+ export async function runExtractTestData() {
7
+ console.log("Test data is defined in streamelements/src/assets (test-messages.ts, test-alerts.ts). No extraction needed.");
8
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Запускается через tsx для загрузки TS-модуля с полями.
3
+ * Использование: tsx generate-fields-from-module-runner.ts <path-to-fields.ts> <path-to-fields.json>
4
+ */
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+ const [, , modulePath, outPath] = process.argv;
9
+ if (!modulePath || !outPath) {
10
+ console.error("Usage: tsx generate-fields-from-module-runner.ts <fields.ts> <fields.json>");
11
+ process.exit(1);
12
+ }
13
+ const absPath = path.resolve(modulePath);
14
+ import(pathToFileURL(absPath).href)
15
+ .then((mod) => {
16
+ const fields = mod.default;
17
+ if (!Array.isArray(fields)) {
18
+ throw new Error(`Module must export default AdvancedField[]`);
19
+ }
20
+ const result = Object.assign({}, ...fields.map((f) => f.fieldObject));
21
+ fs.writeFileSync(outPath, JSON.stringify(result, null, 2), "utf-8");
22
+ console.log("Wrote", outPath);
23
+ })
24
+ .catch((e) => {
25
+ console.error(e);
26
+ process.exit(1);
27
+ });
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Генерирует fields.json из fields.base.json и ключей из объектов testMessages / testAlerts.
3
+ * Путь к каталогу с fields.base.json задаётся WMAKE_FIELDS_DIR.
4
+ * Данные сообщений и алертов передаются объектами (из скриптов streamelements).
5
+ */
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { spawnSync } from "node:child_process";
9
+ const TEST_GROUP = "Tests / Тесты";
10
+ function pascalCase(s) {
11
+ return s.charAt(0).toUpperCase() + s.slice(1);
12
+ }
13
+ /** camelCase → sentence case (e.g. veryShort → "Very short", selfSub → "Self sub") */
14
+ function keyToTitle(key) {
15
+ const words = key
16
+ .replace(/([A-Z])/g, " $1")
17
+ .toLowerCase()
18
+ .trim();
19
+ return words.charAt(0).toUpperCase() + words.slice(1);
20
+ }
21
+ function messageLabel(key) {
22
+ return `${keyToTitle(key)} message`;
23
+ }
24
+ function alertLabel(key) {
25
+ return `${keyToTitle(key)} alert`;
26
+ }
27
+ export function runGenerateFields(options) {
28
+ const { fieldsDir, testMessages: messages, testAlerts: alerts } = options;
29
+ const basePath = path.join(fieldsDir, "fields.base.json");
30
+ const outPath = path.join(fieldsDir, "fields.json");
31
+ const base = JSON.parse(fs.readFileSync(basePath, "utf-8"));
32
+ const messageKeys = Object.keys(messages);
33
+ const alertKeys = Object.keys(alerts);
34
+ const testMessageFields = {};
35
+ for (const key of messageKeys) {
36
+ const fieldId = "testMessage" + pascalCase(key);
37
+ testMessageFields[fieldId] = {
38
+ type: "button",
39
+ label: messageLabel(key),
40
+ group: TEST_GROUP,
41
+ };
42
+ }
43
+ const testAlertFields = {};
44
+ for (const key of alertKeys) {
45
+ const fieldId = "testAlert" + pascalCase(key);
46
+ testAlertFields[fieldId] = {
47
+ type: "button",
48
+ label: alertLabel(key),
49
+ group: TEST_GROUP,
50
+ };
51
+ }
52
+ const insertAfterKey = "blueFlowerAnimationDuration";
53
+ const result = {};
54
+ let inserted = false;
55
+ for (const k of Object.keys(base)) {
56
+ if (k.startsWith("_"))
57
+ continue;
58
+ result[k] = base[k];
59
+ if (k === insertAfterKey) {
60
+ Object.assign(result, testMessageFields, testAlertFields);
61
+ inserted = true;
62
+ }
63
+ }
64
+ if (!inserted) {
65
+ Object.assign(result, testMessageFields, testAlertFields);
66
+ }
67
+ fs.writeFileSync(outPath, JSON.stringify(result, null, 2), "utf-8");
68
+ console.log("Wrote", outPath);
69
+ console.log("Test message buttons:", messageKeys.length);
70
+ console.log("Test alert buttons:", alertKeys.length);
71
+ }
72
+ /**
73
+ * Генерирует fields.json из TS/JS модуля, экспортирующего default AdvancedField[].
74
+ * Использует tsx для загрузки .ts файлов.
75
+ */
76
+ export function runGenerateFieldsFromModule(fieldsModulePath) {
77
+ const absPath = path.resolve(fieldsModulePath);
78
+ const outPath = path.join(path.dirname(absPath), "fields.json");
79
+ const runnerPath = path.join(__dirname, "..", "src", "generate-fields-from-module-runner.ts");
80
+ const r = spawnSync("npx", ["tsx", runnerPath, absPath, outPath], {
81
+ stdio: "inherit",
82
+ shell: true,
83
+ cwd: path.dirname(absPath),
84
+ });
85
+ if (r.status !== 0) {
86
+ throw new Error(`Failed to generate fields from ${fieldsModulePath}`);
87
+ }
88
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Запускается через node для загрузки ESM-модуля @allior/wmake-streamelements-events.
3
+ * Пишет в файл (argv[2]) JSON: { testMessages, testAlerts }.
4
+ * Использование: node get-events-data.mjs <output-path>
5
+ * Вызывать из корня пакета CLI (cwd = cli/), чтобы резолвился node_modules.
6
+ */
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+
10
+ const outPath = process.argv[2];
11
+ if (!outPath) {
12
+ console.error("Usage: node get-events-data.mjs <output-path>");
13
+ process.exit(1);
14
+ }
15
+
16
+ const { testMessages, testAlerts } = await import(
17
+ "@allior/wmake-streamelements-events"
18
+ );
19
+ fs.writeFileSync(
20
+ path.resolve(outPath),
21
+ JSON.stringify({ testMessages, testAlerts }),
22
+ "utf-8",
23
+ );
24
+ console.log("Wrote", outPath);
package/dist/index.js ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { Command } from "commander";
4
+ import * as fs from "fs";
5
+ import { processPath } from "./base64.js";
6
+ import { buildWidget } from "./widget.js";
7
+ import { runGenerateFields } from "./generate-fields.js";
8
+ import { runExtractTestData } from "./extract-test-data.js";
9
+ const program = new Command();
10
+ program.name("wmake").description("CLI for streamiby / wmake").version("1.0.0");
11
+ program
12
+ .command("widget")
13
+ .description("Build chat-demo into widget files and zip in examples/chat-demo/dist")
14
+ .option("--full", "Full bundle (no CDN external), like the old build")
15
+ .action(async (opts) => {
16
+ try {
17
+ await buildWidget({ full: opts.full });
18
+ }
19
+ catch (e) {
20
+ console.error(e.message);
21
+ process.exit(1);
22
+ }
23
+ });
24
+ program
25
+ .command("generate-fields")
26
+ .description("Generate fields.json from fields.base.json and test data objects (WMAKE_FIELDS_DIR)")
27
+ .action(async () => {
28
+ try {
29
+ const fieldsDir = process.env.WMAKE_FIELDS_DIR;
30
+ if (!fieldsDir?.trim()) {
31
+ throw new Error("WMAKE_FIELDS_DIR must be set.");
32
+ }
33
+ const mod = (await import("@allior/wmake-streamelements-events"));
34
+ runGenerateFields({
35
+ fieldsDir: path.resolve(fieldsDir),
36
+ testMessages: mod.testMessages,
37
+ testAlerts: mod.testAlerts,
38
+ });
39
+ }
40
+ catch (e) {
41
+ console.error(e.message);
42
+ process.exit(1);
43
+ }
44
+ });
45
+ program
46
+ .command("extract-test-data")
47
+ .description("No-op: test data lives in streamelements/src/assets as TS objects")
48
+ .action(async () => {
49
+ try {
50
+ await runExtractTestData();
51
+ }
52
+ catch (e) {
53
+ console.error(e.message);
54
+ process.exit(1);
55
+ }
56
+ });
57
+ program
58
+ .command("base64")
59
+ .description("Convert images, videos, SVG to base64 for browsers")
60
+ .argument("<path>", "File or directory path")
61
+ .option("-o, --output <path>", "Output file or directory")
62
+ .option("-f, --full", "Output with additional info")
63
+ .action(async (inputPath, options) => {
64
+ if (!fs.existsSync(inputPath)) {
65
+ console.error("Error: Path does not exist");
66
+ process.exit(1);
67
+ }
68
+ try {
69
+ await processPath(inputPath, options.output ?? null, options.full ?? false);
70
+ }
71
+ catch (e) {
72
+ console.error("Error:", e.message);
73
+ process.exit(1);
74
+ }
75
+ });
76
+ program.parse();
package/dist/widget.js ADDED
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Сборка виджета: html.txt, js.txt, css.txt, fields.txt, data.txt и архив .zip.
3
+ * Все пути относительно текущей папки (откуда вызывается скрипт).
4
+ */
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { spawnSync } from "node:child_process";
8
+ import { runGenerateFields, runGenerateFieldsFromModule, } from "./generate-fields.js";
9
+ /** Текущая папка (каталог проекта виджета). */
10
+ function getProjectDir() {
11
+ return path.resolve(process.cwd());
12
+ }
13
+ function run(cmd, args, cwd, env) {
14
+ const r = spawnSync(cmd, args, {
15
+ cwd,
16
+ stdio: "inherit",
17
+ shell: true,
18
+ env: { ...process.env, ...env },
19
+ });
20
+ return r.status === 0;
21
+ }
22
+ export async function buildWidget(options) {
23
+ const projectDir = getProjectDir();
24
+ const distDir = path.join(projectDir, "dist");
25
+ const buildDir = path.resolve(projectDir, process.env.BUILD_DIR ?? "build");
26
+ const archiveName = process.env.ARCHIVE_NAME ?? "widget";
27
+ console.log("Generating fields...");
28
+ const basePath = path.join(projectDir, "fields.base.json");
29
+ const fieldsTsPath = path.join(projectDir, "fields.ts");
30
+ if (fs.existsSync(basePath)) {
31
+ const mod = (await import("@allior/wmake-streamelements-events"));
32
+ runGenerateFields({
33
+ fieldsDir: projectDir,
34
+ testMessages: mod.testMessages,
35
+ testAlerts: mod.testAlerts,
36
+ });
37
+ }
38
+ else if (fs.existsSync(fieldsTsPath)) {
39
+ runGenerateFieldsFromModule(fieldsTsPath);
40
+ }
41
+ else {
42
+ console.log("No fields.base.json or fields.ts found, skipping fields generation");
43
+ }
44
+ console.log("Building...");
45
+ const buildEnv = options.full ? { BUILD_FULL: "1" } : {};
46
+ if (!run("pnpm", ["run", "build"], projectDir, buildEnv)) {
47
+ throw new Error("Build failed.");
48
+ }
49
+ if (!fs.existsSync(distDir)) {
50
+ throw new Error("dist not found. Run build first.");
51
+ }
52
+ fs.mkdirSync(buildDir, { recursive: true });
53
+ const html = fs.readFileSync(path.join(distDir, "index.html"), "utf-8");
54
+ fs.writeFileSync(path.join(buildDir, "html.txt"), html);
55
+ const assetsDir = path.join(distDir, "assets");
56
+ const findFiles = (dir, ext) => {
57
+ const found = [];
58
+ const walk = (d) => {
59
+ if (!fs.existsSync(d))
60
+ return;
61
+ for (const e of fs.readdirSync(d)) {
62
+ const full = path.join(d, e);
63
+ const stat = fs.statSync(full);
64
+ if (stat.isDirectory())
65
+ walk(full);
66
+ else if (e.endsWith(ext))
67
+ found.push(full);
68
+ }
69
+ };
70
+ walk(dir);
71
+ return found;
72
+ };
73
+ const jsFiles = findFiles(assetsDir, ".js");
74
+ const cssFiles = findFiles(assetsDir, ".css");
75
+ if (jsFiles.length === 0 || cssFiles.length === 0) {
76
+ throw new Error("Expected .js and .css in dist/assets");
77
+ }
78
+ fs.writeFileSync(path.join(buildDir, "js.txt"), fs.readFileSync(jsFiles[0], "utf-8"));
79
+ fs.writeFileSync(path.join(buildDir, "css.txt"), fs.readFileSync(cssFiles[0], "utf-8"));
80
+ const fieldsJsonPath = path.join(projectDir, "fields.json");
81
+ if (fs.existsSync(fieldsJsonPath)) {
82
+ fs.copyFileSync(fieldsJsonPath, path.join(buildDir, "fields.txt"));
83
+ }
84
+ const pkgPath = path.join(projectDir, "package.json");
85
+ const wmakeVersion = fs.existsSync(pkgPath) &&
86
+ (() => {
87
+ try {
88
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
89
+ return typeof pkg.version === "string" ? pkg.version : undefined;
90
+ }
91
+ catch {
92
+ return undefined;
93
+ }
94
+ })();
95
+ const dataPath = path.join(projectDir, "data.json");
96
+ const data = fs.existsSync(dataPath)
97
+ ? JSON.parse(fs.readFileSync(dataPath, "utf-8"))
98
+ : {};
99
+ if (wmakeVersion) {
100
+ data.wmakeVersion = wmakeVersion;
101
+ }
102
+ fs.writeFileSync(path.join(buildDir, "data.txt"), JSON.stringify(data, null, 2));
103
+ console.log("Widget files:", path.join(buildDir, "html.txt"), "js.txt, css.txt, fields.txt, data.txt");
104
+ const zipPath = path.join(distDir, `${archiveName}.zip`);
105
+ const zip = spawnSync("zip", ["-r", zipPath, "."], {
106
+ cwd: buildDir,
107
+ stdio: "pipe",
108
+ });
109
+ if (zip.status === 0) {
110
+ console.log("Archive:", zipPath);
111
+ return;
112
+ }
113
+ try {
114
+ const AdmZip = (await import("adm-zip")).default;
115
+ const archive = new AdmZip();
116
+ for (const f of [
117
+ "html.txt",
118
+ "js.txt",
119
+ "css.txt",
120
+ "fields.txt",
121
+ "data.txt",
122
+ ]) {
123
+ const fp = path.join(buildDir, f);
124
+ if (fs.existsSync(fp))
125
+ archive.addLocalFile(fp, "", f);
126
+ }
127
+ archive.writeZip(zipPath);
128
+ console.log("Archive (adm-zip):", zipPath);
129
+ }
130
+ catch (e) {
131
+ console.error("zip failed and adm-zip fallback error:", e);
132
+ console.error("Install 'zip' (e.g. apk add zip) or ensure adm-zip is installed.");
133
+ throw e;
134
+ }
135
+ }
@@ -0,0 +1,39 @@
1
+ // @ts-check
2
+
3
+ import eslint from '@eslint/js';
4
+ import { defineConfig } from 'eslint/config';
5
+ import react from "eslint-plugin-react";
6
+ import tseslint from 'typescript-eslint';
7
+ import globals from "globals";
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname } from 'path';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+
14
+ export default defineConfig(
15
+ eslint.configs.recommended,
16
+ tseslint.configs.strict,
17
+ tseslint.configs.stylistic,
18
+ {
19
+ files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'],
20
+ plugins: {
21
+ react,
22
+ },
23
+ languageOptions: {
24
+ parserOptions: {
25
+ tsconfigRootDir: __dirname,
26
+ ecmaFeatures: {
27
+ jsx: true,
28
+ },
29
+ },
30
+ globals: {
31
+ ...globals.browser,
32
+ },
33
+ },
34
+ rules: {
35
+ 'react/jsx-uses-react': 'error',
36
+ 'react/jsx-uses-vars': 'error',
37
+ }
38
+ }
39
+ );
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@allior/wmake-cli",
3
+ "version": "0.0.1",
4
+ "description": "Streamiby/wmake CLI: build widgets, base64 encode assets, generate fields",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "wmake-cli": "dist/index.js",
9
+ "@allior/wmake-cli": "dist/index.js"
10
+ },
11
+ "dependencies": {
12
+ "commander": "^11.1.0",
13
+ "mime-types": "^2.1.35",
14
+ "adm-zip": "^0.5.16",
15
+ "@allior/wmake-streamelements-events": "^0.0.3"
16
+ },
17
+ "devDependencies": {
18
+ "@types/mime-types": "^2.1.4",
19
+ "ts-node": "^10.9.0",
20
+ "tsx": "^4.20.6"
21
+ },
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "start": "node dist/index.js",
25
+ "dev": "tsx src/index.ts"
26
+ }
27
+ }
package/src/base64.ts ADDED
@@ -0,0 +1,97 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as mime from "mime-types";
4
+
5
+ export interface FileInfo {
6
+ filename: string;
7
+ base64: string;
8
+ mimeType: string;
9
+ size: number;
10
+ }
11
+
12
+ const SUPPORTED_IMAGE = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"];
13
+ const SUPPORTED_VIDEO = [".mp4", ".webm", ".ogg", ".mov", ".avi"];
14
+ const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
15
+
16
+ export function isSupportedFile(filePath: string): boolean {
17
+ const ext = path.extname(filePath).toLowerCase();
18
+ return [...SUPPORTED_IMAGE, ...SUPPORTED_VIDEO].includes(ext);
19
+ }
20
+
21
+ export function fileToBase64(filePath: string): Promise<FileInfo> {
22
+ return new Promise((resolve, reject) => {
23
+ fs.stat(filePath, (err, stats) => {
24
+ if (err) {
25
+ reject(new Error(`Cannot access file: ${err.message}`));
26
+ return;
27
+ }
28
+ if (!stats.isFile()) {
29
+ reject(new Error("Path is not a file"));
30
+ return;
31
+ }
32
+ if (stats.size > MAX_FILE_SIZE) {
33
+ reject(new Error(`File too large: ${stats.size} bytes (max: ${MAX_FILE_SIZE} bytes)`));
34
+ return;
35
+ }
36
+ if (!isSupportedFile(filePath)) {
37
+ reject(new Error("Unsupported file type"));
38
+ return;
39
+ }
40
+ fs.readFile(filePath, (err, data) => {
41
+ if (err) {
42
+ reject(new Error(`Error reading file: ${err.message}`));
43
+ return;
44
+ }
45
+ const mimeType = mime.lookup(filePath) || "application/octet-stream";
46
+ resolve({
47
+ filename: path.basename(filePath),
48
+ base64: data.toString("base64"),
49
+ mimeType: String(mimeType),
50
+ size: stats.size,
51
+ });
52
+ });
53
+ });
54
+ });
55
+ }
56
+
57
+ export function getOutputPath(output: string, originalFilename: string): string {
58
+ if (fs.existsSync(output) && fs.statSync(output).isDirectory()) {
59
+ const name = path.basename(originalFilename, path.extname(originalFilename));
60
+ return path.join(output, `${name}_base64.txt`);
61
+ }
62
+ return output;
63
+ }
64
+
65
+ export async function processPath(
66
+ inputPath: string,
67
+ output: string | null,
68
+ full: boolean
69
+ ): Promise<void> {
70
+ const stats = fs.statSync(inputPath);
71
+ if (stats.isFile()) {
72
+ const fileInfo = await fileToBase64(inputPath);
73
+ const dataUrl = `data:${fileInfo.mimeType};base64,${fileInfo.base64}`;
74
+ const content = full
75
+ ? `\nFile: ${fileInfo.filename}\nMIME Type: ${fileInfo.mimeType}\nSize: ${fileInfo.size} bytes\nBase64 Data URL:\n${dataUrl}\n${"-".repeat(50)}`
76
+ : dataUrl;
77
+ if (output) {
78
+ const outPath = getOutputPath(output, fileInfo.filename);
79
+ fs.writeFileSync(outPath, content);
80
+ console.log(`Output written to: ${outPath}`);
81
+ } else {
82
+ console.log(content);
83
+ }
84
+ } else if (stats.isDirectory()) {
85
+ const files = fs.readdirSync(inputPath);
86
+ let count = 0;
87
+ for (const file of files) {
88
+ const fullPath = path.join(inputPath, file);
89
+ if (fs.statSync(fullPath).isFile() && isSupportedFile(fullPath)) {
90
+ await processPath(fullPath, output, full);
91
+ count++;
92
+ }
93
+ }
94
+ if (count === 0) console.log("No supported files found in directory");
95
+ else console.log(`\nProcessed ${count} files`);
96
+ }
97
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Раньше создавал test-messages.json и test-alerts.json в streamelements/src/assets.
3
+ * Теперь тестовые данные — объекты в скриптах: streamelements/src/assets/test-messages.ts и test-alerts.ts.
4
+ * Команда оставлена для совместимости (no-op).
5
+ */
6
+
7
+ export async function runExtractTestData(): Promise<void> {
8
+ console.log(
9
+ "Test data is defined in streamelements/src/assets (test-messages.ts, test-alerts.ts). No extraction needed.",
10
+ );
11
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Запускается через tsx для загрузки TS-модуля с полями.
3
+ * Использование: tsx generate-fields-from-module-runner.ts <path-to-fields.ts> <path-to-fields.json>
4
+ */
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+
9
+ const [, , modulePath, outPath] = process.argv;
10
+ if (!modulePath || !outPath) {
11
+ console.error(
12
+ "Usage: tsx generate-fields-from-module-runner.ts <fields.ts> <fields.json>",
13
+ );
14
+ process.exit(1);
15
+ }
16
+
17
+ const absPath = path.resolve(modulePath);
18
+ import(pathToFileURL(absPath).href)
19
+ .then((mod) => {
20
+ const fields = mod.default;
21
+ if (!Array.isArray(fields)) {
22
+ throw new Error(`Module must export default AdvancedField[]`);
23
+ }
24
+ const result = Object.assign(
25
+ {},
26
+ ...fields.map(
27
+ (f: { fieldObject: Record<string, unknown> }) => f.fieldObject,
28
+ ),
29
+ );
30
+ fs.writeFileSync(outPath, JSON.stringify(result, null, 2), "utf-8");
31
+ console.log("Wrote", outPath);
32
+ })
33
+ .catch((e) => {
34
+ console.error(e);
35
+ process.exit(1);
36
+ });
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Генерирует fields.json из fields.base.json и ключей из объектов testMessages / testAlerts.
3
+ * Путь к каталогу с fields.base.json задаётся WMAKE_FIELDS_DIR.
4
+ * Данные сообщений и алертов передаются объектами (из скриптов streamelements).
5
+ */
6
+
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { spawnSync } from "node:child_process";
10
+
11
+ const TEST_GROUP = "Tests / Тесты";
12
+
13
+ type FieldDef = Record<string, unknown>;
14
+ type FieldsMap = Record<string, FieldDef>;
15
+
16
+ function pascalCase(s: string): string {
17
+ return s.charAt(0).toUpperCase() + s.slice(1);
18
+ }
19
+
20
+ /** camelCase → sentence case (e.g. veryShort → "Very short", selfSub → "Self sub") */
21
+ function keyToTitle(key: string): string {
22
+ const words = key
23
+ .replace(/([A-Z])/g, " $1")
24
+ .toLowerCase()
25
+ .trim();
26
+ return words.charAt(0).toUpperCase() + words.slice(1);
27
+ }
28
+
29
+ function messageLabel(key: string): string {
30
+ return `${keyToTitle(key)} message`;
31
+ }
32
+
33
+ function alertLabel(key: string): string {
34
+ return `${keyToTitle(key)} alert`;
35
+ }
36
+
37
+ export type MessagesRecord = Record<string, unknown>;
38
+ export type AlertsRecord = Record<string, unknown>;
39
+
40
+ export function runGenerateFields(options: {
41
+ fieldsDir: string;
42
+ testMessages: MessagesRecord;
43
+ testAlerts: AlertsRecord;
44
+ }): void {
45
+ const { fieldsDir, testMessages: messages, testAlerts: alerts } = options;
46
+
47
+ const basePath = path.join(fieldsDir, "fields.base.json");
48
+ const outPath = path.join(fieldsDir, "fields.json");
49
+
50
+ const base = JSON.parse(fs.readFileSync(basePath, "utf-8")) as FieldsMap;
51
+
52
+ const messageKeys = Object.keys(messages);
53
+ const alertKeys = Object.keys(alerts);
54
+
55
+ const testMessageFields: FieldsMap = {};
56
+ for (const key of messageKeys) {
57
+ const fieldId = "testMessage" + pascalCase(key);
58
+ testMessageFields[fieldId] = {
59
+ type: "button",
60
+ label: messageLabel(key),
61
+ group: TEST_GROUP,
62
+ };
63
+ }
64
+
65
+ const testAlertFields: FieldsMap = {};
66
+ for (const key of alertKeys) {
67
+ const fieldId = "testAlert" + pascalCase(key);
68
+ testAlertFields[fieldId] = {
69
+ type: "button",
70
+ label: alertLabel(key),
71
+ group: TEST_GROUP,
72
+ };
73
+ }
74
+
75
+ const insertAfterKey = "blueFlowerAnimationDuration";
76
+ const result: FieldsMap = {};
77
+ let inserted = false;
78
+ for (const k of Object.keys(base)) {
79
+ if (k.startsWith("_")) continue;
80
+ result[k] = base[k] as FieldDef;
81
+ if (k === insertAfterKey) {
82
+ Object.assign(result, testMessageFields, testAlertFields);
83
+ inserted = true;
84
+ }
85
+ }
86
+ if (!inserted) {
87
+ Object.assign(result, testMessageFields, testAlertFields);
88
+ }
89
+
90
+ fs.writeFileSync(outPath, JSON.stringify(result, null, 2), "utf-8");
91
+ console.log("Wrote", outPath);
92
+ console.log("Test message buttons:", messageKeys.length);
93
+ console.log("Test alert buttons:", alertKeys.length);
94
+ }
95
+
96
+ /**
97
+ * Генерирует fields.json из TS/JS модуля, экспортирующего default AdvancedField[].
98
+ * Использует tsx для загрузки .ts файлов.
99
+ */
100
+ export function runGenerateFieldsFromModule(fieldsModulePath: string): void {
101
+ const absPath = path.resolve(fieldsModulePath);
102
+ const outPath = path.join(path.dirname(absPath), "fields.json");
103
+ const runnerPath = path.join(
104
+ __dirname,
105
+ "..",
106
+ "src",
107
+ "generate-fields-from-module-runner.ts",
108
+ );
109
+ const r = spawnSync("npx", ["tsx", runnerPath, absPath, outPath], {
110
+ stdio: "inherit",
111
+ shell: true,
112
+ cwd: path.dirname(absPath),
113
+ });
114
+ if (r.status !== 0) {
115
+ throw new Error(`Failed to generate fields from ${fieldsModulePath}`);
116
+ }
117
+ }
package/src/index.ts ADDED
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "node:path";
4
+ import { Command } from "commander";
5
+ import * as fs from "fs";
6
+ import { processPath } from "./base64.js";
7
+ import { buildWidget } from "./widget.js";
8
+ import { runGenerateFields } from "./generate-fields.js";
9
+ import { runExtractTestData } from "./extract-test-data.js";
10
+
11
+ const program = new Command();
12
+
13
+ program.name("wmake").description("CLI for streamiby / wmake").version("1.0.0");
14
+
15
+ program
16
+ .command("widget")
17
+ .description(
18
+ "Build chat-demo into widget files and zip in examples/chat-demo/dist",
19
+ )
20
+ .option("--full", "Full bundle (no CDN external), like the old build")
21
+ .action(async (opts: { full?: boolean }) => {
22
+ try {
23
+ await buildWidget({ full: opts.full });
24
+ } catch (e) {
25
+ console.error((e as Error).message);
26
+ process.exit(1);
27
+ }
28
+ });
29
+
30
+ program
31
+ .command("generate-fields")
32
+ .description(
33
+ "Generate fields.json from fields.base.json and test data objects (WMAKE_FIELDS_DIR)",
34
+ )
35
+ .action(async () => {
36
+ try {
37
+ const fieldsDir = process.env.WMAKE_FIELDS_DIR;
38
+ if (!fieldsDir?.trim()) {
39
+ throw new Error("WMAKE_FIELDS_DIR must be set.");
40
+ }
41
+ const mod =
42
+ (await import("@allior/wmake-streamelements-events")) as unknown as {
43
+ testMessages: Record<string, unknown>;
44
+ testAlerts: Record<string, unknown>;
45
+ };
46
+ runGenerateFields({
47
+ fieldsDir: path.resolve(fieldsDir),
48
+ testMessages: mod.testMessages,
49
+ testAlerts: mod.testAlerts,
50
+ });
51
+ } catch (e) {
52
+ console.error((e as Error).message);
53
+ process.exit(1);
54
+ }
55
+ });
56
+
57
+ program
58
+ .command("extract-test-data")
59
+ .description(
60
+ "No-op: test data lives in streamelements/src/assets as TS objects",
61
+ )
62
+ .action(async () => {
63
+ try {
64
+ await runExtractTestData();
65
+ } catch (e) {
66
+ console.error((e as Error).message);
67
+ process.exit(1);
68
+ }
69
+ });
70
+
71
+ program
72
+ .command("base64")
73
+ .description("Convert images, videos, SVG to base64 for browsers")
74
+ .argument("<path>", "File or directory path")
75
+ .option("-o, --output <path>", "Output file or directory")
76
+ .option("-f, --full", "Output with additional info")
77
+ .action(
78
+ async (inputPath: string, options: { output?: string; full?: boolean }) => {
79
+ if (!fs.existsSync(inputPath)) {
80
+ console.error("Error: Path does not exist");
81
+ process.exit(1);
82
+ }
83
+ try {
84
+ await processPath(
85
+ inputPath,
86
+ options.output ?? null,
87
+ options.full ?? false,
88
+ );
89
+ } catch (e) {
90
+ console.error("Error:", (e as Error).message);
91
+ process.exit(1);
92
+ }
93
+ },
94
+ );
95
+
96
+ program.parse();
package/src/widget.ts ADDED
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Сборка виджета: html.txt, js.txt, css.txt, fields.txt, data.txt и архив .zip.
3
+ * Все пути относительно текущей папки (откуда вызывается скрипт).
4
+ */
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { spawnSync } from "node:child_process";
8
+ import {
9
+ runGenerateFields,
10
+ runGenerateFieldsFromModule,
11
+ } from "./generate-fields.js";
12
+
13
+ /** Текущая папка (каталог проекта виджета). */
14
+ function getProjectDir(): string {
15
+ return path.resolve(process.cwd());
16
+ }
17
+
18
+ function run(
19
+ cmd: string,
20
+ args: string[],
21
+ cwd: string,
22
+ env?: NodeJS.ProcessEnv,
23
+ ): boolean {
24
+ const r = spawnSync(cmd, args, {
25
+ cwd,
26
+ stdio: "inherit",
27
+ shell: true,
28
+ env: { ...process.env, ...env },
29
+ });
30
+ return r.status === 0;
31
+ }
32
+
33
+ export async function buildWidget(options: { full?: boolean }): Promise<void> {
34
+ const projectDir = getProjectDir();
35
+ const distDir = path.join(projectDir, "dist");
36
+ const buildDir = path.resolve(projectDir, process.env.BUILD_DIR ?? "build");
37
+ const archiveName = process.env.ARCHIVE_NAME ?? "widget";
38
+
39
+ console.log("Generating fields...");
40
+ const basePath = path.join(projectDir, "fields.base.json");
41
+ const fieldsTsPath = path.join(projectDir, "fields.ts");
42
+ if (fs.existsSync(basePath)) {
43
+ const mod =
44
+ (await import("@allior/wmake-streamelements-events")) as unknown as {
45
+ testMessages: Record<string, unknown>;
46
+ testAlerts: Record<string, unknown>;
47
+ };
48
+ runGenerateFields({
49
+ fieldsDir: projectDir,
50
+ testMessages: mod.testMessages,
51
+ testAlerts: mod.testAlerts,
52
+ });
53
+ } else if (fs.existsSync(fieldsTsPath)) {
54
+ runGenerateFieldsFromModule(fieldsTsPath);
55
+ } else {
56
+ console.log(
57
+ "No fields.base.json or fields.ts found, skipping fields generation",
58
+ );
59
+ }
60
+ console.log("Building...");
61
+ const buildEnv = options.full ? { BUILD_FULL: "1" } : {};
62
+ if (!run("pnpm", ["run", "build"], projectDir, buildEnv)) {
63
+ throw new Error("Build failed.");
64
+ }
65
+
66
+ if (!fs.existsSync(distDir)) {
67
+ throw new Error("dist not found. Run build first.");
68
+ }
69
+
70
+ fs.mkdirSync(buildDir, { recursive: true });
71
+
72
+ const html = fs.readFileSync(path.join(distDir, "index.html"), "utf-8");
73
+ fs.writeFileSync(path.join(buildDir, "html.txt"), html);
74
+
75
+ const assetsDir = path.join(distDir, "assets");
76
+ const findFiles = (dir: string, ext: string): string[] => {
77
+ const found: string[] = [];
78
+ const walk = (d: string) => {
79
+ if (!fs.existsSync(d)) return;
80
+ for (const e of fs.readdirSync(d)) {
81
+ const full = path.join(d, e);
82
+ const stat = fs.statSync(full);
83
+ if (stat.isDirectory()) walk(full);
84
+ else if (e.endsWith(ext)) found.push(full);
85
+ }
86
+ };
87
+ walk(dir);
88
+ return found;
89
+ };
90
+ const jsFiles = findFiles(assetsDir, ".js");
91
+ const cssFiles = findFiles(assetsDir, ".css");
92
+
93
+ if (jsFiles.length === 0 || cssFiles.length === 0) {
94
+ throw new Error("Expected .js and .css in dist/assets");
95
+ }
96
+
97
+ fs.writeFileSync(
98
+ path.join(buildDir, "js.txt"),
99
+ fs.readFileSync(jsFiles[0], "utf-8"),
100
+ );
101
+ fs.writeFileSync(
102
+ path.join(buildDir, "css.txt"),
103
+ fs.readFileSync(cssFiles[0], "utf-8"),
104
+ );
105
+
106
+ const fieldsJsonPath = path.join(projectDir, "fields.json");
107
+ if (fs.existsSync(fieldsJsonPath)) {
108
+ fs.copyFileSync(fieldsJsonPath, path.join(buildDir, "fields.txt"));
109
+ }
110
+
111
+ const pkgPath = path.join(projectDir, "package.json");
112
+ const wmakeVersion =
113
+ fs.existsSync(pkgPath) &&
114
+ (() => {
115
+ try {
116
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
117
+ return typeof pkg.version === "string" ? pkg.version : undefined;
118
+ } catch {
119
+ return undefined;
120
+ }
121
+ })();
122
+ const dataPath = path.join(projectDir, "data.json");
123
+ const data = fs.existsSync(dataPath)
124
+ ? JSON.parse(fs.readFileSync(dataPath, "utf-8"))
125
+ : {};
126
+ if (wmakeVersion) {
127
+ data.wmakeVersion = wmakeVersion;
128
+ }
129
+ fs.writeFileSync(
130
+ path.join(buildDir, "data.txt"),
131
+ JSON.stringify(data, null, 2),
132
+ );
133
+
134
+ console.log(
135
+ "Widget files:",
136
+ path.join(buildDir, "html.txt"),
137
+ "js.txt, css.txt, fields.txt, data.txt",
138
+ );
139
+
140
+ const zipPath = path.join(distDir, `${archiveName}.zip`);
141
+ const zip = spawnSync("zip", ["-r", zipPath, "."], {
142
+ cwd: buildDir,
143
+ stdio: "pipe",
144
+ });
145
+ if (zip.status === 0) {
146
+ console.log("Archive:", zipPath);
147
+ return;
148
+ }
149
+ try {
150
+ const AdmZip = (await import("adm-zip")).default;
151
+ const archive = new AdmZip();
152
+ for (const f of [
153
+ "html.txt",
154
+ "js.txt",
155
+ "css.txt",
156
+ "fields.txt",
157
+ "data.txt",
158
+ ]) {
159
+ const fp = path.join(buildDir, f);
160
+ if (fs.existsSync(fp)) archive.addLocalFile(fp, "", f);
161
+ }
162
+ archive.writeZip(zipPath);
163
+ console.log("Archive (adm-zip):", zipPath);
164
+ } catch (e) {
165
+ console.error("zip failed and adm-zip fallback error:", e);
166
+ console.error(
167
+ "Install 'zip' (e.g. apk add zip) or ensure adm-zip is installed.",
168
+ );
169
+ throw e;
170
+ }
171
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "baseUrl": ".",
14
+ "paths": {
15
+ "@/*": [
16
+ "./src/*"
17
+ ]
18
+ }
19
+ },
20
+ "include": [
21
+ "src/**/*"
22
+ ],
23
+ "exclude": [
24
+ "node_modules",
25
+ "dist"
26
+ ]
27
+ }