create-nowaki 0.9.0 → 0.10.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.
- package/index.js +140 -14
- package/package.json +2 -2
- package/template/AGENTS.md +88 -0
- package/template/CLAUDE.md +10 -0
- package/template/README.md +36 -0
- package/template/_gitignore +12 -0
- package/template/nowaki-env.d.ts +42 -0
- package/template/package.json +1 -1
- package/template/tsconfig.json +33 -0
package/index.js
CHANGED
|
@@ -1,31 +1,157 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
// `npm create nowaki` のスキャフォールダ。依存ゼロ(Node 組み込みのみ)。
|
|
3
|
+
// 対話ウィザード(TTY 時): プロジェクト名・パッケージマネージャ・git・依存インストール。
|
|
4
|
+
// 非対話(CI / `-y` / パイプ)では既定値で黙って生成する。
|
|
5
|
+
//
|
|
6
|
+
// フラグ: -y/--yes(質問を飛ばす), --install(非対話でも入れる), --no-install,
|
|
7
|
+
// --no-git, --pm <npm|pnpm|yarn|bun>
|
|
8
|
+
|
|
9
|
+
import { cp, mkdir, readdir, readFile, writeFile, rename } from "node:fs/promises";
|
|
3
10
|
import path from "node:path";
|
|
4
11
|
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { createInterface } from "node:readline/promises";
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
14
|
+
import process from "node:process";
|
|
15
|
+
|
|
16
|
+
const { stdin, stdout, env, argv, platform } = process;
|
|
17
|
+
|
|
18
|
+
// --- 色(NO_COLOR と非 TTY を尊重)---
|
|
19
|
+
const useColor = !!stdout.isTTY && !env.NO_COLOR;
|
|
20
|
+
const wrap = (code) => (s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : `${s}`);
|
|
21
|
+
const cyan = wrap("36");
|
|
22
|
+
const dim = wrap("2");
|
|
23
|
+
const bold = wrap("1");
|
|
24
|
+
const green = wrap("32");
|
|
25
|
+
const red = wrap("31");
|
|
26
|
+
const yellow = wrap("33");
|
|
27
|
+
|
|
28
|
+
// --- 引数 ---
|
|
29
|
+
const args = argv.slice(2);
|
|
30
|
+
const flags = new Set(args.filter((a) => a.startsWith("-")));
|
|
31
|
+
const positionals = args.filter((a) => !a.startsWith("-"));
|
|
32
|
+
const pmFlag = (() => {
|
|
33
|
+
const i = args.indexOf("--pm");
|
|
34
|
+
return i >= 0 ? args[i + 1] : null;
|
|
35
|
+
})();
|
|
36
|
+
const yes = flags.has("-y") || flags.has("--yes");
|
|
37
|
+
const noInstall = flags.has("--no-install");
|
|
38
|
+
const forceInstall = flags.has("--install");
|
|
39
|
+
const noGit = flags.has("--no-git");
|
|
40
|
+
const interactive = !!stdin.isTTY && !!stdout.isTTY && !yes;
|
|
41
|
+
|
|
42
|
+
const templateDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "template");
|
|
43
|
+
|
|
44
|
+
// --- パッケージマネージャ検出(`npm create` の user-agent から)---
|
|
45
|
+
function detectPM() {
|
|
46
|
+
const ua = env.npm_config_user_agent || "";
|
|
47
|
+
if (ua.startsWith("pnpm")) return "pnpm";
|
|
48
|
+
if (ua.startsWith("yarn")) return "yarn";
|
|
49
|
+
if (ua.startsWith("bun")) return "bun";
|
|
50
|
+
return "npm";
|
|
51
|
+
}
|
|
5
52
|
|
|
6
|
-
|
|
53
|
+
// --- バナー ---
|
|
54
|
+
console.log();
|
|
55
|
+
console.log(` ${cyan(bold("Nowaki"))} ${dim("野分")} ${dim("· create a new app")}`);
|
|
56
|
+
console.log();
|
|
57
|
+
|
|
58
|
+
const rl = interactive ? createInterface({ input: stdin, output: stdout }) : null;
|
|
59
|
+
async function ask(question, def) {
|
|
60
|
+
if (!rl) return def;
|
|
61
|
+
const a = (await rl.question(` ${question} ${dim(`(${def})`)} `)).trim();
|
|
62
|
+
return a || def;
|
|
63
|
+
}
|
|
64
|
+
async function confirm(question, def = true) {
|
|
65
|
+
if (!rl) return def;
|
|
66
|
+
const a = (await rl.question(` ${question} ${dim(def ? "(Y/n)" : "(y/N)")} `)).trim().toLowerCase();
|
|
67
|
+
if (!a) return def;
|
|
68
|
+
return a === "y" || a === "yes";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- 質問 ---
|
|
72
|
+
let name = positionals[0] || (await ask("Project name?", interactive ? "my-app" : "nowaki-app"));
|
|
73
|
+
name = name.trim() || "nowaki-app";
|
|
74
|
+
// package.json の name は valid な npm 名に正規化(ディレクトリ名は入力のまま)
|
|
75
|
+
const pkgName = name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+/, "") || "nowaki-app";
|
|
7
76
|
const dest = path.resolve(process.cwd(), name);
|
|
8
|
-
const templateDir = path.join(
|
|
9
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
10
|
-
"template",
|
|
11
|
-
);
|
|
12
77
|
|
|
13
|
-
//
|
|
78
|
+
// 既存の空でないディレクトリは中止
|
|
14
79
|
try {
|
|
15
80
|
const existing = await readdir(dest);
|
|
16
81
|
if (existing.length > 0) {
|
|
17
|
-
console.error(
|
|
82
|
+
console.error(`\n ${red("✗")} "${name}" already exists and is not empty. Aborting.\n`);
|
|
83
|
+
rl?.close();
|
|
18
84
|
process.exit(1);
|
|
19
85
|
}
|
|
20
86
|
} catch {
|
|
21
|
-
//
|
|
87
|
+
// 無ければOK
|
|
22
88
|
}
|
|
23
89
|
|
|
90
|
+
let pm = pmFlag || detectPM();
|
|
91
|
+
if (interactive && !pmFlag) pm = (await ask("Package manager?", pm)).trim() || pm;
|
|
92
|
+
if (!["npm", "pnpm", "yarn", "bun"].includes(pm)) pm = "npm";
|
|
93
|
+
const doGit = noGit ? false : await confirm("Initialize a git repository?", false);
|
|
94
|
+
// 依存インストール: 対話なら確認(既定 Yes)。非対話(-y / CI / パイプ)は自動実行しない
|
|
95
|
+
//(驚き防止)。非対話で入れたいときは --install。--no-install は常にスキップ。
|
|
96
|
+
const doInstall = noInstall
|
|
97
|
+
? false
|
|
98
|
+
: forceInstall
|
|
99
|
+
? true
|
|
100
|
+
: interactive
|
|
101
|
+
? await confirm(`Install dependencies with ${pm}?`, true)
|
|
102
|
+
: false;
|
|
103
|
+
rl?.close();
|
|
104
|
+
|
|
105
|
+
// --- 生成 ---
|
|
24
106
|
await mkdir(dest, { recursive: true });
|
|
25
107
|
await cp(templateDir, dest, { recursive: true });
|
|
26
108
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
109
|
+
// npm は公開時に .gitignore を除外するので、テンプレートでは _gitignore で持ち、ここで戻す。
|
|
110
|
+
try {
|
|
111
|
+
await rename(path.join(dest, "_gitignore"), path.join(dest, ".gitignore"));
|
|
112
|
+
} catch {
|
|
113
|
+
// _gitignore が無ければ素通し
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// package.json の name を設定
|
|
117
|
+
try {
|
|
118
|
+
const pkgPath = path.join(dest, "package.json");
|
|
119
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
120
|
+
pkg.name = pkgName;
|
|
121
|
+
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
122
|
+
} catch {
|
|
123
|
+
// package.json が無ければ素通し
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log(`\n ${green("✓")} created ${bold(name)} ${dim(`(${path.relative(process.cwd(), dest) || "."})`)}`);
|
|
127
|
+
|
|
128
|
+
// git init(任意)
|
|
129
|
+
if (doGit) {
|
|
130
|
+
try {
|
|
131
|
+
execFileSync("git", ["init", "-q"], { cwd: dest, stdio: "ignore" });
|
|
132
|
+
console.log(` ${green("✓")} initialized a git repository`);
|
|
133
|
+
} catch {
|
|
134
|
+
console.log(` ${yellow("!")} skipped git init (git not found)`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 依存インストール(任意)
|
|
139
|
+
let installed = false;
|
|
140
|
+
if (doInstall) {
|
|
141
|
+
console.log(`\n ${dim(`installing dependencies with ${pm}…`)}\n`);
|
|
142
|
+
try {
|
|
143
|
+
execFileSync(pm, ["install"], { cwd: dest, stdio: "inherit", shell: platform === "win32" });
|
|
144
|
+
installed = true;
|
|
145
|
+
console.log(`\n ${green("✓")} dependencies installed`);
|
|
146
|
+
} catch {
|
|
147
|
+
console.log(`\n ${red("✗")} \`${pm} install\` failed — run it yourself after \`cd ${name}\`.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// --- 次のステップ ---
|
|
152
|
+
const runDev = pm === "npm" || pm === "bun" ? `${pm} run dev` : `${pm} dev`;
|
|
153
|
+
console.log(`\n ${bold("Next steps:")}`);
|
|
154
|
+
console.log(` cd ${name}`);
|
|
155
|
+
if (!installed) console.log(` ${pm} install`);
|
|
156
|
+
console.log(` ${cyan(runDev)} ${dim("# → http://localhost:3000")}`);
|
|
157
|
+
console.log(`\n ${dim("Docs:")} ${cyan("https://nowaki.dev/docs")}\n`);
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-nowaki",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Scaffold a new Nowaki app",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"author": "
|
|
6
|
+
"author": "VorEdge <dev@voredge.com>",
|
|
7
7
|
"homepage": "https://nowaki.dev",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# AGENTS.md — building this Nowaki app
|
|
2
|
+
|
|
3
|
+
Guidance for AI coding agents (Claude Code, Codex, Gemini, Cursor, Copilot, Grok,
|
|
4
|
+
Kimi, …) working in this app. Nowaki is a full-stack framework with a Rust
|
|
5
|
+
toolchain: pages render to HTML on the server, and only the components under
|
|
6
|
+
`islands/` ship JavaScript and hydrate. Full docs: <https://nowaki.dev/docs>.
|
|
7
|
+
|
|
8
|
+
## Commands
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm run dev # nowaki dev — dev server with hot reload (http://localhost:3000)
|
|
12
|
+
npm run build # nowaki build — production build (dist/client + dist/server)
|
|
13
|
+
npm run start # nowaki start — serve the production build
|
|
14
|
+
npm run prerender # nowaki prerender — static export
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Project layout
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
routes/ file-based routes (pages + API). _layout.tsx, _middleware.ts,
|
|
21
|
+
_404.tsx, _500.tsx, [slug].tsx, api/*.ts
|
|
22
|
+
islands/ interactive components — THE ONLY things that ship JS and hydrate
|
|
23
|
+
components/ shared server components (no client JS)
|
|
24
|
+
lib/ shared server code
|
|
25
|
+
actions/ "use server" RPC modules (optional)
|
|
26
|
+
nowaki.config.mjs plugins (optional)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Rules — follow these when generating code
|
|
30
|
+
|
|
31
|
+
1. **Preact, not React.** Import hooks from `preact/hooks`
|
|
32
|
+
(`import { useState } from "preact/hooks"`). `react`, `react-dom`, and
|
|
33
|
+
`react/jsx-runtime` are aliased to `preact/compat`, so many React libraries
|
|
34
|
+
work, but write Preact.
|
|
35
|
+
2. **Interactivity lives in `islands/`.** A component with `useState`, event
|
|
36
|
+
handlers, effects, etc. must be a file under `islands/`. A route or component
|
|
37
|
+
that imports an island stays server-HTML; only the island hydrates. Don't put
|
|
38
|
+
`onClick`/hooks in route or `components/` files.
|
|
39
|
+
3. **Use explicit file extensions in relative imports**:
|
|
40
|
+
`import Counter from "../islands/Counter.tsx"` (not `"../islands/Counter"`).
|
|
41
|
+
4. **Routes** are components with an optional server-only `loader` and `action`:
|
|
42
|
+
```tsx
|
|
43
|
+
// routes/blog/[slug].tsx
|
|
44
|
+
export const loader = async ({ params }) => ({ post: await db.post(params.slug) }); // server-only
|
|
45
|
+
export async function action(ctx) { // runs on non-GET requests
|
|
46
|
+
const form = await ctx.formData();
|
|
47
|
+
return ctx.redirect("/blog"); // Post/Redirect/Get
|
|
48
|
+
}
|
|
49
|
+
export default function Post({ data }) { return <article><h1>{data.post.title}</h1></article>; }
|
|
50
|
+
```
|
|
51
|
+
5. **API routes** are `routes/api/*.ts` with per-method exports; return a value
|
|
52
|
+
(JSON-encoded) or a `Response`:
|
|
53
|
+
```ts
|
|
54
|
+
export const GET = (ctx) => ctx.json({ ok: true });
|
|
55
|
+
export const POST = async (ctx) => ctx.json({ got: await ctx.bodyJson() });
|
|
56
|
+
```
|
|
57
|
+
6. **Islands** are default-exported components:
|
|
58
|
+
```tsx
|
|
59
|
+
// islands/Counter.tsx
|
|
60
|
+
import { useState } from "preact/hooks";
|
|
61
|
+
export default function Counter({ start = 0 }) {
|
|
62
|
+
const [n, setN] = useState(start);
|
|
63
|
+
return <button onClick={() => setN(n + 1)}>count: {n}</button>;
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
7. **Server functions** — a module with a top-of-file `"use server"` directive
|
|
67
|
+
becomes RPC; the client gets a tiny fetch proxy, the implementation stays on
|
|
68
|
+
the server. Validate arguments; read auth via `getContext()`:
|
|
69
|
+
```ts
|
|
70
|
+
// actions/todos.ts
|
|
71
|
+
"use server";
|
|
72
|
+
import { getContext } from "@nowaki-dev/runtime/server/functions.mjs";
|
|
73
|
+
export async function addTodo(text: string) { /* runs on the server */ }
|
|
74
|
+
```
|
|
75
|
+
Call it from an island like a normal async function:
|
|
76
|
+
`import { addTodo } from "../actions/todos.ts";`
|
|
77
|
+
8. **Jetstream islands** (server-reactive, zero client JS) — give an island
|
|
78
|
+
`export const live = { state, on }`; buttons use `data-live="handler"` instead
|
|
79
|
+
of `onClick`. State lives on the server; the server pushes HTML patches.
|
|
80
|
+
|
|
81
|
+
## Don't
|
|
82
|
+
|
|
83
|
+
- Don't add a build tool (Vite/webpack/Babel) or a `tsconfig` paths setup — the
|
|
84
|
+
Rust toolchain handles transform/resolve/bundle.
|
|
85
|
+
- Don't import React directly hoping for React internals; it's Preact under the hood.
|
|
86
|
+
- Don't put interactive components outside `islands/`.
|
|
87
|
+
- Don't read secrets on the client. Only `PUBLIC_*` env vars reach the browser;
|
|
88
|
+
everything else is server-only (loaders, actions, server functions).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This app's agent guidance is in **[AGENTS.md](./AGENTS.md)** — read it first.
|
|
4
|
+
|
|
5
|
+
TL;DR: Nowaki renders pages to HTML; only components under `islands/` ship JS and
|
|
6
|
+
hydrate. Write **Preact** (hooks from `preact/hooks`), use **explicit file
|
|
7
|
+
extensions** in relative imports, put interactive components in `islands/`, and
|
|
8
|
+
use route `loader`/`action`, `routes/api/*.ts`, `"use server"` modules, or
|
|
9
|
+
Jetstream islands (`export const live`) as needed. Commands: `npm run dev`,
|
|
10
|
+
`npm run build`, `npm run start`. Docs: https://nowaki.dev/docs
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Nowaki app
|
|
2
|
+
|
|
3
|
+
A full-stack app built with [Nowaki](https://nowaki.dev) — full-stack like
|
|
4
|
+
Next.js, islands like Astro, on a Rust toolchain. Pages render to HTML; only the
|
|
5
|
+
components under `islands/` ship JavaScript and hydrate.
|
|
6
|
+
|
|
7
|
+
## Develop
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
npm run dev # http://localhost:3000
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Other scripts: `npm run build`, `npm run start`, `npm run prerender`.
|
|
15
|
+
Pass `--host` to expose dev on your LAN, `--open` to open the browser:
|
|
16
|
+
`npm run dev -- --host --open`.
|
|
17
|
+
|
|
18
|
+
## Structure
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
routes/ pages + API (file-based). _layout.tsx, _middleware.ts, [slug].tsx, api/*.ts
|
|
22
|
+
islands/ interactive components — the only things that ship JS
|
|
23
|
+
components/ shared server components (no client JS)
|
|
24
|
+
lib/ shared server code
|
|
25
|
+
actions/ "use server" RPC modules (optional)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Notes
|
|
29
|
+
|
|
30
|
+
- Write **Preact** — import hooks from `preact/hooks`. (`react` is aliased to
|
|
31
|
+
`preact/compat`, so many React libraries work.)
|
|
32
|
+
- Use **explicit file extensions** in relative imports: `../islands/Counter.tsx`.
|
|
33
|
+
- A route's `loader` runs only on the server; `action` handles non-GET requests.
|
|
34
|
+
- `AGENTS.md` documents the conventions for AI coding agents.
|
|
35
|
+
|
|
36
|
+
Docs: <https://nowaki.dev/docs>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Nowaki アンビエント型。CSS / アセット / 仮想モジュールの import をエディタが理解できるように。
|
|
2
|
+
// 編集不要(フレームワークの import 規約を型に伝えるだけ)。
|
|
3
|
+
|
|
4
|
+
declare module "*.module.css" {
|
|
5
|
+
const classes: Record<string, string>;
|
|
6
|
+
export default classes;
|
|
7
|
+
}
|
|
8
|
+
declare module "*.css" {
|
|
9
|
+
const css: string;
|
|
10
|
+
export default css;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// アセット import は配信 URL(文字列)になる。
|
|
14
|
+
declare module "*.svg" { const url: string; export default url; }
|
|
15
|
+
declare module "*.png" { const url: string; export default url; }
|
|
16
|
+
declare module "*.jpg" { const url: string; export default url; }
|
|
17
|
+
declare module "*.jpeg" { const url: string; export default url; }
|
|
18
|
+
declare module "*.gif" { const url: string; export default url; }
|
|
19
|
+
declare module "*.webp" { const url: string; export default url; }
|
|
20
|
+
declare module "*.avif" { const url: string; export default url; }
|
|
21
|
+
declare module "*.ico" { const url: string; export default url; }
|
|
22
|
+
declare module "*.woff" { const url: string; export default url; }
|
|
23
|
+
declare module "*.woff2" { const url: string; export default url; }
|
|
24
|
+
declare module "*.ttf" { const url: string; export default url; }
|
|
25
|
+
declare module "*.otf" { const url: string; export default url; }
|
|
26
|
+
declare module "*.mp4" { const url: string; export default url; }
|
|
27
|
+
declare module "*.webm" { const url: string; export default url; }
|
|
28
|
+
declare module "*.mp3" { const url: string; export default url; }
|
|
29
|
+
declare module "*.wav" { const url: string; export default url; }
|
|
30
|
+
declare module "*.pdf" { const url: string; export default url; }
|
|
31
|
+
|
|
32
|
+
// プラグインの仮想モジュール(nowaki.config の resolveId/load)。任意の export を許す。
|
|
33
|
+
declare module "virtual:*";
|
|
34
|
+
|
|
35
|
+
// import.meta.env(PUBLIC_* と MODE がビルド時に inline される)。
|
|
36
|
+
interface ImportMetaEnv {
|
|
37
|
+
readonly MODE: string;
|
|
38
|
+
readonly [key: `PUBLIC_${string}`]: string | undefined;
|
|
39
|
+
}
|
|
40
|
+
interface ImportMeta {
|
|
41
|
+
readonly env: ImportMetaEnv;
|
|
42
|
+
}
|
package/template/package.json
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"allowImportingTsExtensions": true,
|
|
7
|
+
"verbatimModuleSyntax": false,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"isolatedModules": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
15
|
+
"jsx": "react-jsx",
|
|
16
|
+
"jsxImportSource": "preact",
|
|
17
|
+
"types": [],
|
|
18
|
+
"paths": {
|
|
19
|
+
"react": ["./node_modules/preact/compat"],
|
|
20
|
+
"react-dom": ["./node_modules/preact/compat"],
|
|
21
|
+
"react/jsx-runtime": ["./node_modules/preact/jsx-runtime"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"include": [
|
|
25
|
+
"nowaki-env.d.ts",
|
|
26
|
+
"nowaki.config.*",
|
|
27
|
+
"routes",
|
|
28
|
+
"islands",
|
|
29
|
+
"components",
|
|
30
|
+
"lib",
|
|
31
|
+
"actions"
|
|
32
|
+
]
|
|
33
|
+
}
|