@oss-ma/tpl 1.0.27 → 1.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/package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "name": "@oss-ma/tpl",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
4
4
  "description": "Generate, enforce and maintain clean project architectures",
5
5
  "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/aos01/oss-ma"
9
+ },
10
+ "homepage": "https://github.com/aos01/oss-ma#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/aos01/oss-ma/issues"
13
+ },
6
14
  "bin": {
7
15
  "tpl": "dist/index.js"
8
16
  },
@@ -33,4 +41,4 @@
33
41
  "tsx": "^4.16.2",
34
42
  "typescript": "^5.5.4"
35
43
  }
36
- }
44
+ }
@@ -0,0 +1,9 @@
1
+ # Application
2
+ VITE_APP_NAME={{appName}}
3
+ VITE_APP_VERSION=0.1.0
4
+
5
+ # API
6
+ VITE_API_BASE_URL=https://api.example.com
7
+
8
+ # Feature flags
9
+ VITE_ENABLE_DEVTOOLS=true
@@ -7,50 +7,52 @@
7
7
  "dev": "vite",
8
8
  "build": "tsc -b && vite build",
9
9
  "preview": "vite preview",
10
-
11
10
  "test": "vitest run",
12
- "test:watch": "vitest",
13
-
11
+ "test:watch": "vitest --ui",
12
+ "test:coverage": "vitest run --coverage",
14
13
  "lint": "eslint .",
14
+ "lint:fix": "eslint . --fix",
15
15
  "format": "prettier -w .",
16
16
  "typecheck": "tsc -p tsconfig.json --noEmit",
17
-
18
- "gen:feature": "node scripts/gen-feature.mjs",
19
-
20
17
  "prepare": "husky",
21
18
  "audit": "npm audit --audit-level=high"
22
19
  },
23
20
  "dependencies": {
21
+ "@tanstack/react-query": "^5.56.0",
24
22
  "react": "^18.3.0",
25
- "react-dom": "^18.3.0"
23
+ "react-dom": "^18.3.0",
24
+ "react-router-dom": "^6.26.0",
25
+ "zustand": "^4.5.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@commitlint/cli": "^19.3.0",
29
29
  "@commitlint/config-conventional": "^19.2.0",
30
30
  "@eslint/js": "^9.0.0",
31
+ "@tanstack/react-query-devtools": "^5.56.0",
32
+ "@testing-library/jest-dom": "^6.5.0",
33
+ "@testing-library/react": "^16.0.0",
34
+ "@testing-library/user-event": "^14.5.0",
31
35
  "@types/react": "^18.3.0",
32
36
  "@types/react-dom": "^18.3.0",
37
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
38
+ "@typescript-eslint/parser": "^8.0.0",
33
39
  "@vitejs/plugin-react": "^4.3.0",
40
+ "@vitest/coverage-v8": "^2.0.0",
41
+ "@vitest/ui": "^2.0.0",
34
42
  "eslint": "^9.0.0",
35
43
  "eslint-plugin-react-hooks": "^5.0.0",
36
44
  "eslint-plugin-react-refresh": "^0.4.0",
37
45
  "globals": "^15.0.0",
38
46
  "husky": "^9.0.0",
47
+ "jsdom": "^26.0.0",
39
48
  "lint-staged": "^15.2.0",
40
49
  "prettier": "^3.3.0",
41
50
  "typescript": "^5.5.0",
42
51
  "vite": "^5.4.0",
43
- "vitest": "^2.0.0",
44
- "@typescript-eslint/eslint-plugin": "^8.0.0",
45
- "@typescript-eslint/parser": "^8.0.0",
46
- "jsdom": "^26.0.0"
52
+ "vitest": "^2.0.0"
47
53
  },
48
54
  "lint-staged": {
49
- "*.{ts,tsx,js,jsx,json,md,yml,yaml}": [
50
- "prettier -w"
51
- ],
52
- "*.{ts,tsx,js,jsx}": [
53
- "eslint --fix"
54
- ]
55
+ "*.{ts,tsx,js,jsx,json,md,yml,yaml}": ["prettier -w"],
56
+ "*.{ts,tsx,js,jsx}": ["eslint --fix"]
55
57
  }
56
58
  }
