@tixyel/cli 1.0.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.
@@ -0,0 +1,156 @@
1
+ import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { minify as minifyHTML } from 'html-minifier-terser';
4
+ import JavaScriptObfuscator from 'javascript-obfuscator';
5
+ import postcss from 'postcss';
6
+ import autoprefixer from 'autoprefixer';
7
+ import cssnano from 'cssnano';
8
+ import nested from 'postcss-nested';
9
+ /**
10
+ * Processes and builds a widget
11
+ */
12
+ export async function buildWidget(options) {
13
+ const { widgetPath, config, cliConfig, verbose } = options;
14
+ const entryDir = join(widgetPath, config.entry || 'development');
15
+ const outDir = join(widgetPath, config.outDir || 'finished');
16
+ if (!existsSync(entryDir)) {
17
+ throw new Error(`Entry directory not found: ${entryDir}`);
18
+ }
19
+ // Create output directory
20
+ mkdirSync(outDir, { recursive: true });
21
+ // Get file patterns - use widget config first, fallback to CLI config defaults
22
+ const findPatterns = config.build?.find || cliConfig.build.find;
23
+ const finishedMapping = config.build?.finished || cliConfig.build.finished;
24
+ // Process each type
25
+ const results = {};
26
+ const normalizeList = (value) => (Array.isArray(value) ? value.filter(Boolean) : []);
27
+ const htmlList = normalizeList(findPatterns.html);
28
+ const cssList = normalizeList(findPatterns.css);
29
+ const scriptList = normalizeList(findPatterns.script);
30
+ const fieldsList = normalizeList(findPatterns.fields);
31
+ // Process HTML
32
+ if (htmlList.length > 0) {
33
+ if (verbose)
34
+ console.log(` Processing HTML...`);
35
+ results.html = await processHTML(entryDir, htmlList, cliConfig);
36
+ }
37
+ // Process CSS
38
+ if (cssList.length > 0) {
39
+ if (verbose)
40
+ console.log(` Processing CSS...`);
41
+ results.css = await processCSS(entryDir, cssList, cliConfig);
42
+ }
43
+ // Process JavaScript
44
+ if (scriptList.length > 0) {
45
+ if (verbose)
46
+ console.log(` Processing JavaScript...`);
47
+ results.script = await processJavaScript(entryDir, scriptList, cliConfig);
48
+ }
49
+ // Process Fields (JSON)
50
+ if (fieldsList.length > 0) {
51
+ if (verbose)
52
+ console.log(` Processing Fields...`);
53
+ results.fields = await processFields(entryDir, fieldsList);
54
+ }
55
+ // Write output files based on finished mapping
56
+ for (const [filename, type] of Object.entries(finishedMapping)) {
57
+ const content = results[type];
58
+ if (content) {
59
+ const outputPath = join(outDir, filename);
60
+ writeFileSync(outputPath, content, 'utf-8');
61
+ if (verbose)
62
+ console.log(` ✓ Written: ${filename}`);
63
+ }
64
+ }
65
+ }
66
+ /**
67
+ * Finds and reads files matching pattern
68
+ */
69
+ function findAndReadFiles(baseDir, filenames) {
70
+ const contents = [];
71
+ for (const filename of filenames) {
72
+ const filePath = join(baseDir, filename);
73
+ if (existsSync(filePath)) {
74
+ contents.push(readFileSync(filePath, 'utf-8'));
75
+ }
76
+ }
77
+ return contents;
78
+ }
79
+ /**
80
+ * Processes HTML files - extracts <body> content only
81
+ */
82
+ async function processHTML(baseDir, filenames, cliConfig) {
83
+ const files = findAndReadFiles(baseDir, filenames);
84
+ let mergedHTML = '';
85
+ for (const content of files) {
86
+ // Extract body content
87
+ const bodyMatch = content.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
88
+ if (bodyMatch) {
89
+ mergedHTML += bodyMatch[1] + '\n';
90
+ }
91
+ // Extract and inline <style> tags
92
+ const styleMatches = content.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/gi);
93
+ for (const match of styleMatches) {
94
+ mergedHTML += `<style>${match[1]}</style>\n`;
95
+ }
96
+ // Extract and inline <script> tags
97
+ const scriptMatches = content.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/gi);
98
+ for (const match of scriptMatches) {
99
+ mergedHTML += `<script>${match[1]}</script>\n`;
100
+ }
101
+ }
102
+ // Minify HTML
103
+ const minified = await minifyHTML(mergedHTML, cliConfig.build.obfuscation.html);
104
+ return minified;
105
+ }
106
+ /**
107
+ * Processes CSS files
108
+ */
109
+ async function processCSS(baseDir, filenames, cliConfig) {
110
+ const files = findAndReadFiles(baseDir, filenames);
111
+ let mergedCSS = '';
112
+ for (const content of files) {
113
+ // Process each file separately
114
+ const plugins = [
115
+ autoprefixer({ overrideBrowserslist: ['Chrome 127'], ...cliConfig.build.obfuscation.css.autoprefixer }),
116
+ cssnano(cliConfig.build.obfuscation.css.cssnano),
117
+ ];
118
+ if (cliConfig.build.obfuscation.css.removeNesting) {
119
+ plugins.unshift(nested());
120
+ }
121
+ const result = await postcss(plugins).process(content, { from: undefined });
122
+ mergedCSS += result.css + '\n';
123
+ }
124
+ return mergedCSS.trim();
125
+ }
126
+ /**
127
+ * Processes JavaScript files
128
+ */
129
+ async function processJavaScript(baseDir, filenames, cliConfig) {
130
+ const files = findAndReadFiles(baseDir, filenames);
131
+ let mergedJS = '';
132
+ for (const content of files) {
133
+ // Obfuscate each file separately
134
+ const obfuscated = JavaScriptObfuscator.obfuscate(content, cliConfig.build.obfuscation.javascript);
135
+ mergedJS += obfuscated.getObfuscatedCode() + '\n';
136
+ }
137
+ return mergedJS.trim();
138
+ }
139
+ /**
140
+ * Processes Fields JSON files
141
+ */
142
+ async function processFields(baseDir, filenames) {
143
+ const files = findAndReadFiles(baseDir, filenames);
144
+ // Merge all fields
145
+ let mergedFields = {};
146
+ for (const content of files) {
147
+ try {
148
+ const parsed = JSON.parse(content);
149
+ mergedFields = { ...mergedFields, ...parsed };
150
+ }
151
+ catch (error) {
152
+ console.warn(` ⚠️ Failed to parse fields JSON: ${error}`);
153
+ }
154
+ }
155
+ return JSON.stringify(mergedFields);
156
+ }
@@ -0,0 +1,23 @@
1
+ import type { TixyelConfig } from '../types/tixyel-config.js';
2
+ /**
3
+ * Default search depth for finding .tixyel files
4
+ */
5
+ export declare const DEFAULT_MAX_DEPTH = 3;
6
+ /**
7
+ * Reads and parses a .tixyel configuration file (supports JSONC)
8
+ * Applies default values for missing properties
9
+ */
10
+ export declare function readTixyelConfig(directory: string): Promise<TixyelConfig | null>;
11
+ /**
12
+ * Configuration for CLI settings
13
+ */
14
+ export interface CliConfig {
15
+ /**
16
+ * Maximum depth to search for .tixyel files
17
+ */
18
+ maxDepth: number;
19
+ }
20
+ /**
21
+ * Default CLI configuration
22
+ */
23
+ export declare const defaultCliConfig: CliConfig;
@@ -0,0 +1,34 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import { parse } from 'jsonc-parser';
4
+ import { validateTixyelConfig, applyDefaults } from '../types/tixyel-config.js';
5
+ /**
6
+ * Default search depth for finding .tixyel files
7
+ */
8
+ export const DEFAULT_MAX_DEPTH = 3;
9
+ /**
10
+ * Reads and parses a .tixyel configuration file (supports JSONC)
11
+ * Applies default values for missing properties
12
+ */
13
+ export async function readTixyelConfig(directory) {
14
+ try {
15
+ const configPath = join(directory, '.tixyel');
16
+ const content = await readFile(configPath, 'utf-8');
17
+ const config = parse(content);
18
+ if (!validateTixyelConfig(config)) {
19
+ console.error(`Invalid .tixyel configuration in ${directory}`);
20
+ return null;
21
+ }
22
+ // Apply defaults and return the original config with defaults merged
23
+ return applyDefaults(config);
24
+ }
25
+ catch (error) {
26
+ return null;
27
+ }
28
+ }
29
+ /**
30
+ * Default CLI configuration
31
+ */
32
+ export const defaultCliConfig = {
33
+ maxDepth: DEFAULT_MAX_DEPTH,
34
+ };
@@ -0,0 +1,19 @@
1
+ import type { TixyelConfig } from '../types/tixyel-config.js';
2
+ export interface WidgetInfo {
3
+ /**
4
+ * Absolute path to the widget directory
5
+ */
6
+ path: string;
7
+ /**
8
+ * Relative path from the root directory
9
+ */
10
+ relativePath: string;
11
+ /**
12
+ * Parsed .tixyel configuration
13
+ */
14
+ config: TixyelConfig;
15
+ }
16
+ /**
17
+ * Finds all .tixyel files in the workspace up to a specified depth
18
+ */
19
+ export declare function findWidgets(rootPath: string, maxDepth?: number, ignorePatterns?: string[]): Promise<WidgetInfo[]>;
@@ -0,0 +1,35 @@
1
+ import fastGlobModule from 'fast-glob';
2
+ import { dirname, relative } from 'path';
3
+ import { readTixyelConfig } from './config.js';
4
+ const glob = fastGlobModule;
5
+ /**
6
+ * Finds all .tixyel files in the workspace up to a specified depth
7
+ */
8
+ export async function findWidgets(rootPath, maxDepth = 3, ignorePatterns = []) {
9
+ // Build glob pattern with depth limit
10
+ const depthPattern = Array.from({ length: maxDepth }, (_, i) => '*'.repeat(i + 1)).join(',');
11
+ const pattern = `{${depthPattern}}/.tixyel`;
12
+ // Build ignore patterns
13
+ const ignore = ['node_modules', '.git', 'dist', ...ignorePatterns];
14
+ // Find all .tixyel files
15
+ const configFiles = await glob(pattern, {
16
+ cwd: rootPath,
17
+ absolute: true,
18
+ onlyFiles: true,
19
+ ignore,
20
+ });
21
+ // Read and parse each config
22
+ const widgets = [];
23
+ for (const configFile of configFiles) {
24
+ const widgetPath = dirname(configFile);
25
+ const config = await readTixyelConfig(widgetPath);
26
+ if (config) {
27
+ widgets.push({
28
+ path: widgetPath,
29
+ relativePath: relative(rootPath, widgetPath),
30
+ config,
31
+ });
32
+ }
33
+ }
34
+ return widgets;
35
+ }
@@ -0,0 +1,5 @@
1
+ import type { RequiredCliConfig } from '../types/tixyel-cli-config.js';
2
+ /**
3
+ * Loads tixyel.config.ts or tixyel.config.js from the workspace root
4
+ */
5
+ export declare function loadCliConfig(rootPath: string): Promise<RequiredCliConfig>;
@@ -0,0 +1,66 @@
1
+ import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
2
+ import { resolve } from 'path';
3
+ import { mergeCliConfig } from '../types/tixyel-cli-config.js';
4
+ /**
5
+ * Loads tixyel.config.ts or tixyel.config.js from the workspace root
6
+ */
7
+ export async function loadCliConfig(rootPath) {
8
+ const configTs = resolve(rootPath, 'tixyel.config.ts');
9
+ const configJs = resolve(rootPath, 'tixyel.config.js');
10
+ const configMjs = resolve(rootPath, 'tixyel.config.mjs');
11
+ let userConfig;
12
+ try {
13
+ // Try .mjs first (already ES module)
14
+ if (existsSync(configMjs)) {
15
+ const module = await import(`file://${configMjs}`);
16
+ userConfig = module.default || module.config;
17
+ }
18
+ // Try .js (if package.json has type: module)
19
+ else if (existsSync(configJs)) {
20
+ const module = await import(`file://${configJs}`);
21
+ userConfig = module.default || module.config;
22
+ }
23
+ // Try .ts file (compile on-the-fly)
24
+ else if (existsSync(configTs)) {
25
+ userConfig = await loadTypeScriptConfig(configTs, rootPath);
26
+ }
27
+ }
28
+ catch (error) {
29
+ console.warn(`⚠️ Failed to load tixyel.config: ${error}`);
30
+ }
31
+ return mergeCliConfig(userConfig);
32
+ }
33
+ /**
34
+ * Loads TypeScript config by compiling it on-the-fly
35
+ */
36
+ async function loadTypeScriptConfig(configPath, rootPath) {
37
+ const tempJs = resolve(rootPath, '.tixyel.config.temp.mjs');
38
+ try {
39
+ // Read TypeScript content
40
+ const tsContent = readFileSync(configPath, 'utf-8');
41
+ // Simple conversion: remove type imports and convert to JS
42
+ const jsContent = tsContent
43
+ .replace(/import type .+ from .+;/g, '') // Remove type imports
44
+ .replace(/: TixyelCliConfig/g, ''); // Remove type annotations
45
+ // Write temporary .mjs file
46
+ writeFileSync(tempJs, jsContent, 'utf-8');
47
+ // Import the temporary file
48
+ const module = await import(`file://${tempJs}?t=${Date.now()}`);
49
+ const config = module.default || module.config;
50
+ return config;
51
+ }
52
+ catch (error) {
53
+ throw new Error(`Failed to load TypeScript config: ${error}`);
54
+ }
55
+ finally {
56
+ // Clean up temporary file
57
+ if (existsSync(tempJs)) {
58
+ try {
59
+ unlinkSync(tempJs);
60
+ }
61
+ catch {
62
+ // Ignore cleanup errors
63
+ }
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Version bump types
3
+ */
4
+ export type VersionBump = 'major' | 'minor' | 'patch';
5
+ /**
6
+ * Bumps the version of a widget configuration
7
+ */
8
+ export declare function bumpWidgetVersion(widgetPath: string, bumpType?: VersionBump): Promise<string | null>;
9
+ /**
10
+ * Prompts user to select version bump type
11
+ */
12
+ export declare function promptVersionBump(): Promise<VersionBump>;
@@ -0,0 +1,49 @@
1
+ import { readFile, writeFile } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import { parse } from 'jsonc-parser';
4
+ /**
5
+ * Bumps the version of a widget configuration
6
+ */
7
+ export async function bumpWidgetVersion(widgetPath, bumpType = 'patch') {
8
+ try {
9
+ const configPath = join(widgetPath, '.tixyel');
10
+ const content = await readFile(configPath, 'utf-8');
11
+ const config = parse(content);
12
+ if (!config.version) {
13
+ config.version = '0.0.0';
14
+ }
15
+ // Parse current version
16
+ const [major, minor, patch] = config.version.split('.').map(Number);
17
+ // Calculate new version
18
+ let newVersion;
19
+ switch (bumpType) {
20
+ case 'major':
21
+ newVersion = `${major + 1}.0.0`;
22
+ break;
23
+ case 'minor':
24
+ newVersion = `${major}.${minor + 1}.0`;
25
+ break;
26
+ case 'patch':
27
+ default:
28
+ newVersion = `${major}.${minor}.${patch + 1}`;
29
+ break;
30
+ }
31
+ // Update config
32
+ config.version = newVersion;
33
+ // Write back to file (JSON format)
34
+ const formattedContent = JSON.stringify(config, null, 2);
35
+ await writeFile(configPath, formattedContent, 'utf-8');
36
+ return newVersion;
37
+ }
38
+ catch (error) {
39
+ console.error(`Failed to bump version in ${widgetPath}:`, error);
40
+ return null;
41
+ }
42
+ }
43
+ /**
44
+ * Prompts user to select version bump type
45
+ */
46
+ export async function promptVersionBump() {
47
+ // This will be used with inquirer in the build command
48
+ return 'patch'; // Default, will be replaced with prompt
49
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Finds the workspace root by searching for tixyel.config.ts or tixyel.config.js
3
+ * Searches upwards from the current directory
4
+ */
5
+ export declare function findWorkspaceRoot(startPath?: string): Promise<string | null>;
6
+ /**
7
+ * Gets the relative path from a widget directory to the workspace root config
8
+ */
9
+ export declare function getConfigPathFromWidget(widgetPath: string, workspaceRoot: string): string;
10
+ /**
11
+ * Validates that workspace is initialized
12
+ */
13
+ export declare function validateWorkspaceInit(): Promise<string>;
@@ -0,0 +1,43 @@
1
+ import { resolve, relative } from 'path';
2
+ import { existsSync } from 'fs';
3
+ /**
4
+ * Finds the workspace root by searching for tixyel.config.ts or tixyel.config.js
5
+ * Searches upwards from the current directory
6
+ */
7
+ export async function findWorkspaceRoot(startPath = process.cwd()) {
8
+ let currentPath = resolve(startPath);
9
+ // Limit search to 10 levels up to avoid infinite loops
10
+ for (let i = 0; i < 10; i++) {
11
+ const configTs = resolve(currentPath, 'tixyel.config.ts');
12
+ const configJs = resolve(currentPath, 'tixyel.config.js');
13
+ if (existsSync(configTs) || existsSync(configJs)) {
14
+ return currentPath;
15
+ }
16
+ const parentPath = resolve(currentPath, '..');
17
+ if (parentPath === currentPath) {
18
+ // Reached filesystem root
19
+ break;
20
+ }
21
+ currentPath = parentPath;
22
+ }
23
+ return null;
24
+ }
25
+ /**
26
+ * Gets the relative path from a widget directory to the workspace root config
27
+ */
28
+ export function getConfigPathFromWidget(widgetPath, workspaceRoot) {
29
+ const relPath = relative(widgetPath, resolve(workspaceRoot, 'tixyel.config.ts'));
30
+ // Normalize path separators and ensure it starts with ./
31
+ const normalized = relPath.replace(/\\/g, '/');
32
+ return normalized.startsWith('.') ? normalized : `./${normalized}`;
33
+ }
34
+ /**
35
+ * Validates that workspace is initialized
36
+ */
37
+ export async function validateWorkspaceInit() {
38
+ const workspaceRoot = await findWorkspaceRoot();
39
+ if (!workspaceRoot) {
40
+ throw new Error('❌ Workspace not initialized. Run `tixyel init` in your workspace root first.');
41
+ }
42
+ return workspaceRoot;
43
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@tixyel/cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool for Tixyel widgets",
5
+ "keywords": [
6
+ "cli",
7
+ "widgets",
8
+ "typescript"
9
+ ],
10
+ "author": "Tixyel",
11
+ "license": "Apache-2.0",
12
+ "homepage": "https://github.com/Tixyel/widgets#readme",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/Tixyel/widgets.git",
16
+ "directory": "packages/cli"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/Tixyel/widgets/issues"
20
+ },
21
+ "type": "module",
22
+ "private": false,
23
+ "bin": {
24
+ "tixyel": "./dist/index.js"
25
+ },
26
+ "main": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js",
32
+ "default": "./dist/index.js"
33
+ }
34
+ },
35
+ "files": [
36
+ "dist"
37
+ ],
38
+ "scripts": {
39
+ "build": "tsc",
40
+ "dev": "tsc --watch",
41
+ "check-types": "tsc --noEmit"
42
+ },
43
+ "devDependencies": {
44
+ "@types/inquirer": "^9.0.9",
45
+ "@types/node": "^22.19.1",
46
+ "typescript": "^5.9.3"
47
+ },
48
+ "dependencies": {
49
+ "@types/html-minifier-terser": "^7.0.2",
50
+ "autoprefixer": "^10.4.22",
51
+ "commander": "^14.0.2",
52
+ "cssnano": "^7.1.2",
53
+ "fast-glob": "^3.3.3",
54
+ "html-minifier": "^4.0.0",
55
+ "html-minifier-terser": "^7.2.0",
56
+ "inquirer": "^13.0.2",
57
+ "javascript-obfuscator": "^4.1.1",
58
+ "jsonc-parser": "^3.3.1",
59
+ "postcss": "^8.5.6",
60
+ "postcss-nested": "^7.0.2"
61
+ }
62
+ }