create-fstack-app 1.0.1
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/README.md +67 -0
- package/bin/index.js +8 -0
- package/package.json +43 -0
- package/src/cli.js +46 -0
- package/src/generators/createProject.js +55 -0
- package/src/generators/templateEngine.js +118 -0
- package/src/generators/templates/addons.js +119 -0
- package/src/generators/templates/backend.js +265 -0
- package/src/generators/templates/database.js +45 -0
- package/src/generators/templates/frontend.js +271 -0
- package/src/generators/templates/root.js +170 -0
- package/src/install/installDependencies.js +30 -0
- package/src/install/packageMap.js +3 -0
- package/src/prompts/index.js +106 -0
- package/src/stacks.js +94 -0
- package/src/utils/names.js +15 -0
- package/templates/README.md +10 -0
- package/templates/addons/auth/.gitkeep +1 -0
- package/templates/addons/docker/.gitkeep +1 -0
- package/templates/addons/eslint/.gitkeep +1 -0
- package/templates/addons/testing/.gitkeep +1 -0
- package/templates/backend/django/.gitkeep +1 -0
- package/templates/backend/express/.gitkeep +1 -0
- package/templates/backend/laravel/.gitkeep +1 -0
- package/templates/backend/nest/.gitkeep +1 -0
- package/templates/databases/mongodb/.gitkeep +1 -0
- package/templates/databases/mysql/.gitkeep +1 -0
- package/templates/databases/postgres/.gitkeep +1 -0
- package/templates/frontend/angular/.gitkeep +1 -0
- package/templates/frontend/next/.gitkeep +1 -0
- package/templates/frontend/react/.gitkeep +1 -0
- package/templates/frontend/vue/.gitkeep +1 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { slugToTitle } from "../../utils/names.js";
|
|
2
|
+
|
|
3
|
+
export function createRootFiles({ projectName, stack, config }) {
|
|
4
|
+
const scripts = rootScripts(config);
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
"package.json": `${JSON.stringify({
|
|
8
|
+
name: projectName,
|
|
9
|
+
version: "0.1.0",
|
|
10
|
+
private: true,
|
|
11
|
+
type: "module",
|
|
12
|
+
workspaces: rootWorkspaces(config),
|
|
13
|
+
scripts,
|
|
14
|
+
dependencies: rootDependencies(config),
|
|
15
|
+
devDependencies: rootDevDependencies(config)
|
|
16
|
+
}, null, 2)}\n`,
|
|
17
|
+
"README.md": readme({ projectName, stack, config }),
|
|
18
|
+
".gitignore": gitignore(),
|
|
19
|
+
".env.example": envExample(config)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function rootScripts(config) {
|
|
24
|
+
if (config.structure === "t3") {
|
|
25
|
+
const scripts = {
|
|
26
|
+
dev: "next dev",
|
|
27
|
+
build: "next build",
|
|
28
|
+
start: "next start",
|
|
29
|
+
lint: "next lint"
|
|
30
|
+
};
|
|
31
|
+
return withAddonScripts(scripts, config);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const scripts = {};
|
|
35
|
+
if (hasFrontend(config) && hasNodeBackend(config)) {
|
|
36
|
+
scripts.dev = "concurrently \"npm run dev --workspace frontend\" \"npm run dev --workspace backend\"";
|
|
37
|
+
} else if (hasFrontend(config)) {
|
|
38
|
+
scripts.dev = "npm run dev --workspace frontend";
|
|
39
|
+
} else if (hasNodeBackend(config)) {
|
|
40
|
+
scripts.dev = "npm run dev --workspace backend";
|
|
41
|
+
} else {
|
|
42
|
+
scripts.dev = "echo \"Start your selected framework from its generated folder.\"";
|
|
43
|
+
}
|
|
44
|
+
return withAddonScripts(scripts, config);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function rootDevDependencies(config) {
|
|
48
|
+
if (config.structure === "t3") {
|
|
49
|
+
return withAddonDevDependencies({
|
|
50
|
+
"@types/node": "^20.14.10",
|
|
51
|
+
"@types/react": "^18.3.3",
|
|
52
|
+
"@types/react-dom": "^18.3.0",
|
|
53
|
+
eslint: "^8.57.0",
|
|
54
|
+
"eslint-config-next": "^14.2.5",
|
|
55
|
+
prisma: "^5.16.1",
|
|
56
|
+
typescript: "^5.5.3"
|
|
57
|
+
}, config);
|
|
58
|
+
}
|
|
59
|
+
const deps = hasFrontend(config) && hasNodeBackend(config) ? { concurrently: "^8.2.2" } : {};
|
|
60
|
+
return withAddonDevDependencies(deps, config);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function withAddonDevDependencies(deps, config) {
|
|
64
|
+
if (config.features.includes("ESLint")) deps.eslint = "^8.57.0";
|
|
65
|
+
if (config.features.includes("Prettier")) deps.prettier = "^3.3.2";
|
|
66
|
+
if (config.features.includes("Husky")) deps.husky = "^9.1.1";
|
|
67
|
+
if (config.features.includes("Testing Setup")) deps.vitest = "^2.0.3";
|
|
68
|
+
return deps;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function rootDependencies(config) {
|
|
72
|
+
if (config.structure !== "t3") return {};
|
|
73
|
+
return {
|
|
74
|
+
"@prisma/client": "^5.16.1",
|
|
75
|
+
"@trpc/client": "^10.45.2",
|
|
76
|
+
"@trpc/next": "^10.45.2",
|
|
77
|
+
"@trpc/react-query": "^10.45.2",
|
|
78
|
+
"@trpc/server": "^10.45.2",
|
|
79
|
+
next: "^14.2.5",
|
|
80
|
+
react: "^18.3.1",
|
|
81
|
+
"react-dom": "^18.3.1",
|
|
82
|
+
superjson: "^2.2.1",
|
|
83
|
+
zod: "^3.23.8"
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function rootWorkspaces(config) {
|
|
88
|
+
if (config.structure === "t3") return [];
|
|
89
|
+
const workspaces = [];
|
|
90
|
+
if (hasFrontend(config)) workspaces.push("frontend");
|
|
91
|
+
if (hasNodeBackend(config)) workspaces.push("backend");
|
|
92
|
+
return workspaces;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readme({ projectName, stack, config }) {
|
|
96
|
+
return `# ${slugToTitle(projectName)}
|
|
97
|
+
|
|
98
|
+
Generated with \`create-my-fullstack-app\`.
|
|
99
|
+
|
|
100
|
+
## Stack
|
|
101
|
+
|
|
102
|
+
- Preset: ${stack.label}
|
|
103
|
+
- Frontend: ${config.frontend}
|
|
104
|
+
- Backend: ${config.backend}
|
|
105
|
+
- Database: ${config.database}
|
|
106
|
+
- Styling: ${config.styling}
|
|
107
|
+
- Auth: ${config.auth}
|
|
108
|
+
|
|
109
|
+
## Getting Started
|
|
110
|
+
|
|
111
|
+
\`\`\`bash
|
|
112
|
+
npm install
|
|
113
|
+
npm run dev
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
## Folders
|
|
117
|
+
|
|
118
|
+
- \`frontend/\`: client application when your stack uses a separate frontend
|
|
119
|
+
- \`backend/\`: server application when your stack uses a separate backend
|
|
120
|
+
- \`database/\`: schema, seed, or connection notes when relevant
|
|
121
|
+
- \`src/\`, \`server/\`, \`prisma/\`, \`app/\`: T3-style structure when selected
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function gitignore() {
|
|
126
|
+
return `node_modules
|
|
127
|
+
dist
|
|
128
|
+
build
|
|
129
|
+
.next
|
|
130
|
+
.env
|
|
131
|
+
.DS_Store
|
|
132
|
+
coverage
|
|
133
|
+
*.log
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function envExample(config) {
|
|
138
|
+
const lines = ["NODE_ENV=development"];
|
|
139
|
+
if (config.database === "MongoDB") lines.push("MONGODB_URI=mongodb://localhost:27017/my_fullstack_app");
|
|
140
|
+
if (config.database === "PostgreSQL") lines.push("DATABASE_URL=postgresql://postgres:postgres@localhost:5432/my_fullstack_app");
|
|
141
|
+
if (config.database === "MySQL") lines.push("DATABASE_URL=mysql://root:password@localhost:3306/my_fullstack_app");
|
|
142
|
+
if (config.database === "Firebase") lines.push("VITE_FIREBASE_API_KEY=", "VITE_FIREBASE_PROJECT_ID=");
|
|
143
|
+
if (config.auth !== "None") lines.push("AUTH_SECRET=change-me");
|
|
144
|
+
return `${lines.join("\n")}\n`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function hasFrontend(config) {
|
|
148
|
+
return config.frontend !== "None";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function hasNodeBackend(config) {
|
|
152
|
+
return ["Express.js", "Fastify", "NestJS", "Firebase Functions"].includes(config.backend);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function withAddonScripts(scripts, config) {
|
|
156
|
+
const nextScripts = { ...scripts };
|
|
157
|
+
if (config.features.includes("ESLint") && !nextScripts.lint) {
|
|
158
|
+
nextScripts.lint = "eslint .";
|
|
159
|
+
}
|
|
160
|
+
if (config.features.includes("Prettier")) {
|
|
161
|
+
nextScripts.format = "prettier --write .";
|
|
162
|
+
}
|
|
163
|
+
if (config.features.includes("Testing Setup")) {
|
|
164
|
+
nextScripts.test = "vitest";
|
|
165
|
+
}
|
|
166
|
+
if (config.features.includes("Husky")) {
|
|
167
|
+
nextScripts.prepare = "husky";
|
|
168
|
+
}
|
|
169
|
+
return nextScripts;
|
|
170
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
|
|
5
|
+
export async function installDependencies(targetDir, installTargets) {
|
|
6
|
+
for (const relativeTarget of installTargets) {
|
|
7
|
+
const cwd = path.join(targetDir, relativeTarget);
|
|
8
|
+
const label = relativeTarget === "." ? "root" : relativeTarget;
|
|
9
|
+
const spinner = ora(`Installing ${label} dependencies...`).start();
|
|
10
|
+
|
|
11
|
+
await runNpmInstall(cwd);
|
|
12
|
+
spinner.succeed(`Installed ${label} dependencies`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function runNpmInstall(cwd) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const child = spawn("npm", ["install"], {
|
|
19
|
+
cwd,
|
|
20
|
+
shell: true,
|
|
21
|
+
stdio: "ignore"
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
child.on("error", reject);
|
|
25
|
+
child.on("exit", (code) => {
|
|
26
|
+
if (code === 0) resolve();
|
|
27
|
+
else reject(new Error(`npm install failed in ${cwd}`));
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { input, select, confirm, checkbox } from "@inquirer/prompts";
|
|
2
|
+
import { normalizeProjectName } from "../utils/names.js";
|
|
3
|
+
import { STACKS, stackChoices, resolveStack } from "../stacks.js";
|
|
4
|
+
|
|
5
|
+
const defaultCustom = {
|
|
6
|
+
frontend: "React",
|
|
7
|
+
language: "JavaScript",
|
|
8
|
+
styling: "Tailwind CSS",
|
|
9
|
+
backend: "Express.js",
|
|
10
|
+
database: "MongoDB",
|
|
11
|
+
auth: "None",
|
|
12
|
+
features: ["ESLint", "Prettier"]
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export async function collectAnswers({ projectName, stack, yes = false, install = true }) {
|
|
16
|
+
const resolvedProjectName = projectName
|
|
17
|
+
? projectName.trim()
|
|
18
|
+
: yes
|
|
19
|
+
? "my-app"
|
|
20
|
+
: normalizeProjectName(await input({
|
|
21
|
+
message: "Enter project name:",
|
|
22
|
+
default: "my-app",
|
|
23
|
+
validate: validateProjectName
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
const resolvedStack = stack
|
|
27
|
+
? resolveStack(stack)
|
|
28
|
+
: yes
|
|
29
|
+
? STACKS.MERN
|
|
30
|
+
: await select({
|
|
31
|
+
message: "Choose your stack:",
|
|
32
|
+
choices: stackChoices
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (resolvedStack.id === "custom") {
|
|
36
|
+
return {
|
|
37
|
+
projectName: resolvedProjectName,
|
|
38
|
+
stack: resolvedStack,
|
|
39
|
+
custom: yes ? defaultCustom : await collectCustomAnswers(),
|
|
40
|
+
install: install && (yes || await confirm({ message: "Install dependencies?", default: true }))
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
projectName: resolvedProjectName,
|
|
46
|
+
stack: resolvedStack,
|
|
47
|
+
custom: null,
|
|
48
|
+
install: install && (yes || await confirm({ message: "Install dependencies?", default: true }))
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function collectCustomAnswers() {
|
|
53
|
+
const frontend = await select({
|
|
54
|
+
message: "Choose frontend framework:",
|
|
55
|
+
choices: ["React", "Next.js", "Vue", "Nuxt", "Angular", "Svelte", "SolidJS"].map(toChoice)
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const language = await select({
|
|
59
|
+
message: "Choose language:",
|
|
60
|
+
choices: ["JavaScript", "TypeScript"].map(toChoice)
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const styling = await select({
|
|
64
|
+
message: "Choose styling solution:",
|
|
65
|
+
choices: ["Tailwind CSS", "CSS Modules", "Styled Components", "SCSS", "Chakra UI", "Material UI", "None"].map(toChoice)
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const backend = await select({
|
|
69
|
+
message: "Choose backend framework:",
|
|
70
|
+
choices: ["Express.js", "Fastify", "NestJS", "Django", "Laravel", "Spring Boot", "ASP.NET", "None"].map(toChoice)
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const database = await select({
|
|
74
|
+
message: "Choose database:",
|
|
75
|
+
choices: ["MongoDB", "PostgreSQL", "MySQL", "SQLite", "Firebase", "Supabase", "None"].map(toChoice)
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const auth = await select({
|
|
79
|
+
message: "Add authentication starter?",
|
|
80
|
+
choices: ["JWT", "Clerk", "Auth.js", "Firebase Auth", "Supabase Auth", "None"].map(toChoice)
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const features = await checkbox({
|
|
84
|
+
message: "Select optional features:",
|
|
85
|
+
choices: ["ESLint", "Prettier", "Husky", "Docker", "Testing Setup", "CI/CD", "Zustand", "Redux Toolkit", "Prisma", "Swagger Docs"].map((name) => ({
|
|
86
|
+
name,
|
|
87
|
+
value: name,
|
|
88
|
+
checked: ["ESLint", "Prettier"].includes(name)
|
|
89
|
+
}))
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return { frontend, language, styling, backend, database, auth, features };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function toChoice(value) {
|
|
96
|
+
return { name: value, value };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function validateProjectName(value) {
|
|
100
|
+
const normalized = normalizeProjectName(value);
|
|
101
|
+
if (!normalized) return "Project name is required.";
|
|
102
|
+
if (!/^[a-z0-9._-]+$/.test(normalized)) {
|
|
103
|
+
return "Use letters, numbers, dots, dashes, or underscores.";
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
package/src/stacks.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export const STACKS = {
|
|
2
|
+
MERN: {
|
|
3
|
+
id: "mern",
|
|
4
|
+
label: "MERN",
|
|
5
|
+
frontend: "React",
|
|
6
|
+
backend: "Node + Express",
|
|
7
|
+
database: "MongoDB",
|
|
8
|
+
structure: "split"
|
|
9
|
+
},
|
|
10
|
+
PERN: {
|
|
11
|
+
id: "pern",
|
|
12
|
+
label: "PERN",
|
|
13
|
+
frontend: "React",
|
|
14
|
+
backend: "Node + Express",
|
|
15
|
+
database: "PostgreSQL",
|
|
16
|
+
structure: "split"
|
|
17
|
+
},
|
|
18
|
+
MEAN: {
|
|
19
|
+
id: "mean",
|
|
20
|
+
label: "MEAN",
|
|
21
|
+
frontend: "Angular",
|
|
22
|
+
backend: "Node + Express",
|
|
23
|
+
database: "MongoDB",
|
|
24
|
+
structure: "split"
|
|
25
|
+
},
|
|
26
|
+
T3: {
|
|
27
|
+
id: "t3",
|
|
28
|
+
label: "T3 Stack",
|
|
29
|
+
frontend: "Next.js + TypeScript",
|
|
30
|
+
backend: "tRPC",
|
|
31
|
+
database: "PostgreSQL",
|
|
32
|
+
structure: "t3"
|
|
33
|
+
},
|
|
34
|
+
FIREBASE: {
|
|
35
|
+
id: "firebase",
|
|
36
|
+
label: "Firebase Stack",
|
|
37
|
+
frontend: "React",
|
|
38
|
+
backend: "Firebase Functions",
|
|
39
|
+
database: "Firestore",
|
|
40
|
+
structure: "firebase"
|
|
41
|
+
},
|
|
42
|
+
DJANGO_REACT: {
|
|
43
|
+
id: "django-react",
|
|
44
|
+
label: "Django + React",
|
|
45
|
+
frontend: "React",
|
|
46
|
+
backend: "Django",
|
|
47
|
+
database: "PostgreSQL",
|
|
48
|
+
structure: "split"
|
|
49
|
+
},
|
|
50
|
+
LARAVEL_REACT: {
|
|
51
|
+
id: "laravel-react",
|
|
52
|
+
label: "Laravel + React",
|
|
53
|
+
frontend: "React",
|
|
54
|
+
backend: "Laravel",
|
|
55
|
+
database: "MySQL",
|
|
56
|
+
structure: "split"
|
|
57
|
+
},
|
|
58
|
+
CUSTOM: {
|
|
59
|
+
id: "custom",
|
|
60
|
+
label: "Custom Setup",
|
|
61
|
+
frontend: "User-selected",
|
|
62
|
+
backend: "User-selected",
|
|
63
|
+
database: "User-selected",
|
|
64
|
+
structure: "custom"
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const stackChoices = [
|
|
69
|
+
STACKS.MERN,
|
|
70
|
+
STACKS.PERN,
|
|
71
|
+
STACKS.MEAN,
|
|
72
|
+
STACKS.T3,
|
|
73
|
+
STACKS.FIREBASE,
|
|
74
|
+
STACKS.DJANGO_REACT,
|
|
75
|
+
STACKS.LARAVEL_REACT,
|
|
76
|
+
STACKS.CUSTOM
|
|
77
|
+
].map((stack) => ({
|
|
78
|
+
name: stack.label,
|
|
79
|
+
value: stack
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
export function resolveStack(value) {
|
|
83
|
+
const normalized = String(value).toLowerCase().replace(/[\s_+]+/g, "-");
|
|
84
|
+
const stack = Object.values(STACKS).find((candidate) => {
|
|
85
|
+
return candidate.id === normalized || candidate.label.toLowerCase().replace(/[\s_+]+/g, "-") === normalized;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!stack) {
|
|
89
|
+
const valid = Object.values(STACKS).map((candidate) => candidate.label).join(", ");
|
|
90
|
+
throw new Error(`Unknown stack "${value}". Choose one of: ${valid}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return stack;
|
|
94
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function normalizeProjectName(value) {
|
|
2
|
+
return String(value || "")
|
|
3
|
+
.trim()
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
.replace(/\s+/g, "-")
|
|
6
|
+
.replace(/[^a-z0-9._-]/g, "");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function slugToTitle(value) {
|
|
10
|
+
return normalizeProjectName(value)
|
|
11
|
+
.split(/[-_]/g)
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
14
|
+
.join(" ");
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Templates
|
|
2
|
+
|
|
3
|
+
This CLI uses a modular JavaScript template engine in `src/generators/templates`.
|
|
4
|
+
|
|
5
|
+
The folders below document the intended public template architecture for future static or community templates:
|
|
6
|
+
|
|
7
|
+
- `frontend/react`, `frontend/next`, `frontend/vue`, `frontend/angular`
|
|
8
|
+
- `backend/express`, `backend/django`, `backend/laravel`, `backend/nest`
|
|
9
|
+
- `databases/mongodb`, `databases/postgres`, `databases/mysql`
|
|
10
|
+
- `addons/auth`, `addons/docker`, `addons/testing`, `addons/eslint`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|