@lazarv/create-react-server 0.0.0-experimental-d003259-20250211-2495688e

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 (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/generator.mjs +195 -0
  4. package/globals.d.ts +59 -0
  5. package/index.mjs +87 -0
  6. package/launch.mjs +136 -0
  7. package/lib/code-merge.mjs +246 -0
  8. package/lib/dynamic-checkbox.mjs +259 -0
  9. package/lib/files.mjs +6 -0
  10. package/lib/formatter.mjs +16 -0
  11. package/lib/generate-name.mjs +220 -0
  12. package/lib/theme.mjs +56 -0
  13. package/logo.mjs +3 -0
  14. package/package.json +26 -0
  15. package/steps/alias.mjs +50 -0
  16. package/steps/authentication.mjs +62 -0
  17. package/steps/database.mjs +56 -0
  18. package/steps/deploy.mjs +151 -0
  19. package/steps/features.mjs +415 -0
  20. package/steps/host.mjs +40 -0
  21. package/steps/index.mjs +33 -0
  22. package/steps/integrations.mjs +104 -0
  23. package/steps/name.mjs +69 -0
  24. package/steps/package.mjs +104 -0
  25. package/steps/port.mjs +42 -0
  26. package/steps/preset.mjs +188 -0
  27. package/steps/state-management.mjs +68 -0
  28. package/steps/third-party.mjs +19 -0
  29. package/steps/ui.mjs +87 -0
  30. package/templates/.dockerignore +5 -0
  31. package/templates/.gitignore.template +19 -0
  32. package/templates/.prettierignore +3 -0
  33. package/templates/.prettierrc +11 -0
  34. package/templates/Dockerfile.npm +49 -0
  35. package/templates/Dockerfile.pnpm +71 -0
  36. package/templates/Dockerfile.yarn +71 -0
  37. package/templates/README.docker.md +15 -0
  38. package/templates/README.md +35 -0
  39. package/templates/blank/package.json +6 -0
  40. package/templates/blank/src/App.jsx +5 -0
  41. package/templates/blank-ts/package.json +6 -0
  42. package/templates/blank-ts/src/App.tsx +5 -0
  43. package/templates/eslint.config.template.mjs +98 -0
  44. package/templates/get-started/package.json +6 -0
  45. package/templates/get-started/src/App.jsx +60 -0
  46. package/templates/get-started/src/Button.jsx +14 -0
  47. package/templates/get-started/src/Confetti.jsx +19 -0
  48. package/templates/get-started/src/global.css +3 -0
  49. package/templates/get-started-ts/globals.d.ts +3 -0
  50. package/templates/get-started-ts/package.json +6 -0
  51. package/templates/get-started-ts/src/App.tsx +66 -0
  52. package/templates/get-started-ts/src/Button.tsx +18 -0
  53. package/templates/get-started-ts/src/Confetti.tsx +19 -0
  54. package/templates/get-started-ts/src/global.css +3 -0
  55. package/templates/nextjs/globals.d.ts +3 -0
  56. package/templates/nextjs/react-server.config.json +9 -0
  57. package/templates/nextjs/src/app/layout.tsx +38 -0
  58. package/templates/nextjs/src/app/page.tsx +33 -0
  59. package/templates/nextjs/src/components/Button.tsx +18 -0
  60. package/templates/nextjs/src/components/Confetti.tsx +19 -0
  61. package/templates/nextjs/src/global.css +3 -0
  62. package/templates/package.css.json +5 -0
  63. package/templates/package.eslint.json +19 -0
  64. package/templates/package.eslint.ts.json +7 -0
  65. package/templates/package.json +12 -0
  66. package/templates/package.lightningcss.json +6 -0
  67. package/templates/package.prettier.json +8 -0
  68. package/templates/package.react-compiler.json +7 -0
  69. package/templates/package.swc.json +5 -0
  70. package/templates/package.tailwind.json +7 -0
  71. package/templates/package.ts.json +11 -0
  72. package/templates/postcss.config.mjs +6 -0
  73. package/templates/router/globals.d.ts +3 -0
  74. package/templates/router/react-server.config.json +19 -0
  75. package/templates/router/src/app/@content/about.tsx +98 -0
  76. package/templates/router/src/app/@content/index.tsx +33 -0
  77. package/templates/router/src/app/layout.tsx +44 -0
  78. package/templates/router/src/app/page.tsx +17 -0
  79. package/templates/router/src/components/Button.tsx +18 -0
  80. package/templates/router/src/components/Confetti.tsx +19 -0
  81. package/templates/router/src/components/Navigation.tsx +31 -0
  82. package/templates/router/src/global.css +3 -0
  83. package/templates/shared/public/github.svg +4 -0
  84. package/templates/shared/public/react-server.svg +51 -0
  85. package/templates/tailwind.config.mjs +6 -0
  86. package/templates/tsconfig.css.json +9 -0
  87. package/templates/tsconfig.template.json +15 -0
  88. package/templates/vite.config.lightningcss.mjs +15 -0
  89. package/templates/vite.config.lightningcss.ts +15 -0
  90. package/templates/vite.config.react-compiler.mjs +19 -0
  91. package/templates/vite.config.react-compiler.ts +19 -0
  92. package/templates/vite.config.swc.mjs +6 -0
  93. package/templates/vite.config.swc.ts +6 -0
  94. package/templates/vite.config.ts +3 -0
  95. package/wizard.mjs +122 -0
@@ -0,0 +1,104 @@
1
+ import { execSync } from "node:child_process";
2
+
3
+ import { confirm, select } from "@inquirer/prompts";
4
+ import colors from "picocolors";
5
+
6
+ import { theme } from "../lib/theme.mjs";
7
+
8
+ const isPnpmInstalled = async () => {
9
+ try {
10
+ execSync("pnpm --version", {
11
+ stdio: "ignore",
12
+ });
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ };
18
+
19
+ const isYarnInstalled = async () => {
20
+ try {
21
+ execSync("yarn --version", {
22
+ stdio: "ignore",
23
+ });
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ };
29
+
30
+ const lockFilename = {
31
+ npm: "package-lock.json",
32
+ pnpm: "pnpm-lock.yaml",
33
+ yarn: "yarn.lock",
34
+ };
35
+
36
+ const displayName = {
37
+ yarn: "Yarn",
38
+ };
39
+
40
+ const ciInstallArgs = {
41
+ npm: "--loglevel notice",
42
+ pnpm: "--reporter=append-only",
43
+ yarn: "--no-progress --inline-builds",
44
+ };
45
+
46
+ export default async (context) => {
47
+ const npmUseragent = process.env.npm_config_user_agent;
48
+ const defaultPackageManager =
49
+ npmUseragent?.includes("pnpm") || !context.props.custom
50
+ ? "pnpm"
51
+ : npmUseragent?.includes("yarn")
52
+ ? "yarn"
53
+ : "npm";
54
+ const packageManager = !context.props.custom
55
+ ? defaultPackageManager
56
+ : await select(
57
+ {
58
+ message: "Package manager",
59
+ default: defaultPackageManager,
60
+ choices: [
61
+ { name: "npm", value: "npm" },
62
+ {
63
+ name: "pnpm",
64
+ value: "pnpm",
65
+ disabled: !(await isPnpmInstalled()) ? "(not installed)" : false,
66
+ },
67
+ {
68
+ name: "Yarn",
69
+ value: "yarn",
70
+ disabled: !(await isYarnInstalled()) ? "(not installed)" : false,
71
+ },
72
+ ],
73
+ theme,
74
+ },
75
+ context
76
+ );
77
+
78
+ let install = true;
79
+ if (context.props.custom) {
80
+ install = await confirm(
81
+ {
82
+ message: `Install dependencies using ${colors.cyan(displayName[packageManager] ?? packageManager)}?`,
83
+ default: true,
84
+ theme,
85
+ },
86
+ context
87
+ );
88
+ }
89
+
90
+ return {
91
+ ...context,
92
+ props: {
93
+ ...context.props,
94
+ packageManager: {
95
+ name: packageManager,
96
+ lock: lockFilename[packageManager],
97
+ run: packageManager === "npm" ? "npm run" : packageManager,
98
+ start: packageManager === "npm" ? "npm start" : packageManager,
99
+ install,
100
+ ciInstallArgs: ciInstallArgs[packageManager],
101
+ },
102
+ },
103
+ };
104
+ };
package/steps/port.mjs ADDED
@@ -0,0 +1,42 @@
1
+ import { number } from "@inquirer/prompts";
2
+
3
+ import { theme } from "../lib/theme.mjs";
4
+
5
+ export default async (context) => {
6
+ const port = context.env.options.port
7
+ ? parseInt(context.env.options.port)
8
+ : !context.props.custom || context.env.hasOptions
9
+ ? 3000
10
+ : await number(
11
+ {
12
+ message: "Port",
13
+ default: 3000,
14
+ description: "The port to use for the server",
15
+ min: 1,
16
+ max: 65535,
17
+ theme,
18
+ },
19
+ context
20
+ );
21
+ return {
22
+ ...context,
23
+ props: {
24
+ ...context.props,
25
+ port,
26
+ },
27
+ partials:
28
+ port !== 3000
29
+ ? {
30
+ ...context.partials,
31
+ "react-server.config.json": {
32
+ ...context.partials["react-server.config.json"],
33
+ type: "json",
34
+ merge: [
35
+ ...(context.partials["react-server.config.json"]?.merge ?? []),
36
+ { server: { port } },
37
+ ],
38
+ },
39
+ }
40
+ : context.partials,
41
+ };
42
+ };
@@ -0,0 +1,188 @@
1
+ import { join, relative } from "node:path";
2
+
3
+ import { confirm, select, Separator } from "@inquirer/prompts";
4
+ import glob from "fast-glob";
5
+
6
+ import { json } from "../lib/files.mjs";
7
+ import { theme } from "../lib/theme.mjs";
8
+
9
+ const defaultFeatures = ["git", "eslint", "prettier", "tailwind"];
10
+ const presets = {
11
+ blank: {
12
+ features: [],
13
+ },
14
+ "blank-ts": {
15
+ features: ["ts"],
16
+ },
17
+ "get-started": {
18
+ features: defaultFeatures,
19
+ },
20
+ "get-started-ts": {
21
+ features: ["ts", ...defaultFeatures],
22
+ alias: "~/*",
23
+ },
24
+ router: {
25
+ features: ["ts", ...defaultFeatures],
26
+ alias: "~/*",
27
+ },
28
+ nextjs: {
29
+ features: ["ts", ...defaultFeatures, "react-swc"],
30
+ alias: "~/*",
31
+ },
32
+ };
33
+
34
+ export default async (context) => {
35
+ const choices = [
36
+ {
37
+ name: "Get started (TypeScript)",
38
+ value: "get-started-ts",
39
+ description: "A simple project to get you started using TypeScript",
40
+ shared: true,
41
+ },
42
+ new Separator(),
43
+ {
44
+ name: "Blank",
45
+ value: "blank",
46
+ description: "A blank project with no additional files",
47
+ },
48
+ {
49
+ name: "Blank (TypeScript)",
50
+ value: "blank-ts",
51
+ description: "A blank TypeScript project with no additional files",
52
+ },
53
+ {
54
+ name: "Get started (JavaScript)",
55
+ value: "get-started",
56
+ description: "A simple project to get you started",
57
+ shared: true,
58
+ },
59
+ {
60
+ name: "File-system based routing",
61
+ value: "router",
62
+ description:
63
+ "A TypeScript project utilizing type-safe file-system based routing",
64
+ shared: true,
65
+ },
66
+ {
67
+ name: "Next.js App Router configuration",
68
+ value: "nextjs",
69
+ description:
70
+ "A TypeScript project utilizing a partially Next.js-compatible file-system based routing configuration",
71
+ shared: true,
72
+ },
73
+ {
74
+ name: "Blog",
75
+ value: "blog",
76
+ description: "A simple blog example project",
77
+ disabled: "(coming soon)",
78
+ },
79
+ {
80
+ name: "Portfolio",
81
+ value: "portfolio",
82
+ description: "A simple portfolio example project",
83
+ disabled: "(coming soon)",
84
+ },
85
+ {
86
+ name: "Documentation",
87
+ value: "docs",
88
+ description: "A simple documentation example project",
89
+ disabled: "(coming soon)",
90
+ },
91
+ new Separator(),
92
+ ];
93
+
94
+ const type =
95
+ context.env.options.preset ??
96
+ (await select(
97
+ {
98
+ message: "Preset",
99
+ choices,
100
+ theme,
101
+ },
102
+ context
103
+ ));
104
+
105
+ let custom = false;
106
+ if (!context.env.hasOptions) {
107
+ custom = await confirm(
108
+ {
109
+ message: "Do you want to customize your project?",
110
+ default: false,
111
+ theme,
112
+ },
113
+ context
114
+ );
115
+ }
116
+
117
+ const template = async (context) => {
118
+ const templateAppDir = join(context.env.templateDir, type);
119
+ const files = (
120
+ await glob(["**/*"], {
121
+ cwd: templateAppDir,
122
+ onlyFiles: true,
123
+ absolute: true,
124
+ })
125
+ ).map((file) => [
126
+ file,
127
+ join(context.env.projectDir, relative(templateAppDir, file)),
128
+ ]);
129
+
130
+ let sharedFiles = [];
131
+ const choice = choices.find((choice) => choice.value === type);
132
+ if (choice.shared) {
133
+ const sharedTemplateAppDir = join(context.env.templateDir, "shared");
134
+ sharedFiles = (
135
+ await glob(["**/*"], {
136
+ cwd: sharedTemplateAppDir,
137
+ onlyFiles: true,
138
+ absolute: true,
139
+ })
140
+ ).map((file) => [
141
+ file,
142
+ join(context.env.projectDir, relative(sharedTemplateAppDir, file)),
143
+ ]);
144
+ }
145
+
146
+ return {
147
+ ...context,
148
+ interactive: !context.env.options.preset,
149
+ env: {
150
+ ...context.env,
151
+ templateAppDir,
152
+ },
153
+ files: [...(context.files ?? []), ...files, ...sharedFiles],
154
+ partials: {
155
+ ...context.partials,
156
+ ...(files.includes("package.json")
157
+ ? {
158
+ "package.json": {
159
+ ...context.partials["package.json"],
160
+ merge: [
161
+ ...(context.partials["package.json"]?.merge ?? []),
162
+ await json(join(templateAppDir, "package.json")),
163
+ ],
164
+ },
165
+ }
166
+ : {}),
167
+ },
168
+ };
169
+ };
170
+
171
+ return {
172
+ ...context,
173
+ props: {
174
+ ...context.props,
175
+ template,
176
+ custom,
177
+ preset: {
178
+ type,
179
+ ...presets[type],
180
+ },
181
+ },
182
+ template,
183
+ };
184
+ };
185
+
186
+ export async function prepare(context) {
187
+ return context.template(context);
188
+ }
@@ -0,0 +1,68 @@
1
+ import { select, Separator } from "@inquirer/prompts";
2
+
3
+ import { theme } from "../lib/theme.mjs";
4
+
5
+ export default [
6
+ (context) => context.props.thirdParty,
7
+ async (context) => {
8
+ const auth = !context.props.custom
9
+ ? "none"
10
+ : await select(
11
+ {
12
+ message: "State management provider",
13
+ choices: [
14
+ {
15
+ name: "None",
16
+ value: "none",
17
+ description: "No state management",
18
+ },
19
+ new Separator(),
20
+ {
21
+ name: "TanStack Query",
22
+ value: "tanstack-query",
23
+ description: "Add TanStack Query integration",
24
+ disabled: "(coming soon)",
25
+ },
26
+ {
27
+ name: "Zustand",
28
+ value: "zustand",
29
+ description: "Add Zustand integration",
30
+ disabled: "(coming soon)",
31
+ },
32
+ {
33
+ name: "Jotai",
34
+ value: "jotai",
35
+ description: "Add Jotai integration",
36
+ disabled: "(coming soon)",
37
+ },
38
+ {
39
+ name: "Valtio",
40
+ value: "valtio",
41
+ description: "Add Valtio integration",
42
+ disabled: "(coming soon)",
43
+ },
44
+ {
45
+ name: "Redux",
46
+ value: "redux",
47
+ description: "Add Redux integration",
48
+ disabled: "(coming soon)",
49
+ },
50
+ {
51
+ name: "MobX",
52
+ value: "mobx",
53
+ description: "Add MobX integration",
54
+ disabled: "(coming soon)",
55
+ },
56
+ new Separator(),
57
+ ],
58
+ theme,
59
+ },
60
+ context
61
+ );
62
+
63
+ return {
64
+ ...context,
65
+ props: { ...context.props, auth },
66
+ };
67
+ },
68
+ ];
@@ -0,0 +1,19 @@
1
+ import { confirm } from "@inquirer/prompts";
2
+
3
+ import { theme } from "../lib/theme.mjs";
4
+
5
+ export default async (context) => {
6
+ const answer = await confirm({
7
+ message: "Use third-party integrations?",
8
+ default: false,
9
+ theme,
10
+ });
11
+
12
+ return {
13
+ ...context,
14
+ props: {
15
+ ...context.props,
16
+ thirdParty: answer,
17
+ },
18
+ };
19
+ };
package/steps/ui.mjs ADDED
@@ -0,0 +1,87 @@
1
+ import { select, Separator } from "@inquirer/prompts";
2
+ import colors from "picocolors";
3
+
4
+ import { theme } from "../lib/theme.mjs";
5
+
6
+ export default [
7
+ (context) => context.props.thirdParty,
8
+ async (context) => {
9
+ const ui = !context.props.custom
10
+ ? "none"
11
+ : await select(
12
+ {
13
+ message: "UI Framework",
14
+ choices: [
15
+ {
16
+ name: "None",
17
+ value: "none",
18
+ description: "No UI framework",
19
+ },
20
+ new Separator(),
21
+ {
22
+ name: `shadcn/ui ${colors.yellow("(recommended)")}`,
23
+ value: "shadcn",
24
+ description: "Shadcn/UI",
25
+ disabled: "(coming soon)",
26
+ },
27
+ {
28
+ name: "Radix UI",
29
+ value: "radix-ui",
30
+ description: "Radix UI",
31
+ disabled: "(coming soon)",
32
+ },
33
+ {
34
+ name: "Mantine",
35
+ value: "mantine",
36
+ description: "Mantine",
37
+ disabled: "(coming soon)",
38
+ },
39
+ {
40
+ name: "Material UI",
41
+ value: "mui",
42
+ description: "Material UI",
43
+ disabled: "(coming soon)",
44
+ },
45
+ {
46
+ name: "Chakra UI",
47
+ value: "chakra",
48
+ description: "Chakra UI",
49
+ disabled: "(coming soon)",
50
+ },
51
+ {
52
+ name: "Ant Design",
53
+ value: "ant",
54
+ description: "Ant Design",
55
+ disabled: "(coming soon)",
56
+ },
57
+ {
58
+ name: "Next UI",
59
+ value: "next-ui",
60
+ description: "Next UI",
61
+ disabled: "(coming soon)",
62
+ },
63
+ {
64
+ name: "Grommet",
65
+ value: "grommet",
66
+ description: "Grommet",
67
+ disabled: "(coming soon)",
68
+ },
69
+ {
70
+ name: "Flowbite",
71
+ value: "flowbite",
72
+ description: "Flowbite",
73
+ disabled: "(coming soon)",
74
+ },
75
+ new Separator(),
76
+ ],
77
+ theme,
78
+ },
79
+ context
80
+ );
81
+
82
+ return {
83
+ ...context,
84
+ props: { ...context.props, ui },
85
+ };
86
+ },
87
+ ];
@@ -0,0 +1,5 @@
1
+ **/*
2
+ !react-server.config.*
3
+ !react-server.*.config.*
4
+ !src
5
+ !public
@@ -0,0 +1,19 @@
1
+ # Dependencies
2
+ node_modules
3
+
4
+ # @lazarv/react-server specific
5
+ .react-server
6
+
7
+ # Environment files
8
+ .env
9
+ .env.local
10
+ .env.*.local
11
+
12
+ # Logs
13
+ *.log
14
+
15
+ # Cache
16
+ .eslintcache
17
+
18
+ # Vercel
19
+ .vercel
@@ -0,0 +1,3 @@
1
+ *.md
2
+ *-lock.*
3
+ *.lock
@@ -0,0 +1,11 @@
1
+ {
2
+ "printWidth": 80,
3
+ "tabWidth": 2,
4
+ "useTabs": false,
5
+ "semi": true,
6
+ "singleQuote": false,
7
+ "quoteProps": "as-needed",
8
+ "trailingComma": "es5",
9
+ "bracketSpacing": true,
10
+ "bracketSameLine": false
11
+ }
@@ -0,0 +1,49 @@
1
+ # Define build argument for port with default value
2
+ ARG PORT=3000
3
+
4
+ # Stage 1: Build
5
+ FROM node:20-alpine AS builder
6
+
7
+ # Set working directory
8
+ WORKDIR /app
9
+
10
+ # Copy source files
11
+ COPY . .
12
+
13
+ # Install dependencies
14
+ RUN --mount=type=cache,target=/root/.npm \
15
+ npm ci
16
+
17
+ # Build the application
18
+ RUN npm run build
19
+
20
+ # Stage 2: Production
21
+ FROM node:20-alpine AS runner
22
+
23
+ # Forward the build argument
24
+ ARG PORT
25
+ ENV PORT=$PORT
26
+
27
+ # Set working directory
28
+ WORKDIR /app
29
+
30
+ # Copy package.json, node_modules and .react-server from builder stage
31
+ COPY --from=builder /app/package.json ./
32
+ COPY --from=builder /app/node_modules ./node_modules
33
+ COPY --from=builder /app/.react-server ./.react-server
34
+
35
+ # Prune dev dependencies
36
+ RUN --mount=type=cache,target=/root/.npm \
37
+ npm prune --production
38
+
39
+ # Run as non-root user
40
+ RUN addgroup -g 1001 -S nodejs && \
41
+ adduser -S nodejs -u 1001 && \
42
+ chown -R nodejs:nodejs /app
43
+ USER nodejs
44
+
45
+ # Expose the port your app runs on
46
+ EXPOSE ${PORT}
47
+
48
+ # Start the application
49
+ CMD ["npm", "start", "--", "--host"]
@@ -0,0 +1,71 @@
1
+ # Define build argument for port with default value
2
+ ARG PORT=3000
3
+
4
+ # Stage 1: Build
5
+ FROM node:20-alpine AS builder
6
+
7
+ # Set working directory
8
+ WORKDIR /app
9
+
10
+ # Copy package files first to check for corepack and lock files
11
+ COPY package.json pnpm-lock.yaml ./
12
+
13
+ # Install pnpm using corepack or npm
14
+ RUN --mount=type=cache,target=/root/.npm \
15
+ if grep -q "\"packageManager\":" "package.json"; then \
16
+ corepack enable && corepack prepare; \
17
+ else \
18
+ npm install -g pnpm; \
19
+ fi
20
+
21
+ # Install dependencies using pnpm
22
+ RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
23
+ pnpm install --frozen-lockfile
24
+
25
+ # Copy source files and .react-server directory
26
+ COPY . .
27
+
28
+ # Build using pnpm
29
+ RUN pnpm run build
30
+
31
+ # Stage 2: Production
32
+ FROM node:20-alpine AS runner
33
+
34
+ # Forward the build argument
35
+ ARG PORT
36
+ ENV PORT=$PORT
37
+
38
+ # Set working directory
39
+ WORKDIR /app
40
+
41
+ # Copy package files and lock files
42
+ COPY --from=builder /app/package.json /app/pnpm-lock.yaml ./
43
+
44
+ # Install pnpm in production stage
45
+ RUN --mount=type=cache,target=/root/.npm \
46
+ if grep -q "\"packageManager\":" "package.json"; then \
47
+ corepack enable && corepack prepare; \
48
+ else \
49
+ npm install -g pnpm; \
50
+ fi
51
+
52
+ # Copy package.json, pnpm-lock.yaml, node_modules and .react-server from builder stage
53
+ COPY --from=builder /app/package.json /app/pnpm-lock.yaml ./
54
+ COPY --from=builder /app/node_modules ./node_modules
55
+ COPY --from=builder /app/.react-server ./.react-server
56
+
57
+ # Prune dev dependencies using pnpm
58
+ RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
59
+ pnpm prune --prod
60
+
61
+ # Run as non-root user
62
+ RUN addgroup -g 1001 -S nodejs && \
63
+ adduser -S nodejs -u 1001 && \
64
+ chown -R nodejs:nodejs /app
65
+ USER nodejs
66
+
67
+ # Expose the port your app runs on
68
+ EXPOSE ${PORT}
69
+
70
+ # Start the application using pnpm
71
+ CMD ["pnpm", "start", "--host"]