create-cirro 0.0.29

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 osawa-naotaka
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.
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from "node:url";
3
+ import { createProject } from "../src/create.js";
4
+
5
+ const targetDir = process.argv[2];
6
+
7
+ if (!targetDir) {
8
+ console.error("Usage: pnpm create cirro <directory>");
9
+ console.error("Example: pnpm create cirro my-site");
10
+ process.exit(1);
11
+ }
12
+
13
+ const templateDir = fileURLToPath(new URL("../template", import.meta.url));
14
+
15
+ try {
16
+ const { projectName, target } = createProject(targetDir, templateDir);
17
+ console.log(`Created a new Cirro site "${projectName}" at ${target}`);
18
+ console.log("");
19
+ console.log("Next steps:");
20
+ console.log(` cd ${targetDir}`);
21
+ console.log(" pnpm install");
22
+ console.log(" pnpm dev");
23
+ } catch (err) {
24
+ console.error(`create-cirro: ${err instanceof Error ? err.message : String(err)}`);
25
+ process.exit(1);
26
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "create-cirro",
3
+ "version": "0.0.29",
4
+ "description": "Scaffold a new Cirro site (React islands SSG with strict CSP)",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/osawa-naotaka/cirro.git"
10
+ },
11
+ "bin": {
12
+ "create-cirro": "./bin/create-cirro.js"
13
+ },
14
+ "files": [
15
+ "bin",
16
+ "src",
17
+ "template"
18
+ ],
19
+ "scripts": {
20
+ "test": "vitest run"
21
+ },
22
+ "devDependencies": {
23
+ "vitest": "^4.1.10"
24
+ }
25
+ }
package/src/create.js ADDED
@@ -0,0 +1,28 @@
1
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import { basename, join, resolve } from "node:path";
3
+
4
+ // スキャフォールディングの本体(16_SCAFFOLDING.md 4.2)。テンプレートを targetDir へコピーし、
5
+ // _package.json → package.json({{PROJECT_NAME}} 置換)・_gitignore → .gitignore にリネームする。
6
+ // 失敗はすべて throw(bin 側がメッセージを表示して非ゼロ終了する)。
7
+ export function createProject(targetDir, templateDir) {
8
+ const target = resolve(targetDir);
9
+ const projectName = basename(target);
10
+
11
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(projectName)) {
12
+ throw new Error(`invalid project name "${projectName}": use lowercase letters, digits, "-", ".", "_" (it becomes the npm package name)`);
13
+ }
14
+ if (existsSync(target) && readdirSync(target).length > 0) {
15
+ throw new Error(`target directory is not empty: ${target}`);
16
+ }
17
+
18
+ mkdirSync(target, { recursive: true });
19
+ cpSync(templateDir, target, { recursive: true });
20
+
21
+ const pkgTemplatePath = join(target, "_package.json");
22
+ const pkg = readFileSync(pkgTemplatePath, "utf-8").replaceAll("{{PROJECT_NAME}}", projectName);
23
+ writeFileSync(join(target, "package.json"), pkg);
24
+ rmSync(pkgTemplatePath);
25
+ renameSync(join(target, "_gitignore"), join(target, ".gitignore"));
26
+
27
+ return { projectName, target };
28
+ }
@@ -0,0 +1,2 @@
1
+ node_modules/
2
+ dist/
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "cirro dev",
8
+ "build": "tsc && cirro build",
9
+ "preview": "vite preview",
10
+ "typecheck": "tsc"
11
+ },
12
+ "dependencies": {
13
+ "cirrojs": "^0.0.29",
14
+ "react": "^19.2.7",
15
+ "react-dom": "^19.2.7"
16
+ },
17
+ "devDependencies": {
18
+ "@types/react": "^19.2.17",
19
+ "@types/react-dom": "^19.2.3",
20
+ "@vitejs/plugin-react": "^6.0.3",
21
+ "typescript": "^7.0.2",
22
+ "vite": "^8.1.4"
23
+ }
24
+ }
@@ -0,0 +1,11 @@
1
+ import { useState } from "react";
2
+
3
+ // クライアントで閉じるリアクティブ要素(島)の最小例。
4
+ export function Counter({ initial = 0 }: { initial?: number }) {
5
+ const [count, setCount] = useState(initial);
6
+ return (
7
+ <button type="button" onClick={() => setCount((c) => c + 1)}>
8
+ count: {count}
9
+ </button>
10
+ );
11
+ }
@@ -0,0 +1,5 @@
1
+ import { createIsland } from "cirrojs/server";
2
+ import islands from "./registry";
3
+
4
+ // 純データの registry から型安全な <Island> を生成する(renderToString はこちら側=サーバー専用)。
5
+ export const Island = createIsland(islands);
@@ -0,0 +1,6 @@
1
+ import { Counter } from "./Counter";
2
+
3
+ // 島の単一の真実。ここに登録した名前を <Island name="..."> とクライアントの両方が参照する。
4
+ export default {
5
+ counter: Counter,
6
+ } as const;
@@ -0,0 +1,24 @@
1
+ import { Link } from "cirrojs";
2
+ import { defineCascadeLayer, resetCss } from "cirrojs/layout";
3
+
4
+ // About ページ(静的・島なし → このページはクライアント JS の島が存在しない)。
5
+ export function AboutPage() {
6
+ defineCascadeLayer();
7
+ resetCss();
8
+ return (
9
+ <html lang="ja">
10
+ <head>
11
+ <meta charSet="utf-8" />
12
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
13
+ <title>About - cirro</title>
14
+ </head>
15
+ <body>
16
+ <h1>About</h1>
17
+ <p>cirro プロトタイプの About ページです。島を含まない純粋な静的ページです。</p>
18
+ <p>
19
+ <Link to="/">← home</Link>
20
+ </p>
21
+ </body>
22
+ </html>
23
+ );
24
+ }
@@ -0,0 +1,32 @@
1
+ import { at, genCssFn, Link } from "cirrojs";
2
+ import { defineCascadeLayer, resetCss } from "cirrojs/layout";
3
+ import { Island } from "../islands/Island";
4
+
5
+ // ホームページ(本文は静的 HTML、Counter だけが島)。
6
+ export function HomePage() {
7
+ // reset css
8
+ defineCascadeLayer();
9
+ resetCss();
10
+
11
+ const cssPC = genCssFn((fn) => at("@media (min-width: 800px)", fn()));
12
+
13
+ const pageTitle = cssPC({ padding: "1rem", font_size: "2rem" });
14
+
15
+ return (
16
+ <html lang="ja">
17
+ <head>
18
+ <meta charSet="utf-8" />
19
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
20
+ <title>cirro prototype</title>
21
+ </head>
22
+ <body>
23
+ <h1 className={pageTitle}>cirro プロトタイプ</h1>
24
+ <p>この本文は静的 HTML です。下のカウンターだけがクライアントで動く「島」です。</p>
25
+ <nav>
26
+ <Link to="/about">about</Link> | <Link to="/posts/hello">post: hello</Link> | <Link to="/posts/world">post: world</Link>
27
+ </nav>
28
+ <Island name="counter" props={{ initial: 3 }} />
29
+ </body>
30
+ </html>
31
+ );
32
+ }
@@ -0,0 +1,27 @@
1
+ import { defineCascadeLayer, resetCss } from "cirrojs/layout";
2
+ import { Island } from "../islands/Island";
3
+ import type { PageProps } from "cirrojs";
4
+ import { Link } from "cirrojs";
5
+
6
+ // 動的ルート /posts/[slug] のページ。params.slug を受け取る。
7
+ export function PostPage({ params }: PageProps<undefined, { slug: string }>) {
8
+ defineCascadeLayer();
9
+ resetCss();
10
+ return (
11
+ <html lang="ja">
12
+ <head>
13
+ <meta charSet="utf-8" />
14
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
15
+ <title>{`post: ${params.slug}`}</title>
16
+ </head>
17
+ <body>
18
+ <h1>{`Post: ${params.slug}`}</h1>
19
+ <p>これは動的ルート /posts/[slug] のページです。</p>
20
+ <p>
21
+ <Link to="/">← home</Link>
22
+ </p>
23
+ <Island name="counter" props={{ initial: 1 }} />
24
+ </body>
25
+ </html>
26
+ );
27
+ }
@@ -0,0 +1,20 @@
1
+ import { createRouteFn } from "cirrojs";
2
+ import { AboutPage } from "./pages/about";
3
+ import { HomePage } from "./pages/home";
4
+ import { PostPage } from "./pages/post";
5
+
6
+ export { runWithRegistry } from "cirrojs";
7
+
8
+ const { defineRoutes, route } = createRouteFn();
9
+
10
+ // サイトのルート定義(Config Base Routing)。
11
+ export default defineRoutes(
12
+ route({ type: "static", path: "/index.html", component: HomePage }),
13
+ route({ type: "static", path: "/about.html", component: AboutPage }),
14
+ route({
15
+ type: "dynamic",
16
+ path: ({ slug }) => `/posts/${slug}.html`,
17
+ getStaticPaths: () => [{ slug: "hello" }, { slug: "world" }],
18
+ component: PostPage,
19
+ }),
20
+ );
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "verbatimModuleSyntax": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
12
+ "types": ["vite/client"]
13
+ },
14
+ "include": ["src", "vite.config.ts"]
15
+ }
@@ -0,0 +1,8 @@
1
+ import react from "@vitejs/plugin-react";
2
+ import { cirro } from "cirrojs/vite";
3
+ import { defineConfig } from "vite";
4
+
5
+ // React プラグインは利用者が明示的に追加する(cirro は内包しない)。
6
+ export default defineConfig({
7
+ plugins: [react(), cirro({ routes: "./src/routes.ts", islands: "./src/islands/registry.ts" })],
8
+ });