Skip to content

Commit 184ebc2

Browse files
committed
Add project template generator foundation
1 parent ebd2178 commit 184ebc2

3,636 files changed

Lines changed: 727 additions & 1360248 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,8 @@ replit.nix
1515
.config/
1616
.upm/
1717
.cache/
18+
19+
*.tgz
20+
.launchstack/history.json
21+
launchstack.config.json
22+
sprint-*.sh

.npmignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,9 @@ replit.nix
1515
.env
1616
.env.*
1717
*.log
18+
19+
sprint-*.sh
20+
*.tgz
21+
launchstack.config.json
22+
Dockerfile
23+
docker-compose.yml

dist/cli.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env node
2+
import "./chunk-Y6FXYEAI.mjs";
23

34
// src/cli.ts
45
import { Command as Command12 } from "commander";

dist/index.d.mts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,32 @@ declare class LaunchStackError extends Error {
5959
constructor(message: string, status?: number, details?: unknown);
6060
}
6161

62-
export { type ChangelogEntry, type CreateChangelogInput, type CreateDeploymentInput, type CreateLaunchInput, type Deployment, type Launch, LaunchStackClient, type LaunchStackConfig, LaunchStackError };
62+
type TemplateName = "api";
63+
type TemplateFile = {
64+
source: string;
65+
destination: string;
66+
};
67+
type ProjectTemplate = {
68+
name: TemplateName;
69+
rootDirectory: string;
70+
};
71+
type GenerateProjectOptions = {
72+
projectName: string;
73+
destinationDirectory: string;
74+
template: TemplateName;
75+
overwrite?: boolean;
76+
};
77+
78+
declare function generateProject(options: GenerateProjectOptions): string;
79+
80+
declare function ensureDestinationAvailable(destinationDirectory: string, overwrite?: boolean): void;
81+
declare function copyDirectory(sourceDirectory: string, destinationDirectory: string): void;
82+
83+
declare function validateProjectName(projectName: string): void;
84+
declare function toDisplayName(projectName: string): string;
85+
86+
type TemplateVariables = Record<string, string>;
87+
declare function renderTemplate(content: string, variables: TemplateVariables): string;
88+
declare function renderDirectory(directory: string, variables: TemplateVariables): void;
89+
90+
export { type ChangelogEntry, type CreateChangelogInput, type CreateDeploymentInput, type CreateLaunchInput, type Deployment, type GenerateProjectOptions, type Launch, LaunchStackClient, type LaunchStackConfig, LaunchStackError, type ProjectTemplate, type TemplateFile, type TemplateName, copyDirectory, ensureDestinationAvailable, generateProject, renderDirectory, renderTemplate, toDisplayName, validateProjectName };

dist/index.d.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,32 @@ declare class LaunchStackError extends Error {
5959
constructor(message: string, status?: number, details?: unknown);
6060
}
6161

62-
export { type ChangelogEntry, type CreateChangelogInput, type CreateDeploymentInput, type CreateLaunchInput, type Deployment, type Launch, LaunchStackClient, type LaunchStackConfig, LaunchStackError };
62+
type TemplateName = "api";
63+
type TemplateFile = {
64+
source: string;
65+
destination: string;
66+
};
67+
type ProjectTemplate = {
68+
name: TemplateName;
69+
rootDirectory: string;
70+
};
71+
type GenerateProjectOptions = {
72+
projectName: string;
73+
destinationDirectory: string;
74+
template: TemplateName;
75+
overwrite?: boolean;
76+
};
77+
78+
declare function generateProject(options: GenerateProjectOptions): string;
79+
80+
declare function ensureDestinationAvailable(destinationDirectory: string, overwrite?: boolean): void;
81+
declare function copyDirectory(sourceDirectory: string, destinationDirectory: string): void;
82+
83+
declare function validateProjectName(projectName: string): void;
84+
declare function toDisplayName(projectName: string): string;
85+
86+
type TemplateVariables = Record<string, string>;
87+
declare function renderTemplate(content: string, variables: TemplateVariables): string;
88+
declare function renderDirectory(directory: string, variables: TemplateVariables): void;
89+
90+
export { type ChangelogEntry, type CreateChangelogInput, type CreateDeploymentInput, type CreateLaunchInput, type Deployment, type GenerateProjectOptions, type Launch, LaunchStackClient, type LaunchStackConfig, LaunchStackError, type ProjectTemplate, type TemplateFile, type TemplateName, copyDirectory, ensureDestinationAvailable, generateProject, renderDirectory, renderTemplate, toDisplayName, validateProjectName };

