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
This commit is contained in:
parent
376e74884e
commit
4bc8c6a933
17 changed files with 3510 additions and 6 deletions
73
packages/create-waybetter/src/names.js
Normal file
73
packages/create-waybetter/src/names.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// 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`;
|
||||
Loading…
Add table
Add a link
Reference in a new issue