waybetter/packages/create-waybetter/src/names.js
CRT 4bc8c6a933 create-waybetter: npm create waybetter scaffolds plugins (JS/TS), themes and whole apps
- plugin: *.plugin.js with the store header and start/stop on api.scope, storage, observe,
  onKey, the UI kit (toast with action, ui.h settings dialog) and api.theme/--wb-* vars;
  --ts makes a TypeScript project typed by the waybetter package (build keeps the header first)
- theme: JSON with "$schema" -> theme.schema.json, palette derived like themeFromColors,
  font roles (family, tweet), presets
- app: Tauri 2 + waybetter-host + runtime + shell models: SiteSpec, SiteAdapter, shell/tab-chrome/
  site-tab capabilities, tabs_sync, plugin store, a site-only command; presets for youtube.com,
  kick.com, twitch.tv with placeholder selectors; host crate via submodule, path or git
- tests generate into temp dirs: headers via parse_meta (new plugin_meta example), plugin code
  against createCorePluginApi's members, themes against the schema, app TS with tsc against the
  built package, app Rust with cargo metadata/check/test
- wired into bun run check (typecheck incl. checkJs, build:packages, test:create) and pack:check
2026-09-28 23:17:32 +07:00

73 lines
3.1 KiB
JavaScript

// Names and small validators shared by the generators. Pure; no I/O.
/** A usage error: shown to the user as-is, exit code 1, no stack. */
export class UsageError extends Error {}
/** `Hide Trends!` -> `hide-trends` (ASCII letters and digits, dashes between). */
export function slugify(value, max = 64) {
return String(value)
.normalize('NFKD')
.replace(/\p{M}/gu, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, max)
.replace(/-+$/, '');
}
/** `hide trends 2` -> `HideTrends2`; always a valid JS/Rust type identifier. */
export function pascalCase(value) {
const words = String(value).normalize('NFKD').replace(/\p{M}/gu, '').replace(/[^A-Za-z0-9]+/g, ' ').trim().split(/\s+/).filter(Boolean);
const name = words.map(word => word[0].toUpperCase() + word.slice(1)).join('');
return /^[A-Za-z]/.test(name) ? name : `Plugin${name}`;
}
/** `wayBetter for Kick` -> `wayBetterForKick` (camelCase, for `data-*` keys). */
export function camelCase(value) {
const name = pascalCase(value);
return name[0].toLowerCase() + name.slice(1);
}
/**
* A value for a `@name` / `@description` / ... plugin header line, or a theme string. The plugin
* store reads each header line up to its end, drops control characters and keeps 300 characters
* (waybetter-host `plugins::parse_meta`), so values are one line and cannot end the comment.
*/
export function headerValue(label, value, { max = 300, required = false } = {}) {
const text = String(value ?? '').trim();
if (required && !text) throw new UsageError(`${label} is required.`);
if (/[\u0000-\u001f\u007f]/.test(text)) throw new UsageError(`${label} must be one line of text.`);
if (text.includes('*/')) throw new UsageError(`${label} cannot contain "*/".`);
if (text.length > max) throw new UsageError(`${label} is longer than ${max} characters.`);
return text;
}
/** Semver-ish plugin version (`1.0.0`, `0.3.1-beta.2`). */
export function versionValue(value) {
const text = String(value ?? '1.0.0').trim() || '1.0.0';
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(text)) throw new UsageError(`Version "${text}" is not like 1.0.0.`);
return text;
}
export const HEX = /^#[0-9a-fA-F]{6}$/;
/** `#abc` / `abc123` / `#ABC123` -> `#aabbcc` style lowercase six-digit hex. */
export function hexValue(label, value) {
let text = String(value ?? '').trim().toLowerCase();
if (!text.startsWith('#')) text = `#${text}`;
const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/.exec(text);
if (short) text = `#${short[1]}${short[1]}${short[2]}${short[2]}${short[3]}${short[3]}`;
if (!HEX.test(text)) throw new UsageError(`${label} must be a hex color like #1d9bf0 (got "${value}").`);
return text;
}
/** A JS/TS string literal. */
export const str = value => JSON.stringify(String(value));
/** A Rust string literal (JSON escapes are valid Rust escapes for the characters we allow). */
export function rustStr(value) {
return JSON.stringify(String(value)).replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => `\\u{${hex}}`);
}
/** Joins lines with `\n` and ends the file with one newline. */
export const lines = (...parts) => `${parts.flat().join('\n').replace(/\n+$/, '')}\n`;