@oss-ma/tpl 1.0.29 → 1.0.31
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/dist/engine/prompt.js +23 -52
- package/dist/engine/render.js +33 -2
- package/package.json +3 -4
- package/resources/templates/react-ts/files/package.json +6 -6
- package/resources/templates/react-ts/files/src/app/App.tsx +10 -1
- package/resources/templates/react-ts/files/src/app/main.tsx +15 -1
- package/resources/templates/react-ts/files/src/features/example/ExampleFeature.tsx +19 -3
- package/resources/templates/react-ts/files/src/pages/HomePage.tsx +24 -0
- package/resources/templates/react-ts/template.yaml +17 -2
package/dist/engine/prompt.js
CHANGED
|
@@ -1,67 +1,38 @@
|
|
|
1
1
|
// cli/src/engine/prompt.ts
|
|
2
|
-
import
|
|
2
|
+
import { input, select } from "@inquirer/prompts";
|
|
3
3
|
export async function askQuestions(questions, opts = {}) {
|
|
4
4
|
if (!questions?.length)
|
|
5
5
|
return {};
|
|
6
|
-
// --yes :
|
|
6
|
+
// --yes : no interaction, use defaults
|
|
7
7
|
if (opts.yes) {
|
|
8
8
|
const answers = {};
|
|
9
9
|
for (const q of questions)
|
|
10
10
|
answers[q.name] = String(q.default ?? "");
|
|
11
11
|
return answers;
|
|
12
12
|
}
|
|
13
|
-
const defs = questions.map((q) => {
|
|
14
|
-
const base = {
|
|
15
|
-
name: q.name,
|
|
16
|
-
message: q.message,
|
|
17
|
-
initial: q.default
|
|
18
|
-
};
|
|
19
|
-
if (q.choices?.length) {
|
|
20
|
-
return { type: "select", ...base, choices: q.choices.map((c) => ({ title: c, value: c })) };
|
|
21
|
-
}
|
|
22
|
-
return { type: "text", ...base };
|
|
23
|
-
});
|
|
24
|
-
// Sur certains terminaux Windows, prompts peut déclencher onCancel inopinément.
|
|
25
|
-
// Stratégie: si cancel -> fallback sur defaults au lieu de crash.
|
|
26
|
-
const res = (await prompts(defs, {
|
|
27
|
-
onCancel: () => true
|
|
28
|
-
}));
|
|
29
13
|
const out = {};
|
|
30
14
|
for (const q of questions) {
|
|
31
|
-
|
|
32
|
-
|
|
15
|
+
try {
|
|
16
|
+
if (q.choices?.length) {
|
|
17
|
+
const answer = await select({
|
|
18
|
+
message: q.message,
|
|
19
|
+
default: q.default,
|
|
20
|
+
choices: q.choices.map((c) => ({ name: c, value: c })),
|
|
21
|
+
});
|
|
22
|
+
out[q.name] = String(answer);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const answer = await input({
|
|
26
|
+
message: q.message,
|
|
27
|
+
default: String(q.default ?? ""),
|
|
28
|
+
});
|
|
29
|
+
out[q.name] = answer.trim() || String(q.default ?? "");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// User cancelled (Ctrl+C) → fallback to default
|
|
34
|
+
out[q.name] = String(q.default ?? "");
|
|
35
|
+
}
|
|
33
36
|
}
|
|
34
37
|
return out;
|
|
35
38
|
}
|
|
36
|
-
// import prompts from "prompts";
|
|
37
|
-
// import type { TemplateQuestion } from "./loadTemplate.js";
|
|
38
|
-
// export async function askQuestions(
|
|
39
|
-
// questions: TemplateQuestion[] | undefined,
|
|
40
|
-
// opts: { yes?: boolean } = {}
|
|
41
|
-
// ): Promise<Record<string, string>> {
|
|
42
|
-
// if (!questions?.length) return {};
|
|
43
|
-
// if (opts.yes) {
|
|
44
|
-
// const answers: Record<string, string> = {};
|
|
45
|
-
// for (const q of questions) {
|
|
46
|
-
// answers[q.name] = String(q.default ?? "");
|
|
47
|
-
// }
|
|
48
|
-
// return answers;
|
|
49
|
-
// }
|
|
50
|
-
// const defs = questions.map((q) => {
|
|
51
|
-
// const base: any = {
|
|
52
|
-
// name: q.name,
|
|
53
|
-
// message: q.message,
|
|
54
|
-
// initial: q.default
|
|
55
|
-
// };
|
|
56
|
-
// if (q.choices?.length) {
|
|
57
|
-
// return { type: "select", ...base, choices: q.choices.map((c) => ({ title: c, value: c })) };
|
|
58
|
-
// }
|
|
59
|
-
// return { type: "text", ...base };
|
|
60
|
-
// });
|
|
61
|
-
// const res = await prompts(defs, {
|
|
62
|
-
// onCancel: () => {
|
|
63
|
-
// throw new Error("Cancelled by user.");
|
|
64
|
-
// }
|
|
65
|
-
// });
|
|
66
|
-
// return res as Record<string, string>;
|
|
67
|
-
// }
|
package/dist/engine/render.js
CHANGED
|
@@ -1,7 +1,38 @@
|
|
|
1
1
|
// cli/src/engine/render.ts
|
|
2
|
+
/**
|
|
3
|
+
* Render a template string with variable substitution and conditional blocks.
|
|
4
|
+
*
|
|
5
|
+
* Supported syntax:
|
|
6
|
+
* {{variableName}} — replaced by vars[variableName]
|
|
7
|
+
* {{#if variableName}}...{{/if}} — block included only if vars[variableName] is truthy and !== "none"
|
|
8
|
+
* {{#unless variableName}}...{{/unless}} — block included only if vars[variableName] is falsy or === "none"
|
|
9
|
+
*
|
|
10
|
+
* Blocks can span multiple lines.
|
|
11
|
+
* Nested blocks are not supported.
|
|
12
|
+
*/
|
|
2
13
|
export function renderString(input, vars) {
|
|
3
|
-
|
|
14
|
+
// 1) Process {{#if var}}...{{/if}} blocks
|
|
15
|
+
let output = input.replace(/\{\{#if\s+([a-zA-Z0-9_]+)\s*\}\}([\s\S]*?)\{\{\/if\}\}/g, (_, key, content) => {
|
|
16
|
+
const val = vars[key];
|
|
17
|
+
return isActive(val) ? content : "";
|
|
18
|
+
});
|
|
19
|
+
// 2) Process {{#unless var}}...{{/unless}} blocks
|
|
20
|
+
output = output.replace(/\{\{#unless\s+([a-zA-Z0-9_]+)\s*\}\}([\s\S]*?)\{\{\/unless\}\}/g, (_, key, content) => {
|
|
21
|
+
const val = vars[key];
|
|
22
|
+
return isActive(val) ? "" : content;
|
|
23
|
+
});
|
|
24
|
+
// 3) Replace {{variable}} placeholders
|
|
25
|
+
output = output.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, key) => {
|
|
4
26
|
const v = vars[key];
|
|
5
|
-
return v !== undefined ? String(v) : `{{${key}}}`;
|
|
27
|
+
return v !== undefined ? String(v) : `{{${key}}}`;
|
|
6
28
|
});
|
|
29
|
+
// 4) Clean up blank lines left by removed blocks (max 1 consecutive blank line)
|
|
30
|
+
output = output.replace(/\n{3,}/g, "\n\n");
|
|
31
|
+
return output;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A variable is "active" if it exists, is not empty, and is not "none".
|
|
35
|
+
*/
|
|
36
|
+
function isActive(val) {
|
|
37
|
+
return val !== undefined && val !== "" && val !== "none";
|
|
7
38
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-ma/tpl",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.31",
|
|
4
4
|
"description": "Generate, enforce and maintain clean project architectures",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -27,18 +27,17 @@
|
|
|
27
27
|
"test:smoke": "node --test dist/tests/smoke.test.js"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"@inquirer/prompts": "^8.2.1",
|
|
30
31
|
"commander": "^12.1.0",
|
|
31
32
|
"execa": "^9.3.0",
|
|
32
33
|
"fs-extra": "^11.2.0",
|
|
33
34
|
"picocolors": "^1.0.0",
|
|
34
|
-
"prompts": "^2.4.2",
|
|
35
35
|
"yaml": "^2.5.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/fs-extra": "^11.0.4",
|
|
39
39
|
"@types/node": "^22.7.0",
|
|
40
|
-
"@types/prompts": "^2.4.9",
|
|
41
40
|
"tsx": "^4.16.2",
|
|
42
41
|
"typescript": "^5.5.4"
|
|
43
42
|
}
|
|
44
|
-
}
|
|
43
|
+
}
|
|
@@ -18,17 +18,17 @@
|
|
|
18
18
|
"audit": "npm audit --audit-level=high"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@tanstack/react-query": "^5.56.0",
|
|
22
21
|
"react": "^18.3.0",
|
|
23
|
-
"react-dom": "^18.3.0",
|
|
24
|
-
"react-router-dom": "^6.26.0",
|
|
25
|
-
"zustand": "^4.5.0"
|
|
22
|
+
"react-dom": "^18.3.0"{{#if routing}},
|
|
23
|
+
"react-router-dom": "^6.26.0"{{/if}}{{#if state}},
|
|
24
|
+
"zustand": "^4.5.0"{{/if}}{{#if fetching}},
|
|
25
|
+
"@tanstack/react-query": "^5.56.0"{{/if}}
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@commitlint/cli": "^19.3.0",
|
|
29
29
|
"@commitlint/config-conventional": "^19.2.0",
|
|
30
|
-
"@eslint/js": "^9.0.0",
|
|
31
|
-
"@tanstack/react-query-devtools": "^5.56.0",
|
|
30
|
+
"@eslint/js": "^9.0.0",{{#if fetching}}
|
|
31
|
+
"@tanstack/react-query-devtools": "^5.56.0",{{/if}}
|
|
32
32
|
"@testing-library/jest-dom": "^6.5.0",
|
|
33
33
|
"@testing-library/react": "^16.0.0",
|
|
34
34
|
"@testing-library/user-event": "^14.5.0",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
{{#if routing}}
|
|
1
2
|
import { Routes, Route } from "react-router-dom";
|
|
2
3
|
import { HomePage } from "@/pages/HomePage";
|
|
3
4
|
import { NotFoundPage } from "@/pages/NotFoundPage";
|
|
@@ -9,4 +10,12 @@ export function App() {
|
|
|
9
10
|
<Route path="*" element={<NotFoundPage />} />
|
|
10
11
|
</Routes>
|
|
11
12
|
);
|
|
12
|
-
}
|
|
13
|
+
}
|
|
14
|
+
{{/if}}
|
|
15
|
+
{{#unless routing}}
|
|
16
|
+
import { HomePage } from "@/pages/HomePage";
|
|
17
|
+
|
|
18
|
+
export function App() {
|
|
19
|
+
return <HomePage />;
|
|
20
|
+
}
|
|
21
|
+
{{/unless}}
|
|
@@ -1,27 +1,41 @@
|
|
|
1
1
|
import { StrictMode } from "react";
|
|
2
2
|
import { createRoot } from "react-dom/client";
|
|
3
|
+
{{#if routing}}
|
|
3
4
|
import { BrowserRouter } from "react-router-dom";
|
|
5
|
+
{{/if}}
|
|
6
|
+
{{#if fetching}}
|
|
4
7
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
5
8
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
|
9
|
+
{{/if}}
|
|
6
10
|
import { App } from "@/app/App";
|
|
7
11
|
import "@/app/index.css";
|
|
12
|
+
{{#if fetching}}
|
|
8
13
|
|
|
9
14
|
const queryClient = new QueryClient({
|
|
10
15
|
defaultOptions: {
|
|
11
16
|
queries: {
|
|
12
|
-
staleTime: 1000 * 60 * 5,
|
|
17
|
+
staleTime: 1000 * 60 * 5,
|
|
13
18
|
retry: 1,
|
|
14
19
|
},
|
|
15
20
|
},
|
|
16
21
|
});
|
|
22
|
+
{{/if}}
|
|
17
23
|
|
|
18
24
|
createRoot(document.getElementById("root")!).render(
|
|
19
25
|
<StrictMode>
|
|
26
|
+
{{#if fetching}}
|
|
20
27
|
<QueryClientProvider client={queryClient}>
|
|
28
|
+
{{/if}}
|
|
29
|
+
{{#if routing}}
|
|
21
30
|
<BrowserRouter>
|
|
31
|
+
{{/if}}
|
|
22
32
|
<App />
|
|
33
|
+
{{#if routing}}
|
|
23
34
|
</BrowserRouter>
|
|
35
|
+
{{/if}}
|
|
36
|
+
{{#if fetching}}
|
|
24
37
|
<ReactQueryDevtools initialIsOpen={false} />
|
|
25
38
|
</QueryClientProvider>
|
|
39
|
+
{{/if}}
|
|
26
40
|
</StrictMode>
|
|
27
41
|
);
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
{{#if fetching}}
|
|
1
2
|
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
{{/if}}
|
|
4
|
+
{{#if state}}
|
|
2
5
|
import { useCounterStore } from "./store";
|
|
6
|
+
{{/if}}
|
|
3
7
|
import { Button } from "@/shared/ui/Button";
|
|
8
|
+
{{#if fetching}}
|
|
4
9
|
|
|
5
10
|
interface Post {
|
|
6
11
|
id: number;
|
|
@@ -12,18 +17,23 @@ async function fetchPosts(): Promise<Post[]> {
|
|
|
12
17
|
if (!res.ok) throw new Error("Failed to fetch posts");
|
|
13
18
|
return res.json();
|
|
14
19
|
}
|
|
20
|
+
{{/if}}
|
|
15
21
|
|
|
16
22
|
export function ExampleFeature() {
|
|
23
|
+
{{#if state}}
|
|
17
24
|
const { count, increment, decrement, reset } = useCounterStore();
|
|
25
|
+
{{/if}}
|
|
26
|
+
{{#if fetching}}
|
|
18
27
|
|
|
19
28
|
const { data: posts, isLoading, isError } = useQuery({
|
|
20
29
|
queryKey: ["posts"],
|
|
21
30
|
queryFn: fetchPosts,
|
|
22
31
|
});
|
|
32
|
+
{{/if}}
|
|
23
33
|
|
|
24
34
|
return (
|
|
25
35
|
<section style={{ marginTop: "2rem" }}>
|
|
26
|
-
{
|
|
36
|
+
{{#if state}}
|
|
27
37
|
<div style={{ marginBottom: "2rem" }}>
|
|
28
38
|
<h2>Counter (Zustand)</h2>
|
|
29
39
|
<p style={{ fontSize: "2rem", margin: "0.5rem 0" }}>{count}</p>
|
|
@@ -33,8 +43,8 @@ export function ExampleFeature() {
|
|
|
33
43
|
<Button onClick={reset} variant="secondary">Reset</Button>
|
|
34
44
|
</div>
|
|
35
45
|
</div>
|
|
36
|
-
|
|
37
|
-
{
|
|
46
|
+
{{/if}}
|
|
47
|
+
{{#if fetching}}
|
|
38
48
|
<div>
|
|
39
49
|
<h2>Posts (TanStack Query)</h2>
|
|
40
50
|
{isLoading && <p>Loading...</p>}
|
|
@@ -49,6 +59,12 @@ export function ExampleFeature() {
|
|
|
49
59
|
</ul>
|
|
50
60
|
)}
|
|
51
61
|
</div>
|
|
62
|
+
{{/if}}
|
|
63
|
+
{{#unless state}}
|
|
64
|
+
{{#unless fetching}}
|
|
65
|
+
<p>Feature module — add your components here.</p>
|
|
66
|
+
{{/unless}}
|
|
67
|
+
{{/unless}}
|
|
52
68
|
</section>
|
|
53
69
|
);
|
|
54
70
|
}
|
|
@@ -1,13 +1,37 @@
|
|
|
1
|
+
{{#if state}}
|
|
1
2
|
import { ExampleFeature } from "@/features/example/ExampleFeature";
|
|
3
|
+
{{/if}}
|
|
4
|
+
{{#unless state}}
|
|
5
|
+
{{#unless fetching}}
|
|
6
|
+
import { Button } from "@/shared/ui/Button";
|
|
7
|
+
import { useState } from "react";
|
|
8
|
+
{{/unless}}
|
|
9
|
+
{{/unless}}
|
|
2
10
|
|
|
3
11
|
export function HomePage() {
|
|
12
|
+
{{#unless state}}
|
|
13
|
+
{{#unless fetching}}
|
|
14
|
+
const [msg, setMsg] = useState<string | null>(null);
|
|
15
|
+
{{/unless}}
|
|
16
|
+
{{/unless}}
|
|
17
|
+
|
|
4
18
|
return (
|
|
5
19
|
<main style={{ padding: "2rem", maxWidth: 800, margin: "0 auto" }}>
|
|
6
20
|
<h1>{{appName}}</h1>
|
|
7
21
|
<p style={{ marginTop: "0.5rem", opacity: 0.7 }}>
|
|
8
22
|
React + TypeScript + Vite
|
|
9
23
|
</p>
|
|
24
|
+
{{#if state}}
|
|
10
25
|
<ExampleFeature />
|
|
26
|
+
{{/if}}
|
|
27
|
+
{{#unless state}}
|
|
28
|
+
{{#unless fetching}}
|
|
29
|
+
<section style={{ marginTop: "2rem" }}>
|
|
30
|
+
<Button onClick={() => setMsg("Hello!")}>Click me</Button>
|
|
31
|
+
{msg && <p style={{ marginTop: "0.5rem" }}>{msg}</p>}
|
|
32
|
+
</section>
|
|
33
|
+
{{/unless}}
|
|
34
|
+
{{/unless}}
|
|
11
35
|
</main>
|
|
12
36
|
);
|
|
13
37
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
name: react-ts
|
|
2
|
-
version:
|
|
3
|
-
description: "Template React + TypeScript (Vite) with
|
|
2
|
+
version: 2.0.0
|
|
3
|
+
description: "Template React + TypeScript (Vite) with optional routing, state and fetching"
|
|
4
4
|
engine: "v1"
|
|
5
5
|
|
|
6
6
|
questions:
|
|
@@ -13,6 +13,21 @@ questions:
|
|
|
13
13
|
choices: ["npm", "pnpm", "yarn"]
|
|
14
14
|
default: "npm"
|
|
15
15
|
|
|
16
|
+
- name: routing
|
|
17
|
+
message: "Routing ?"
|
|
18
|
+
choices: ["react-router", "none"]
|
|
19
|
+
default: "react-router"
|
|
20
|
+
|
|
21
|
+
- name: state
|
|
22
|
+
message: "State management ?"
|
|
23
|
+
choices: ["zustand", "none"]
|
|
24
|
+
default: "zustand"
|
|
25
|
+
|
|
26
|
+
- name: fetching
|
|
27
|
+
message: "Data fetching ?"
|
|
28
|
+
choices: ["tanstack-query", "none"]
|
|
29
|
+
default: "tanstack-query"
|
|
30
|
+
|
|
16
31
|
hooks:
|
|
17
32
|
postGenerate:
|
|
18
33
|
- run: "git init"
|