streetui 1.0.0

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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -0
  3. package/dist/bin.cjs +894 -0
  4. package/dist/bin.cjs.map +1 -0
  5. package/dist/bin.d.cts +1 -0
  6. package/dist/bin.d.ts +1 -0
  7. package/dist/bin.js +892 -0
  8. package/dist/bin.js.map +1 -0
  9. package/dist/compile-B0q07Hzq.d.cts +656 -0
  10. package/dist/compile-B0q07Hzq.d.ts +656 -0
  11. package/dist/create-bin.cjs +896 -0
  12. package/dist/create-bin.cjs.map +1 -0
  13. package/dist/create-bin.d.cts +1 -0
  14. package/dist/create-bin.d.ts +1 -0
  15. package/dist/create-bin.js +894 -0
  16. package/dist/create-bin.js.map +1 -0
  17. package/dist/hydration-diagnostics-BE6xVWD1.d.cts +89 -0
  18. package/dist/hydration-diagnostics-Bck5dMbz.d.ts +89 -0
  19. package/dist/index.cjs +4284 -0
  20. package/dist/index.cjs.map +1 -0
  21. package/dist/index.d.cts +1759 -0
  22. package/dist/index.d.ts +1759 -0
  23. package/dist/index.js +4114 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/server-84Rz4g8W.d.cts +165 -0
  26. package/dist/server-D9GPmB49.d.ts +165 -0
  27. package/dist/server.cjs +972 -0
  28. package/dist/server.cjs.map +1 -0
  29. package/dist/server.d.cts +2 -0
  30. package/dist/server.d.ts +2 -0
  31. package/dist/server.js +940 -0
  32. package/dist/server.js.map +1 -0
  33. package/dist/testing.cjs +1754 -0
  34. package/dist/testing.cjs.map +1 -0
  35. package/dist/testing.d.cts +113 -0
  36. package/dist/testing.d.ts +113 -0
  37. package/dist/testing.js +1719 -0
  38. package/dist/testing.js.map +1 -0
  39. package/package.json +113 -0
  40. package/templates/basic/README.md +39 -0
  41. package/templates/basic/_gitignore +15 -0
  42. package/templates/basic/_package.json +21 -0
  43. package/templates/basic/public/styles.css +40 -0
  44. package/templates/basic/src/app.ts +62 -0
  45. package/templates/basic/src/main.ts +39 -0
  46. package/templates/basic/src/server.ts +40 -0
  47. package/templates/basic/streetui.config.ts +6 -0
  48. package/templates/basic/tsconfig.json +16 -0
  49. package/templates/ssr/README.md +46 -0
  50. package/templates/ssr/_gitignore +15 -0
  51. package/templates/ssr/_package.json +21 -0
  52. package/templates/ssr/public/favicon.svg +4 -0
  53. package/templates/ssr/public/styles.css +61 -0
  54. package/templates/ssr/src/app.ts +135 -0
  55. package/templates/ssr/src/main.ts +53 -0
  56. package/templates/ssr/src/server.ts +47 -0
  57. package/templates/ssr/streetui.config.ts +14 -0
  58. package/templates/ssr/tsconfig.json +16 -0
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Browser entry. Reads the SSR state snapshot, rebuilds identical state, and
3
+ * hydrates the server-rendered markup in place.
4
+ */
5
+
6
+ import { createRenderer, readState } from 'streetui';
7
+ import { BrowserDOMAdapter } from 'streetui';
8
+ import { createState, compileApp, STATE_KEY, type AppSnapshot, type AppState } from './app.js';
9
+
10
+ export interface HydrateResult {
11
+ readonly state: AppState;
12
+ unmount(): void;
13
+ }
14
+
15
+ export function hydrateApp(appContainer: Element, stateRoot?: Element | Document): HydrateResult {
16
+ const dom = new BrowserDOMAdapter();
17
+ const transferred = readState(dom, stateRoot ?? appContainer);
18
+ const seed = transferred[STATE_KEY] as AppSnapshot | undefined;
19
+
20
+ const state = createState(seed);
21
+ const compiled = compileApp(state);
22
+
23
+ const renderer = createRenderer({ domAdapter: dom });
24
+ const handle = renderer.hydrate(compiled, appContainer);
25
+
26
+ return { state, unmount: () => handle.unmount() };
27
+ }
28
+
29
+ if (typeof document !== 'undefined') {
30
+ const boot = (): void => {
31
+ const app = document.getElementById('app');
32
+ if (app !== null) hydrateApp(app, document);
33
+ };
34
+ if (document.readyState === 'loading') {
35
+ document.addEventListener('DOMContentLoaded', boot, { once: true });
36
+ } else {
37
+ boot();
38
+ }
39
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Server entry. The StreetUI CLI bundles this for Node and calls `render` for
3
+ * every non-asset request, embedding a state snapshot for hydration.
4
+ */
5
+
6
+ import { renderToString, serializeState } from 'streetui';
7
+ import { createState, compileApp, snapshot, STATE_KEY } from './app.js';
8
+
9
+ export interface RenderRequest {
10
+ readonly url: string;
11
+ }
12
+ export interface RenderResult {
13
+ readonly html: string;
14
+ }
15
+
16
+ export function render(_request: RenderRequest): RenderResult {
17
+ const state = createState();
18
+ const compiled = compileApp(state);
19
+ const body = renderToString(compiled);
20
+ const island = serializeState({ [STATE_KEY]: snapshot(state) });
21
+
22
+ const html = [
23
+ '<!doctype html>',
24
+ '<html lang="en">',
25
+ '<head>',
26
+ '<meta charset="utf-8" />',
27
+ '<meta name="viewport" content="width=device-width, initial-scale=1" />',
28
+ '<title>__PROJECT_NAME__</title>',
29
+ '<link rel="stylesheet" href="/styles.css" />',
30
+ '</head>',
31
+ '<body>',
32
+ `<div id="app">${body}</div>`,
33
+ island,
34
+ '<script type="module" src="/main.js"></script>',
35
+ '</body>',
36
+ '</html>',
37
+ ].join('\n');
38
+
39
+ return { html };
40
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from 'streetui';
2
+
3
+ /** StreetUI project configuration. All fields are optional. */
4
+ export default defineConfig({
5
+ port: 3000,
6
+ });
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "types": [],
8
+ "strict": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "exactOptionalPropertyTypes": true,
11
+ "verbatimModuleSyntax": true,
12
+ "skipLibCheck": true,
13
+ "noEmit": true
14
+ },
15
+ "include": ["src", "streetui.config.ts"]
16
+ }
@@ -0,0 +1,46 @@
1
+ # __PROJECT_NAME__
2
+
3
+ A StreetUI application with server-side rendering and client hydration,
4
+ scaffolded with the StreetUI CLI.
5
+
6
+ ## Getting started
7
+
8
+ ```bash
9
+ npm install
10
+ npm run dev
11
+ ```
12
+
13
+ Then open the URL printed in your terminal (default http://localhost:3000).
14
+ Editing files under `src/` rebuilds the app and reloads the browser.
15
+
16
+ ## Scripts
17
+
18
+ | Command | What it does |
19
+ | --------------- | --------------------------------------------------------- |
20
+ | `npm run dev` | Start the dev server with live reload |
21
+ | `npm run build` | Produce a production build in `dist/` |
22
+ | `npm run start` | Serve the production build |
23
+ | `npm run typecheck` | Type-check the project with `tsc` |
24
+
25
+ ## Project structure
26
+
27
+ ```
28
+ __PROJECT_NAME__/
29
+ ├── public/ Static assets copied as-is (styles.css, favicon.svg)
30
+ ├── src/
31
+ │ ├── app.ts The universal app: one definition for server + browser
32
+ │ ├── server.ts Server entry — exports render(request) → HTML
33
+ │ └── main.ts Browser entry — hydrates the server HTML in place
34
+ ├── streetui.config.ts Project configuration (all fields optional)
35
+ └── package.json
36
+ ```
37
+
38
+ ## How it works
39
+
40
+ `src/app.ts` defines the UI once using the StreetUI DSL and reactive signals.
41
+ `src/server.ts` renders it to HTML for each request and embeds a state snapshot;
42
+ `src/main.ts` reads that snapshot in the browser and hydrates the existing DOM so
43
+ the app becomes interactive without re-rendering.
44
+
45
+ This project depends on a single package — `streetui` — and all of its source
46
+ imports from `"streetui"`.
@@ -0,0 +1,15 @@
1
+ # Build output
2
+ dist/
3
+ .streetui/
4
+
5
+ # Dependencies
6
+ node_modules/
7
+
8
+ # Logs
9
+ *.log
10
+ npm-debug.log*
11
+
12
+ # Editor / OS
13
+ .DS_Store
14
+ .idea/
15
+ .vscode/
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "streetui dev",
8
+ "build": "streetui build",
9
+ "start": "streetui start",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "streetui": "__FRAMEWORK_VERSION__"
14
+ },
15
+ "devDependencies": {
16
+ "typescript": "^5.6.3"
17
+ },
18
+ "engines": {
19
+ "node": ">=18.0.0"
20
+ }
21
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <rect width="32" height="32" rx="7" fill="#4f46e5" />
3
+ <path d="M10 21c0 1.7 1.6 3 4 3s6-1.1 6-3.4c0-4.3-9-2.6-9-6.4C11 12 12.8 11 15 11c2.1 0 3.6 1 3.6 2.4" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" />
4
+ </svg>
@@ -0,0 +1,61 @@
1
+ :root {
2
+ color-scheme: light dark;
3
+ --fg: #1a1a2e;
4
+ --bg: #fafafa;
5
+ --accent: #4f46e5;
6
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
7
+ }
8
+
9
+ body {
10
+ margin: 0;
11
+ padding: 2rem;
12
+ color: var(--fg);
13
+ background: var(--bg);
14
+ line-height: 1.5;
15
+ }
16
+
17
+ #app {
18
+ max-width: 40rem;
19
+ margin: 0 auto;
20
+ }
21
+
22
+ h1 {
23
+ color: var(--accent);
24
+ }
25
+
26
+ [role="navigation"] {
27
+ display: flex;
28
+ gap: 0.5rem;
29
+ margin-bottom: 1.5rem;
30
+ }
31
+
32
+ button {
33
+ font: inherit;
34
+ padding: 0.4rem 0.9rem;
35
+ border: 1px solid var(--accent);
36
+ border-radius: 0.4rem;
37
+ background: var(--accent);
38
+ color: #fff;
39
+ cursor: pointer;
40
+ }
41
+
42
+ button:hover {
43
+ opacity: 0.9;
44
+ }
45
+
46
+ input {
47
+ font: inherit;
48
+ padding: 0.4rem 0.6rem;
49
+ border: 1px solid #ccc;
50
+ border-radius: 0.4rem;
51
+ margin-right: 0.5rem;
52
+ }
53
+
54
+ form {
55
+ margin-top: 1rem;
56
+ }
57
+
58
+ [role="status"] {
59
+ margin-top: 0.75rem;
60
+ font-weight: 600;
61
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * __PROJECT_NAME__ — the universal application, shared by the server and the
3
+ * browser. This single definition is rendered to HTML on the server and then
4
+ * hydrated in place on the client, so there is exactly one source of truth for
5
+ * the UI and its reactive state.
6
+ *
7
+ * It demonstrates the StreetUI essentials a new app needs:
8
+ * • signals — `count`, `view`, `name` reactive state
9
+ * • conditional views — `when(...)` swaps Home / About without a framework
10
+ * • navigation — buttons flip the `view` signal (and the URL)
11
+ * • a form — an input bound to a signal, submitted to show a greeting
12
+ *
13
+ * State is JSON-serialisable so the server can embed a snapshot and the browser
14
+ * can rebuild the identical state before hydrating (no flash, no divergence).
15
+ */
16
+
17
+ import { streetui, type PageDSL } from 'streetui';
18
+ import { signal, derived, type Signal } from 'streetui';
19
+ import { compile, type CompiledApplication } from 'streetui';
20
+
21
+ /** The set of top-level views. Kept tiny on purpose. */
22
+ export type View = 'home' | 'about';
23
+
24
+ export interface AppState {
25
+ readonly count: Signal<number>;
26
+ readonly view: Signal<View>;
27
+ readonly name: Signal<string>;
28
+ readonly greeting: Signal<string>;
29
+ }
30
+
31
+ /** Plain snapshot embedded in the SSR HTML and used to reseed on the client. */
32
+ export interface AppSnapshot {
33
+ readonly count: number;
34
+ readonly view: View;
35
+ readonly name: string;
36
+ readonly greeting: string;
37
+ }
38
+
39
+ /** The key under which this app's state is stored in the SSR island. */
40
+ export const STATE_KEY = '__PROJECT_NAME__';
41
+
42
+ /** Build reactive state, optionally seeded from a server snapshot. */
43
+ export function createState(seed?: Partial<AppSnapshot>): AppState {
44
+ return {
45
+ count: signal(seed?.count ?? 0),
46
+ view: signal<View>(seed?.view ?? 'home'),
47
+ name: signal(seed?.name ?? ''),
48
+ greeting: signal(seed?.greeting ?? ''),
49
+ };
50
+ }
51
+
52
+ /** Read a serialisable snapshot out of the live state. */
53
+ export function snapshot(state: AppState): AppSnapshot {
54
+ return {
55
+ count: state.count.peek(),
56
+ view: state.view.peek(),
57
+ name: state.name.peek(),
58
+ greeting: state.greeting.peek(),
59
+ };
60
+ }
61
+
62
+ /** Map a request path to the initial view (so SSR renders the right page). */
63
+ export function viewForPath(path: string): View {
64
+ return path.replace(/[?#].*$/, '').replace(/\/+$/, '') === '/about' ? 'about' : 'home';
65
+ }
66
+
67
+ /** The one UI definition. Identical on the server and in the browser. */
68
+ export function buildApp(page: PageDSL, state: AppState): void {
69
+ const isHome = derived(() => state.view.get() === 'home');
70
+ const isAbout = derived(() => state.view.get() === 'about');
71
+ const hasGreeting = derived(() => state.greeting.get().length > 0);
72
+ const countLabel = derived(() => `You clicked ${state.count.get()} times`);
73
+
74
+ page.heading('__PROJECT_NAME__', { id: 'brand', level: 1 });
75
+
76
+ page.section('nav', (n) => {
77
+ n.button('Home', { id: 'nav-home', onClick: () => navigate(state, 'home') });
78
+ n.button('About', { id: 'nav-about', onClick: () => navigate(state, 'about') });
79
+ }, { id: 'nav', role: 'navigation' });
80
+
81
+ page.when(isHome, (home) => {
82
+ home.section('home', (s) => {
83
+ s.text('Welcome to your new StreetUI app.', { id: 'home-tagline' });
84
+ s.text(countLabel, { id: 'count' });
85
+ s.button('Click me', {
86
+ id: 'increment',
87
+ onClick: () => state.count.set(state.count.peek() + 1),
88
+ });
89
+
90
+ s.form('greet-form', (f) => {
91
+ f.input({ id: 'name-input', bind: state.name, placeholder: 'Your name' });
92
+ f.button('Say hello', {
93
+ id: 'greet-submit',
94
+ onClick: () => submitGreeting(state),
95
+ });
96
+ f.when(hasGreeting, (g) =>
97
+ g.text(derived(() => state.greeting.get()), { id: 'greeting', role: 'status' }),
98
+ );
99
+ }, { id: 'greet-form', onSubmit: () => submitGreeting(state) });
100
+ }, { id: 'home' });
101
+ });
102
+
103
+ page.when(isAbout, (about) => {
104
+ about.section('about', (s) => {
105
+ s.heading('About', { id: 'about-title', level: 2 });
106
+ s.text(
107
+ 'This project was scaffolded with the StreetUI CLI. Edit src/app.ts to ' +
108
+ 'change what renders on both the server and the client.',
109
+ { id: 'about-body' },
110
+ );
111
+ }, { id: 'about' });
112
+ });
113
+ }
114
+
115
+ /** Flip the active view and keep the URL in sync when a browser is present. */
116
+ function navigate(state: AppState, view: View): void {
117
+ state.view.set(view);
118
+ if (typeof history !== 'undefined' && typeof location !== 'undefined') {
119
+ const path = view === 'about' ? '/about' : '/';
120
+ if (location.pathname !== path) history.pushState({ view }, '', path);
121
+ }
122
+ }
123
+
124
+ /** Turn the name field into a greeting. */
125
+ function submitGreeting(state: AppState): void {
126
+ const name = state.name.peek().trim();
127
+ state.greeting.set(name.length > 0 ? `Hello, ${name}!` : 'Hello there!');
128
+ }
129
+
130
+ /** Compile the app for a given state into a renderable graph. */
131
+ export function compileApp(state: AppState): CompiledApplication {
132
+ const app = streetui.app({ name: '__PROJECT_NAME__' });
133
+ app.page('home', (page) => buildApp(page, state));
134
+ return compile(app);
135
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Browser entry. Runs after the server HTML has painted: it reads the embedded
3
+ * state snapshot, rebuilds the identical reactive state, and HYDRATES the
4
+ * existing markup — adopting the server DOM instead of re-rendering it.
5
+ */
6
+
7
+ import { createRenderer, readState } from 'streetui';
8
+ import { BrowserDOMAdapter } from 'streetui';
9
+ import { createState, compileApp, viewForPath, STATE_KEY, type AppSnapshot, type AppState } from './app.js';
10
+
11
+ export interface HydrateResult {
12
+ readonly state: AppState;
13
+ unmount(): void;
14
+ }
15
+
16
+ /** Hydrate the app inside `appContainer`, seeded from the SSR state island. */
17
+ export function hydrateApp(appContainer: Element, stateRoot?: Element | Document): HydrateResult {
18
+ const dom = new BrowserDOMAdapter();
19
+ const transferred = readState(dom, stateRoot ?? appContainer);
20
+ const seed = transferred[STATE_KEY] as AppSnapshot | undefined;
21
+
22
+ const state = createState(seed);
23
+ const compiled = compileApp(state);
24
+
25
+ const renderer = createRenderer({ domAdapter: dom });
26
+ const handle = renderer.hydrate(compiled, appContainer);
27
+
28
+ // Keep the view in sync with browser back/forward navigation.
29
+ const onPop = (): void => state.view.set(viewForPath(location.pathname));
30
+ window.addEventListener('popstate', onPop);
31
+
32
+ return {
33
+ state,
34
+ unmount: () => {
35
+ window.removeEventListener('popstate', onPop);
36
+ handle.unmount();
37
+ },
38
+ };
39
+ }
40
+
41
+ // Auto-boot in a real browser; guarded so importing in a non-DOM environment
42
+ // (e.g. a test) does nothing.
43
+ if (typeof document !== 'undefined') {
44
+ const boot = (): void => {
45
+ const app = document.getElementById('app');
46
+ if (app !== null) hydrateApp(app, document);
47
+ };
48
+ if (document.readyState === 'loading') {
49
+ document.addEventListener('DOMContentLoaded', boot, { once: true });
50
+ } else {
51
+ boot();
52
+ }
53
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Server entry. The StreetUI CLI bundles this for Node and calls the exported
3
+ * `render(request)` for every non-asset request. It renders the app to HTML for
4
+ * the requested URL and embeds a state snapshot so the browser can hydrate the
5
+ * exact same app in place.
6
+ *
7
+ * Pure server code: no `window`, no `document`.
8
+ */
9
+
10
+ import { renderToString, serializeState } from 'streetui';
11
+ import { createState, compileApp, snapshot, viewForPath, STATE_KEY } from './app.js';
12
+
13
+ export interface RenderRequest {
14
+ readonly url: string;
15
+ }
16
+ export interface RenderResult {
17
+ readonly html: string;
18
+ readonly status?: number;
19
+ }
20
+
21
+ /** Render a full HTML document for the requested URL. */
22
+ export function render(request: RenderRequest): RenderResult {
23
+ const state = createState({ view: viewForPath(request.url) });
24
+ const compiled = compileApp(state);
25
+ const body = renderToString(compiled);
26
+ const island = serializeState({ [STATE_KEY]: snapshot(state) });
27
+
28
+ const html = [
29
+ '<!doctype html>',
30
+ '<html lang="en">',
31
+ '<head>',
32
+ '<meta charset="utf-8" />',
33
+ '<meta name="viewport" content="width=device-width, initial-scale=1" />',
34
+ '<title>__PROJECT_NAME__</title>',
35
+ '<link rel="icon" href="/favicon.svg" />',
36
+ '<link rel="stylesheet" href="/styles.css" />',
37
+ '</head>',
38
+ '<body>',
39
+ `<div id="app">${body}</div>`,
40
+ island,
41
+ '<script type="module" src="/main.js"></script>',
42
+ '</body>',
43
+ '</html>',
44
+ ].join('\n');
45
+
46
+ return { html };
47
+ }
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from 'streetui';
2
+
3
+ /**
4
+ * StreetUI project configuration. Every field is optional and shown here with
5
+ * its default — delete anything you do not need to change.
6
+ */
7
+ export default defineConfig({
8
+ port: 3000,
9
+ host: 'localhost',
10
+ clientEntry: 'src/main.ts',
11
+ serverEntry: 'src/server.ts',
12
+ outDir: 'dist',
13
+ publicDir: 'public',
14
+ });
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "types": [],
8
+ "strict": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "exactOptionalPropertyTypes": true,
11
+ "verbatimModuleSyntax": true,
12
+ "skipLibCheck": true,
13
+ "noEmit": true
14
+ },
15
+ "include": ["src", "streetui.config.ts"]
16
+ }