@tyndall/create-app 0.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 +24 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +249 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# @tyndall/create-app
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
Scaffolding package for creating new Hyper applications with a React baseline.
|
|
5
|
+
|
|
6
|
+
## Responsibilities
|
|
7
|
+
- Validate scaffold arguments and target directory safety.
|
|
8
|
+
- Generate deterministic starter files (`package.json`, `hyper.config.ts`, `tsconfig.json`, `biome.json`, pages, public assets).
|
|
9
|
+
- Configure generated apps to use `cloud-front lint` / `cloud-front format`.
|
|
10
|
+
- Apply an initial Biome format + lint baseline through `@tyndall/lint` when CLI scaffolding runs.
|
|
11
|
+
|
|
12
|
+
## Public API Highlights
|
|
13
|
+
- `createCloudApp(targetDir, options)`
|
|
14
|
+
- `CreateCloudAppOptions.lintFormat.enabled` for optional post-scaffold quality application in programmatic usage.
|
|
15
|
+
- CLI: `@tyndall/create-app <dir> [--ui react] [--no-lint-format]`
|
|
16
|
+
|
|
17
|
+
## Development
|
|
18
|
+
- Build: `bun run --filter @tyndall/create-app build`
|
|
19
|
+
- Test (from workspace root): `bun test`
|
|
20
|
+
|
|
21
|
+
## Documentation
|
|
22
|
+
- Package specification: [spec.md](./spec.md)
|
|
23
|
+
- Package architecture: [architecture.md](./architecture.md)
|
|
24
|
+
- Package changes: [CHANGELOG.md](./CHANGELOG.md)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { type FormatOptions, type LintOptions } from "@tyndall/lint";
|
|
3
|
+
declare const SUPPORTED_UI: readonly ["react"];
|
|
4
|
+
export type UiLibrary = (typeof SUPPORTED_UI)[number];
|
|
5
|
+
interface LintToolResult {
|
|
6
|
+
code: number;
|
|
7
|
+
error?: Error;
|
|
8
|
+
}
|
|
9
|
+
export interface ScaffoldLintFormatOptions {
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
runLint?: (options: LintOptions) => Promise<LintToolResult>;
|
|
12
|
+
runFormat?: (options: FormatOptions) => Promise<LintToolResult>;
|
|
13
|
+
}
|
|
14
|
+
export interface CreateCloudAppOptions {
|
|
15
|
+
ui?: UiLibrary;
|
|
16
|
+
lintFormat?: ScaffoldLintFormatOptions;
|
|
17
|
+
}
|
|
18
|
+
export interface CreateCloudAppResult {
|
|
19
|
+
rootDir: string;
|
|
20
|
+
ui: UiLibrary;
|
|
21
|
+
files: string[];
|
|
22
|
+
lintFormatApplied: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare const createCloudApp: (targetDir: string, options?: CreateCloudAppOptions) => Promise<CreateCloudAppResult>;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAIA,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,WAAW,EACjB,MAAM,eAAe,CAAC;AAEvB,QAAA,MAAM,YAAY,oBAAqB,CAAC;AACxC,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtD,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAC5D,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;CACjE;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,CAAC,EAAE,SAAS,CAAC;IACf,UAAU,CAAC,EAAE,yBAAyB,CAAC;CACxC;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,SAAS,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAwLD,eAAO,MAAM,cAAc,GACzB,WAAW,MAAM,EACjB,UAAS,qBAA0B,KAClC,OAAO,CAAC,oBAAoB,CAmB9B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { runFormat as defaultRunFormat, runLint as defaultRunLint, } from "@tyndall/lint";
|
|
6
|
+
const SUPPORTED_UI = ["react"];
|
|
7
|
+
const USAGE = `@tyndall/create-app <dir> [--ui react]
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
--ui <library> UI adapter to scaffold (react only)
|
|
11
|
+
--no-lint-format Skip automatic Biome format/lint baseline
|
|
12
|
+
-h, --help Show this help message
|
|
13
|
+
`;
|
|
14
|
+
const normalizeUi = (ui) => {
|
|
15
|
+
const normalized = (ui ?? "react").toLowerCase();
|
|
16
|
+
// Keep UI selection strict to avoid generating incompatible scaffolds.
|
|
17
|
+
if (!SUPPORTED_UI.includes(normalized)) {
|
|
18
|
+
throw new Error(`Unsupported ui library: ${ui}. Supported: ${SUPPORTED_UI.join(", ")}`);
|
|
19
|
+
}
|
|
20
|
+
return normalized;
|
|
21
|
+
};
|
|
22
|
+
const ensureEmptyDir = async (rootDir) => {
|
|
23
|
+
let info = null;
|
|
24
|
+
try {
|
|
25
|
+
info = await stat(rootDir);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
29
|
+
await mkdir(rootDir, { recursive: true });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
if (!info.isDirectory()) {
|
|
35
|
+
throw new Error("Target path exists and is not a directory.");
|
|
36
|
+
}
|
|
37
|
+
const entries = await readdir(rootDir);
|
|
38
|
+
// Protect against clobbering an existing project directory.
|
|
39
|
+
if (entries.length > 0) {
|
|
40
|
+
throw new Error("Target directory must be empty.");
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const toPackageName = (rawName) => {
|
|
44
|
+
const cleaned = rawName
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
47
|
+
.replace(/^-+/, "")
|
|
48
|
+
.replace(/-+$/, "");
|
|
49
|
+
return cleaned.length > 0 ? cleaned : "hyper-app";
|
|
50
|
+
};
|
|
51
|
+
const buildTemplateFiles = (appName, ui) => {
|
|
52
|
+
const packageName = toPackageName(appName);
|
|
53
|
+
const files = {
|
|
54
|
+
"package.json": JSON.stringify({
|
|
55
|
+
name: packageName,
|
|
56
|
+
private: true,
|
|
57
|
+
type: "module",
|
|
58
|
+
scripts: {
|
|
59
|
+
dev: "cloud-front dev",
|
|
60
|
+
build: "cloud-front build",
|
|
61
|
+
start: "cloud-front start",
|
|
62
|
+
lint: "cloud-front lint",
|
|
63
|
+
format: "cloud-front format",
|
|
64
|
+
},
|
|
65
|
+
dependencies: {
|
|
66
|
+
react: "^18.2.0",
|
|
67
|
+
"react-dom": "^18.2.0",
|
|
68
|
+
"@tyndall/react": "workspace:*",
|
|
69
|
+
},
|
|
70
|
+
devDependencies: {
|
|
71
|
+
"@tyndall/cli": "workspace:*",
|
|
72
|
+
"@tyndall/lint": "workspace:*",
|
|
73
|
+
},
|
|
74
|
+
}, null, 2),
|
|
75
|
+
"biome.json": JSON.stringify({
|
|
76
|
+
$schema: "https://biomejs.dev/schemas/latest/schema.json",
|
|
77
|
+
formatter: {
|
|
78
|
+
enabled: true,
|
|
79
|
+
indentStyle: "space",
|
|
80
|
+
indentWidth: 2,
|
|
81
|
+
lineWidth: 100,
|
|
82
|
+
},
|
|
83
|
+
linter: {
|
|
84
|
+
enabled: true,
|
|
85
|
+
rules: {
|
|
86
|
+
recommended: true,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
javascript: {
|
|
90
|
+
formatter: {
|
|
91
|
+
quoteStyle: "double",
|
|
92
|
+
semicolons: "always",
|
|
93
|
+
trailingCommas: "all",
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
}, null, 2) + "\n",
|
|
97
|
+
"hyper.config.ts": ui === "react"
|
|
98
|
+
? [
|
|
99
|
+
"import { createReactUiAdapterFactory } from \"@tyndall/react\";",
|
|
100
|
+
"",
|
|
101
|
+
"export default {",
|
|
102
|
+
" ui: {",
|
|
103
|
+
" adapter: createReactUiAdapterFactory(),",
|
|
104
|
+
" },",
|
|
105
|
+
"};",
|
|
106
|
+
"",
|
|
107
|
+
].join("\n")
|
|
108
|
+
: `export default {\n ui: { adapter: \"${ui}\" },\n};\n`,
|
|
109
|
+
"tsconfig.json": JSON.stringify({
|
|
110
|
+
compilerOptions: {
|
|
111
|
+
target: "ES2020",
|
|
112
|
+
module: "ESNext",
|
|
113
|
+
moduleResolution: "Bundler",
|
|
114
|
+
jsx: "react-jsx",
|
|
115
|
+
strict: true,
|
|
116
|
+
},
|
|
117
|
+
include: ["src"],
|
|
118
|
+
}, null, 2),
|
|
119
|
+
"README.md": `# ${appName}\n\nThis app was scaffolded with @tyndall/create-app.\n\n## Commands\n- \`bun run dev\`\n- \`bun run build\`\n- \`bun run start\`\n- \`bun run lint\`\n- \`bun run format\`\n`,
|
|
120
|
+
".gitignore": "node_modules\ndist\n.hyper\n",
|
|
121
|
+
"public/robots.txt": "User-agent: *\nAllow: /\n",
|
|
122
|
+
"src/pages/index.tsx": `export default function Home() {\n return (\n <main>\n <h1>Hyper App</h1>\n <p>Welcome to your new Hyper project.</p>\n </main>\n );\n}\n`,
|
|
123
|
+
};
|
|
124
|
+
return files;
|
|
125
|
+
};
|
|
126
|
+
const writeFiles = async (rootDir, files) => {
|
|
127
|
+
const written = [];
|
|
128
|
+
for (const [relativePath, contents] of Object.entries(files)) {
|
|
129
|
+
const filePath = join(rootDir, relativePath);
|
|
130
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
131
|
+
await writeFile(filePath, contents, "utf-8");
|
|
132
|
+
written.push(relativePath);
|
|
133
|
+
}
|
|
134
|
+
return written;
|
|
135
|
+
};
|
|
136
|
+
const runLintFormatStep = async (mode, operation) => {
|
|
137
|
+
const result = await operation();
|
|
138
|
+
if (result.code === 0) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const errorDetail = result.error?.message ? ` ${result.error.message}` : "";
|
|
142
|
+
throw new Error(`Failed to apply ${mode} baseline (exit code: ${result.code}).${errorDetail}`);
|
|
143
|
+
};
|
|
144
|
+
const applyLintFormatBaseline = async (rootDir, options) => {
|
|
145
|
+
const runFormat = options.runFormat ?? defaultRunFormat;
|
|
146
|
+
const runLint = options.runLint ?? defaultRunLint;
|
|
147
|
+
await runLintFormatStep("format", () => runFormat({
|
|
148
|
+
cwd: rootDir,
|
|
149
|
+
write: true,
|
|
150
|
+
files: ["."],
|
|
151
|
+
}));
|
|
152
|
+
await runLintFormatStep("lint", () => runLint({
|
|
153
|
+
cwd: rootDir,
|
|
154
|
+
fix: true,
|
|
155
|
+
files: ["."],
|
|
156
|
+
}));
|
|
157
|
+
};
|
|
158
|
+
export const createCloudApp = async (targetDir, options = {}) => {
|
|
159
|
+
if (!targetDir || targetDir.trim().length === 0) {
|
|
160
|
+
throw new Error("Target directory is required.");
|
|
161
|
+
}
|
|
162
|
+
const ui = normalizeUi(options.ui);
|
|
163
|
+
const rootDir = resolve(process.cwd(), targetDir);
|
|
164
|
+
await ensureEmptyDir(rootDir);
|
|
165
|
+
const appName = basename(rootDir);
|
|
166
|
+
const files = buildTemplateFiles(appName, ui);
|
|
167
|
+
const written = await writeFiles(rootDir, files);
|
|
168
|
+
const lintFormat = options.lintFormat ?? {};
|
|
169
|
+
const lintFormatApplied = lintFormat.enabled ?? false;
|
|
170
|
+
if (lintFormatApplied) {
|
|
171
|
+
await applyLintFormatBaseline(rootDir, lintFormat);
|
|
172
|
+
}
|
|
173
|
+
return { rootDir, ui, files: written, lintFormatApplied };
|
|
174
|
+
};
|
|
175
|
+
const parseCliArgs = (argv) => {
|
|
176
|
+
const args = [...argv];
|
|
177
|
+
let targetDir = "";
|
|
178
|
+
let ui;
|
|
179
|
+
let showHelp = false;
|
|
180
|
+
let applyLintFormat = true;
|
|
181
|
+
while (args.length > 0) {
|
|
182
|
+
const next = args.shift();
|
|
183
|
+
if (!next) {
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
if (next === "-h" || next === "--help") {
|
|
187
|
+
showHelp = true;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (next === "--ui") {
|
|
191
|
+
const value = args.shift();
|
|
192
|
+
if (!value) {
|
|
193
|
+
throw new Error("--ui requires a value.");
|
|
194
|
+
}
|
|
195
|
+
ui = normalizeUi(value);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (next === "--no-lint-format") {
|
|
199
|
+
applyLintFormat = false;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (next.startsWith("--ui=")) {
|
|
203
|
+
const value = next.split("=").slice(1).join("=");
|
|
204
|
+
ui = normalizeUi(value);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (next.startsWith("--")) {
|
|
208
|
+
throw new Error(`Unknown option: ${next}`);
|
|
209
|
+
}
|
|
210
|
+
if (!targetDir) {
|
|
211
|
+
targetDir = next;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
throw new Error(`Unexpected argument: ${next}`);
|
|
215
|
+
}
|
|
216
|
+
return { targetDir, ui, showHelp, applyLintFormat };
|
|
217
|
+
};
|
|
218
|
+
const isMain = () => {
|
|
219
|
+
if (!process.argv[1]) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
return resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
223
|
+
};
|
|
224
|
+
if (isMain()) {
|
|
225
|
+
try {
|
|
226
|
+
const { targetDir, ui, showHelp, applyLintFormat } = parseCliArgs(process.argv.slice(2));
|
|
227
|
+
if (showHelp || !targetDir) {
|
|
228
|
+
const exitCode = showHelp ? 0 : 1;
|
|
229
|
+
if (!showHelp) {
|
|
230
|
+
console.error("Target directory is required.");
|
|
231
|
+
}
|
|
232
|
+
console.log(USAGE);
|
|
233
|
+
process.exit(exitCode);
|
|
234
|
+
}
|
|
235
|
+
const result = await createCloudApp(targetDir, {
|
|
236
|
+
ui,
|
|
237
|
+
lintFormat: { enabled: applyLintFormat },
|
|
238
|
+
});
|
|
239
|
+
console.log(`Created Hyper app at ${result.rootDir} (ui: ${result.ui}).`);
|
|
240
|
+
if (result.lintFormatApplied) {
|
|
241
|
+
console.log("Applied Biome lint/format baseline via @tyndall/lint.");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
246
|
+
console.error(`@tyndall/create-app failed: ${message}`);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tyndall/create-app",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"create-cloud-app": "dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"main": "dist/index.js",
|
|
12
|
+
"types": "dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"bun": "./src/index.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.json"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@tyndall/lint": "workspace:*"
|
|
28
|
+
}
|
|
29
|
+
}
|