@loom-js/create 0.0.0-stage → 0.1.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 (42) hide show
  1. package/README.md +51 -2
  2. package/bin/create.mjs +18 -0
  3. package/dist/index.mjs +1202 -0
  4. package/dist/templates/biome/js/base/public/styles/base.css +17 -0
  5. package/dist/templates/biome/js/base/src/app.js +23 -0
  6. package/dist/templates/biome/js/base/src/pages/about.js +12 -0
  7. package/dist/templates/biome/js/base/src/pages/home.js +22 -0
  8. package/dist/templates/biome/js/base/src/routes.js +3 -0
  9. package/dist/templates/biome/js/server/src/prerender.entry.js +22 -0
  10. package/dist/templates/biome/js/tailwind/scripts/dev.mjs +66 -0
  11. package/dist/templates/biome/js/tailwind/src/app.js +23 -0
  12. package/dist/templates/biome/js/ui/pink/src/pages/home.js +21 -0
  13. package/dist/templates/biome/ts/base/public/styles/base.css +17 -0
  14. package/dist/templates/biome/ts/base/src/app.ts +23 -0
  15. package/dist/templates/biome/ts/base/src/pages/about.ts +12 -0
  16. package/dist/templates/biome/ts/base/src/pages/home.ts +22 -0
  17. package/dist/templates/biome/ts/base/src/routes.ts +3 -0
  18. package/dist/templates/biome/ts/base/src/types.d.ts +4 -0
  19. package/dist/templates/biome/ts/server/src/prerender.entry.ts +26 -0
  20. package/dist/templates/biome/ts/tailwind/scripts/dev.mjs +66 -0
  21. package/dist/templates/biome/ts/tailwind/src/app.ts +23 -0
  22. package/dist/templates/biome/ts/ui/pink/src/pages/home.ts +21 -0
  23. package/dist/templates/prettier/js/base/public/styles/base.css +17 -0
  24. package/dist/templates/prettier/js/base/src/app.js +23 -0
  25. package/dist/templates/prettier/js/base/src/pages/about.js +12 -0
  26. package/dist/templates/prettier/js/base/src/pages/home.js +22 -0
  27. package/dist/templates/prettier/js/base/src/routes.js +3 -0
  28. package/dist/templates/prettier/js/server/src/prerender.entry.js +22 -0
  29. package/dist/templates/prettier/js/tailwind/scripts/dev.mjs +66 -0
  30. package/dist/templates/prettier/js/tailwind/src/app.js +23 -0
  31. package/dist/templates/prettier/js/ui/pink/src/pages/home.js +21 -0
  32. package/dist/templates/prettier/ts/base/public/styles/base.css +17 -0
  33. package/dist/templates/prettier/ts/base/src/app.ts +23 -0
  34. package/dist/templates/prettier/ts/base/src/pages/about.ts +12 -0
  35. package/dist/templates/prettier/ts/base/src/pages/home.ts +22 -0
  36. package/dist/templates/prettier/ts/base/src/routes.ts +3 -0
  37. package/dist/templates/prettier/ts/base/src/types.d.ts +4 -0
  38. package/dist/templates/prettier/ts/server/src/prerender.entry.ts +26 -0
  39. package/dist/templates/prettier/ts/tailwind/scripts/dev.mjs +66 -0
  40. package/dist/templates/prettier/ts/tailwind/src/app.ts +23 -0
  41. package/dist/templates/prettier/ts/ui/pink/src/pages/home.ts +21 -0
  42. package/package.json +51 -3
