create-tigra 3.0.3 → 3.1.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.
Files changed (91) hide show
  1. package/README.md +8 -2
  2. package/bin/create-tigra.js +5 -19
  3. package/modules/email-verification/client/hooks/useVerification.ts +3 -3
  4. package/package.json +1 -1
  5. package/template/.agents/skills/security-audit/AI-AND-LLM.md +83 -0
  6. package/template/.agents/skills/security-audit/ATTACK-CLASSES.md +130 -0
  7. package/template/.agents/skills/security-audit/CLIENT-SIDE.md +83 -0
  8. package/template/.agents/skills/security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
  9. package/template/.agents/skills/security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
  10. package/template/.agents/skills/security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
  11. package/template/.agents/skills/security-audit/HUNTING.md +251 -0
  12. package/template/.agents/skills/security-audit/LICENSE +21 -0
  13. package/template/.agents/skills/security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
  14. package/template/.agents/skills/security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
  15. package/template/.agents/skills/security-audit/RECONNAISSANCE.md +156 -0
  16. package/template/.agents/skills/security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
  17. package/template/.agents/skills/security-audit/SKILL.md +192 -0
  18. package/template/.agents/skills/security-audit/SOURCE.md +5 -0
  19. package/template/.agents/skills/security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
  20. package/template/.agents/skills/security-audit/VALIDATION-AND-REPORTING.md +186 -0
  21. package/template/.agents/skills/security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
  22. package/template/.agents/skills/security-audit/report-schema.json +461 -0
  23. package/template/.agents/skills/security-audit/validate-coverage-ledger.cjs +872 -0
  24. package/template/.agents/skills/security-audit/validate-coverage-ledger.test.cjs +740 -0
  25. package/template/.agents/skills/security-audit/validate-findings.cjs +773 -0
  26. package/template/.agents/skills/security-audit/validate-findings.test.cjs +652 -0
  27. package/template/AGENTS.md +45 -0
  28. package/template/client/AGENTS.md +22 -0
  29. package/template/client/Dockerfile +11 -1
  30. package/template/client/package-lock.json +410 -324
  31. package/template/client/package.json +4 -4
  32. package/template/client/scripts/next-dev.cjs +47 -0
  33. package/template/client/src/app/(auth)/layout.tsx +9 -0
  34. package/template/client/src/app/(auth)/loading.tsx +7 -0
  35. package/template/client/src/app/(main)/layout.tsx +11 -0
  36. package/template/client/src/app/(main)/loading.tsx +7 -0
  37. package/template/client/src/app/globals.css +4 -0
  38. package/template/client/src/app/loading.tsx +2 -6
  39. package/template/client/src/app/not-found.tsx +2 -3
  40. package/template/client/src/app/providers.tsx +6 -3
  41. package/template/client/src/components/common/AppLink.tsx +84 -0
  42. package/template/client/src/components/common/EmptyState.tsx +2 -2
  43. package/template/client/src/components/common/Pagination.tsx +3 -2
  44. package/template/client/src/components/common/RouteLoadingShell.tsx +21 -0
  45. package/template/client/src/components/common/SmoothNavigationProvider.tsx +151 -0
  46. package/template/client/src/components/layout/Header.tsx +12 -12
  47. package/template/client/src/features/admin/hooks/useAdminSessions.ts +2 -2
  48. package/template/client/src/features/admin/hooks/useAdminUsers.ts +3 -3
  49. package/template/client/src/features/auth/components/AuthInitializer.tsx +3 -2
  50. package/template/client/src/features/auth/components/LoginForm.tsx +3 -3
  51. package/template/client/src/features/auth/components/RegisterForm.tsx +3 -3
  52. package/template/client/src/features/auth/hooks/useAuth.ts +2 -2
  53. package/template/client/src/features/auth/hooks/usePasswordReset.ts +2 -2
  54. package/template/client/src/hooks/useAppRouter.ts +40 -0
  55. package/template/client/src/instrumentation-client.ts +29 -21
  56. package/template/client/src/instrumentation.ts +21 -17
  57. package/template/client/src/styles/themes/default.css +1 -1
  58. package/template/gitignore +0 -6
  59. package/template/server/AGENTS.md +28 -0
  60. package/template/server/Dockerfile +9 -3
  61. package/template/server/package-lock.json +671 -522
  62. package/template/server/package.json +8 -8
  63. package/template/server/src/modules/auth/__tests__/auth.service.test.ts +25 -41
  64. package/template/server/src/modules/auth/auth.repo.ts +0 -12
  65. package/template/server/src/modules/auth/auth.service.ts +29 -57
  66. package/template/_claude/QUICK_REFERENCE.md +0 -193
  67. package/template/_claude/README.md +0 -53
  68. package/template/_claude/commands/create-client.md +0 -878
  69. package/template/_claude/commands/create-server.md +0 -388
  70. package/template/_claude/hooks/restrict-paths.sh +0 -51
  71. package/template/_claude/rules/client/01-project-structure.md +0 -147
  72. package/template/_claude/rules/client/02-components-and-types.md +0 -146
  73. package/template/_claude/rules/client/03-data-and-state.md +0 -195
  74. package/template/_claude/rules/client/04-design-system.md +0 -408
  75. package/template/_claude/rules/client/05-security.md +0 -55
  76. package/template/_claude/rules/client/06-ux-checklist.md +0 -111
  77. package/template/_claude/rules/client/07-deployment.md +0 -99
  78. package/template/_claude/rules/client/08-lockfile-cross-platform.md +0 -79
  79. package/template/_claude/rules/client/core.md +0 -46
  80. package/template/_claude/rules/global/completion-reports.md +0 -178
  81. package/template/_claude/rules/global/core.md +0 -104
  82. package/template/_claude/rules/global/investigation-before-conclusions.md +0 -57
  83. package/template/_claude/rules/server/core.md +0 -52
  84. package/template/_claude/rules/server/database.md +0 -124
  85. package/template/_claude/rules/server/deployment.md +0 -78
  86. package/template/_claude/rules/server/project-conventions.md +0 -254
  87. package/template/_claude/rules/server/response-handling.md +0 -144
  88. package/template/_claude/settings.json +0 -15
  89. package/template/_claude/skills/clean-ui/SKILL.md +0 -63
  90. package/template/_claude/skills/role/SKILL.md +0 -39
  91. package/template/_claude/skills/theme/SKILL.md +0 -109