dist/index.js

Lines changed: 165 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
2121
var index_exports = {};
2222
__export(index_exports, {
2323
LaunchStackClient: () => LaunchStackClient,
24-
LaunchStackError: () => LaunchStackError
24+
LaunchStackError: () => LaunchStackError,
25+
copyDirectory: () => copyDirectory,
26+
ensureDestinationAvailable: () => ensureDestinationAvailable,
27+
generateProject: () => generateProject,
28+
renderDirectory: () => renderDirectory,
29+
renderTemplate: () => renderTemplate,
30+
toDisplayName: () => toDisplayName,
31+
validateProjectName: () => validateProjectName
2532
});
2633
module.exports = __toCommonJS(index_exports);
2734

@@ -94,8 +101,164 @@ var LaunchStackClient = class {
94101
});
95102
}
96103
};
104+
105+
// src/generator/generate.ts
106+
var import_node_path4 = require("path");
107+
108+
// src/generator/files.ts
109+
var import_node_fs = require("fs");
110+
var import_node_path = require("path");
111+
var RENAMED_TEMPLATE_FILES = {
112+
"_gitignore": ".gitignore",
113+
"_npmrc": ".npmrc",
114+
"_env.example": ".env.example"
115+
};
116+
function ensureDestinationAvailable(destinationDirectory, overwrite = false) {
117+
if (!(0, import_node_fs.existsSync)(destinationDirectory)) {
118+
return;
119+
}
120+
const contents = (0, import_node_fs.readdirSync)(destinationDirectory);
121+
if (contents.length > 0 && !overwrite) {
122+
throw new Error(
123+
`Destination is not empty: ${destinationDirectory}. Use overwrite to continue.`
124+
);
125+
}
126+
}
127+
function copyDirectory(sourceDirectory, destinationDirectory) {
128+
if (!(0, import_node_fs.existsSync)(sourceDirectory)) {
129+
throw new Error(`Template directory not found: ${sourceDirectory}`);
130+
}
131+
(0, import_node_fs.mkdirSync)(destinationDirectory, { recursive: true });
132+
(0, import_node_fs.cpSync)(sourceDirectory, destinationDirectory, {
133+
recursive: true,
134+
force: true
135+
});
136+
renameTemplateFiles(destinationDirectory);
137+
}
138+
function renameTemplateFiles(directory) {
139+
for (const entry of (0, import_node_fs.readdirSync)(directory)) {
140+
const path = (0, import_node_path.join)(directory, entry);
141+
const stats = (0, import_node_fs.statSync)(path);
142+
if (stats.isDirectory()) {
143+
renameTemplateFiles(path);
144+
continue;
145+
}
146+
const replacementName = RENAMED_TEMPLATE_FILES[(0, import_node_path.basename)(path)];
147+
if (!replacementName) {
148+
continue;
149+
}
150+
const content = (0, import_node_fs.readFileSync)(path);
151+
const replacementPath = (0, import_node_path.resolve)(directory, replacementName);
152+
(0, import_node_fs.writeFileSync)(replacementPath, content);
153+
const { unlinkSync } = require("fs");
154+
unlinkSync(path);
155+
}
156+
}
157+
158+
// src/generator/names.ts
159+
var PROJECT_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
160+
function validateProjectName(projectName) {
161+
if (!projectName.trim()) {
162+
throw new Error("Project name is required.");
163+
}
164+
if (!PROJECT_NAME_PATTERN.test(projectName)) {
165+
throw new Error(
166+
"Project name must use lowercase letters, numbers, and hyphens only."
167+
);
168+
}
169+
}
170+
function toDisplayName(projectName) {
171+
return projectName.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
172+
}
173+
174+
// src/generator/paths.ts
175+
var import_node_fs2 = require("fs");
176+
var import_node_path2 = require("path");
177+
function findPackageRoot() {
178+
const candidates = [
179+
process.cwd(),
180+
(0, import_node_path2.resolve)(process.cwd(), ".."),
181+
(0, import_node_path2.resolve)(process.cwd(), "../..")
182+
];
183+
for (const candidate of candidates) {
184+
if ((0, import_node_fs2.existsSync)((0, import_node_path2.resolve)(candidate, "package.json"))) {
185+
return candidate;
186+
}
187+
}
188+
throw new Error("Could not locate the LaunchStack package root.");
189+
}
190+
function getPackageRoot() {
191+
return findPackageRoot();
192+
}
193+
function getTemplateDirectory(templateName) {
194+
const packageRoot = getPackageRoot();
195+
const candidates = [
196+
(0, import_node_path2.resolve)(packageRoot, "src", "templates", templateName),
197+
(0, import_node_path2.resolve)(packageRoot, "dist", "templates", templateName),
198+
(0, import_node_path2.resolve)(packageRoot, "templates", templateName)
199+
];
200+
const templateDirectory = candidates.find(
201+
(candidate) => (0, import_node_fs2.existsSync)(candidate)
202+
);
203+
if (!templateDirectory) {
204+
throw new Error(`Template directory not found: ${templateName}`);
205+
}
206+
return templateDirectory;
207+
}
208+
209+
// src/generator/template.ts
210+
var import_node_fs3 = require("fs");
211+
var import_node_path3 = require("path");
212+
function renderTemplate(content, variables) {
213+
return Object.entries(variables).reduce(
214+
(rendered, [key, value]) => rendered.split(`{{${key}}}`).join(value),
215+
content
216+
);
217+
}
218+
function renderDirectory(directory, variables) {
219+
if (!(0, import_node_fs3.existsSync)(directory)) {
220+
throw new Error(`Directory not found: ${directory}`);
221+
}
222+
for (const entry of (0, import_node_fs3.readdirSync)(directory)) {
223+
const path = (0, import_node_path3.join)(directory, entry);
224+
const stats = (0, import_node_fs3.statSync)(path);
225+
if (stats.isDirectory()) {
226+
renderDirectory(path, variables);
227+
continue;
228+
}
229+
const content = (0, import_node_fs3.readFileSync)(path, "utf8");
230+
const rendered = renderTemplate(content, variables);
231+
if (rendered !== content) {
232+
(0, import_node_fs3.writeFileSync)(path, rendered);
233+
}
234+
}
235+
}
236+
237+
// src/generator/generate.ts
238+
function generateProject(options) {
239+
validateProjectName(options.projectName);
240+
const destinationDirectory = (0, import_node_path4.resolve)(options.destinationDirectory);
241+
ensureDestinationAvailable(
242+
destinationDirectory,
243+
options.overwrite ?? false
244+
);
245+
const templateDirectory = getTemplateDirectory(options.template);
246+
copyDirectory(templateDirectory, destinationDirectory);
247+
renderDirectory(destinationDirectory, {
248+
PROJECT_NAME: options.projectName,
249+
PROJECT_DISPLAY_NAME: toDisplayName(options.projectName)
250+
});
251+
return destinationDirectory;
252+
}
97253
// Annotate the CommonJS export names for ESM import in node:
98254
0 && (module.exports = {
99255
LaunchStackClient,
100-
LaunchStackError
256+
LaunchStackError,
257+
copyDirectory,
258+
ensureDestinationAvailable,
259+
generateProject,
260+
renderDirectory,
261+
renderTemplate,
262+
toDisplayName,
263+
validateProjectName
101264
});

0 commit comments

Comments
 (0)