@@ -1,10 +1,12 @@
1
- import { Example } from "../features/example/Example.js";
1
+ import { Routes, Route } from "react-router-dom";
2
+ import { HomePage } from "@/pages/HomePage";
3
+ import { NotFoundPage } from "@/pages/NotFoundPage";
2
4
 
3
5
  export function App() {
4
6
  return (
5
- <main style={{ padding: 16 }}>
6
- <h1>{{appName}}</h1>
7
- <Example />
8
- </main>
7
+ <Routes>
8
+ <Route path="/" element={<HomePage />} />
9
+ <Route path="*" element={<NotFoundPage />} />
10
+ </Routes>
9
11
  );
10
12
  }
@@ -0,0 +1,24 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ margin: 0;
6
+ padding: 0;
7
+ }
8
+
9
+ :root {
10
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
11
+ line-height: 1.5;
12
+ font-weight: 400;
13
+ color-scheme: light dark;
14
+ color: rgba(255, 255, 255, 0.87);
15
+ background-color: #242424;
16
+ }
17
+
18
+ body {
19
+ min-height: 100vh;
20
+ }
21
+
22
+ #root {
23
+ min-height: 100vh;
24
+ }
@@ -1,9 +1,27 @@
1
- import React from "react";
2
- import ReactDOM from "react-dom/client";
3
- import { App } from "./App.js";
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { BrowserRouter } from "react-router-dom";
4
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
+ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
6
+ import { App } from "@/app/App";
7
+ import "@/app/index.css";
4
8
 