@@ -3,7 +3,6 @@
3
3
  import type React from 'react';
4
4
  import { Suspense } from 'react';
5
5
 
6
- import Link from 'next/link';
7
6
  import { useSearchParams } from 'next/navigation';
8
7
  import { useForm } from 'react-hook-form';
9
8
  import { zodResolver } from '@hookform/resolvers/zod';
@@ -13,6 +12,7 @@ import { Loader2 } from 'lucide-react';
13
12
  import { Button } from '@/components/ui/button';
14
13
  import { Input } from '@/components/ui/input';
15
14
  import { Label } from '@/components/ui/label';
15
+ import { AppLink } from '@/components/common/AppLink';
16
16
  import { useAuth } from '../hooks/useAuth';
17
17
  import { ROUTES } from '@/lib/constants/routes';
18
18
  import { cn } from '@/lib/utils';
@@ -100,12 +100,12 @@ const LoginFormInner = (): React.ReactElement => {
100
100
 
101
101
  <p className="text-center text-sm text-muted-foreground">
102
102
  Don&apos;t have an account?{' '}
103
- <Link
103
+ <AppLink
104
104
  href={ROUTES.REGISTER}
105
105
  className="font-medium text-primary transition-colors duration-150 active:opacity-70 md:hover:text-primary/80"
106
106
  >
107
107
  Create account
108
- </Link>
108
+ </AppLink>
109
109
  </p>
110
110
  </div>
111
111
  );
@@ -2,7 +2,6 @@
2
2
 
3
3
  import type React from 'react';
4
4
 
5
- import Link from 'next/link';
6
5
  import { useForm } from 'react-hook-form';
7
6
  import { zodResolver } from '@hookform/resolvers/zod';
