create-smaoog 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aggelos Gesoulis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # create-smaoog
2
+
3
+ Create a new [smaoog](https://www.npmjs.com/package/smaoog) app ready to go.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npm create smaoog my-app
9
+ ```
10
+
11
+ or equivalently:
12
+
13
+ ```bash
14
+ npx create-smaoog my-app
15
+ ```
16
+
17
+ Then:
18
+
19
+ ```bash
20
+ cd my-app
21
+ npm install
22
+ npm run dev
23
+ ```
24
+
25
+ ## TypeScript and JavaScript
26
+
27
+ A TypeScript app is generated by default. For a JavaScript app, pass `--js`:
28
+
29
+ ```bash
30
+ npm create smaoog my-app -- --js
31
+ ```
32
+
33
+ ## What you get
34
+
35
+ - a sample file route in `src/routes/`
36
+ - a user-owned `src/document.tsx`
37
+ - a hydration entry at `src/client-entry.ts`
38
+ - Tailwind CSS wired up via `src/app.css`
39
+ - `public/favicon.ico`
40
+ - `dev` / `build` / `start` scripts
41
+
42
+ ## License
43
+
44
+ [MIT](./LICENSE)
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "create-smaoog",
3
+ "version": "0.0.1",
4
+ "description": "Scaffold a new smaoog app.",
5
+ "license": "MIT",
6
+ "author": "Aggelos Gesoulis",
7
+ "type": "module",
8
+ "keywords": [
9
+ "smaoog",
10
+ "create",
11
+ "scaffold",
12
+ "starter",
13
+ "react",
14
+ "ssr"
15
+ ],
16
+ "homepage": "https://github.com/anges244/smaoog/tree/main/packages/create-smaoog#readme",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/anges244/smaoog.git",
20
+ "directory": "packages/create-smaoog"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/anges244/smaoog/issues"
24
+ },
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "bin": {
29
+ "create-smaoog": "./src/index.js"
30
+ },
31
+ "files": [
32
+ "src",
33
+ "template",
34
+ "!src/**/*.test.js"
35
+ ],
36
+ "scripts": {
37
+ "preview": "node src/preview.js"
38
+ }
39
+ }
package/src/index.js ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readdirSync } from "node:fs";
3
+ import { basename, resolve } from "node:path";
4
+ import { scaffold } from "./scaffold.js";
5
+
6
+ // Parse argv into { appName, js, help }. The first non-flag argument is the app
7
+ // name; `--js` selects the JavaScript template (`--ts` is the default).
8
+ function parseArgs(argv) {
9
+ let appName;
10
+ let js = false;
11
+ let help = false;
12
+ for (const arg of argv) {
13
+ if (arg === "--js") js = true;
14
+ else if (arg === "--ts") js = false;
15
+ else if (arg === "--help" || arg === "-h") help = true;
16
+ else if (!arg.startsWith("-") && appName === undefined) appName = arg;
17
+ }
18
+ return { appName, js, help };
19
+ }
20
+
21
+ const USAGE = `Usage: create-smaoog <app-name> [--js]
22
+
23
+ Creates a new smaoog app in ./<app-name>.
24
+
25
+ --js Generate a JavaScript app (TypeScript is the default)
26
+ --help Show this message`;
27
+
28
+ // A new app must land in an empty (or absent) directory so nothing is clobbered.
29
+ function isEmptyDir(dir) {
30
+ if (!existsSync(dir)) return true;
31
+ return readdirSync(dir).length === 0;
32
+ }
33
+
34
+ async function main() {
35
+ const { appName, js, help } = parseArgs(process.argv.slice(2));
36
+
37
+ if (help || !appName) {
38
+ console.log(USAGE);
39
+ process.exit(help ? 0 : 1);
40
+ }
41
+
42
+ const targetDir = resolve(process.cwd(), appName);
43
+ if (!isEmptyDir(targetDir)) {
44
+ console.error(`Cannot create app: "${appName}" already exists and is not empty.`);
45
+ process.exit(1);
46
+ }
47
+
48
+ // The package name is the destination folder name (the arg may be a path).
49
+ await scaffold({ targetDir, appName: basename(targetDir), js });
50
+
51
+ const lang = js ? "JavaScript" : "TypeScript";
52
+ console.log(`\nCreated a ${lang} smaoog app in ${targetDir}\n`);
53
+ console.log("Next steps:\n");
54
+ console.log(` cd ${appName}`);
55
+ console.log(" npm install");
56
+ console.log(" npm run dev\n");
57
+ }
58
+
59
+ main().catch((err) => {
60
+ console.error(err);
61
+ process.exit(1);
62
+ });
package/src/preview.js ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ // Local preview of the create-smaoog template.
3
+ //
4
+ // Scaffolds the template into `examples/preview` (a workspace member, so the
5
+ // generated `smaoog` dependency is rewritten from "latest" to "workspace:*" and
6
+ // resolves to this repo's framework source instead of a published release),
7
+ // installs, and starts the dev server. This is the fastest way to see what a
8
+ // freshly scaffolded app looks like while iterating on the template or the
9
+ // framework. The preview directory is gitignored and recreated on each run.
10
+ //
11
+ // Usage:
12
+ // node src/preview.js [--js] [--no-dev]
13
+ //
14
+ // --js Preview the JavaScript template (TypeScript is the default)
15
+ // --no-dev Scaffold and install only; skip starting the dev server
16
+ import { spawnSync } from "node:child_process";
17
+ import { readFile, rm, writeFile } from "node:fs/promises";
18
+ import { fileURLToPath } from "node:url";
19
+ import { join } from "node:path";
20
+ import { scaffold } from "./scaffold.js";
21
+
22
+ const REPO_ROOT = fileURLToPath(new URL("../../..", import.meta.url));
23
+ const PREVIEW_DIR = join(REPO_ROOT, "examples", "preview");
24
+
25
+ function parseArgs(argv) {
26
+ return {
27
+ js: argv.includes("--js"),
28
+ dev: !argv.includes("--no-dev"),
29
+ };
30
+ }
31
+
32
+ function run(command, args, opts = {}) {
33
+ const result = spawnSync(command, args, { stdio: "inherit", cwd: REPO_ROOT, ...opts });
34
+ if (result.error) throw result.error;
35
+ if (result.status !== 0) {
36
+ throw new Error(`\`${command} ${args.join(" ")}\` exited with code ${result.status}`);
37
+ }
38
+ }
39
+
40
+ async function main() {
41
+ const { js, dev } = parseArgs(process.argv.slice(2));
42
+ const lang = js ? "JavaScript" : "TypeScript";
43
+
44
+ // Start from a clean directory so a previous run's files never linger.
45
+ await rm(PREVIEW_DIR, { recursive: true, force: true });
46
+ await scaffold({ targetDir: PREVIEW_DIR, appName: "preview", js });
47
+
48
+ // Point the generated app at this repo's framework source rather than the
49
+ // published "latest" release, so the preview reflects local changes.
50
+ const pkgPath = join(PREVIEW_DIR, "package.json");
51
+ const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
52
+ pkg.name = "@smaoog-examples/preview";
53
+ pkg.dependencies.smaoog = "workspace:*";
54
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
55
+
56
+ console.log(`\nScaffolded the ${lang} template into examples/preview\n`);
57
+
58
+ // Install at the workspace root so pnpm links the local `smaoog` package.
59
+ // Installing adds an importer entry for the (gitignored) preview app to the
60
+ // committed lockfile; snapshot and restore it so the working tree stays clean.
61
+ // node_modules is already linked at this point, so the dev server is
62
+ // unaffected by restoring the on-disk lockfile.
63
+ const lockPath = join(REPO_ROOT, "pnpm-lock.yaml");
64
+ const lockBefore = await readFile(lockPath, "utf8").catch(() => null);
65
+ run("pnpm", ["install"]);
66
+ if (lockBefore !== null) await writeFile(lockPath, lockBefore);
67
+
68
+ if (!dev) {
69
+ console.log("\nSkipping dev server (--no-dev). Start it with:\n");
70
+ console.log(" pnpm --filter @smaoog-examples/preview dev\n");
71
+ return;
72
+ }
73
+
74
+ console.log("\nStarting the preview dev server...\n");
75
+ run("pnpm", ["--filter", "@smaoog-examples/preview", "dev"]);
76
+ }
77
+
78
+ main().catch((err) => {
79
+ console.error(err?.message ?? err);
80
+ process.exit(1);
81
+ });
@@ -0,0 +1,178 @@
1
+ import { cp, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, join, relative } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const TEMPLATE_DIR = fileURLToPath(new URL("../template", import.meta.url));
6
+
7
+ // Pinned versions for the generated app. Kept here as the single source of truth
8
+ // for what a new smaoog app depends on. `smaoog` itself tracks the "latest"
9
+ // dist-tag so a freshly scaffolded app always pulls the newest framework release
10
+ // during the alpha, without create-smaoog needing a version bump per release.
11
+ const VERSIONS = {
12
+ react: "^19.2.0",
13
+ "react-dom": "^19.2.0",
14
+ smaoog: "latest",
15
+ "@tailwindcss/vite": "^4.3.1",
16
+ tailwindcss: "^4.3.1",
17
+ // Kept in sync with the repo's typescript devDependency, so the version a
18
+ // generated app pins is the same major the scaffold typecheck test validates
19
+ // smaoog's declarations against.
20
+ typescript: "^6.0.3",
21
+ "@types/react": "^19.2.0",
22
+ "@types/react-dom": "^19.2.0",
23
+ };
24
+
25
+ // Files that are binary and must be copied byte-for-byte (no text transform).
26
+ const BINARY = new Set([".ico", ".png", ".jpg", ".jpeg", ".webp", ".woff", ".woff2"]);
27
+
28
+ async function walk(dir) {
29
+ const out = [];
30
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
31
+ const full = join(dir, entry.name);
32
+ if (entry.isDirectory()) out.push(...(await walk(full)));
33
+ else out.push(full);
34
+ }
35
+ return out;
36
+ }
37
+
38
+ function isBinary(path) {
39
+ const dot = path.lastIndexOf(".");
40
+ return dot !== -1 && BINARY.has(path.slice(dot));
41
+ }
42
+
43
+ // The single TypeScript template is the source of truth; the JavaScript app is
44
+ // derived from it (extensions renamed, no tsconfig). Deriving JS rewrites the few
45
+ // extension-bearing references and erases the small amount of type-only syntax the
46
+ // template carries (see toJsContent).
47
+ function jsRename(rel) {
48
+ if (rel.endsWith(".tsx")) return rel.slice(0, -4) + ".jsx";
49
+ if (rel.endsWith(".ts")) return rel.slice(0, -3) + ".js";
50
+ return rel;
51
+ }
52
+
53
+ export function toJsContent(content) {
54
+ // The Document references the client entry by its real filename, which becomes
55
+ // .js in a JavaScript app. Type-only imports and the small amount of template
56
+ // annotation syntax are removed so the JS variant is still derived from the TS
57
+ // template without maintaining a second source tree. The template confines its
58
+ // types to these strippable forms on purpose, so a zero-dependency text pass
59
+ // (no TypeScript/esbuild) is enough to derive valid JavaScript.
60
+ return content
61
+ .replaceAll("client-entry.ts", "client-entry.js")
62
+ .replace(/^import\s+type\s+[^;]+;\n/gm, "")
63
+ .replace(/^type\s+\w+\s*=[^\n]*;\n/gm, "")
64
+ .replaceAll(": DocumentProps", "")
65
+ .replace(/: SmaoogRequest\b/g, "")
66
+ .replace(/: SmaoogResponse<[^>]*>/g, "")
67
+ .replaceAll(": Props", "");
68
+ }
69
+
70
+ // Turn a directory name into a valid npm package name: lowercase, only
71
+ // url-safe characters, no leading dot/dash/underscore. Falls back to "app" if
72
+ // nothing usable remains (e.g. a name that was all punctuation).
73
+ function toPackageName(name) {
74
+ const slug = String(name)
75
+ .trim()
76
+ .toLowerCase()
77
+ .replace(/[^a-z0-9._-]+/g, "-")
78
+ .replace(/^[._-]+/, "")
79
+ .slice(0, 214)
80
+ .replace(/[._-]+$/, "");
81
+ return slug || "app";
82
+ }
83
+
84
+ function packageJson(appName, js) {
85
+ const pick = (...names) => Object.fromEntries(names.map((n) => [n, VERSIONS[n]]));
86
+ return (
87
+ JSON.stringify(
88
+ {
89
+ name: toPackageName(appName),
90
+ version: "0.0.1",
91
+ private: true,
92
+ type: "module",
93
+ scripts: { dev: "smaoog dev", build: "smaoog build", start: "smaoog start" },
94
+ dependencies: pick("react", "react-dom", "smaoog"),
95
+ devDependencies: js
96
+ ? pick("@tailwindcss/vite", "tailwindcss")
97
+ : pick(
98
+ "@tailwindcss/vite",
99
+ "@types/react",
100
+ "@types/react-dom",
101
+ "tailwindcss",
102
+ "typescript",
103
+ ),
104
+ },
105
+ null,
106
+ 2,
107
+ ) + "\n"
108
+ );
109
+ }
110
+
111
+ function readme(appName) {
112
+ return [
113
+ `# ${appName}`,
114
+ "",
115
+ "A new app built with [smaoog](https://github.com/anges244/smaoog).",
116
+ "",
117
+ "## Development",
118
+ "",
119
+ "```bash",
120
+ "npm install",
121
+ "npm run dev",
122
+ "```",
123
+ "",
124
+ "## Production",
125
+ "",
126
+ "```bash",
127
+ "npm run build",
128
+ "npm run start",
129
+ "```",
130
+ "",
131
+ ].join("\n");
132
+ }
133
+
134
+ // Generate a new smaoog app into `targetDir`. `js` selects the JavaScript variant
135
+ // (derived from the TypeScript template). Returns the list of created
136
+ // app-relative paths.
137
+ export async function scaffold({ targetDir, appName, js = false }) {
138
+ await mkdir(targetDir, { recursive: true });
139
+ const created = [];
140
+
141
+ const write = async (rel, contents) => {
142
+ const dest = join(targetDir, rel);
143
+ await mkdir(dirname(dest), { recursive: true });
144
+ await writeFile(dest, contents);
145
+ created.push(rel);
146
+ };
147
+
148
+ for (const file of await walk(TEMPLATE_DIR)) {
149
+ let rel = relative(TEMPLATE_DIR, file).split("\\").join("/");
150
+
151
+ // tsconfig only belongs to the TypeScript app.
152
+ if (rel === "tsconfig.json" && js) continue;
153
+ // npm strips a literal .gitignore from a published package, so the template
154
+ // ships it as `gitignore`; restore the dot on scaffold.
155
+ if (rel === "gitignore") rel = ".gitignore";
156
+
157
+ if (isBinary(file)) {
158
+ const dest = join(targetDir, rel);
159
+ await mkdir(dirname(dest), { recursive: true });
160
+ await cp(file, dest);
161
+ created.push(rel);
162
+ continue;
163
+ }
164
+
165
+ let content = await readFile(file, "utf8");
166
+ if (js) {
167
+ rel = jsRename(rel);
168
+ content = toJsContent(content);
169
+ }
170
+ await write(rel, content);
171
+ }
172
+
173
+ await write("package.json", packageJson(appName, js));
174
+ await write("README.md", readme(appName));
175
+
176
+ created.sort();
177
+ return created;
178
+ }
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ .smaoog
3
+ *.log
4
+ .DS_Store
Binary file
@@ -0,0 +1 @@
1
+ @import "tailwindcss";
@@ -0,0 +1,3 @@
1
+ // The browser entry. Importing "smaoog/client" boots hydration, <Link>
2
+ // navigation, and <Form> behavior. Add any browser-only setup below.
3
+ import "smaoog/client";
@@ -0,0 +1,21 @@
1
+ import { ClientEntry, ReactRefresh, Stylesheet } from "smaoog";
2
+ import type { DocumentProps } from "smaoog";
3
+
4
+ export default function Document({ children, head }: DocumentProps) {
5
+ return (
6
+ <html lang="en">
7
+ <head>
8
+ <meta charSet="utf-8" />
9
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
10
+ <link rel="icon" href="/favicon.ico" />
11
+ <Stylesheet href="src/app.css" />
12
+ {head}
13
+ <ReactRefresh />
14
+ </head>
15
+ <body>
16
+ <main id="root">{children}</main>
17
+ <ClientEntry src="src/client-entry.ts" />
18
+ </body>
19
+ </html>
20
+ );
21
+ }
@@ -0,0 +1,31 @@
1
+ import type { SmaoogRequest, SmaoogResponse } from "smaoog";
2
+
3
+ type Props = { renderedAt: string };
4
+
5
+ // A route can export a handler per HTTP method. This GET runs on the server and
6
+ // passes its result to the component below as props. So the HTML you receive is
7
+ // already rendered (view source to confirm), no client fetch required.
8
+ export function GET(req: SmaoogRequest, res: SmaoogResponse<Props>) {
9
+ return res.render({ renderedAt: new Date().toLocaleString() });
10
+ }
11
+
12
+ export const meta = {
13
+ title: "smaoog app",
14
+ description: "A new app built with smaoog.",
15
+ };
16
+
17
+ export default function Home({ renderedAt }: Props) {
18
+ return (
19
+ <main className="flex min-h-screen flex-col items-center justify-center gap-4 bg-slate-50 text-slate-900">
20
+ <h1 className="text-4xl font-bold">Welcome to smaoog</h1>
21
+ <p className="text-slate-600">Server-rendered at {renderedAt}.</p>
22
+ <p className="text-slate-600">
23
+ Edit{" "}
24
+ <code className="rounded bg-slate-200 px-1.5 py-0.5">
25
+ src/routes/index
26
+ </code>{" "}
27
+ and reload.
28
+ </p>
29
+ </main>
30
+ );
31
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "noEmit": true,
10
+ "verbatimModuleSyntax": true
11
+ },
12
+ "include": ["src"]
13
+ }