typebulb 0.1.5
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 +45 -0
- package/dist/index.js +183 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# typebulb
|
|
2
|
+
|
|
3
|
+
Run single-file TypeScript apps. A `.bulb.md` file bundles code, HTML, CSS, and server-side logic in one markdown file — `typebulb` compiles and serves it locally with hot reload.
|
|
4
|
+
|
|
5
|
+
Create bulbs on [typebulb.com](https://typebulb.com) and export, or generate `.bulb.md` files with any AI coding tool.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npx typebulb my-bulb.bulb.md
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install globally:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
npm install -g typebulb
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
typebulb <file.bulb.md> Run a bulb
|
|
23
|
+
typebulb . Find .bulb.md in current directory
|
|
24
|
+
typebulb --watch <file> Watch for changes and reload
|
|
25
|
+
typebulb --port 3333 <file> Custom port
|
|
26
|
+
typebulb --headless <file> Serve without opening browser
|
|
27
|
+
typebulb --server <file> Run server.ts only, no web server
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- **Hot reload** — `--watch` recompiles on save and refreshes the browser
|
|
33
|
+
- **Filesystem access** — `tb.fs.read()` and `tb.fs.write()` for local files
|
|
34
|
+
- **Server-side code** — Add a `**server.ts**` section; exported functions become callable from the browser via `tb.server.fn()`
|
|
35
|
+
- **Console bulbs** — Bulbs with only `**server.ts**` (no `**code.tsx**`) run directly in Node and print to stdout. Use `--server` to force this mode for any bulb.
|
|
36
|
+
- **Auto-installs packages** — npm dependencies in server code are installed automatically
|
|
37
|
+
- **Env files** — `.env` and `.env.local` auto-loaded from cwd
|
|
38
|
+
|
|
39
|
+
## Limitations
|
|
40
|
+
|
|
41
|
+
- **Inference** — `tb.infer()` is not yet supported locally. Bulbs that use inference will render but cannot run LLM calls.
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import*as h from"fs/promises";import{readFileSync as We,existsSync as ce}from"fs";import*as m from"path";import{pathToFileURL as Ue}from"url";import{execFile as Je}from"child_process";import{promisify as Le}from"util";import{EventEmitter as J}from"events";var ve={code:{path:"code.tsx",language:"typescript"},css:{path:"styles.css",language:"css"},html:{path:"index.html",language:"html"},config:{path:"config.json",language:"json"},notes:{path:"notes.md",language:"markdown"},data:{path:"data.txt",language:"text"},infer:{path:"infer.md",language:"markdown"},insight:{path:"insight.json",language:"json"},server:{path:"server.ts",language:"typescript"}};function _(s){try{let e=s.split(`
|
|
3
|
+
`),t=0;if(e[t]?.trim()!=="---")return null;t++;let r=[];for(;t<e.length&&e[t]?.trim()!=="---";)r.push(e[t]),t++;if(e[t]?.trim()!=="---")return null;t++;let n=be(r);if(!n)return null;let o=new Map;for(;t<e.length;){let c=e[t]?.trim()?.match(/^\*\*(.+)\*\*$/);if(c){let l=c[1].trim();for(t++;t<e.length&&e[t]?.trim()==="";)t++;let p=e[t]?.match(/^(`{3,})(\w*)\s*$/);if(!p){t++;continue}let a=p[1];t++;let u=[];for(;t<e.length&&!e[t]?.match(new RegExp(`^${a}\\s*$`));)u.push(e[t]),t++;t++,o.set(l,u.join(`
|
|
4
|
+
`))}else t++}return{frontmatter:n,files:o}}catch{return null}}function be(s){let e={};for(let t of s){let r=t.indexOf(":");if(r===-1)continue;let n=t.slice(0,r).trim(),o=t.slice(r+1).trim();switch(n){case"format":e.format=o;break;case"name":e.name=ye(o);break}}return!e.format?.startsWith("typebulb")||!e.name?null:e}function ye(s){return s.startsWith('"')&&s.endsWith('"')?s.slice(1,-1).replace(/\\"/g,'"'):s.startsWith("'")&&s.endsWith("'")?s.slice(1,-1):s}function C(s){let e=t=>s.files.get(ve[t].path)||"";return{name:s.frontmatter.name,code:e("code"),css:e("css"),html:e("html"),config:e("config"),notes:e("notes"),data:e("data"),infer:e("infer"),insight:e("insight"),server:e("server")}}function xe(s){try{return JSON.parse(s),!0}catch{}return!!(/^\s*<[\s\S]*>/.test(s)||/^---\s*$/m.test(s)||/^\w[\w\s]*:[ \t]/m.test(s))}function H(s){let e=s.trim();return e?xe(e)?[e]:s.split(/\n\n\n+/).map(t=>t.trim()).filter(Boolean):[]}function F(s){if(!s.trim())return{};try{return JSON.parse(s)}catch{return{}}}import{transform as q}from"sucrase";import*as g from"typescript";var Se=[/\bconst\s+enum\b/,/\bexport\s*=/,/\bimport\s+\w+\s*=/,/\bnamespace\s+\w+/,/\bmodule\s+\w+\s*\{/],Ee={target:g.ScriptTarget.ES2022,module:g.ModuleKind.ESNext,moduleResolution:g.ModuleResolutionKind.NodeNext,jsx:g.JsxEmit.ReactJSX,isolatedModules:!0,verbatimModuleSyntax:!0,inlineSourceMap:!0,inlineSources:!0,importHelpers:!1,skipLibCheck:!0};function z(s){return Se.some(e=>e.test(s))}function Pe(s,e){try{let{code:t}=q(s,{transforms:["typescript","jsx"],jsxRuntime:"automatic",jsxImportSource:e.jsxImportSource||"react",production:!0});return{code:t}}catch(t){return{code:"",error:String(t)}}}function K(s,e){try{let t=g.transpileModule(s,{compilerOptions:{...Ee,jsxImportSource:e.jsxImportSource||void 0},fileName:"code.tsx",reportDiagnostics:!0});if(t.diagnostics&&t.diagnostics.length>0){let r=t.diagnostics.filter(n=>n.category===g.DiagnosticCategory.Error).map(n=>g.flattenDiagnosticMessageText(n.messageText,`
|
|
5
|
+
`)).join(`
|
|
6
|
+
`);if(r)return{code:t.outputText||"",error:r}}return{code:t.outputText||""}}catch(t){return{code:"",error:String(t)}}}function G(s,e={}){return z(s)?K(s,e):Pe(s,e)}function Y(s){if(z(s))return K(s,{});try{let{code:e}=q(s,{transforms:["typescript"]});return{code:e}}catch(e){return{code:"",error:String(e)}}}var R="https://esm.sh",I="https://cdn.jsdelivr.net/npm/",O="https://data.jsdelivr.com/v1/package/npm/";function Z(s){let e=(s||"").replace(/^\/+/,"").replace(/\/+$/,"");return e?e.split("/"):[]}var f=class s{constructor(e,t,r){let n=typeof e=="string"?s.parse(e):e;this.name=n.name,this.version=S(t??n.version),this.subpath=S(r??n.subpath)}static parse(e){let t=Z(e||"");if(!t.length)return new s({name:""});if(t[0].startsWith("@")){let n=t[0],[o,i]=X(t[1]??""),c=S(t.slice(2).join("/"));return new s({name:`${n}/${o}`,version:i,subpath:c})}else{let[n,o]=X(t[0]),i=S(t.slice(1).join("/"));return new s({name:n,version:o,subpath:i})}}static fromUrl(e){try{let t=new URL(e),r=new URL(R).host,n=new URL(I).host;if(t.host===r){let o=Z(t.pathname.replace(/^\/v\d+\//,"/"));if(!o.length)return;let i=o[0].startsWith("@")?`${o[0]}/${o[1]??""}`:o[0];return s.parse(i)}if(t.host===n){let o=t.pathname.split("/npm/")[1];if(!o)return;let i=o.split("/")[0]||"";return s.parse(i)}return}catch{return}}static versionFromUrl(e){return s.fromUrl(e)?.version}format(){let e=this.version?`${this.name}@${this.version}`:this.name;return this.subpath?`${e}/${this.subpath}`:e}root(){return this.name}static rootOf(e){return s.parse(e).name}withVersion(e){return new s({name:this.name,version:S(e),subpath:this.subpath})}withPreferredVersion(e,t){let r=e||t;return r?this.withVersion(r):this}static isBare(e){if(!e||e.startsWith(".")||e.startsWith("/"))return!1;let t=e.toLowerCase();return!t.startsWith("http://")&&!t.startsWith("https://")}},S=s=>s&&s.length?s:void 0,X=s=>{let e=s.indexOf("@");return e<0?[s,void 0]:[s.slice(0,e),S(s.slice(e+1))]};async function w(s){try{return await s()}catch{return}}var j=class{constructor(e,t){this.cache=e,this.http=t,this.esmHost=R,this.jsDelivrBase=I,this.jsDelivrMeta=O,this.pinMs=1e4,this.versionsIndexMs=1440*60*1e3,this.metaTtlMs=10080*60*1e3,this.pinCache=new Map}normalizeRelative(e){let t=e||"";return t.startsWith("./")?t.slice(2):t.replace(/^\/+/,"")}ensureLeadingDotSlash(e){return e.startsWith("./")?e:`./${e}`}baseDir(e){let t=typeof e=="string"?new f(e):e;return`${this.jsDelivrBase}${t.name}${t.version?`@${t.version}`:""}/`}file(e,t){return new URL(this.normalizeRelative(t),this.baseDir(e)).toString()}packageJson(e){return this.file(e,"package.json")}buildEsmUrl(e,t={}){let{target:r="es2022",bundle:n=!1,external:o}=t,i=new URLSearchParams({target:r});return n&&i.append("bundle",""),o?.length&&i.append("external",o.join(",")),`${this.esmHost}/${e}?${i.toString()}`}async pinEsmUrl(e,t="es2022"){let r=this.buildEsmUrl(e,{target:t}),n=await w(()=>this.http.head(r));return n?.ok?n.url||r:void 0}async resolveExactVersion(e){let t=Date.now(),r=this.pinCache.get(e);if(r&&t-r.ts<this.pinMs)return r.value;let n=await this.tryResolveFromUrls([this.buildEsmUrl(e),`${this.esmHost}/${e}`]);return this.pinCache.set(e,{value:n,ts:t}),n}async tryResolveFromUrls(e){for(let t of e){let r=await w(()=>this.http.head(t)),n=this.parseVersionFromUrl(r?.url||t);if(n)return n}}async fetchVersionsIndex(e){if(await this.cache.isNegative(e))return;let t=await this.cache.getIndex(e);if(t&&Date.now()-t.updatedAt<this.versionsIndexMs)return{versions:t.versions,distTags:t.distTags};let r=await w(()=>this.http.getJson(`${this.jsDelivrMeta}${encodeURIComponent(e)}`));if(!r?.versions?.length){await this.cache.recordNegative(e);return}await this.cache.clearNegative(e);let n=r.distTags&&Object.keys(r.distTags).length?r.distTags:void 0;return await this.cache.setIndex(e,r.versions,n),r}parseVersionFromUrl(e){let t=f.fromUrl(e)?.version;return t&&/\d+\.\d+\.\d+/.test(t)?t:void 0}async fetchPackageMeta(e,t){let r=await this.cache.getMeta(e,t);if(r&&Date.now()-r.updatedAt<this.metaTtlMs){let{dependencies:a,peerDependencies:u,peerDependenciesMeta:d}=r;return{name:e,version:t,dependencies:a,peerDependencies:u,peerDependenciesMeta:d}}let n=this.packageJson(new f(`${e}@${t}`)),o=await w(()=>this.http.getJson(n));if(!o)return;let i=a=>a&&Object.keys(a).length?a:void 0,c=i(o.dependencies),l=i(o.peerDependencies),p=i(o.peerDependenciesMeta);return await this.cache.setMeta(e,t,c,l,p),{name:e,version:t,dependencies:c,peerDependencies:l,peerDependenciesMeta:p}}};var Q=s=>s.startsWith("@types/"),$=s=>Object.keys(s?.peerDependencies||{}).filter(e=>!Q(e)),B=s=>Object.keys(s?.dependencies||{}).filter(e=>!Q(e)),Re=s=>$(s).filter(e=>!s?.peerDependenciesMeta?.[e]?.optional),M=class{constructor(e){this.cdn=e}async resolve(e,t){let r=await this.fetchMeta(e),{allRoots:n,autoAddedPeers:o}=await this.expandWithPeers(r,t),i=this.computeFlags(n);return{allRoots:n,flags:i,autoAddedPeers:o}}async fetchMeta(e){return Promise.all(e.map(async({name:t,version:r})=>({name:t,version:r,meta:await this.cdn.fetchPackageMeta(t,r)})))}async expandWithPeers(e,t){let r=new Map(e.map(o=>[o.name,o])),n=[];for(let o of e)for(let i of Re(o.meta))!r.has(i)&&!n.some(c=>c.name===i)&&n.push({name:i,requiredBy:o.name});for(let{name:o,requiredBy:i}of n)try{let c=await t(o),l=await this.cdn.fetchPackageMeta(o,c);r.set(o,{name:o,version:c,meta:l})}catch(c){console.warn(`[typebulb] Failed to resolve peer "${o}" for "${i}":`,c)}return{allRoots:[...r.values()],autoAddedPeers:n.filter(o=>r.has(o.name))}}computeFlags(e){let t=new Set(e.flatMap(o=>$(o.meta))),r=new Map;for(let o of e)for(let i of B(o.meta))r.set(i,(r.get(i)||0)+1);let n=new Set([...r.entries()].filter(([,o])=>o>=2).map(([o])=>o));return new Map(e.map(o=>[o.name,{isPeerRoot:t.has(o.name),hasPeers:$(o.meta).length>0,isSharedDep:n.has(o.name)}]))}};var T=class{constructor(e,t,r){this.cache=e,this.cdn=t,this.semver=r}selectVersionFromIndex(e,t,r){return this.semver.selectBestVersion(e,{range:t,distTags:r})}async learnExactVersion(e){let t=await w(()=>this.cdn.fetchVersionsIndex(e));if(t?.versions?.length){let r=this.semver.selectBestVersion(t.versions,{distTags:t.distTags});if(r)return r}return this.cdn.resolveExactVersion(e)}async resolveExactForRoot(e,t){if(!t)return this.learnExactVersion(e);let r=await this.cache.getPinnedExact(e,t);if(r){if(this.semver.isExactVersion(r))return r;console.warn("[versionResolver] Rejecting invalid cached version for",e,":",r)}let n=await w(()=>this.cdn.fetchVersionsIndex(e));if(n?.versions?.length){let i=this.selectVersionFromIndex(n.versions,t,n.distTags);if(i){if(this.semver.isExactVersion(i))return await this.cache.setPinnedExact(e,t,i),i}else{console.warn("[versionResolver] Invalidating stale versions cache for",e,"- range",t,"not satisfied"),await this.cache.invalidateVersionsCache(e);let c=await w(()=>this.cdn.fetchVersionsIndex(e));if(c?.versions?.length){let l=this.selectVersionFromIndex(c.versions,t,c.distTags);if(l&&this.semver.isExactVersion(l))return await this.cache.setPinnedExact(e,t,l),l}}}let o=await this.cdn.resolveExactVersion(`${e}@${t}`);if(o&&this.semver.isExactVersion(o))return await this.cache.setPinnedExact(e,t,o),o}async effectivePackage(e,t){let r=new f(e),n=r.root(),o=t[n],i=o?await w(()=>this.cache.getPinnedExact(n,o))??await w(()=>this.resolveExactForRoot(n,o)):void 0;return{effectivePackage:i?r.withVersion(i).format():e,root:n,range:o,pinned:i}}};import{init as Ie,parse as je}from"es-module-lexer";var E=class s{constructor(e,t,r,n){this.version=e,this.cdn=t,this.peer=r,this.cache=n}extractImportsSync(e){let t=new Set;for(let r of s.importPatterns){r.lastIndex=0;for(let n of e.matchAll(r))f.isBare(n[1])&&t.add(n[1])}return Array.from(t)}async extractImports(e){let t=new Set,r=n=>{f.isBare(n)&&t.add(n)};try{await Ie;let[n]=je(e);n.forEach(o=>r(e.slice(o.s,o.e).trim()))}catch{return this.extractImportsSync(e)}return Array.from(t)}async buildImportMap(e,t){let r=await this.extractImports(e),n=[...new Set(r.map(f.rootOf))],o=await Promise.all(n.map(async a=>({name:a,version:await this.resolveVersion(a,t)}))),{allRoots:i,flags:c,autoAddedPeers:l}=await this.peer.resolve(o,a=>this.resolveVersion(a,t)),p=this.buildEntries([...r,...l.map(a=>a.name)],i,c,t);return{importMap:{imports:Object.fromEntries(p)},prefetchUrls:p.map(([,a])=>a)}}async resolveVersion(e,t){let r=t[e],n=await this.version.resolveExactForRoot(e,r);if(!n){let o=r?`${e}@${r}`:e,i=await w(()=>this.cdn.pinEsmUrl(o));if(!i)throw new Error(`Cannot resolve version for ${e} - network may be unavailable.`);n=f.versionFromUrl(i),n&&r&&await w(()=>this.cache.setPinnedExact(e,r,n))}if(!n)throw new Error(`Cannot resolve version for ${e}`);return n}buildEntries(e,t,r,n){let o=new Map(t.map(d=>[d.name,d])),i=d=>{let v=o.get(f.rootOf(d));return new f(d).withPreferredVersion(v.version,n[v.name]).format()},c=new Set([...r.entries()].filter(([,d])=>d.isPeerRoot||d.isSharedDep).map(([d])=>d)),l=new Set(e.filter(d=>d!==f.rootOf(d)).map(f.rootOf)),p=[],a=new Set,u=new Set(e.filter(d=>d===f.rootOf(d)));for(let d of e){let v=f.rootOf(d),b=o.get(v),{isPeerRoot:x,hasPeers:ue,isSharedDep:fe}=r.get(v),me=l.has(v),he=d!==v,ge=!(x||fe)&&(me||!ue),k=this.singletonDepsOf(b,c),we=he&&u.has(v)?[...k,v]:k.length?k:void 0;p.push([d,this.cdn.buildEsmUrl(i(d),{bundle:ge,external:we})]),a.add(d)}for(let d of c)o.has(d)&&(a.has(d)||p.push([d,this.cdn.buildEsmUrl(i(d),{})]),p.push([`${d}/`,`${this.cdn.esmHost}/${i(d)}/`]));return p}singletonDepsOf(e,t){return[...new Set([...$(e.meta),...B(e.meta)])].filter(r=>t.has(r))}};E.importPatterns=[/\bimport\s+(?:[^'";]*?from\s*)?['"]([^'"]+)['"]/g,/\bexport\s+[^'";]*?from\s*['"]([^'"]+)['"]/g];import{gt as ee,satisfies as $e,maxSatisfying as A,major as Me,prerelease as Te,rsort as _e,valid as Ce}from"semver";var D=class{cmp(e,t){return e===t?0:ee(e,t)?1:ee(t,e)?-1:0}satisfies(e,t){return!e||!e.trim()?!0:!!$e(t,e,{includePrerelease:!0})}pickMaxSatisfying(e,t){if(!e?.length)return;let r=A(e,t,{includePrerelease:!0});return r===null?void 0:r}pickLatest(e){return e?.length?_e(e)[0]:void 0}selectBestVersion(e,t){if(!e?.length)return;let r=t?.range?.trim()||"*",n=t?.preferStable??!0,o=t?.distTags?.latest;if(o&&e.includes(o)&&this.satisfies(r,o))return o;if(n){let c=A(e,r,{includePrerelease:!1});if(c)return c}return A(e,r,{includePrerelease:!0})??void 0}majorOf(e){return Me(e)}isPrerelease(e){return Te(e)!==null}isExactVersion(e){return Ce(e)!==null}},V=new D;function te(s,e){let t=new j(s,e),r=new M(t),n=new T(s,t,V);return{packageService:new E(n,t,r,s),versionResolver:n,cdnClient:t,peerResolver:r}}var N=class{pins=new Map;indexes=new Map;negatives=new Set;meta=new Map;async getPinnedExact(e,t){return this.pins.get(`${e}@${t}`)}async setPinnedExact(e,t,r){this.pins.set(`${e}@${t}`,r)}async getIndex(e){return this.indexes.get(e)}async setIndex(e,t,r){this.indexes.set(e,{versions:t,distTags:r,updatedAt:Date.now()})}async invalidateVersionsCache(e){this.indexes.delete(e)}async isNegative(e){return this.negatives.has(e)}async recordNegative(e){this.negatives.add(e)}async clearNegative(e){this.negatives.delete(e)}async getMeta(e,t){return this.meta.get(`${e}@${t}`)}async setMeta(e,t,r,n,o){this.meta.set(`${e}@${t}`,{dependencies:r,peerDependencies:n,peerDependenciesMeta:o,updatedAt:Date.now()})}},De={async getJson(s){try{let e=await fetch(s,{redirect:"follow"});return e.ok?await e.json():void 0}catch{return}},async head(s){try{let e=await fetch(s,{method:"HEAD",redirect:"follow"});return{ok:e.ok,url:e.url}}catch{return}}},ke=new N,{packageService:re,versionResolver:At,cdnClient:Vt,peerResolver:Nt}=te(ke,De);var se=`
|
|
7
|
+
(() => {
|
|
8
|
+
// JSON parser (handles jsonish - unquoted keys)
|
|
9
|
+
const parseJson = (str) => {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(str);
|
|
12
|
+
} catch {
|
|
13
|
+
const fixed = str.replace(/(?<!")(\\b[a-zA-Z_][a-zA-Z0-9_]*\\b)\\s*:/g, '"$1":');
|
|
14
|
+
return JSON.parse(fixed);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Read from window each time so updates are visible
|
|
19
|
+
const getData = () => window.__TB_DATA__ || [];
|
|
20
|
+
|
|
21
|
+
// Filesystem API - calls back to the local server
|
|
22
|
+
const fs = {
|
|
23
|
+
read: async (path) => {
|
|
24
|
+
const resp = await fetch('/__fs/read', {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27
|
+
body: JSON.stringify({ path })
|
|
28
|
+
});
|
|
29
|
+
if (!resp.ok) {
|
|
30
|
+
const err = await resp.json().catch(() => ({ error: 'Failed to read file' }));
|
|
31
|
+
throw new Error(err.error || 'Failed to read file: ' + path);
|
|
32
|
+
}
|
|
33
|
+
const { content } = await resp.json();
|
|
34
|
+
return content;
|
|
35
|
+
},
|
|
36
|
+
write: async (path, content) => {
|
|
37
|
+
const resp = await fetch('/__fs/write', {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify({ path, content })
|
|
41
|
+
});
|
|
42
|
+
if (!resp.ok) {
|
|
43
|
+
const err = await resp.json().catch(() => ({ error: 'Failed to write file' }));
|
|
44
|
+
throw new Error(err.error || 'Failed to write file: ' + path);
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Clipboard helper
|
|
51
|
+
const copy = async (text) => {
|
|
52
|
+
try {
|
|
53
|
+
await navigator.clipboard.writeText(String(text));
|
|
54
|
+
return true;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// tb namespace
|
|
61
|
+
globalThis.tb = Object.freeze({
|
|
62
|
+
data: (index) => getData()[index],
|
|
63
|
+
json: (index) => parseJson(getData()[index]),
|
|
64
|
+
insight: () => window.__TB_INSIGHT__ ? parseJson(window.__TB_INSIGHT__) : undefined,
|
|
65
|
+
|
|
66
|
+
// Inference not available locally (yet)
|
|
67
|
+
infer: () => Promise.reject(new Error('tb.infer() is not available in the local CLI. Coming in a future version.')),
|
|
68
|
+
inferenceState: () => 'idle',
|
|
69
|
+
setData: () => {},
|
|
70
|
+
resetInferenceState: () => {},
|
|
71
|
+
|
|
72
|
+
// Dump just logs to console in local mode
|
|
73
|
+
dump: async (...args) => console.log('[tb.dump]', ...args),
|
|
74
|
+
|
|
75
|
+
// Clipboard
|
|
76
|
+
copy,
|
|
77
|
+
|
|
78
|
+
// Server API - call functions from **server.ts**
|
|
79
|
+
api: async (name, ...args) => {
|
|
80
|
+
const resp = await fetch('/__api/' + name, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { 'Content-Type': 'application/json' },
|
|
83
|
+
body: JSON.stringify({ args })
|
|
84
|
+
});
|
|
85
|
+
const data = await resp.json();
|
|
86
|
+
if (!resp.ok) throw new Error(data.error || 'API call failed');
|
|
87
|
+
return data.result;
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// Server proxy - tb.server.fn(...) delegates to tb.api('fn', ...)
|
|
91
|
+
server: new Proxy({}, {
|
|
92
|
+
get: (_, name) => (...args) => globalThis.tb.api(name, ...args)
|
|
93
|
+
}),
|
|
94
|
+
|
|
95
|
+
// Filesystem - local CLI extension
|
|
96
|
+
fs
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Hot reload listener
|
|
100
|
+
if (window.__TYPEBULB_WATCH__) {
|
|
101
|
+
const es = new EventSource('/__reload');
|
|
102
|
+
es.addEventListener('reload', () => {
|
|
103
|
+
console.log('[typebulb] Reloading...');
|
|
104
|
+
window.location.reload();
|
|
105
|
+
});
|
|
106
|
+
es.onerror = () => {
|
|
107
|
+
// Server closed, stop trying
|
|
108
|
+
es.close();
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
})();
|
|
112
|
+
`;function ne(s){let{name:e,code:t,css:r,html:n,data:o,insight:i,importMap:c,watch:l}=s,p=n.trim()||'<div id="app"></div>',a=u=>u.replace(/<\/script/gi,"<\\/script");return`<!DOCTYPE html>
|
|
113
|
+
<html>
|
|
114
|
+
<head>
|
|
115
|
+
<meta charset="utf-8">
|
|
116
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
117
|
+
<title>${Fe(e)} - typebulb</title>
|
|
118
|
+
<script type="importmap">
|
|
119
|
+
${JSON.stringify(c,null,2)}
|
|
120
|
+
</script>
|
|
121
|
+
<style>
|
|
122
|
+
/* Reset and base styles */
|
|
123
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
124
|
+
body { margin: 0; font-family: system-ui, -apple-system, sans-serif; }
|
|
125
|
+
</style>
|
|
126
|
+
<style>
|
|
127
|
+
${r}
|
|
128
|
+
</style>
|
|
129
|
+
</head>
|
|
130
|
+
<body>
|
|
131
|
+
${p}
|
|
132
|
+
|
|
133
|
+
${o.length>0?`<script>window.__TB_DATA__ = ${a(JSON.stringify(o))};</script>`:""}
|
|
134
|
+
${i?`<script>window.__TB_INSIGHT__ = ${a(JSON.stringify(i))};</script>`:""}
|
|
135
|
+
${l?"<script>window.__TYPEBULB_WATCH__ = true;</script>":""}
|
|
136
|
+
|
|
137
|
+
<script>
|
|
138
|
+
${se}
|
|
139
|
+
</script>
|
|
140
|
+
|
|
141
|
+
<script type="module">
|
|
142
|
+
${a(t)}
|
|
143
|
+
</script>
|
|
144
|
+
</body>
|
|
145
|
+
</html>`}function Fe(s){return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}import{Hono as Oe}from"hono";import{serve as Be}from"@hono/node-server";import{streamSSE as Ae}from"hono/streaming";import*as P from"fs/promises";import*as y from"path";async function ie(s){let{getHtml:e,basePath:t,port:r,reloadEmitter:n,getServerExports:o}=s,i=new Oe;i.get("/",l=>l.html(e())),i.post("/__fs/read",async l=>{try{let{path:p}=await l.req.json(),a=oe(p,t),u=await P.readFile(a,"utf-8");return l.json({content:u})}catch(p){let a=p instanceof Error?p.message:"Unknown error";return l.json({error:a},400)}}),i.post("/__fs/write",async l=>{try{let{path:p,content:a}=await l.req.json(),u=oe(p,t);return await P.mkdir(y.dirname(u),{recursive:!0}),await P.writeFile(u,a,"utf-8"),l.json({success:!0})}catch(p){let a=p instanceof Error?p.message:"Unknown error";return l.json({error:a},400)}}),i.post("/__api/:name",async l=>{try{let p=o?.(),a=l.req.param("name");if(!p||typeof p[a]!="function")return l.json({error:`API function '${a}' not found`},404);let{args:u}=await l.req.json(),d=await p[a](...u||[]);return l.json({result:d})}catch(p){let a=p instanceof Error?p.message:"Unknown error";return l.json({error:a},500)}}),n&&i.get("/__reload",l=>Ae(l,async p=>{let a=()=>{p.writeSSE({event:"reload",data:""})};for(n.on("reload",a),p.onAbort(()=>{n.removeListener("reload",a)});;)await p.sleep(3e4)}));let c=Be({fetch:i.fetch,port:r});return{port:r,close:()=>c.close()}}function oe(s,e){let t=y.resolve(e,s),r=y.normalize(e);if(!y.normalize(t).startsWith(r))throw new Error("Path traversal detected - access denied");return t}async function W(s){let e=await import("net");return new Promise(t=>{let r=e.createServer();r.listen(s,()=>{let n=r.address(),o=typeof n=="object"&&n?n.port:s;r.close(()=>t(o))}),r.on("error",()=>{t(W(s+1))})})}import Ve from"open";async function ae(s){await Ve(s)}import Ne from"chokidar";function U(s){let{bulbPath:e,emitter:t}=s,r=Ne.watch(e,{persistent:!0,ignoreInitial:!0,awaitWriteFinish:{stabilityThreshold:100,pollInterval:50}});return r.on("change",()=>{t.emit("reload")}),()=>r.close()}var He=Le(Je),qe="0.1.3";function ze(s){let e={file:"",port:3e3,watch:!1,open:!0,server:!1,help:!1,version:!1};for(let t=0;t<s.length;t++){let r=s[t];if(r==="--help"||r==="-h")e.help=!0;else if(r==="--version"||r==="-V")e.version=!0;else if(r==="--watch"||r==="-w")e.watch=!0;else if(r==="--headless")e.open=!1;else if(r==="--server")e.server=!0;else if(r==="--port"||r==="-p"){let n=s[++t],o=parseInt(n,10);isNaN(o)&&(console.error(`Invalid port: ${n}`),process.exit(1)),e.port=o}else r.startsWith("-")||(e.file=r)}return e}function Ke(){console.log(`
|
|
146
|
+
typebulb - Local bulb runner for Typebulb
|
|
147
|
+
|
|
148
|
+
Usage:
|
|
149
|
+
typebulb <file.bulb.md> Run a specific bulb file
|
|
150
|
+
typebulb . Find and run .bulb.md in current directory
|
|
151
|
+
|
|
152
|
+
Options:
|
|
153
|
+
-w, --watch Watch for changes and reload
|
|
154
|
+
-p, --port <port> Use a specific port (default: 3000)
|
|
155
|
+
--headless Serve without opening browser
|
|
156
|
+
--server Run server.ts only, no web server
|
|
157
|
+
-V, --version Show version number
|
|
158
|
+
-h, --help Show this help message
|
|
159
|
+
|
|
160
|
+
Filesystem API:
|
|
161
|
+
Bulbs can read and write local files via tb.fs:
|
|
162
|
+
await tb.fs.read('file.txt')
|
|
163
|
+
await tb.fs.write('output.html', content)
|
|
164
|
+
|
|
165
|
+
Server API:
|
|
166
|
+
Add a **server.ts** section to run Node.js code server-side.
|
|
167
|
+
Exported functions become callable from the browser:
|
|
168
|
+
// in **server.ts**: export async function query(sql) { ... }
|
|
169
|
+
// in **code.tsx**: const rows = await tb.server.query(sql)
|
|
170
|
+
.env and .env.local are auto-loaded from the working directory.
|
|
171
|
+
|
|
172
|
+
Examples:
|
|
173
|
+
typebulb my-editor.bulb.md
|
|
174
|
+
typebulb --watch --port 8080 my-editor.bulb.md
|
|
175
|
+
typebulb .
|
|
176
|
+
`)}async function Ge(s){let t=(await h.readdir(s)).find(r=>r.endsWith(".bulb.md"));return t?m.join(s,t):null}function pe(){L([".env",".env.local"],process.cwd(),!0)}function L(s,e,t=!1){for(let r of s){let n=m.resolve(e,r);try{let o=We(n,"utf-8");for(let i of o.split(`
|
|
177
|
+
`)){let c=i.trim();if(!c||c.startsWith("#"))continue;let l=c.indexOf("=");if(l===-1)continue;let p=c.slice(0,l).trim(),a=c.slice(l+1).trim();(a.startsWith('"')&&a.endsWith('"')||a.startsWith("'")&&a.endsWith("'"))&&(a=a.slice(1,-1)),process.env[p]??=a}}catch{t||console.warn(` Warning: env file not found: ${r}`)}}}function Ye(s){let e=new Set,t=/\bimport\s+(?:[\s\S]*?\s+from\s+)?['"]([^./][^'"]*)['"]/g,r;for(;r=t.exec(s);){let n=r[1];if(n.startsWith("node:"))continue;let o=n.startsWith("@")?n.split("/").slice(0,2).join("/"):n.split("/")[0];e.add(o)}return[...e]}async function Ze(s,e){let t=s.filter(n=>!ce(m.join(e,"node_modules",n)));if(t.length===0)return;let r=m.join(e,"package.json");ce(r)||await h.writeFile(r,JSON.stringify({name:"typebulb-server",private:!0})),console.log(` Installing: ${t.join(", ")}`),await He("npm",["install","--no-audit","--no-fund",...t],{cwd:e,shell:!0})}async function de(s,e){let t=Y(s);if(t.error)throw new Error(`Server compilation error: ${t.error}`);let r=m.join(e,".typebulb");await h.mkdir(r,{recursive:!0});let n=Ye(t.code);n.length>0&&await Ze(n,r);let o=m.join(r,"server.mjs");return await h.writeFile(o,t.code,"utf-8"),await import(`${Ue(o).href}?t=${Date.now()}`)}async function le(s,e){let t=await h.readFile(s,"utf-8"),r=_(t);if(!r)throw new Error("Invalid .bulb.md file format");let n=C(r),o=F(n.config),i=H(n.data),c=G(n.code,{jsxImportSource:o.jsxImportSource});c.error&&console.error("Compilation error:",c.error);let{importMap:l}=await re.buildImportMap(c.code,o.dependencies??{}),p=ne({name:n.name,code:c.code,css:n.css,html:n.html,data:i,insight:n.insight,importMap:l,watch:e}),a=m.dirname(s);o.env?.length&&L(o.env,a);let u=null;return n.server&&(u=await de(n.server,a)),{html:p,bulb:n,serverExports:u}}async function Xe(s,e){pe();let t=async()=>{let r=await h.readFile(s,"utf-8"),n=_(r);if(!n)throw new Error("Invalid .bulb.md file format");let o=C(n),i=F(o.config),c=m.dirname(s);i.env?.length&&L(i.env,c),await de(o.server,c)};if(console.log(`Running ${m.basename(s)}...`),await t(),e){console.log(`Watching for changes...
|
|
178
|
+
`);let r=new J;r.on("reload",async()=>{try{console.log("Re-running..."),await t()}catch(n){console.error("Error:",n)}}),U({bulbPath:s,emitter:r})}}async function Qe(){let s=ze(process.argv.slice(2));s.version&&(console.log(`typebulb ${qe}`),process.exit(0)),s.help&&(Ke(),process.exit(0));let e;if(!s.file||s.file==="."){let b=await Ge(process.cwd());b||(console.error("No .bulb.md file found in current directory"),process.exit(1)),e=b}else e=m.resolve(s.file);try{await h.access(e)}catch{console.error(`File not found: ${e}`),process.exit(1)}e.endsWith(".bulb.md")||(console.error("File must have .bulb.md extension"),process.exit(1));let t=await h.readFile(e,"utf-8"),r=_(t);if(r){let b=C(r);if(b.server&&(!b.code||s.server)){await Xe(e,s.watch);return}}let n=process.cwd(),o=s.watch?new J:void 0;pe(),console.log(`Loading ${m.basename(e)}...`);let{html:i,bulb:c,serverExports:l}=await le(e,s.watch),p=await W(s.port),a=await ie({getHtml:()=>i,basePath:n,port:p,reloadEmitter:o,getServerExports:()=>l}),u=`http://localhost:${p}`;console.log(`
|
|
179
|
+
${c.name}`),console.log(` ${u}
|
|
180
|
+
`),s.watch&&console.log(` Watching for changes...
|
|
181
|
+
`);let d;if(s.watch&&o){let b=new J;b.on("reload",async()=>{try{console.log("Recompiling...");let x=await le(e,!0);i=x.html,l=x.serverExports,o.emit("reload"),console.log(`Done. Browser reloading...
|
|
182
|
+
`)}catch(x){console.error("Compile error:",x)}}),d=U({bulbPath:e,emitter:b})}s.open&&await ae(u);let v=async()=>{console.log(`
|
|
183
|
+
Shutting down...`),a.close(),d?.();let b=m.join(m.dirname(e),".typebulb","server.mjs");await h.rm(b,{force:!0}).catch(()=>{}),process.exit(0)};process.on("SIGINT",v),process.on("SIGTERM",v)}Qe().catch(s=>{console.error("Error:",s.message),process.exit(1)});
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "typebulb",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "Local bulb runner CLI for Typebulb",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"engines": { "node": ">=18" },
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"typebulb": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc --build",
|
|
16
|
+
"bundle": "node esbuild.config.mjs",
|
|
17
|
+
"clean": "rimraf dist",
|
|
18
|
+
"dev": "tsc --build && node dist/index.js",
|
|
19
|
+
"prepublishOnly": "rimraf dist && node esbuild.config.mjs"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@hono/node-server": "^1.14.1",
|
|
23
|
+
"chokidar": "^4.0.3",
|
|
24
|
+
"es-module-lexer": "^1.7.0",
|
|
25
|
+
"hono": "^4.7.5",
|
|
26
|
+
"open": "^10.1.0",
|
|
27
|
+
"semver": "^7.7.3",
|
|
28
|
+
"sucrase": "^3.35.0",
|
|
29
|
+
"typescript": "^5.4.5"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.15.21",
|
|
33
|
+
"esbuild": "^0.27.3",
|
|
34
|
+
"rimraf": "^6.0.1",
|
|
35
|
+
"typebulb-resolver": "workspace:*"
|
|
36
|
+
}
|
|
37
|
+
}
|