5
- ReactDOM.createRoot(document.getElementById("root")!).render(
6
- <React.StrictMode>
7
- <App />
8
- </React.StrictMode>
9
+ const queryClient = new QueryClient({
10
+ defaultOptions: {
11
+ queries: {
12
+ staleTime: 1000 * 60 * 5, // 5 minutes
13
+ retry: 1,
14
+ },
15
+ },
16
+ });
17
+
18
+ createRoot(document.getElementById("root")!).render(
19
+ <StrictMode>
20
+ <QueryClientProvider client={queryClient}>
21
+ <BrowserRouter>
22
+ <App />
23
+ </BrowserRouter>
24
+ <ReactQueryDevtools initialIsOpen={false} />
25
+ </QueryClientProvider>
26
+ </StrictMode>
9
27
  );
@@ -0,0 +1,12 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_APP_NAME: string;
5
+ readonly VITE_APP_VERSION: string;
6
+ readonly VITE_API_BASE_URL: string;
7
+ readonly VITE_ENABLE_DEVTOOLS: string;
8
+ }
9
+
10
+ interface ImportMeta {
11
+ readonly env: ImportMetaEnv;
12
+ }
@@ -0,0 +1,54 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import { useCounterStore } from "./store";
3
+ import { Button } from "@/shared/ui/Button";
4
+
5
+ interface Post {
6
+ id: number;
7
+ title: string;
8
+ }
9
+
10
+ async function fetchPosts(): Promise<Post[]> {
11
+ const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=3");
12
+ if (!res.ok) throw new Error("Failed to fetch posts");
13
+ return res.json();
14
+ }
15
+
16
+ export function ExampleFeature() {
17
+ const { count, increment, decrement, reset } = useCounterStore();
18
+
19
+ const { data: posts, isLoading, isError } = useQuery({
20
+ queryKey: ["posts"],
21
+ queryFn: fetchPosts,
22
+ });
23
+
24
+ return (
25
+ <section style={{ marginTop: "2rem" }}>
26
+ {/* Zustand counter */}
27
+ <div style={{ marginBottom: "2rem" }}>
28
+ <h2>Counter (Zustand)</h2>
29
+ <p style={{ fontSize: "2rem", margin: "0.5rem 0" }}>{count}</p>
30
+ <div style={{ display: "flex", gap: "0.5rem" }}>
31
+ <Button onClick={decrement}>−</Button>
32
+ <Button onClick={increment}>+</Button>
33
+ <Button onClick={reset} variant="secondary">Reset</Button>
34
+ </div>
35
+ </div>
36
+
37
+ {/* React Query fetch */}
38
+ <div>
39
+ <h2>Posts (TanStack Query)</h2>
40
+ {isLoading && <p>Loading...</p>}
41
+ {isError && <p>Error loading posts.</p>}
42
+ {posts && (
43
+ <ul style={{ marginTop: "0.5rem", paddingLeft: "1.2rem" }}>
44
+ {posts.map((post) => (
45
+ <li key={post.id} style={{ marginBottom: "0.25rem" }}>
46
+ {post.title}
47
+ </li>
48
+ ))}
49
+ </ul>
50
+ )}
51
+ </div>
52
+ </section>
53
+ );
54
+ }
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { useCounterStore } from "@/features/example/store";
3
+
4
+ describe("useCounterStore", () => {
5
+ beforeEach(() => {
6
+ useCounterStore.getState().reset();
7
+ });
8
+
9
+ it("starts at 0", () => {
10
+ expect(useCounterStore.getState().count).toBe(0);
11
+ });
12
+
13
+ it("increments", () => {
14
+ useCounterStore.getState().increment();
15
+ expect(useCounterStore.getState().count).toBe(1);
16
+ });
17
+
18
+ it("decrements", () => {
19
+ useCounterStore.getState().increment();
20
+ useCounterStore.getState().decrement();
21
+ expect(useCounterStore.getState().count).toBe(0);
22
+ });
23
+
24
+ it("resets to 0", () => {
25
+ useCounterStore.getState().increment();
26
+ useCounterStore.getState().increment();
27
+ useCounterStore.getState().reset();
28
+ expect(useCounterStore.getState().count).toBe(0);
29
+ });
30
+ });
@@ -0,0 +1,15 @@
1
+ import { create } from "zustand";
2
+
3
+ interface CounterState {
4
+ count: number;
5
+ increment: () => void;
6
+ decrement: () => void;
7
+ reset: () => void;
8
+ }
9
+
10
+ export const useCounterStore = create<CounterState>((set) => ({
11
+ count: 0,
12
+ increment: () => set((state) => ({ count: state.count + 1 })),
13
+ decrement: () => set((state) => ({ count: state.count - 1 })),
14
+ reset: () => set({ count: 0 }),
15
+ }));
@@ -0,0 +1,13 @@
1
+ import { ExampleFeature } from "@/features/example/ExampleFeature";
2
+
3
+ export function HomePage() {
4
+ return (
5
+ <main style={{ padding: "2rem", maxWidth: 800, margin: "0 auto" }}>
6
+ <h1>{{appName}}</h1>
7
+ <p style={{ marginTop: "0.5rem", opacity: 0.7 }}>
8
+ React + TypeScript + Vite
9
+ </p>
10
+ <ExampleFeature />
11
+ </main>
12
+ );
13
+ }
@@ -0,0 +1,13 @@
1
+ import { Link } from "react-router-dom";
2
+
3
+ export function NotFoundPage() {
4
+ return (
5
+ <main style={{ padding: "2rem", textAlign: "center" }}>
6
+ <h1>404</h1>
7
+ <p>Page not found.</p>
8
+ <Link to="/" style={{ marginTop: "1rem", display: "inline-block" }}>
9
+ Go home
10
+ </Link>
11
+ </main>
12
+ );
13
+ }
@@ -0,0 +1,28 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import { Button } from "@/shared/ui/Button";
4
+
5
+ describe("Button", () => {
6
+ it("renders children", () => {
7
+ render(<Button>Click me</Button>);
8
+ expect(screen.getByRole("button", { name: /click me/i })).toBeInTheDocument();
9
+ });
10
+
11
+ it("calls onClick when clicked", () => {
12
+ const handler = vi.fn();
13
+ render(<Button onClick={handler}>Click</Button>);
14
+ fireEvent.click(screen.getByRole("button"));
15
+ expect(handler).toHaveBeenCalledOnce();
16
+ });
17
+
18
+ it("applies secondary variant", () => {
19
+ render(<Button variant="secondary">Cancel</Button>);
20
+ const btn = screen.getByRole("button");
21
+ expect(btn).toHaveStyle({ background: "#3a3a3a" });
22
+ });
23
+
24
+ it("is disabled when disabled prop is set", () => {
25
+ render(<Button disabled>Disabled</Button>);
26
+ expect(screen.getByRole("button")).toBeDisabled();
27
+ });
28
+ });
@@ -1,19 +1,31 @@
1
- type Props = {
2
- children: React.ReactNode;
3
- onClick?: () => void;
4
- };
1
+ import type { ButtonHTMLAttributes } from "react";
2
+
3
+ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
4
+ variant?: "primary" | "secondary";
5
+ }
6
+
7
+ export function Button({
8
+ variant = "primary",
9
+ children,
10
+ style,
11
+ ...props
12
+ }: ButtonProps) {
13
+ const base: React.CSSProperties = {
14
+ padding: "0.4rem 1rem",
15
+ borderRadius: 6,
16
+ border: "none",
17
+ cursor: "pointer",
18
+ fontWeight: 500,
19
+ fontSize: "0.9rem",
20
+ };
21
+
22
+ const variants: Record<string, React.CSSProperties> = {
23
+ primary: { background: "#646cff", color: "#fff" },
24
+ secondary: { background: "#3a3a3a", color: "#fff" },
25
+ };
5
26
 
6
- export function Button({ children, onClick }: Props) {
7
27
  return (
8
- <button
9
- onClick={onClick}
10
- style={{
11
- padding: "8px 12px",
12
- borderRadius: 10,
13
- border: "1px solid #ddd",
14
- cursor: "pointer"
15
- }}
16
- >
28
+ <button style={{ ...base, ...variants[variant], ...style }} {...props}>
17
29
  {children}
18
30
  </button>
19
31
  );
@@ -0,0 +1 @@
1
+ import "@testing-library/jest-dom";
@@ -1,21 +1,24 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "target": "ES2022",
3
+ "target": "ES2020",
4
4
  "useDefineForClassFields": true,
5
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
6
  "module": "ESNext",
7
7
  "skipLibCheck": true,
8
-
9
- "moduleResolution": "Bundler",
10
- "resolveJsonModule": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
11
10
  "isolatedModules": true,
11
+ "moduleDetection": "force",
12
12
  "noEmit": true,
13
13
  "jsx": "react-jsx",
14
-
15
14
  "strict": true,
16
15
  "noUnusedLocals": true,
17
16
  "noUnusedParameters": true,
18
- "noFallthroughCasesInSwitch": true
17
+ "noFallthroughCasesInSwitch": true,
18
+ "baseUrl": ".",
19
+ "paths": {
20
+ "@/*": ["./src/*"]
21
+ }
19
22
  },
20
- "include": ["src", "vitest.setup.ts"]
23
+ "include": ["src"]
21
24
  }
