@ekg_gg/devkit 0.0.27 → 0.0.28

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,43 @@
1
+ # @ekg_gg/devkit
2
+
3
+ The official development toolkit for building [EKG.gg](https://ekg.gg) streaming widgets. Provides a local dev server with live preview, hot reloading, test events, and a build pipeline for publishing widgets to the EKG.gg marketplace.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npm create ekg@latest my-widget
9
+ cd my-widget
10
+ npm run dev
11
+ ```
12
+
13
+ This scaffolds a new widget project and starts the dev server at `http://localhost:5173`.
14
+
15
+ For a full guide on building widgets, see the [Getting Started](https://github.com/ekggg/getting-started) documentation.
16
+
17
+ ## CLI Commands
18
+
19
+ ### `ekg dev [dir]`
20
+
21
+ Starts the local development server with:
22
+
23
+ - Live widget preview at configurable dimensions
24
+ - Hot reloading on file changes
25
+ - Settings panel for testing widget configuration
26
+ - Test event buttons for every supported event type
27
+ - Auto-generated TypeScript types from your manifest
28
+
29
+ ### `ekg build [dir]`
30
+
31
+ Validates your manifest, compiles TypeScript, and outputs a `dist/` folder ready to upload to the [EKG.gg artist portal](https://ekg.gg/app/artist/widgets).
32
+
33
+ ### `ekg sync [dir]`
34
+
35
+ Re-downloads the EKG runtime files and regenerates TypeScript types without starting the dev server. Use `--force` to bypass the cache.
36
+
37
+ ## Type Safety
38
+
39
+ The devkit auto-generates an `ekg.d.ts` file from your `manifest.json`, providing full TypeScript autocomplete for `ctx.settings` and `ctx.assets` specific to your widget. Types update automatically as you edit the manifest.
40
+
41
+ ## How It Works
42
+
43
+ The dev server downloads the EKG widget runtime (QuickJS WASM, event schemas, and type definitions) from the EKG.gg servers and caches them locally. Your widget script is compiled with [tsdown](https://tsdown.dev/) and runs inside the same sandboxed environment used in production — so what you see locally is what streamers will see in OBS.
package/dist/index.mjs CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import e from"yargs";import{hideBin as t}from"yargs/helpers";import n from"ajv/dist/2020.js";import r from"chalk";import i from"node:fs/promises";import a from"node:path";import{build as o}from"tsdown";import{fileURLToPath as s}from"node:url";import{z as c}from"zod/v4";import{createServer as l}from"vite";c.object({width:c.number().min(0).catch(1e3),height:c.number().min(0).catch(1e3),settings:c.record(c.string(),c.unknown()).catch({}),events:c.record(c.string(),c.unknown()).catch({}),persistedState:c.unknown().optional()});const u=c.object({$schema:c.literal(`https://ekg.gg/schemas/manifest.json`).default(`https://ekg.gg/schemas/manifest.json`),name:c.string().optional(),version:c.string().optional(),description:c.string().optional(),template:c.string().default(``),css:c.string().default(``),js:c.string().default(``),assets:c.record(c.string(),c.object({type:c.string().default(``),file:c.string().default(``)})).optional(),settings:c.record(c.string(),c.looseObject({type:c.string().default(``),name:c.string().default(``),description:c.string().optional()})).optional()});async function d(e,t){let n=a.resolve(e),r=await f(n),i=a.join(r,`manifest.json`),o=s(new URL(`..`,import.meta.url)),c=t?o:a.join(o,`..`,`..`),l=a.join(o,t?`client`:`dist`),u=e=>a.relative(l,e),d=a.join(o,`.runtime`);return{root:n,ekg:d,state:a.join(d,`state.json`),widget:r,manifest:i,node_modules:c,server:l,relative:u}}async function f(e){for await(let t of i.glob(`${e}/**/manifest.json`))if(!t.includes(`.runtime`)&&!t.includes(`dist`))return a.dirname(t);throw`No manifest.json found in ${e}`}async function p(e,t){let n=[`assets/js/devkit.d.ts`,`assets/js/devkit.js`,`assets/js/widget-worker.js`,`assets/js/emscripten-module.wasm`,`schemas/events.json`,`schemas/manifest.json`,`schemas/fonts.json`];await i.mkdir(e,{recursive:!0});try{await i.writeFile(a.join(e,`state.json`),`{}`,{flag:`wx`})}catch{}let r=await Promise.allSettled(n.map(t=>i.stat(`${e}/${a.basename(t)}`))),o=r.every(e=>e.status===`fulfilled`&&e.value.isFile());if(!(r.every(e=>e.status===`fulfilled`&&e.value.isFile()&&e.value.mtimeMs>Date.now()-6048e5)&&!t))try{await Promise.all(n.map(t=>m(e,t)))}catch(e){if(o&&!t)console.log(`Failed to update EKG types and devkit binary. Continuing with existing files.`);else throw`Failed to download devkit: ${e}`}}async function m(e,t){let n=await(await fetch(`https://ekg.gg/${t}?t=${Date.now()}`)).bytes();await i.writeFile(`${e}/${a.basename(t)}`,n)}async function h(e,t){let n=(e,t)=>`/// <reference types="@ekg_gg/devkit" />
2
+ import e from"yargs";import{hideBin as t}from"yargs/helpers";import n from"ajv/dist/2020.js";import r from"chalk";import i from"node:fs/promises";import a from"node:path";import{build as o}from"tsdown";import{fileURLToPath as s,pathToFileURL as c}from"node:url";import{createServer as l,normalizePath as u}from"vite";import{z as d}from"zod/v4";d.object({width:d.number().min(0).catch(1e3),height:d.number().min(0).catch(1e3),settings:d.record(d.string(),d.unknown()).catch({}),events:d.record(d.string(),d.unknown()).catch({}),persistedState:d.unknown().optional()});const f=d.object({$schema:d.literal(`https://ekg.gg/schemas/manifest.json`).default(`https://ekg.gg/schemas/manifest.json`),name:d.string().optional(),version:d.string().optional(),description:d.string().optional(),template:d.string().default(``),css:d.string().default(``),js:d.string().default(``),assets:d.record(d.string(),d.object({type:d.string().default(``),file:d.string().default(``)})).optional(),settings:d.record(d.string(),d.looseObject({type:d.string().default(``),name:d.string().default(``),description:d.string().optional()})).optional()});async function p(e,t){let n=a.resolve(e),r=await m(n),i=a.join(r,`manifest.json`),o=s(new URL(`..`,import.meta.url)),c=t?o:a.join(o,`..`,`..`),l=a.join(o,t?`client`:`dist`),d=e=>u(a.relative(l,e)),f=a.join(o,`.runtime`),p=a.join(f,`state.json`);return{root:u(n),ekg:u(f),state:u(p),widget:u(r),manifest:u(i),node_modules:u(c),server:u(l),relative:d}}async function m(e){for await(let t of i.glob(`**/manifest.json`,{cwd:e}))if(!t.includes(`.runtime`)&&!t.includes(`dist`))return a.dirname(a.join(e,t));throw`No manifest.json found in ${e}`}async function h(e,t){let n=[`assets/js/devkit.d.ts`,`assets/js/devkit.js`,`assets/js/widget-worker.js`,`assets/js/emscripten-module.wasm`,`schemas/events.json`,`schemas/manifest.json`,`schemas/fonts.json`];await i.mkdir(e,{recursive:!0});try{await i.writeFile(a.join(e,`state.json`),`{}`,{flag:`wx`})}catch{}let r=await Promise.allSettled(n.map(t=>i.stat(a.join(e,a.basename(t))))),o=r.every(e=>e.status===`fulfilled`&&e.value.isFile());if(!(r.every(e=>e.status===`fulfilled`&&e.value.isFile()&&e.value.mtimeMs>Date.now()-6048e5)&&!t))try{await Promise.all(n.map(t=>g(e,t)))}catch(e){if(o&&!t)console.log(`Failed to update EKG types and devkit binary. Continuing with existing files.`);else throw`Failed to download devkit: ${e}`}}async function g(e,t){let n=await(await fetch(`https://ekg.gg/${t}?t=${Date.now()}`)).bytes();await i.writeFile(a.join(e,a.basename(t)),n)}async function _(e,t){let n=(e,t)=>`/// <reference types="@ekg_gg/devkit" />
3
3
 
4
4
  declare namespace EKG {
5
5
  interface WidgetAssets ${e}
6
6
  interface WidgetSettings ${t}
7
7
  }
8
- `,r=(e,t)=>{let n=Object.entries(e).map(([e,n])=>` ${e}: ${t(n)}\n`).join(``);return n?`{\n${n} }`:`{}`},o=e=>{if(e.choices)return Object.keys(e.choices).map(e=>JSON.stringify(e)).join(` | `);switch(e.type){case`string`:case`color`:case`image`:case`audio`:case`font`:return`string`;case`string_array`:case`color_array`:case`reward_ids`:return`string[]`;case`decimal`:case`integer`:return`number`;case`decimal_array`:case`integer_array`:return`number[]`;case`boolean`:return`boolean`;default:return`any`}};try{let s=await i.readFile(t),c=u.parse(JSON.parse(s.toString(`utf8`))),l=r(c.assets??{},()=>`string`),d=r(c.settings??{},o);await i.writeFile(a.join(e,`ekg.d.ts`),n(l,d))}catch{try{await i.writeFile(a.join(e,`ekg.d.ts`),n(`{}`,`{}`),{flag:`wx`})}catch{}}}async function g(e,t){let s=await d(e,t);await p(s.ekg);let c=new n({allErrors:!0,discriminator:!0}),l=JSON.parse(await i.readFile(a.join(s.ekg,`manifest.json`),{encoding:`utf8`}));l.properties.settings.additionalProperties.unevaluatedProperties=!0;let u=c.compile(l),f=await _(s.manifest);if(!u(f)&&u.errors){console.log(r.red(`Invalid manifest.json`));for(let e of u.errors)console.log(r.red(`${r.bold(e.instancePath.slice(1))}: ${e.message}`));process.exit(1)}let m=new Set([`manifest.json`,f.css,f.template]);for(let e of Object.values(f.assets??{}))m.add(e.file);for(let e of Object.values(f.settings??{}))if([`image`,`audio`].includes(e.type)&&e.default&&m.add(e.default),e.type===`font`&&e.custom)for(let t of Object.keys(e.custom))m.add(t);let h=(await Promise.all(m.values().map(async e=>{try{if((await i.stat(a.join(s.widget,e))).isFile())return null}catch{}return e}))).filter(e=>e!==null);if(h.length){for(let e of h)console.log(r.red(`Missing file: ${r.bold(e)}`));process.exit(1)}let g=a.extname(f.js);try{await o({config:!1,tsconfig:!1,logLevel:`error`,outDir:a.join(s.root,`dist`),copy:m.values().map(e=>({from:a.join(s.widget,e),to:a.join(s.root,`dist`,e)})).toArray(),entry:a.join(s.widget,f.js),loader:{[g]:`ts`},outExtensions:()=>({js:g}),format:`esm`,platform:`neutral`,target:`es2023`,dts:!1})}catch(e){if(typeof e==`object`&&e&&`errors`in e&&Array.isArray(e.errors))for(let t of e.errors)t instanceof Error?console.log(t.message):console.error(t);else console.error(e);process.exit(1)}console.log(r.green(`${r.bold(`Success!`)} Widget files were written to ${a.join(s.root,`dist`)}`))}async function _(e){try{return JSON.parse(await i.readFile(e,{encoding:`utf8`}))}catch(e){console.log(r.red(`Failed to read widget's manifest.json`)),console.error(e),process.exit(1)}}async function v(e,t){let n=await d(e,t);await p(n.ekg);let r=await l({configFile:t?`vite.config.ts`:!1,root:n.server,define:{"import.meta.hot":!1},server:{fs:{allow:[n.node_modules,n.widget]},watch:{ignored:[`!${n.ekg}/**`]}},plugins:[{name:`EKG Dev Kit`,configureServer(e){e.ws.on(`ekg:state`,e=>{i.writeFile(n.state,JSON.stringify(e,null,2)).catch(e=>console.error(`Failed to write state file.`,e))}),e.ws.on(`ekg:manifest`,e=>{i.writeFile(n.manifest,JSON.stringify(e,null,2)).catch(e=>console.error(`Failed to write manifest file.`,e))})},resolveId(e){if(e.startsWith(`ekg:`))return`\0${e}`},load(e){let t=[`json`,`css`,`hbs`].join(`|`);switch(e){case`\0ekg:devkit`:return`
9
- export { manager } from '/@fs${n.ekg}/devkit.js'
10
- export { default as EventSchema } from '/@fs${n.ekg}/events.json'
11
- export { default as Fonts } from '/@fs${n.ekg}/fonts.json'
8
+ `,r=(e,t)=>{let n=Object.entries(e).map(([e,n])=>` ${e}: ${t(n)}\n`).join(``);return n?`{\n${n} }`:`{}`},o=e=>{if(e.choices)return Object.keys(e.choices).map(e=>JSON.stringify(e)).join(` | `);switch(e.type){case`string`:case`color`:case`image`:case`audio`:case`font`:return`string`;case`string_array`:case`color_array`:case`reward_ids`:return`string[]`;case`decimal`:case`integer`:return`number`;case`decimal_array`:case`integer_array`:return`number[]`;case`boolean`:return`boolean`;default:return`any`}};try{let s=await i.readFile(t),c=f.parse(JSON.parse(s.toString(`utf8`))),l=r(c.assets??{},()=>`string`),u=r(c.settings??{},o);await i.writeFile(a.join(e,`ekg.d.ts`),n(l,u))}catch{try{await i.writeFile(a.join(e,`ekg.d.ts`),n(`{}`,`{}`),{flag:`wx`})}catch{}}}async function v(e,t){let s=await p(e,t);await h(s.ekg);let c=new n({allErrors:!0,discriminator:!0}),l=JSON.parse(await i.readFile(a.join(s.ekg,`manifest.json`),{encoding:`utf8`}));l.properties.settings.additionalProperties.unevaluatedProperties=!0;let u=c.compile(l),d=await y(s.manifest);if(!u(d)&&u.errors){console.log(r.red(`Invalid manifest.json`));for(let e of u.errors)console.log(r.red(`${r.bold(e.instancePath.slice(1))}: ${e.message}`));process.exit(1)}let f=new Set([`manifest.json`,d.css,d.template]);for(let e of Object.values(d.assets??{}))f.add(e.file);for(let e of Object.values(d.settings??{}))if([`image`,`audio`].includes(e.type)&&e.default&&f.add(e.default),e.type===`font`&&e.custom)for(let t of Object.keys(e.custom))f.add(t);let m=(await Promise.all(f.values().map(async e=>{try{if((await i.stat(a.join(s.widget,e))).isFile())return null}catch{}return e}))).filter(e=>e!==null);if(m.length){for(let e of m)console.log(r.red(`Missing file: ${r.bold(e)}`));process.exit(1)}let g=a.extname(d.js);try{await o({config:!1,tsconfig:!1,logLevel:`error`,outDir:a.join(s.root,`dist`),copy:f.values().map(e=>({from:a.join(s.widget,e),to:a.join(s.root,`dist`,e)})).toArray(),entry:a.join(s.widget,d.js),loader:{[g]:`ts`},outExtensions:()=>({js:g}),format:`esm`,platform:`neutral`,target:`es2023`,dts:!1})}catch(e){if(typeof e==`object`&&e&&`errors`in e&&Array.isArray(e.errors))for(let t of e.errors)t instanceof Error?console.log(t.message):console.error(t);else console.error(e);process.exit(1)}console.log(r.green(`${r.bold(`Success!`)} Widget files were written to ${a.join(s.root,`dist`)}`))}async function y(e){try{return JSON.parse(await i.readFile(e,{encoding:`utf8`}))}catch(e){console.log(r.red(`Failed to read widget's manifest.json`)),console.error(e),process.exit(1)}}async function b(e,t){let n=await p(e,t),r=(...e)=>`/@fs${c(a.join(...e)).pathname}`;await h(n.ekg);let o=await l({configFile:t?`vite.config.ts`:!1,root:n.server,define:{"import.meta.hot":!1},server:{fs:{allow:[n.node_modules,n.widget]},watch:{ignored:[`!${n.ekg}/**`]}},plugins:[{name:`EKG Dev Kit`,configureServer(e){e.ws.on(`ekg:state`,e=>{i.writeFile(n.state,JSON.stringify(e,null,2)).catch(e=>console.error(`Failed to write state file.`,e))}),e.ws.on(`ekg:manifest`,e=>{i.writeFile(n.manifest,JSON.stringify(e,null,2)).catch(e=>console.error(`Failed to write manifest file.`,e))})},resolveId(e){if(e.startsWith(`ekg:`))return`\0${e}`},load(e){let t=[`json`,`css`,`hbs`].join(`|`);switch(e){case`\0ekg:devkit`:return`
9
+ export { manager } from '${r(n.ekg,`devkit.js`)}'
10
+ export { default as EventSchema } from '${r(n.ekg,`events.json`)}'
11
+ export { default as Fonts } from '${r(n.ekg,`fonts.json`)}'
12
12
  `;case`\0ekg:widget`:return`
13
- export { default as state } from '/@fs${n.state}?raw'
13
+ export { default as state } from '${r(n.state)}?raw'
14
14
 
15
- const inline = import.meta.glob(['./**', '!**/*.(${t})'], {
15
+ const inline = import.meta.glob(['./**', '!./**/*.(${t})'], {
16
16
  base: '${n.relative(n.widget)}',
17
17
  query: '?inline',
18
18
  import: 'default',
@@ -29,4 +29,4 @@ declare namespace EKG {
29
29
  })
30
30
 
31
31
  export const widget = { ...inline, ...raw }
32
- `}},transform(e,t){if(t.startsWith(n.widget)&&!t.startsWith(n.ekg)&&!e.startsWith(`export default `))return{code:`export default ${JSON.stringify(e)}`}}}]});r.watcher.add(n.manifest),r.watcher.on(`change`,e=>e===n.manifest&&h(n.root,n.manifest)),await h(n.root,n.manifest),await r.listen(),r.printUrls(),r.bindCLIShortcuts({print:!0})}async function y(e,t){let n=await d(e,!1);await p(n.ekg,t),await h(n.root,n.manifest)}e(t(process.argv)).command(`dev [dir]`,`Runs the EKG devkit, watching for changes`,e=>e.positional(`dir`,{type:`string`,describe:`Folder to watch`,default:`.`}).option(`dev`,{alias:`d`,type:`boolean`,description:`Internal option when developing the EKG CLI`,hidden:!0}),e=>v(e.dir,e.dev??!1)).command(`build [dir]`,`Builds the widget into a directory ready for uploading to EKG`,e=>e.positional(`dir`,{type:`string`,describe:`Folder to watch`,default:`.`}).option(`dev`,{alias:`d`,type:`boolean`,description:`Internal option when developing the EKG CLI`,hidden:!0}),e=>g(e.dir,e.dev??!1)).command(`sync [dir]`,`Regenerates the EKG types`,e=>e.positional(`dir`,{type:`string`,describe:`Folder to watch`,default:`.`}).option(`force`,{alias:`f`,type:`boolean`,description:`Force downloading types and devkit binary from EKG servers`}),({dir:e,force:t})=>y(e,t)).demandCommand().help().parse();export{};
32
+ `}},transform(e,t){if(t.startsWith(n.widget)&&!t.startsWith(n.ekg)&&!e.startsWith(`export default `))return{code:`export default ${JSON.stringify(e)}`}}}]});o.watcher.add(n.manifest),o.watcher.on(`change`,e=>{u(e)===n.manifest&&_(n.root,n.manifest)}),await _(n.root,n.manifest),await o.listen(),o.printUrls(),o.bindCLIShortcuts({print:!0})}async function x(e,t){let n=await p(e,!1);await h(n.ekg,t),await _(n.root,n.manifest)}e(t(process.argv)).command(`dev [dir]`,`Runs the EKG devkit, watching for changes`,e=>e.positional(`dir`,{type:`string`,describe:`Folder to watch`,default:`.`}).option(`dev`,{alias:`d`,type:`boolean`,description:`Internal option when developing the EKG CLI`,hidden:!0}),e=>b(e.dir,e.dev??!1)).command(`build [dir]`,`Builds the widget into a directory ready for uploading to EKG`,e=>e.positional(`dir`,{type:`string`,describe:`Folder to watch`,default:`.`}).option(`dev`,{alias:`d`,type:`boolean`,description:`Internal option when developing the EKG CLI`,hidden:!0}),e=>v(e.dir,e.dev??!1)).command(`sync [dir]`,`Regenerates the EKG types`,e=>e.positional(`dir`,{type:`string`,describe:`Folder to watch`,default:`.`}).option(`force`,{alias:`f`,type:`boolean`,description:`Force downloading types and devkit binary from EKG servers`}),({dir:e,force:t})=>x(e,t)).demandCommand().help().parse();export{};
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@ekg_gg/devkit",
3
- "version": "0.0.27",
3
+ "version": "0.0.28",
4
+ "description": "Development toolkit for building EKG.gg streaming widgets — local dev server, live preview, and build pipeline.",
4
5
  "type": "module",
5
6
  "repository": "github:ekggg/devkit",
6
7
  "bugs": "https://github.com/ekggg/devkit/issues",
@@ -1 +0,0 @@
1
- {}