8
7
  import { z } from 'zod';
@@ -11,6 +10,7 @@ import { Loader2 } from 'lucide-react';
11
10
  import { Button } from '@/components/ui/button';
12
11
  import { Input } from '@/components/ui/input';
13
12
  import { Label } from '@/components/ui/label';
13
+ import { AppLink } from '@/components/common/AppLink';
14
14
  import { useAuth } from '../hooks/useAuth';
15
15
  import { ROUTES } from '@/lib/constants/routes';
16
16
  import { cn } from '@/lib/utils';
@@ -166,12 +166,12 @@ export const RegisterForm = (): React.ReactElement => {
166
166
 
167
167
  <p className="text-center text-sm text-muted-foreground">
168
168
  Already have an account?{' '}
169
- <Link
169
+ <AppLink
170
170
  href={ROUTES.LOGIN}
171
171
  className="font-medium text-primary transition-colors duration-150 active:opacity-70 md:hover:text-primary/80"
172
172
  >
173
173
  Sign in
174
- </Link>
174
+ </AppLink>
175
175
  </p>
176
176
  </div>
177
177
  );
@@ -3,10 +3,10 @@
3
3
  import { useCallback, useRef } from 'react';
4
4
 
5
5
  import { useMutation, useQueryClient } from '@tanstack/react-query';
6
- import { useRouter } from 'next/navigation';
7
6
  import { toast } from 'sonner';
8
7
 
9
8
  import { useAppDispatch, useAppSelector } from '@/store/hooks';
9
+ import { useAppRouter } from '@/hooks/useAppRouter';
10
10
  import { getErrorMessage, isErrorCode, ERROR_CODES } from '@/lib/utils/error';
11
11
  import { ROUTES } from '@/lib/constants/routes';
12
12
  import { authService } from '../services/auth.service';