@@ -1,6 +1,22 @@
1
1
  import { defineConfig } from "vite";
2
2
  import react from "@vitejs/plugin-react";
3
+ import { resolve } from "path";
3
4
 
4
5
  export default defineConfig({
5
- plugins: [react()]
6
+ plugins: [react()],
7
+ resolve: {
8
+ alias: {
9
+ "@": resolve(__dirname, "./src"),
10
+ },
11
+ },
12
+ test: {
13
+ globals: true,
14
+ environment: "jsdom",
15
+ setupFiles: ["./src/tests/setup.ts"],
16
+ coverage: {
17
+ provider: "v8",
18
+ reporter: ["text", "html"],
19
+ exclude: ["node_modules/", "src/tests/"],
20
+ },
21
+ },
6
22
  });
@@ -1,14 +0,0 @@
1
- import { useState } from "react";
2
- import { Button } from "../../shared/ui/Button.js";
3
-
4
- export function Example() {
5
- const [msg, setMsg] = useState<string | null>(null);
6
-
7
- return (
8
- <section style={{ marginTop: 16 }}>
9
- <p>Feature module example.</p>
10
- <Button onClick={() => setMsg("Hello!")}>Click</Button>
11
- {msg && <p style={{ marginTop: 8 }}>{msg}</p>}
12
- </section>
13
- );
14
- }
@@ -1,10 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "composite": true,
4
- "module": "ESNext",
5
- "moduleResolution": "Bundler",
6
- "skipLibCheck": true,
7
- "allowSyntheticDefaultImports": true
8
- },
9
- "include": ["vite.config.ts", "vitest.config.ts"]
10
- }
@@ -1,9 +0,0 @@
1
- import { defineConfig } from "vitest/config";
2
- import react from "@vitejs/plugin-react";
3
- export default defineConfig({
4
- plugins: [react()],
5
- test: {
6
- environment: "jsdom",
7
- setupFiles: ["./vitest.setup.ts"]
8
- }
9
- });