@@ -0,0 +1,17 @@
1
+ /* Layered, so a UI kit's or Tailwind's rules win over these defaults. */
2
+ @layer base {
3
+ body {
4
+ font-family: system-ui, sans-serif;
5
+ margin: 0;
6
+ }
7
+
8
+ .app-nav {
9
+ display: flex;
10
+ gap: 1rem;
11
+ padding: 1rem 1.25rem;
12
+ }
13
+
14
+ .app-main {
15
+ padding: 0 1.25rem;
16
+ }
17
+ }
@@ -0,0 +1,23 @@
1
+ import { component, createRoutes, RouteLink } from '@loom-js/core';
2
+
3
+ // Pages load on demand. Registering routes touches no DOM, so module scope
4
+ // is safe on the server too.
5
+ const Routes = createRoutes({
6
+ config: {
7
+ '/': () => import('@app/pages/home'),
8
+ '/about': () => import('@app/pages/about')
9
+ }
10
+ });
11
+
12
+ /** The composed app: the browser entry and the server render both call it. */
13
+ export const App = component(
14
+ (html, props) => html`
15
+ <div class="app">
16
+ <nav class="app-nav">
17
+ <${RouteLink} href="/">Home</>
18
+ <${RouteLink} href="/about">About</>
19
+ </nav>
20
+ <main class="app-main">${Routes(props)}</main>
21
+ </div>
22
+ `
23
+ );
@@ -0,0 +1,12 @@
1
+ import { component } from '@loom-js/core';
2
+
3
+ export default component(
4
+ (html) => html`
5
+ <section>
6
+ <h1>About</h1>
7
+ <p>
8
+ A second route, so client-side navigation has somewhere to go.
9
+ </p>
10
+ </section>
11
+ `
12
+ );
@@ -0,0 +1,22 @@
1
+ import { activity, component } from '@loom-js/core';
2
+
3
+ const count = activity(0);
4
+
5
+ export default component(
6
+ (html) => html`
7
+ <section>
8
+ <h1>Welcome to loom</h1>
9
+ <p>
10
+ Edit
11
+ <code>src/pages/home.ts</code>
12
+ and save to reload.
13
+ </p>
14
+ <button
15
+ $click=${() => count.update(count.value() + 1)}
16
+ type="button"
17
+ >
18
+ ${count.effect(({ value }) => `Clicked ${value} times`)}
19
+ </button>
20
+ </section>
21
+ `
22
+ );
@@ -0,0 +1,3 @@
1
+ // Every route that gets an HTML shell. The config and the router read this
2
+ // list; add a route here and in `app.ts` together.
3
+ export const routes = ['/', '/about'];
@@ -0,0 +1,22 @@
1
+ // The server module: built next to the client so markup and state match the
2
+ // shipped app. `loom build` calls `prerenderRoute` once per route.
3
+
4
+ import {
5
+ dehydrate,
6
+ renderToString,
7
+ serializeState
8
+ } from '@loom-js/core/server';
9
+
10
+ import { App } from './app';
11
+
12
+ export { routes } from './routes';
13
+
14
+ /** Renders one route: its markup plus the state the client primes from. */
15
+ export const prerenderRoute = async (url, window) => {
16
+ const html = await renderToString(App(), {
17
+ url,
18
+ window
19
+ });
20
+
21
+ return { html, state: serializeState(dehydrate(window)) };
22
+ };
@@ -0,0 +1,66 @@
1
+ // `dev`: the Tailwind CLI in watch mode next to `loom dev`. When either one
2
+ // exits, the other is stopped, and its exit code is passed on.
3
+
4
+ import { spawn } from 'node:child_process';
5
+ import { pathToFileURL } from 'node:url';
6
+
7
+ const TASKS = [
8
+ [
9
+ 'tailwindcss',
10
+ // `always`: plain `--watch` exits when stdin closes (any non-TTY run).
11
+ [
12
+ '-i',
13
+ './src/styles.css',
14
+ '-o',
15
+ './.generated/styles.css',
16
+ '--watch=always'
17
+ ]
18
+ ],
19
+ ['loom', ['dev']]
20
+ ];
21
+
22
+ /** Starts every task; resolves with the first exit code once all stopped. */
23
+ export const runTogether = (tasks, start = spawn) =>
24
+ new Promise((resolve) => {
25
+ let exitCode;
26
+ const stopped = new Set();
27
+ const children = tasks.map(([command, args]) =>
28
+ start(command, args, {
29
+ // Package binaries are `.cmd` shims on Windows.
30
+ shell: process.platform === 'win32',
31
+ stdio: 'inherit'
32
+ })
33
+ );
34
+ const onStopped = (child, code) => {
35
+ if (stopped.has(child)) {
36
+ return;
37
+ }
38
+
39
+ stopped.add(child);
40
+ exitCode ??= code ?? 1;
41
+
42
+ for (const other of children) {
43
+ if (!stopped.has(other)) {
44
+ other.kill();
45
+ }
46
+ }
47
+
48
+ if (stopped.size === children.length) {
49
+ resolve(exitCode);
50
+ }
51
+ };
52
+
53
+ for (const child of children) {
54
+ child.on('exit', (code) => onStopped(child, code));
55
+ // A binary that can't start stops the other task too.
56
+ child.on('error', () => onStopped(child, 1));
57
+ }
58
+ });
59
+
60
+ // Runs only as the `dev` script, not when imported.
61
+ if (
62
+ process.argv[1] &&
63
+ import.meta.url === pathToFileURL(process.argv[1]).href
64
+ ) {
65
+ process.exitCode = await runTogether(TASKS);
66
+ }
@@ -0,0 +1,23 @@
1
+ import { component, createRoutes, RouteLink } from '@loom-js/core';
2
+
3
+ // Pages load on demand. Registering routes touches no DOM, so module scope
4
+ // is safe on the server too.
5
+ const Routes = createRoutes({
6
+ config: {
7
+ '/': () => import('@app/pages/home'),
8
+ '/about': () => import('@app/pages/about')
9
+ }
10
+ });
11
+
12
+ /** The composed app: the browser entry and the server render both call it. */
13
+ export const App = component(
14
+ (html, props) => html`
15
+ <div class="mx-auto flex max-w-3xl flex-col gap-6 px-5 py-6">
16
+ <nav class="flex gap-4 font-medium" id="app-nav">
17
+ <${RouteLink} href="/">Home</>
18
+ <${RouteLink} href="/about">About</>
19
+ </nav>
20
+ <main>${Routes(props)}</main>
21
+ </div>
22
+ `
23
+ );
@@ -0,0 +1,21 @@
1
+ import { activity, component } from '@loom-js/core';
2
+ import { PinkButton, PinkCard } from '@loom-js/pink';
3
+
4
+ const count = activity(0);
5
+
6
+ export default component(
7
+ (html) => html`
8
+ <section>
9
+ <h1>Welcome to loom</h1>
10
+ <${PinkCard} id="welcome-card">
11
+ <p>Edit <code>src/pages/home.ts</code> and save to reload.</p>
12
+ <${PinkButton}
13
+ icon="icon-check"
14
+ onClick=${() => count.update(count.value() + 1)}
15
+ >
16
+ ${count.effect(({ value }) => `Clicked ${value} times`)}
17
+ </>
18
+ </>
19
+ </section>
20
+ `
21
+ );
@@ -0,0 +1,17 @@
1
+ /* Layered, so a UI kit's or Tailwind's rules win over these defaults. */
2
+ @layer base {
3
+ body {
4
+ font-family: system-ui, sans-serif;
5
+ margin: 0;
6
+ }
7
+
8
+ .app-nav {
9
+ display: flex;
10
+ gap: 1rem;
11
+ padding: 1rem 1.25rem;
12
+ }
13
+
14
+ .app-main {
15
+ padding: 0 1.25rem;
16
+ }
17
+ }
@@ -0,0 +1,23 @@
1
+ import { component, createRoutes, RouteLink } from '@loom-js/core';
2
+
3
+ // Pages load on demand. Registering routes touches no DOM, so module scope
4
+ // is safe on the server too.
5
+ const Routes = createRoutes({
6
+ config: {
7
+ '/': () => import('@app/pages/home'),
8
+ '/about': () => import('@app/pages/about')
9
+ }
10
+ });
11
+
12
+ /** The composed app: the browser entry and the server render both call it. */
13
+ export const App = component(
14
+ (html, props) => html`
15
+ <div class="app">
16
+ <nav class="app-nav">
17
+ <${RouteLink} href="/">Home</>
18
+ <${RouteLink} href="/about">About</>
19
+ </nav>
20
+ <main class="app-main">${Routes(props)}</main>
21
+ </div>
22
+ `
23
+ );
@@ -0,0 +1,12 @@
1
+ import { component } from '@loom-js/core';
2
+
3
+ export default component(
4
+ (html) => html`
5
+ <section>
6
+ <h1>About</h1>
7
+ <p>
8
+ A second route, so client-side navigation has somewhere to go.
9
+ </p>
10
+ </section>
11
+ `
12
+ );
@@ -0,0 +1,22 @@
1
+ import { activity, component } from '@loom-js/core';
2
+
3
+ const count = activity(0);
4
+
5
+ export default component(
6
+ (html) => html`
7
+ <section>
8
+ <h1>Welcome to loom</h1>
9
+ <p>
10
+ Edit
11
+ <code>src/pages/home.ts</code>
12
+ and save to reload.
13
+ </p>
14
+ <button
15
+ $click=${() => count.update(count.value() + 1)}
16
+ type="button"
17
+ >
18
+ ${count.effect(({ value }) => `Clicked ${value} times`)}
19
+ </button>
20
+ </section>
21
+ `
22
+ );
@@ -0,0 +1,3 @@
1
+ // Every route that gets an HTML shell. The config and the router read this
2
+ // list; add a route here and in `app.ts` together.
3
+ export const routes = ['/', '/about'];
@@ -0,0 +1,4 @@
1
+ // `true` in `loom dev` builds, `false` in production; set by @loom-js/build.
2
+ declare const __DEV__: boolean;
3
+
4
+ declare module '*.css';
@@ -0,0 +1,26 @@
1
+ // The server module: built next to the client so markup and state match the
2
+ // shipped app. `loom build` calls `prerenderRoute` once per route.
3
+
4
+ import {
5
+ dehydrate,
6
+ renderToString,
7
+ serializeState
8
+ } from '@loom-js/core/server';
9
+
10
+ import { App } from './app';
11
+
12
+ export { routes } from './routes';
13
+
14
+ // The server entry's types duplicate core's, so the same runtime value needs
15
+ // a cast at this one boundary.
16
+ type ServerRenderable = Parameters<typeof renderToString>[0];
17
+
18
+ /** Renders one route: its markup plus the state the client primes from. */
19
+ export const prerenderRoute = async (url: string, window: object) => {
20
+ const html = await renderToString(App() as unknown as ServerRenderable, {
21
+ url,
22
+ window
23
+ });
24
+
25
+ return { html, state: serializeState(dehydrate(window)) };
26
+ };
@@ -0,0 +1,66 @@
1
+ // `dev`: the Tailwind CLI in watch mode next to `loom dev`. When either one
2
+ // exits, the other is stopped, and its exit code is passed on.
3
+
4
+ import { spawn } from 'node:child_process';
5
+ import { pathToFileURL } from 'node:url';
6
+
7
+ const TASKS = [
8
+ [
9
+ 'tailwindcss',
10
+ // `always`: plain `--watch` exits when stdin closes (any non-TTY run).
11
+ [
12
+ '-i',
13
+ './src/styles.css',
14
+ '-o',
15
+ './.generated/styles.css',
16
+ '--watch=always'
17
+ ]
18
+ ],
19
+ ['loom', ['dev']]
20
+ ];
21
+
22
+ /** Starts every task; resolves with the first exit code once all stopped. */
23
+ export const runTogether = (tasks, start = spawn) =>
24
+ new Promise((resolve) => {
25
+ let exitCode;
26
+ const stopped = new Set();
27
+ const children = tasks.map(([command, args]) =>
28
+ start(command, args, {
29
+ // Package binaries are `.cmd` shims on Windows.
30
+ shell: process.platform === 'win32',
31
+ stdio: 'inherit'
32
+ })
33
+ );
34
+ const onStopped = (child, code) => {
35
+ if (stopped.has(child)) {
36
+ return;
37
+ }
38
+
39
+ stopped.add(child);
40
+ exitCode ??= code ?? 1;
41
+
42
+ for (const other of children) {
43
+ if (!stopped.has(other)) {
44
+ other.kill();
45
+ }
46
+ }
47
+
48
+ if (stopped.size === children.length) {
49
+ resolve(exitCode);
50
+ }
51
+ };
52
+
53
+ for (const child of children) {
54
+ child.on('exit', (code) => onStopped(child, code));
55
+ // A binary that can't start stops the other task too.
56
+ child.on('error', () => onStopped(child, 1));
57
+ }
58
+ });
59
+
60
+ // Runs only as the `dev` script, not when imported.
61
+ if (
62
+ process.argv[1] &&
63
+ import.meta.url === pathToFileURL(process.argv[1]).href
64
+ ) {
65
+ process.exitCode = await runTogether(TASKS);
66
+ }
@@ -0,0 +1,23 @@
1
+ import { component, createRoutes, RouteLink } from '@loom-js/core';
2
+
3
+ // Pages load on demand. Registering routes touches no DOM, so module scope
4
+ // is safe on the server too.
5
+ const Routes = createRoutes({
6
+ config: {
7
+ '/': () => import('@app/pages/home'),
8
+ '/about': () => import('@app/pages/about')
9
+ }
10
+ });
11
+
12
+ /** The composed app: the browser entry and the server render both call it. */
13
+ export const App = component(
14
+ (html, props) => html`
15
+ <div class="mx-auto flex max-w-3xl flex-col gap-6 px-5 py-6">
16
+ <nav class="flex gap-4 font-medium" id="app-nav">
17
+ <${RouteLink} href="/">Home</>
18
+ <${RouteLink} href="/about">About</>
19
+ </nav>
20
+ <main>${Routes(props)}</main>
21
+ </div>
22
+ `
23
+ );
@@ -0,0 +1,21 @@
1
+ import { activity, component } from '@loom-js/core';
2
+ import { PinkButton, PinkCard } from '@loom-js/pink';
3
+
4
+ const count = activity(0);
5
+
6
+ export default component(
7
+ (html) => html`
8
+ <section>
9
+ <h1>Welcome to loom</h1>
10
+ <${PinkCard} id="welcome-card">
11
+ <p>Edit <code>src/pages/home.ts</code> and save to reload.</p>
12
+ <${PinkButton}
13
+ icon="icon-check"
14
+ onClick=${() => count.update(count.value() + 1)}
15
+ >
16
+ ${count.effect(({ value }) => `Clicked ${value} times`)}
17
+ </>
18
+ </>
19
+ </section>
20
+ `
21
+ );
@@ -0,0 +1,17 @@
1
+ /* Layered, so a UI kit's or Tailwind's rules win over these defaults. */
2
+ @layer base {
3
+ body {
4
+ font-family: system-ui, sans-serif;
5
+ margin: 0;
6
+ }
7
+
8
+ .app-nav {
9
+ display: flex;
10
+ gap: 1rem;
11
+ padding: 1rem 1.25rem;
12
+ }
13
+
14
+ .app-main {
15
+ padding: 0 1.25rem;
16
+ }
17
+ }
@@ -0,0 +1,23 @@
1
+ import { component, createRoutes, RouteLink } from '@loom-js/core';
2
+
3
+ // Pages load on demand. Registering routes touches no DOM, so module scope
4
+ // is safe on the server too.
5
+ const Routes = createRoutes({
6
+ config: {
7
+ '/': () => import('@app/pages/home'),
8
+ '/about': () => import('@app/pages/about')
9
+ }
10
+ });
11
+
12
+ /** The composed app: the browser entry and the server render both call it. */
13
+ export const App = component(
14
+ (html, props) => html`
15
+ <div class="app">
16
+ <nav class="app-nav">
17
+ <${RouteLink} href="/">Home</>
18
+ <${RouteLink} href="/about">About</>
19
+ </nav>
20
+ <main class="app-main">${Routes(props)}</main>
21
+ </div>
22
+ `
23
+ );
@@ -0,0 +1,12 @@
1
+ import { component } from '@loom-js/core';
2
+
3
+ export default component(
4
+ (html) => html`
5
+ <section>
6
+ <h1>About</h1>
7
+ <p>
8
+ A second route, so client-side navigation has somewhere to go.
9
+ </p>
10
+ </section>
11
+ `
12
+ );
@@ -0,0 +1,22 @@
1
+ import { activity, component } from '@loom-js/core';
2
+
3
+ const count = activity(0);
4
+
5
+ export default component(
6
+ (html) => html`
7
+ <section>
8
+ <h1>Welcome to loom</h1>
9
+ <p>
10
+ Edit
11
+ <code>src/pages/home.ts</code>
12
+ and save to reload.
13
+ </p>
14
+ <button
15
+ $click=${() => count.update(count.value() + 1)}
16
+ type="button"
17
+ >
18
+ ${count.effect(({ value }) => `Clicked ${value} times`)}
19
+ </button>
20
+ </section>
21
+ `
22
+ );
@@ -0,0 +1,3 @@
1
+ // Every route that gets an HTML shell. The config and the router read this
2
+ // list; add a route here and in `app.ts` together.
3
+ export const routes = ['/', '/about'];
@@ -0,0 +1,22 @@
1
+ // The server module: built next to the client so markup and state match the
2
+ // shipped app. `loom build` calls `prerenderRoute` once per route.
3
+
4
+ import {
5
+ dehydrate,
6
+ renderToString,
7
+ serializeState
8
+ } from '@loom-js/core/server';
9
+
10
+ import { App } from './app';
11
+
12
+ export { routes } from './routes';
13
+
14
+ /** Renders one route: its markup plus the state the client primes from. */
15
+ export const prerenderRoute = async (url, window) => {
16
+ const html = await renderToString(App(), {
17
+ url,
18
+ window
19
+ });
20
+
21
+ return { html, state: serializeState(dehydrate(window)) };
22
+ };
@@ -0,0 +1,66 @@
1
+ // `dev`: the Tailwind CLI in watch mode next to `loom dev`. When either one
2
+ // exits, the other is stopped, and its exit code is passed on.
3
+
4
+ import { spawn } from 'node:child_process';
5
+ import { pathToFileURL } from 'node:url';
6
+
7
+ const TASKS = [
8
+ [
9
+ 'tailwindcss',
10
+ // `always`: plain `--watch` exits when stdin closes (any non-TTY run).
11
+ [
12
+ '-i',
13
+ './src/styles.css',
14
+ '-o',
15
+ './.generated/styles.css',
16
+ '--watch=always'
17
+ ]
18
+ ],
19
+ ['loom', ['dev']]
20
+ ];
21
+
22
+ /** Starts every task; resolves with the first exit code once all stopped. */
23
+ export const runTogether = (tasks, start = spawn) =>
24
+ new Promise((resolve) => {
25
+ let exitCode;
26
+ const stopped = new Set();
27
+ const children = tasks.map(([command, args]) =>
28
+ start(command, args, {
29
+ // Package binaries are `.cmd` shims on Windows.
30
+ shell: process.platform === 'win32',
31
+ stdio: 'inherit'
32
+ })
33
+ );
34
+ const onStopped = (child, code) => {
35
+ if (stopped.has(child)) {
36
+ return;
37
+ }
38
+
39
+ stopped.add(child);
40
+ exitCode ??= code ?? 1;
41
+
42
+ for (const other of children) {
43
+ if (!stopped.has(other)) {
44
+ other.kill();
45
+ }
46
+ }
47
+
48
+ if (stopped.size === children.length) {
49
+ resolve(exitCode);
50
+ }
51
+ };
52
+
53
+ for (const child of children) {
54
+ child.on('exit', (code) => onStopped(child, code));
55
+ // A binary that can't start stops the other task too.
56
+ child.on('error', () => onStopped(child, 1));
57
+ }
58
+ });
59
+
60
+ // Runs only as the `dev` script, not when imported.
61
+ if (
62
+ process.argv[1] &&
63
+ import.meta.url === pathToFileURL(process.argv[1]).href
64
+ ) {
65
+ process.exitCode = await runTogether(TASKS);
66
+ }
@@ -0,0 +1,23 @@
1
+ import { component, createRoutes, RouteLink } from '@loom-js/core';
2
+
3
+ // Pages load on demand. Registering routes touches no DOM, so module scope
4
+ // is safe on the server too.
5
+ const Routes = createRoutes({
6
+ config: {
7
+ '/': () => import('@app/pages/home'),
8
+ '/about': () => import('@app/pages/about')
9
+ }
10
+ });
11
+
12
+ /** The composed app: the browser entry and the server render both call it. */
13
+ export const App = component(
14
+ (html, props) => html`
15
+ <div class="mx-auto flex max-w-3xl flex-col gap-6 px-5 py-6">
16
+ <nav class="flex gap-4 font-medium" id="app-nav">
17
+ <${RouteLink} href="/">Home</>
18
+ <${RouteLink} href="/about">About</>
19
+ </nav>
20
+ <main>${Routes(props)}</main>
21
+ </div>
22
+ `
23
+ );