@@ -28,7 +28,7 @@ interface UseAuthReturn {
28
28
 
29
29
  export const useAuth = (): UseAuthReturn => {
30
30
  const dispatch = useAppDispatch();
31
- const router = useRouter();
31
+ const router = useAppRouter();
32
32
  const queryClient = useQueryClient();
33
33
  const { user, isAuthenticated, isInitializing, isLoggingOut } = useAppSelector((state) => state.auth);
34
34
  const pendingRedirectRef = useRef<string>(ROUTES.DASHBOARD);
@@ -1,9 +1,9 @@
1
1
  'use client';
2
2
 
3
3
  import { useMutation } from '@tanstack/react-query';
4
- import { useRouter } from 'next/navigation';
5
4
  import { toast } from 'sonner';
6
5
 
6
+ import { useAppRouter } from '@/hooks/useAppRouter';
7
7
  import { ROUTES } from '@/lib/constants/routes';
8
8
  import { getErrorMessage } from '@/lib/utils/error';
9
9
  import { authService } from '../services/auth.service';
@@ -36,7 +36,7 @@ interface UseResetPasswordReturn {
36
36
  }
37
37
 
38
38
  export const useResetPassword = (): UseResetPasswordReturn => {
39
- const router = useRouter();
39
+ const router = useAppRouter();
40
40
 
41
41
  const mutation = useMutation({
42
42
  mutationFn: (data: { token: string; newPassword: string }) =>
@@ -0,0 +1,40 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useMemo } from 'react';
4
+
5
+ import { useRouter } from 'next/navigation';
6
+
7
+ import { useSmoothNavigation } from '@/components/common/SmoothNavigationProvider';
8
+
9
+ type AppRouter = ReturnType<typeof useRouter>;
10
+
11
+ /**
12
+ * App Router wrapper that keeps Next.js from performing its instant scroll.
13
+ * Pass `{ scroll: false }` explicitly when navigation should preserve position.
14
+ */
15
+ export const useAppRouter = (): AppRouter => {
16
+ const router = useRouter();
17
+ const { startNavigation } = useSmoothNavigation();
18
+
19
+ const push = useCallback<AppRouter['push']>(
20
+ (href, options) => {
21
+ if (options?.scroll !== false) {
22
+ startNavigation(href);
23
+ }
24
+ router.push(href, { ...options, scroll: false });
25
+ },
26
+ [router, startNavigation]
27
+ );
28
+
29
+ const replace = useCallback<AppRouter['replace']>(
30
+ (href, options) => {
31
+ if (options?.scroll !== false) {
32
+ startNavigation(href);
33
+ }
34
+ router.replace(href, { ...options, scroll: false });
35
+ },
36
+ [router, startNavigation]
37
+ );
38
+
39
+ return useMemo(() => ({ ...router, push, replace }), [router, push, replace]);
40
+ };
@@ -1,30 +1,38 @@
1
1
  /**
2
- * Browser (client) instrumentation — env-gated Sentry, INERT by default.
2
+ * Browser instrumentation.
3
3
  *
4
- * Next.js 15.3+/16 loads this module in the browser before the app hydrates.
5
- * We init Sentry ONLY when the public DSN is set, so with no DSN this file is a
6
- * no-op and ships nothing that phones home.
7
- *
8
- * `process.env.NEXT_PUBLIC_*` is inlined at build time by Next — reference it
9
- * literally (not via a computed key) so the value is statically replaced.
10
- *
11
- * Like the server side, this stays runtime + env-gated: we do NOT wrap
12
- * next.config.ts with `withSentryConfig` (that would couple `next build` to a
13
- * source-map upload auth token). withSentryConfig is the opt-in upgrade.
4
+ * Keep local dev cold: do not import @sentry/nextjs unless production has a
5
+ * public DSN. This avoids compiling the Sentry SDK for every local dev server.
14
6
  */
15
- import * as Sentry from '@sentry/nextjs';
7
+ type SentryNext = typeof import('@sentry/nextjs');
8
+ type RouterTransitionStart = SentryNext['captureRouterTransitionStart'];
16
9
 
17
10
  const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN;
11
+ const sentryEnabled = process.env.NODE_ENV === 'production' && Boolean(dsn);
12
+
13
+ let sentryPromise: Promise<SentryNext> | null = null;
14
+
15
+ function loadSentry(): Promise<SentryNext | null> {
16
+ if (!sentryEnabled || !dsn) return Promise.resolve(null);
18
17
 
19
- if (dsn) {
20
- Sentry.init({
21
- dsn,
22
- tracesSampleRate: process.env.NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE
23
- ? Number(process.env.NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE)
24
- : 0.1,
18
+ sentryPromise ??= import('@sentry/nextjs').then((Sentry) => {
19
+ Sentry.init({
20
+ dsn,
21
+ tracesSampleRate: process.env.NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE
22
+ ? Number(process.env.NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE)
23
+ : 0.1,
24
+ });
25
+
26
+ return Sentry;
25
27
  });
28
+
29
+ return sentryPromise;
26
30
  }
27
31
 
28
- // Instruments App Router navigations for tracing. Harmless when Sentry is
29
- // uninitialized (no DSN).
30
- export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
32
+ void loadSentry();
33
+
34
+ export const onRouterTransitionStart = ((...args: Parameters<RouterTransitionStart>) => {
35
+ void loadSentry().then((Sentry) => {
36
+ Sentry?.captureRouterTransitionStart(...args);
37
+ });
38
+ }) as RouterTransitionStart;
@@ -1,28 +1,30 @@
1
1
  /**
2
- * Server/Edge runtime instrumentation — env-gated Sentry, INERT by default.
2
+ * Server/Edge runtime instrumentation.
3
3
  *
4
- * Next.js calls `register()` once per server runtime at startup. We init Sentry
5
- * ONLY when a DSN is present (a server `SENTRY_DSN`, or the public DSN shared
6
- * with the browser), so with no DSN this is a clean no-op and `next build`
7
- * succeeds with zero Sentry credentials.
8
- *
9
- * NOTE: we deliberately do NOT wrap next.config.ts with `withSentryConfig`.
10
- * That adds build-time source-map upload which requires a SENTRY_AUTH_TOKEN and
11
- * would couple `next build` to credentials. Source-map upload is the opt-in
12
- * upgrade — add `withSentryConfig` + an auth token when you want it. This setup
13
- * stays purely runtime + env-gated.
4
+ * Keep local dev cold: importing @sentry/nextjs at module top-level makes Next
5
+ * compile the Sentry SDK before the first page, even with no DSN.
14
6
  */
15
- import * as Sentry from '@sentry/nextjs';
7
+ type SentryNext = typeof import('@sentry/nextjs');
8
+ type RequestErrorHandler = SentryNext['captureRequestError'];
16
9
 
17
10
  const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN ?? process.env.SENTRY_DSN;
11
+ const sentryEnabled = process.env.NODE_ENV === 'production' && Boolean(dsn);
18
12
 
19
13
  const tracesSampleRate = process.env.NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE
20
14
  ? Number(process.env.NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE)
21
15
  : 0.1;
22
16
 
17
+ let sentryPromise: Promise<SentryNext> | null = null;
18
+
19
+ function loadSentry(): Promise<SentryNext | null> {
20
+ if (!sentryEnabled) return Promise.resolve(null);
21
+ sentryPromise ??= import('@sentry/nextjs');
22
+ return sentryPromise;
23
+ }
24
+
23
25
  export async function register(): Promise<void> {
24
- // No DSN → stay inert.
25
- if (!dsn) return;
26
+ const Sentry = await loadSentry();
27
+ if (!Sentry || !dsn) return;
26
28
 
27
29
  if (process.env.NEXT_RUNTIME === 'nodejs' || process.env.NEXT_RUNTIME === 'edge') {
28
30
  Sentry.init({
@@ -33,6 +35,8 @@ export async function register(): Promise<void> {
33
35
  }
34
36
  }
35
37
 
36
- // Capture errors from Server Components, route handlers, and middleware.
37
- // Safe no-op when Sentry was never initialized (no DSN).
38
- export const onRequestError = Sentry.captureRequestError;
38
+ export const onRequestError = (async (...args: Parameters<RequestErrorHandler>) => {
39
+ const Sentry = await loadSentry();
40
+ if (!Sentry) return;
41
+ return Sentry.captureRequestError(...args);
42
+ }) as RequestErrorHandler;
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Default Theme — Claude-inspired warm palette
2
+ * Default Theme — warm neutral palette
3
3
  * Accent: Terracotta orange
4
4
  * Vibe: Earthy, warm, approachable
5
5
  *
@@ -20,12 +20,6 @@ Thumbs.db
20
20
  .vscode/
21
21
  .idea/
22
22
 
23
- # Claude Code local settings
24
- .claude/settings.local.json
25
-
26
- # Developer role (personal per developer)
27
- .developer-role
28
-
29
23
  # Playwright MCP artifacts
30
24
  .playwright-mcp/
31
25
 
@@ -0,0 +1,28 @@
1
+ # Server Codex instructions
2
+
3
+ ## Architecture and naming
4
+
5
+ - Request flow is route → controller → service → repository → Prisma. Routes register HTTP/schema metadata; controllers translate HTTP input/output; services own business rules; repositories contain database queries only.
6
+ - Domain modules live in `src/modules/<domain>/` with `<domain>.routes.ts`, `.controller.ts`, `.service.ts`, `.repo.ts`, and `.schemas.ts`.
7
+ - All API routes use the `/api/v1` prefix. Use the shared `successResponse`, `paginatedResponse`, and typed `AppError` contracts rather than custom response/error shapes.
8
+ - Use the shared logger and outbound HTTP client. Never add ad-hoc `console.log`, raw `fetch`, or per-call HTTP clients that bypass redaction, timeouts, and error mapping.
9
+
10
+ ## Sources of truth
11
+
12
+ - `prisma/schema.prisma` owns models, relations, indexes, and database naming. Change schema through reviewed Prisma migrations; production is forward-only with `prisma migrate deploy`.
13
+ - `src/config/env.ts` owns runtime configuration validation. Keep `.env.example` and `.env.example.production` synchronized, but keep real secrets out of the repository and image.
14
+ - Zod route schemas own external input validation. Services own authorization and business invariants; repositories must not decide access policy.
15
+
16
+ ## Security and deployment
17
+
18
+ - Preserve httpOnly cookie auth, production CORS allowlists, state-changing request origin checks, rate limiting, safe client IP handling, generic 500 responses, and server-side-only diagnostic logging.
19
+ - Use Prisma parameterization and explicit safe projections; never expose password hashes, tokens, internal errors, or secret-bearing upstream payloads.
20
+ - Keep public uploads under the static public root. Private uploads stay outside it and are served only through authenticated, owner-scoped routes; preserve filename/path validation.
21
+ - The production image runs non-root, applies `prisma migrate deploy` before `node dist/server.js`, and probes unauthenticated `GET /api/v1/live`. Preserve writable ownership for runtime storage and add explicit Alpine packages for new native runtime dependencies.
22
+
23
+ ## Mandatory security completion gate
24
+
25
+ - Follow the root `AGENTS.md` security workflow for every meaningful server change: use `security-audit` guidance during implementation and run `codex-security:security-diff-scan` on the resulting patch when the Codex Security plugin is available.
26
+ - Review the concrete trust boundary, attacker-controlled input, affected principal/resource, authorization decision, validation point, and observable impact. Missing best practices without a reachable boundary failure are hardening notes, not confirmed vulnerabilities.
27
+ - Add or update focused regression coverage for changed authorization, validation, origin/CORS, cookie/session, upload/path, outbound-request, and data-isolation behavior. Never rely on client checks as security evidence.
28
+ - If a required security check cannot run, state the exact blocker and safe follow-up instead of reporting the server change as fully verified.
@@ -1,7 +1,11 @@
1
+ # syntax=docker/dockerfile:1
1
2
  # ===================================================================
2
3
  # Multi-stage Dockerfile for Production Deployment
3
4
  # Optimized for Coolify, Docker Compose, and Kubernetes
4
5
  # ===================================================================
6
+ # BuildKit cache mounts (--mount=type=cache) keep the npm download cache warm
7
+ # ACROSS builds, so a lockfile-change rebuild re-installs without re-downloading.
8
+ # Requires the `# syntax` line above; auto-enabled on Docker 23+ / any BuildKit.
5
9
 
6
10
  # ===================================================================
7
11
  # Stage 1: Dependencies (cached layer)
@@ -15,7 +19,8 @@ WORKDIR /app
15
19
  COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* ./
16
20
 
17
21
  # Install dependencies (use the lockfile that exists)
18
- RUN if [ -f pnpm-lock.yaml ]; then \
22
+ RUN --mount=type=cache,target=/root/.npm \
23
+ if [ -f pnpm-lock.yaml ]; then \
19
24
  npm install -g pnpm && pnpm install --prod --frozen-lockfile; \
20
25
  elif [ -f yarn.lock ]; then \
21
26
  yarn install --production --frozen-lockfile; \
@@ -32,7 +37,7 @@ RUN if [ -f pnpm-lock.yaml ]; then \
32
37
  # --no-save keeps package.json and the lockfile untouched. Because this stage
33
38
  # runs on the same Alpine (musl) base as the runtime stage, the engine binaries
34
39
  # downloaded here are the correct ones for production.
35
- RUN npm install --no-save --no-audit --no-fund \
40
+ RUN --mount=type=cache,target=/root/.npm npm install --no-save --no-audit --no-fund \
36
41
  "prisma@$(node -p "require('@prisma/client/package.json').version")"
37
42
 
38
43
  # ===================================================================
@@ -50,7 +55,8 @@ WORKDIR /app
50
55
  # Copy package files and install ALL dependencies (including dev)
51
56
  COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* ./
52
57
 
53
- RUN if [ -f pnpm-lock.yaml ]; then \
58
+ RUN --mount=type=cache,target=/root/.npm \
59
+ if [ -f pnpm-lock.yaml ]; then \
54
60
  npm install -g pnpm && pnpm install --frozen-lockfile; \
55
61
  elif [ -f yarn.lock ]; then \
56
62
  yarn install --frozen-lockfile; \