@tixyel/cli 1.0.0 → 2.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/dist/widget.js ADDED
@@ -0,0 +1,499 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { minify as minifyHTML } from 'html-minifier-terser';
3
+ import JavaScriptObfuscator from 'javascript-obfuscator';
4
+ import { dirname, join, relative, resolve } from 'path';
5
+ import { readFile as readFilePromise } from 'fs/promises';
6
+ import { mkdir, writeFile } from 'fs/promises';
7
+ import autoprefixer from 'autoprefixer';
8
+ import { parse } from 'jsonc-parser';
9
+ import nested from 'postcss-nested';
10
+ import FastGlob from 'fast-glob';
11
+ import inquirer from 'inquirer';
12
+ import postcss from 'postcss';
13
+ import cssnano from 'cssnano';
14
+ import JSZip from 'jszip';
15
+ export async function createWidget(path, metadata, config, root) {
16
+ try {
17
+ console.log(`📁 Creating ${metadata?.name} at: ${path}`);
18
+ // Create directory
19
+ await mkdir(path, { recursive: true });
20
+ // Calculate config path if workspace root is provided
21
+ const configPath = root ? getConfigPathFromWidget(path, root) : undefined;
22
+ // Prompt for metadata if not provided
23
+ const widgetMetadata = await getMetadata(metadata);
24
+ // Create .tixyel configuration
25
+ const dotTixyel = {
26
+ name: metadata?.name,
27
+ version: '0.0.0',
28
+ description: widgetMetadata?.description || '',
29
+ config: configPath,
30
+ metadata: {
31
+ ...config.metadata,
32
+ ...widgetMetadata,
33
+ name: undefined,
34
+ description: undefined,
35
+ },
36
+ dirs: config.dirs || {
37
+ entry: 'development',
38
+ output: 'finished',
39
+ compacted: 'compacted',
40
+ },
41
+ };
42
+ await writeFile(resolve(path, '.tixyel'), JSON.stringify(dotTixyel, null, 2), 'utf-8');
43
+ // Create scaffold files from the workspace config
44
+ const scaffold = config.scaffold || [];
45
+ let created = { files: 0, folders: 0 };
46
+ async function processScaffoldItem(item, basePath) {
47
+ const fullPath = resolve(basePath, item.name);
48
+ if (item.type === 'folder') {
49
+ await mkdir(fullPath, { recursive: true });
50
+ created.folders++;
51
+ // Process folder children if any
52
+ if (item.content && Array.isArray(item.content) && item.content.length) {
53
+ for (const child of item.content) {
54
+ await processScaffoldItem(child, fullPath);
55
+ }
56
+ }
57
+ }
58
+ else if (item.type === 'file') {
59
+ // Write file
60
+ await writeFile(fullPath, item.content || '', 'utf-8');
61
+ created.files++;
62
+ }
63
+ }
64
+ for (const item of scaffold) {
65
+ await processScaffoldItem(item, path);
66
+ }
67
+ console.log(`✅ Widget ${dotTixyel.name} created successfully!`);
68
+ if (dotTixyel.description && dotTixyel.description.length) {
69
+ console.log(` - Description: ${dotTixyel.description}`);
70
+ }
71
+ if (dotTixyel.metadata?.tags && dotTixyel.metadata?.tags.length) {
72
+ console.log(` - Tags: ${dotTixyel.metadata.tags.join(', ')}`);
73
+ }
74
+ if (dotTixyel.config) {
75
+ console.log(` - Config Path: ${dotTixyel.config}`);
76
+ }
77
+ if (created.folders > 0 || created.files > 0) {
78
+ console.log(` - Scaffolded: ${created.folders} folders, ${created.files} files`);
79
+ }
80
+ }
81
+ catch (error) {
82
+ console.error('❌ Failed to create widget:', error);
83
+ throw error;
84
+ }
85
+ }
86
+ async function getMetadata(existingMetadata) {
87
+ const answers = await inquirer.prompt([
88
+ {
89
+ type: 'input',
90
+ name: 'description',
91
+ message: 'Widget description:',
92
+ default: existingMetadata?.description || '',
93
+ },
94
+ {
95
+ type: 'input',
96
+ name: 'tags',
97
+ message: 'Widget tags (comma-separated):',
98
+ default: existingMetadata?.tags ? existingMetadata.tags.join(', ') : '',
99
+ },
100
+ ]);
101
+ const tagsArray = answers.tags
102
+ ? answers.tags
103
+ .split(',')
104
+ .map((tag) => tag.trim())
105
+ .filter((tag) => tag.length)
106
+ : [];
107
+ return {
108
+ description: answers.description,
109
+ tags: tagsArray,
110
+ };
111
+ }
112
+ function getConfigPathFromWidget(widgetPath, workspaceRoot) {
113
+ const configTs = resolve(workspaceRoot, 'tixyel.config.ts');
114
+ const configJs = resolve(workspaceRoot, 'tixyel.config.js');
115
+ const configPathMjs = resolve(workspaceRoot, 'tixyel.config.mjs');
116
+ const targetConfig = existsSync(configTs) ? configTs : existsSync(configJs) ? configJs : existsSync(configPathMjs) ? configPathMjs : null;
117
+ if (!targetConfig) {
118
+ throw new Error('❌ Workspace configuration file not found.');
119
+ }
120
+ const relativePath = relative(widgetPath, targetConfig);
121
+ const normalized = relativePath.replace(/\\/g, '/');
122
+ return normalized.startsWith('.') ? normalized : `./${normalized}`;
123
+ }
124
+ export async function getNextWidgetNumber(parentPath) {
125
+ try {
126
+ if (!existsSync(parentPath)) {
127
+ return '01';
128
+ }
129
+ const entries = readdirSync(parentPath);
130
+ const widgetNumbers = entries
131
+ .filter((name) => /^\d+\s*-\s*/.test(name))
132
+ .map((name) => parseInt(name.split('-')[0], 10))
133
+ .filter((num) => !isNaN(num));
134
+ const maxNum = widgetNumbers.length > 0 ? Math.max(...widgetNumbers) : 0;
135
+ return String(maxNum + 1).padStart(2, '0');
136
+ }
137
+ catch (error) {
138
+ return '01';
139
+ }
140
+ }
141
+ async function merge(config) {
142
+ const merged = {
143
+ ...config,
144
+ version: config.version || '0.0.0',
145
+ description: config.description || '',
146
+ metadata: {
147
+ ...config.metadata,
148
+ },
149
+ dirs: {
150
+ entry: 'development',
151
+ output: 'finished',
152
+ compacted: 'compacted',
153
+ ...config.dirs,
154
+ },
155
+ };
156
+ return merged;
157
+ }
158
+ export function validateDotTixyel(config) {
159
+ if (typeof config !== 'object' || !config || config === null || !config.name || typeof config.name !== 'string' || !config.name.trim().length) {
160
+ throw new Error('❌ Invalid .tixyel: "name" is required and must be a non-empty string.');
161
+ }
162
+ return true;
163
+ }
164
+ export async function readDotTixyel(path) {
165
+ try {
166
+ const dotTixyelPath = resolve(path, '.tixyel');
167
+ if (!existsSync(dotTixyelPath)) {
168
+ return null;
169
+ }
170
+ const content = await readFilePromise(dotTixyelPath, 'utf-8');
171
+ const config = parse(content);
172
+ if (!validateDotTixyel(config)) {
173
+ console.error(`Invalid .tixyel configuration in ${path}`);
174
+ return null;
175
+ }
176
+ return merge(config);
177
+ }
178
+ catch (error) {
179
+ return null;
180
+ }
181
+ }
182
+ export async function findWidgets(root, depth, ignore) {
183
+ // Build glob pattern with depth limit
184
+ const depthPattern = Array.from({ length: depth }, (_, i) => '*'.repeat(i + 1)).join(',');
185
+ const pattern = `{${depthPattern}}/.tixyel`;
186
+ // Build ignore patterns (folders and files)
187
+ const ignorePatterns = ['node_modules', '.git', 'dist', ...ignore];
188
+ // Find all .tixyel files
189
+ const dotTixyels = await FastGlob(pattern, {
190
+ cwd: root,
191
+ absolute: true,
192
+ onlyFiles: true,
193
+ ignore: ignorePatterns,
194
+ });
195
+ const widgets = [];
196
+ for (const dotTixyelPath of dotTixyels) {
197
+ const path = dirname(dotTixyelPath);
198
+ const config = await readDotTixyel(path);
199
+ if (config) {
200
+ widgets.push({
201
+ path,
202
+ relativePath: relative(root, path),
203
+ config,
204
+ });
205
+ }
206
+ }
207
+ return widgets;
208
+ }
209
+ export async function build(widget, versionBump, verbose = false, workspaceConfig) {
210
+ console.log(`🔨 Building widget: ${widget.config.name}`);
211
+ if (verbose) {
212
+ console.log(` - Path: ${widget.path}`);
213
+ }
214
+ if (versionBump !== 'none') {
215
+ const newVersion = await bumpVersion(widget.path, versionBump);
216
+ if (newVersion) {
217
+ console.log(` - Bumped version to: ${newVersion}`);
218
+ widget.config.version = newVersion;
219
+ }
220
+ }
221
+ if (widget.config && workspaceConfig) {
222
+ try {
223
+ await processBuild(widget, workspaceConfig, verbose);
224
+ }
225
+ catch (error) {
226
+ console.error(`❌ Build failed for widget ${widget.config.name}:`, error);
227
+ throw error;
228
+ }
229
+ }
230
+ else {
231
+ console.warn(`⚠️ Skipping build for widget ${widget.config.name}: Missing configuration.`);
232
+ throw new Error('Missing configuration for build process.');
233
+ }
234
+ console.log(`✅ Build completed for widget: ${widget.config.name}`);
235
+ }
236
+ export async function processBuild(widget, workspaceConfig, verbose = false) {
237
+ const entryDir = join(widget.path, widget.config.dirs?.entry || workspaceConfig.dirs?.entry || 'development');
238
+ const outDir = join(widget.path, widget.config.dirs?.output || workspaceConfig.dirs?.output || 'finished');
239
+ const compactedDir = join(widget.path, widget.config.dirs?.compacted || workspaceConfig.dirs?.compacted || 'widgetIO');
240
+ if (!existsSync(entryDir)) {
241
+ throw new Error(`Entry directory not found: ${entryDir}`);
242
+ }
243
+ mkdirSync(outDir, { recursive: true });
244
+ const findPatterns = workspaceConfig.build?.find || {
245
+ html: ['index.html'],
246
+ script: ['script.js'],
247
+ css: ['styles.css'],
248
+ fields: ['fields.json'],
249
+ };
250
+ const resultMapping = workspaceConfig.build?.result || {
251
+ 'HTML.html': 'html',
252
+ 'SCRIPT.js': 'script',
253
+ 'CSS.css': 'css',
254
+ 'FIELDS.json': 'fields',
255
+ };
256
+ const compactedMapping = workspaceConfig.build?.widgetIO || {
257
+ 'html.txt': 'html',
258
+ 'js.txt': 'script',
259
+ 'css.txt': 'css',
260
+ 'fields.txt': 'fields',
261
+ };
262
+ // const results: Record<string, string> = {};
263
+ const normalizeList = (value) => (Array.isArray(value) ? value.filter(Boolean) : []);
264
+ const findAndRead = (baseDir, patterns) => {
265
+ const contents = [];
266
+ for (const pattern of patterns) {
267
+ const fullPath = join(baseDir, pattern);
268
+ if (existsSync(fullPath)) {
269
+ const content = readFileSync(fullPath, 'utf-8');
270
+ contents.push(content);
271
+ }
272
+ }
273
+ return contents;
274
+ };
275
+ // watermark for the files
276
+ var ref = ` ____ _ _ _
277
+ / __ \\ | |_ (_) __ __ _ _ ___ | |
278
+ / / _\` | | __| | | \\ \\/ / | | | | / _ \\ | |
279
+ | | (_| | | |_ | | > < | |_| | | __/ | |
280
+ \\ \\__,_| \\__| |_| /_/\\_\\ \\__, | \\___| |_|
281
+ \\____/ |___/\n`;
282
+ var watermark = {
283
+ html: [
284
+ `<!---`,
285
+ ref,
286
+ `Generated by Tixyel Widgets SDK`,
287
+ `Widget name: ${widget.config.name}`,
288
+ `Version: ${widget.config.version}`,
289
+ `Description: ${widget.config.description}`,
290
+ `Tags: ${widget.config.metadata?.tags?.join(',')}`,
291
+ `Made by: ${widget.config.metadata?.author}`,
292
+ widget.config.metadata?.clientId ? `Made for ${widget.config.metadata?.clientId}` : undefined,
293
+ ' ',
294
+ 'DO NOT EDIT, SHARE OR DISTRIBUTE THIS CODE WITHOUT PERMISSION FROM THE AUTHOR',
295
+ `--->`,
296
+ ]
297
+ .filter(Boolean)
298
+ .join('\n'),
299
+ css: [
300
+ `/**`,
301
+ ref,
302
+ `Generated by Tixyel Widgets SDK`,
303
+ `Widget name: ${widget.config.name}`,
304
+ `Version: ${widget.config.version}`,
305
+ `Description: ${widget.config.description}`,
306
+ `Tags: ${widget.config.metadata?.tags?.join(',')}`,
307
+ `Made by: ${widget.config.metadata?.author}`,
308
+ widget.config.metadata?.clientId ? `Made for ${widget.config.metadata?.clientId}` : undefined,
309
+ ' ',
310
+ 'DO NOT EDIT, SHARE OR DISTRIBUTE THIS CODE WITHOUT PERMISSION FROM THE AUTHOR',
311
+ `*/`,
312
+ ]
313
+ .filter(Boolean)
314
+ .join('\n'),
315
+ script: [
316
+ `/**`,
317
+ ref,
318
+ `Generated by Tixyel Widgets SDK`,
319
+ `Widget name: ${widget.config.name}`,
320
+ `Version: ${widget.config.version}`,
321
+ `Description: ${widget.config.description}`,
322
+ `Tags: ${widget.config.metadata?.tags?.join(',')}`,
323
+ `Made by: ${widget.config.metadata?.author}`,
324
+ widget.config.metadata?.clientId ? `Made for ${widget.config.metadata?.clientId}` : undefined,
325
+ ' ',
326
+ 'DO NOT EDIT, SHARE OR DISTRIBUTE THIS CODE WITHOUT PERMISSION FROM THE AUTHOR',
327
+ `*/`,
328
+ ]
329
+ .filter(Boolean)
330
+ .join('\n'),
331
+ };
332
+ const results = Object.fromEntries(await Promise.all(Object.entries(findPatterns).map(async ([key, patterns]) => {
333
+ let result = '';
334
+ const list = normalizeList(patterns);
335
+ if (list.length === 0) {
336
+ return [key, ''];
337
+ }
338
+ // Process HTML
339
+ if (['html', 'HTML'].some((k) => key === k)) {
340
+ result += watermark.html + '\n';
341
+ if (verbose)
342
+ console.log(` - Processing HTML...`);
343
+ const files = findAndRead(entryDir, list);
344
+ let mergedHTML = '';
345
+ for await (const fileContent of files) {
346
+ // Extract body content
347
+ const bodyMatch = fileContent.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
348
+ if (bodyMatch && bodyMatch[1]) {
349
+ mergedHTML += bodyMatch[1].trim() + '\n';
350
+ }
351
+ // Extract and inline styles
352
+ const styleMatches = fileContent.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/gi);
353
+ for (const match of styleMatches) {
354
+ mergedHTML += `<style>${match[1]}</style>\n`;
355
+ }
356
+ // Extract and inline <script> tags
357
+ const scriptMatches = fileContent.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/gi);
358
+ for (const match of scriptMatches) {
359
+ if (match[1]) {
360
+ mergedHTML += `<script>${match[1]}</script>\n`;
361
+ }
362
+ }
363
+ }
364
+ const minified = await minifyHTML(mergedHTML, workspaceConfig.build?.obfuscation?.html);
365
+ result += minified.trim();
366
+ }
367
+ else if (['css', 'style', 'styles'].some((k) => key === k)) {
368
+ result += watermark.css + '\n';
369
+ if (verbose)
370
+ console.log(` - Processing CSS...`);
371
+ const files = findAndRead(entryDir, list);
372
+ let mergedCSS = '';
373
+ for await (const content of files) {
374
+ const plugin = [
375
+ autoprefixer({
376
+ overrideBrowserslist: ['Chrome 127'],
377
+ ...workspaceConfig.build?.obfuscation?.css?.autoprefixer,
378
+ }),
379
+ cssnano(workspaceConfig.build?.obfuscation?.css?.cssnano),
380
+ ];
381
+ if (workspaceConfig.build?.obfuscation?.css?.removeNesting) {
382
+ plugin.unshift(nested());
383
+ }
384
+ const processed = await postcss(plugin).process(content, { from: undefined });
385
+ mergedCSS += processed.css + '\n';
386
+ }
387
+ result += mergedCSS.trim();
388
+ }
389
+ else if (['script', 'js', 'javascript'].some((k) => key === k)) {
390
+ result += watermark.script + '\n';
391
+ if (verbose)
392
+ console.log(` - Processing JavaScript...`);
393
+ const files = findAndRead(entryDir, list);
394
+ let mergedJS = '';
395
+ for await (const content of files) {
396
+ const obfuscated = JavaScriptObfuscator.obfuscate(content, workspaceConfig.build?.obfuscation?.javascript);
397
+ mergedJS += obfuscated.getObfuscatedCode() + '\n';
398
+ }
399
+ result += mergedJS.trim();
400
+ }
401
+ else if (['fields', 'FIELDS', 'fielddata', 'fieldData'].some((k) => key === k)) {
402
+ if (verbose)
403
+ console.log(` - Processing Json...`);
404
+ const files = findAndRead(entryDir, list);
405
+ let mergedFields = {};
406
+ for await (const content of files) {
407
+ try {
408
+ const noComments = parse(content);
409
+ const parsed = JSON.parse(JSON.stringify(noComments));
410
+ mergedFields = {
411
+ ...mergedFields,
412
+ ...parsed,
413
+ };
414
+ }
415
+ catch (error) {
416
+ console.warn(` ⚠️ Failed to parse fields JSON: ${error}`);
417
+ throw error;
418
+ }
419
+ }
420
+ result += JSON.stringify(mergedFields, null, 2);
421
+ }
422
+ else {
423
+ if (verbose)
424
+ console.log(` - Unknown build key: ${key}, the available keys are html, css, script and fields.`);
425
+ result += '';
426
+ }
427
+ return [key, result];
428
+ })));
429
+ for await (const [filename, key] of Object.entries(resultMapping)) {
430
+ const content = results[key];
431
+ if (content) {
432
+ const outPath = join(outDir, filename);
433
+ writeFileSync(outPath, content, 'utf-8');
434
+ if (verbose)
435
+ console.log(` ✓ Written: ${outPath}`);
436
+ }
437
+ }
438
+ // widgetIO zip
439
+ try {
440
+ const zip = new JSZip();
441
+ for await (const [filename, key] of Object.entries(compactedMapping)) {
442
+ const content = results[key];
443
+ if (content) {
444
+ zip.file(filename, content);
445
+ if (verbose)
446
+ console.log(` ✓ Added to ZIP: ${filename}`);
447
+ }
448
+ }
449
+ zip.file('widget.ini', `[HTML]\npath = "html.txt"\n\n[CSS]\npath = "css.txt"\n\n[JS]\npath = "js.txt"\n\n[FIELDS]\npath = "fields.txt"\n\n[DATA]\npath = "data.txt"`);
450
+ // check if data.txt exists in results, otherwise create empty
451
+ const dataContent = results['data'] || '{}';
452
+ zip.file('data.txt', dataContent);
453
+ const result = await zip
454
+ .generateInternalStream({ type: 'base64' })
455
+ .accumulate()
456
+ .then((data) => data);
457
+ const zipPath = join(compactedDir + '/' + widget.config.version || '0.0.0', `${widget.config.name}.zip`);
458
+ mkdirSync(compactedDir + '/' + widget.config.version, { recursive: true });
459
+ writeFileSync(zipPath, result, 'base64');
460
+ }
461
+ catch (error) {
462
+ console.error(`❌ Failed to create widgetIO ZIP for widget ${widget.config.name}:`, error);
463
+ throw error;
464
+ }
465
+ }
466
+ export async function bumpVersion(widgetPath, bumpType = 'patch') {
467
+ try {
468
+ const configPath = join(widgetPath, '.tixyel');
469
+ const content = await readFilePromise(configPath, 'utf-8');
470
+ const config = parse(content);
471
+ if (!config.version) {
472
+ config.version = '0.0.0';
473
+ }
474
+ const [major, minor, patch] = config.version.split('.').map(Number);
475
+ let newVersion;
476
+ switch (bumpType) {
477
+ case 'major': {
478
+ newVersion = `${major + 1}.0.0`;
479
+ break;
480
+ }
481
+ case 'minor': {
482
+ newVersion = `${major}.${minor + 1}.0`;
483
+ break;
484
+ }
485
+ case 'patch': {
486
+ newVersion = `${major}.${minor}.${patch + 1}`;
487
+ break;
488
+ }
489
+ }
490
+ config.version = newVersion;
491
+ const formattedContent = JSON.stringify(config, null, 2);
492
+ await writeFile(configPath, formattedContent, 'utf-8');
493
+ return newVersion;
494
+ }
495
+ catch (error) {
496
+ console.error(`Failed to bump version in ${widgetPath}:`, error);
497
+ return null;
498
+ }
499
+ }
@@ -0,0 +1,111 @@
1
+ import type { Options as HtmlMinifierOptions } from 'html-minifier-terser';
2
+ import type { ObfuscatorOptions } from 'javascript-obfuscator';
3
+ import type autoprefixer from 'autoprefixer';
4
+ import type cssnanoPlugin from 'cssnano';
5
+ export type ScaffoldItem = ScaffoldFile | ScaffoldFolder;
6
+ export interface ScaffoldFile {
7
+ name: string;
8
+ type: 'file';
9
+ content: string;
10
+ }
11
+ export interface ScaffoldFolder {
12
+ name: string;
13
+ type: 'folder';
14
+ content?: ScaffoldItem[];
15
+ }
16
+ export interface WorkspaceConfig<Find extends BuildFindMap = BuildFindMap> {
17
+ /**
18
+ * Search options for locating widget files
19
+ */
20
+ search?: {
21
+ /**
22
+ * Maximum directory depth to search
23
+ */
24
+ maxDepth?: number;
25
+ /**
26
+ * Folders and files to ignore during search
27
+ */
28
+ ignore?: string[];
29
+ };
30
+ /**
31
+ * Metadata applied to all widgets in the workspace
32
+ */
33
+ metadata?: {
34
+ name?: string;
35
+ author?: string;
36
+ clientId?: string;
37
+ description?: string;
38
+ tags?: string[];
39
+ };
40
+ dirs?: {
41
+ entry?: string;
42
+ output?: string;
43
+ compacted?: string;
44
+ };
45
+ /**
46
+ * Scaffold structure to create when generating a new widget
47
+ */
48
+ scaffold?: ScaffoldItem[];
49
+ /**
50
+ * Build configuration
51
+ */
52
+ build?: {
53
+ /**
54
+ * Run builds in parallel
55
+ */
56
+ parallel?: boolean;
57
+ /**
58
+ * Verbose output during build
59
+ */
60
+ verbose?: boolean;
61
+ /**
62
+ * File patterns to locate widget files
63
+ */
64
+ find?: Find;
65
+ /**
66
+ * Mapping of output files to find keys
67
+ */
68
+ result?: BuildResultMap<Find>;
69
+ /**
70
+ * Mapping of widgetIO output files to find keys
71
+ */
72
+ widgetIO?: BuildResultMap<Find>;
73
+ /**
74
+ * Obfuscation and minification settings
75
+ */
76
+ obfuscation?: {
77
+ /**
78
+ * JavaScript obfuscation options
79
+ */
80
+ javascript?: ObfuscatorOptions;
81
+ /**
82
+ * CSS minification options
83
+ */
84
+ css?: {
85
+ /**
86
+ * Remove nesting rules
87
+ */
88
+ removeNesting?: boolean;
89
+ /**
90
+ * Autoprefixer options
91
+ */
92
+ autoprefixer?: autoprefixer.Options;
93
+ /**
94
+ * cssnano options
95
+ */
96
+ cssnano?: cssnanoPlugin.Options;
97
+ };
98
+ /**
99
+ * HTML minification options
100
+ */
101
+ html?: HtmlMinifierOptions;
102
+ };
103
+ };
104
+ }
105
+ type BuildFindMap = Record<string, string[]>;
106
+ type BuildResultMap<Find extends BuildFindMap> = Record<string, keyof Find>;
107
+ export declare function defineWorkspaceConfig<const Find extends BuildFindMap>(config: WorkspaceConfig<Find>): WorkspaceConfig<Find>;
108
+ export declare function findWorkspaceRoot(startPath?: string): Promise<string | null>;
109
+ export declare function validateWorkspace(): Promise<string>;
110
+ export declare function loadWorkspace(path: string): Promise<WorkspaceConfig<BuildFindMap>>;
111
+ export {};