create-mainz 0.1.0-alpha.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.
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/bin/create-mainz.js +14 -0
- package/package.json +33 -0
- package/src/cli/main.js +233 -0
- package/src/cli/main.test.js +45 -0
- package/src/templates/index.js +5 -0
- package/src/templates/load-template.js +27 -0
- package/src/templates/materialize-template.js +89 -0
- package/src/templates/materialize-template.test.js +26 -0
- package/templates/project/bun/.gitkeep +1 -0
- package/templates/project/deno/empty/files/deno.json.tpl +22 -0
- package/templates/project/deno/empty/files/mainz.config.ts.tpl +6 -0
- package/templates/project/deno/empty/template.json +5 -0
- package/templates/project/deno/starter/files/app/deno.json.tpl +3 -0
- package/templates/project/deno/starter/files/app/index.html.tpl +12 -0
- package/templates/project/deno/starter/files/app/src/app.ts.tpl +10 -0
- package/templates/project/deno/starter/files/app/src/components/Counter.tsx.tpl +31 -0
- package/templates/project/deno/starter/files/app/src/main.tsx.tpl +6 -0
- package/templates/project/deno/starter/files/app/src/pages/Home.page.tsx.tpl +21 -0
- package/templates/project/deno/starter/files/app/src/pages/NotFound.page.tsx.tpl +18 -0
- package/templates/project/deno/starter/files/deno.json.tpl +25 -0
- package/templates/project/deno/starter/files/mainz.config.ts.tpl +14 -0
- package/templates/project/deno/starter/template.json +5 -0
- package/templates/project/node/empty/files/.npmrc.tpl +1 -0
- package/templates/project/node/empty/files/mainz.config.ts.tpl +6 -0
- package/templates/project/node/empty/files/package.json.tpl +18 -0
- package/templates/project/node/empty/files/tsconfig.json.tpl +17 -0
- package/templates/project/node/empty/template.json +5 -0
- package/templates/project/node/starter/files/.npmrc.tpl +1 -0
- package/templates/project/node/starter/files/app/index.html.tpl +12 -0
- package/templates/project/node/starter/files/app/package.json.tpl +5 -0
- package/templates/project/node/starter/files/app/src/app.ts.tpl +10 -0
- package/templates/project/node/starter/files/app/src/components/Counter.tsx.tpl +31 -0
- package/templates/project/node/starter/files/app/src/main.tsx.tpl +6 -0
- package/templates/project/node/starter/files/app/src/pages/Home.page.tsx.tpl +21 -0
- package/templates/project/node/starter/files/app/src/pages/NotFound.page.tsx.tpl +18 -0
- package/templates/project/node/starter/files/mainz.config.ts.tpl +14 -0
- package/templates/project/node/starter/files/package.json.tpl +21 -0
- package/templates/project/node/starter/files/tsconfig.json.tpl +17 -0
- package/templates/project/node/starter/template.json +5 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 soguten
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# create-mainz
|
|
2
|
+
|
|
3
|
+
Simple unscoped npm package to create Mainz projects from the templates owned by
|
|
4
|
+
[`soguten/mainz`](https://github.com/soguten/mainz).
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm create mainz my-app
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Starter template:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx create-mainz my-app --template starter
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Deno runtime:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx create-mainz my-app --runtime deno
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Development
|
|
25
|
+
|
|
26
|
+
Sync templates from the `main` branch of [`soguten/mainz`](https://github.com/soguten/mainz):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
node ./scripts/sync-templates.mjs
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Run tests:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
node --test
|
|
36
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { main } from "../src/cli/main.js";
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
const exitCode = await main(process.argv.slice(2));
|
|
8
|
+
if (exitCode !== 0) {
|
|
9
|
+
process.exit(exitCode);
|
|
10
|
+
}
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error(error instanceof Error ? error.stack ?? error.message : error);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-mainz",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Simple npm project creator for Mainz",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/soguten/create-mainz.git"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"create-mainz": "bin/create-mainz.js"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20.19.0"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"bin",
|
|
22
|
+
"src",
|
|
23
|
+
"templates",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"smoke": "node ./bin/create-mainz.js --help",
|
|
28
|
+
"sync:templates": "node ./scripts/sync-templates.mjs",
|
|
29
|
+
"test": "node --test",
|
|
30
|
+
"test:init": "node --test ./src/cli/main.test.js",
|
|
31
|
+
"test:templates": "node --test ./src/templates/materialize-template.test.js"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/cli/main.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { basename, resolve } from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import {
|
|
5
|
+
instantiateTemplate,
|
|
6
|
+
materializeTemplatePlan,
|
|
7
|
+
resolveBuiltInTemplateRoot,
|
|
8
|
+
} from "../templates/index.js";
|
|
9
|
+
|
|
10
|
+
class CliUsageError extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "CliUsageError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function main(args = process.argv.slice(2)) {
|
|
18
|
+
try {
|
|
19
|
+
return await runCli(args);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error instanceof Error) {
|
|
22
|
+
console.error(`[create-mainz] ${error.message}`);
|
|
23
|
+
console.error('[create-mainz] Run "create-mainz --help" for usage.');
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function runCli(args) {
|
|
32
|
+
if (
|
|
33
|
+
args.length === 0 ||
|
|
34
|
+
args.includes("--help") ||
|
|
35
|
+
args.includes("-h") ||
|
|
36
|
+
args[0] === "help"
|
|
37
|
+
) {
|
|
38
|
+
printHelp();
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const options = parseInitOptions(args);
|
|
43
|
+
const outputDir = options.name
|
|
44
|
+
? resolve(process.cwd(), options.name)
|
|
45
|
+
: process.cwd();
|
|
46
|
+
const runtime = options.runtime ?? "node";
|
|
47
|
+
const templateName = options.template ?? "empty";
|
|
48
|
+
const projectName = sanitizeProjectName(basename(outputDir) || "mainz-app");
|
|
49
|
+
const templateRoot = resolveBuiltInTemplateRoot(runtime, templateName);
|
|
50
|
+
const templateParams = buildTemplateParams({
|
|
51
|
+
runtime,
|
|
52
|
+
projectName,
|
|
53
|
+
mainzSpecifier: options.mainzSpecifier,
|
|
54
|
+
});
|
|
55
|
+
const plan = await instantiateTemplate({
|
|
56
|
+
templateRoot,
|
|
57
|
+
params: templateParams,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
validateTemplateCompatibility(plan.manifest, runtime, templateName);
|
|
61
|
+
|
|
62
|
+
await materializeTemplatePlan({
|
|
63
|
+
plan,
|
|
64
|
+
outputDir,
|
|
65
|
+
beforeWrite: assertCanCreateFile,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
console.log(
|
|
69
|
+
`[create-mainz] Created Mainz ${templateName} project in ${outputDir}.`,
|
|
70
|
+
);
|
|
71
|
+
console.log(
|
|
72
|
+
`[create-mainz] Created ${plan.files.map((file) => file.path).join(", ")}.`,
|
|
73
|
+
);
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseInitOptions(args) {
|
|
78
|
+
const options = {
|
|
79
|
+
name: undefined,
|
|
80
|
+
mainzSpecifier: undefined,
|
|
81
|
+
runtime: undefined,
|
|
82
|
+
template: undefined,
|
|
83
|
+
};
|
|
84
|
+
let positionalName;
|
|
85
|
+
|
|
86
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
87
|
+
const current = args[index];
|
|
88
|
+
|
|
89
|
+
if (current === "--mainz") {
|
|
90
|
+
options.mainzSpecifier = readOptionValue(current, args[index + 1]);
|
|
91
|
+
index += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (current === "--runtime") {
|
|
96
|
+
const runtime = readOptionValue(current, args[index + 1]);
|
|
97
|
+
if (runtime !== "node" && runtime !== "deno") {
|
|
98
|
+
throw new CliUsageError(
|
|
99
|
+
`Unsupported runtime "${runtime}". Use "node" or "deno".`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
options.runtime = runtime;
|
|
104
|
+
index += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (current === "--template") {
|
|
109
|
+
const template = readOptionValue(current, args[index + 1]);
|
|
110
|
+
if (template !== "empty" && template !== "starter") {
|
|
111
|
+
throw new CliUsageError(
|
|
112
|
+
`Unsupported template "${template}". Use "empty" or "starter".`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
options.template = template;
|
|
117
|
+
index += 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (current.startsWith("--")) {
|
|
122
|
+
throw new CliUsageError(`Unknown option "${current}".`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (positionalName) {
|
|
126
|
+
throw new CliUsageError(
|
|
127
|
+
`Received multiple project names "${positionalName}" and "${current}".`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
positionalName = current;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
options.name = positionalName;
|
|
135
|
+
return options;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function readOptionValue(option, value) {
|
|
139
|
+
if (!value || value.startsWith("--")) {
|
|
140
|
+
throw new CliUsageError(`Option "${option}" requires a value.`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function buildTemplateParams(options) {
|
|
147
|
+
if (options.runtime === "deno") {
|
|
148
|
+
const mainzSpecifier = options.mainzSpecifier ?? "jsr:@mainz/mainz";
|
|
149
|
+
return {
|
|
150
|
+
projectName: options.projectName,
|
|
151
|
+
mainzSpecifier,
|
|
152
|
+
mainzSubpathPrefix: renderGeneratedMainzSubpathPrefix(mainzSpecifier),
|
|
153
|
+
mainzCliSpecifier: "jsr:@mainz/cli-deno",
|
|
154
|
+
denoConfigPath: "deno.json",
|
|
155
|
+
appName: "app",
|
|
156
|
+
appId: "app",
|
|
157
|
+
appNavigation: "enhanced-mpa",
|
|
158
|
+
appTitle: options.projectName,
|
|
159
|
+
rootDir: "./app",
|
|
160
|
+
outDir: "dist/app",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
projectName: options.projectName,
|
|
166
|
+
mainzSpecifier: options.mainzSpecifier ?? "latest",
|
|
167
|
+
appName: "app",
|
|
168
|
+
appId: "app",
|
|
169
|
+
appNavigation: "enhanced-mpa",
|
|
170
|
+
appTitle: options.projectName,
|
|
171
|
+
rootDir: "./app",
|
|
172
|
+
outDir: "dist/app",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function renderGeneratedMainzSubpathPrefix(mainzSpecifier) {
|
|
177
|
+
const trimmed = mainzSpecifier.trim().replace(/\/+$/, "");
|
|
178
|
+
if (trimmed.startsWith("jsr:@")) {
|
|
179
|
+
return `jsr:/${trimmed.slice("jsr:".length)}/`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return `${trimmed}/`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function sanitizeProjectName(value) {
|
|
186
|
+
return value.toLowerCase().replace(/[^a-z0-9-_]/g, "-");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function validateTemplateCompatibility(manifest, runtime, templateName) {
|
|
190
|
+
if (manifest.kind !== "project") {
|
|
191
|
+
throw new CliUsageError(`Template "${templateName}" is not a project template.`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (manifest.runtime !== runtime) {
|
|
195
|
+
throw new CliUsageError(
|
|
196
|
+
`Template "${templateName}" only supports runtime "${manifest.runtime}".`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function assertCanCreateFile(path) {
|
|
202
|
+
try {
|
|
203
|
+
await access(path);
|
|
204
|
+
throw new CliUsageError(`Refusing to overwrite existing file "${path}".`);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function printHelp() {
|
|
215
|
+
console.log(
|
|
216
|
+
[
|
|
217
|
+
"create-mainz",
|
|
218
|
+
"",
|
|
219
|
+
"Usage:",
|
|
220
|
+
" create-mainz [<name>] [--template <empty|starter>] [--runtime <node|deno>] [--mainz <specifier>]",
|
|
221
|
+
"",
|
|
222
|
+
"Options:",
|
|
223
|
+
" --template <empty|starter> Choose the project template. Defaults to empty.",
|
|
224
|
+
" --runtime <node|deno> Choose which Mainz runtime template to generate.",
|
|
225
|
+
" --mainz <specifier> Override the Mainz package specifier written to the project.",
|
|
226
|
+
"",
|
|
227
|
+
"Examples:",
|
|
228
|
+
" npm create mainz@latest my-app",
|
|
229
|
+
" npm create mainz@latest my-app -- --template starter",
|
|
230
|
+
" npm create mainz@latest my-deno-app -- --runtime deno",
|
|
231
|
+
].join("\n"),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { main } from "./main.js";
|
|
7
|
+
|
|
8
|
+
test("creates a node empty project", async () => {
|
|
9
|
+
const root = await mkdtemp(join(tmpdir(), "create-mainz-node-"));
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const exitCode = await main([root]);
|
|
13
|
+
assert.equal(exitCode, 0);
|
|
14
|
+
|
|
15
|
+
const packageJson = JSON.parse(
|
|
16
|
+
await readFile(join(root, "package.json"), "utf8"),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
assert.equal(
|
|
20
|
+
packageJson.name,
|
|
21
|
+
root.split(/[/\\]/).at(-1)?.toLowerCase(),
|
|
22
|
+
);
|
|
23
|
+
assert.equal(packageJson.dependencies.mainz, "latest");
|
|
24
|
+
} finally {
|
|
25
|
+
await rm(root, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("creates a deno starter project", async () => {
|
|
30
|
+
const parent = await mkdtemp(join(tmpdir(), "create-mainz-deno-"));
|
|
31
|
+
const root = join(parent, "demo-app");
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const exitCode = await main([root, "--runtime", "deno", "--template", "starter"]);
|
|
35
|
+
assert.equal(exitCode, 0);
|
|
36
|
+
|
|
37
|
+
const denoJson = JSON.parse(await readFile(join(root, "deno.json"), "utf8"));
|
|
38
|
+
assert.equal(denoJson.imports.mainz, "jsr:@mainz/mainz");
|
|
39
|
+
|
|
40
|
+
const configSource = await readFile(join(root, "mainz.config.ts"), "utf8");
|
|
41
|
+
assert.match(configSource, /name: "app"/);
|
|
42
|
+
} finally {
|
|
43
|
+
await rm(parent, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const repoRoot = resolve(moduleDir, "..", "..");
|
|
7
|
+
|
|
8
|
+
export function resolveBuiltInTemplateRoot(runtime, name) {
|
|
9
|
+
return resolve(repoRoot, "templates", "project", runtime, name);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function loadTemplate(templateRoot) {
|
|
13
|
+
const manifestPath = resolve(templateRoot, "template.json");
|
|
14
|
+
const manifestSource = await readFile(manifestPath, "utf8");
|
|
15
|
+
const manifest = JSON.parse(manifestSource);
|
|
16
|
+
|
|
17
|
+
if (manifest.kind !== "project" || !manifest.name) {
|
|
18
|
+
throw new Error(`Invalid template manifest at "${manifestPath}".`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
manifest,
|
|
23
|
+
manifestSource,
|
|
24
|
+
root: templateRoot,
|
|
25
|
+
filesRoot: resolve(templateRoot, "files"),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, relative, resolve } from "node:path";
|
|
3
|
+
import { loadTemplate } from "./load-template.js";
|
|
4
|
+
|
|
5
|
+
export async function instantiateTemplate(options) {
|
|
6
|
+
const template = await loadTemplate(resolveRequiredTemplateRoot(options.templateRoot));
|
|
7
|
+
const relativePaths = await collectTemplateFiles(template.filesRoot);
|
|
8
|
+
const params = options.params ?? {};
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
manifest: JSON.parse(replaceTemplateTokens(template.manifestSource, params)),
|
|
12
|
+
files: await Promise.all(
|
|
13
|
+
relativePaths.map(async (relativePath) => {
|
|
14
|
+
const sourcePath = resolve(template.filesRoot, relativePath);
|
|
15
|
+
const renderedPath = stripTemplateSuffix(
|
|
16
|
+
replaceTemplateTokens(relativePath, params),
|
|
17
|
+
);
|
|
18
|
+
const renderedContent = replaceTemplateTokens(
|
|
19
|
+
await readFile(sourcePath, "utf8"),
|
|
20
|
+
params,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
path: renderedPath,
|
|
25
|
+
content: renderedContent,
|
|
26
|
+
};
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function materializeTemplatePlan(options) {
|
|
33
|
+
const filesWithAbsolutePaths = options.plan.files.map((file) => ({
|
|
34
|
+
file,
|
|
35
|
+
absolutePath: resolve(options.outputDir, file.path),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
for (const { file, absolutePath } of filesWithAbsolutePaths) {
|
|
39
|
+
if (typeof options.beforeWrite === "function") {
|
|
40
|
+
await options.beforeWrite(absolutePath, file);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const { file, absolutePath } of filesWithAbsolutePaths) {
|
|
45
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
46
|
+
await writeFile(absolutePath, file.content, "utf8");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return options.plan;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveRequiredTemplateRoot(templateRoot) {
|
|
53
|
+
if (!templateRoot) {
|
|
54
|
+
throw new Error("Template root is required.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return templateRoot;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function collectTemplateFiles(root, current = root) {
|
|
61
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
62
|
+
const files = [];
|
|
63
|
+
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
const absolutePath = resolve(current, entry.name);
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
files.push(...(await collectTemplateFiles(root, absolutePath)));
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
files.push(relative(root, absolutePath));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return files;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function replaceTemplateTokens(value, params) {
|
|
78
|
+
return value.replace(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g, (_match, key) => {
|
|
79
|
+
if (!(key in params) || params[key] === undefined || params[key] === null) {
|
|
80
|
+
throw new Error(`Missing template parameter "${key}".`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return String(params[key]);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function stripTemplateSuffix(path) {
|
|
88
|
+
return path.endsWith(".tpl") ? path.slice(0, -".tpl".length) : path;
|
|
89
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { instantiateTemplate, resolveBuiltInTemplateRoot } from "./index.js";
|
|
4
|
+
|
|
5
|
+
test("instantiates node starter template", async () => {
|
|
6
|
+
const plan = await instantiateTemplate({
|
|
7
|
+
templateRoot: resolveBuiltInTemplateRoot("node", "starter"),
|
|
8
|
+
params: {
|
|
9
|
+
projectName: "demo-app",
|
|
10
|
+
mainzSpecifier: "latest",
|
|
11
|
+
appName: "app",
|
|
12
|
+
appId: "app",
|
|
13
|
+
appNavigation: "enhanced-mpa",
|
|
14
|
+
appTitle: "demo-app",
|
|
15
|
+
rootDir: "./app",
|
|
16
|
+
outDir: "dist/app",
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
assert.equal(plan.manifest.kind, "project");
|
|
21
|
+
assert.equal(plan.manifest.runtime, "node");
|
|
22
|
+
assert.ok(plan.files.some((file) => file.path === "package.json"));
|
|
23
|
+
assert.ok(
|
|
24
|
+
plan.files.some((file) => file.path.replaceAll("\\", "/") === "app/src/app.ts"),
|
|
25
|
+
);
|
|
26
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["dom", "esnext"],
|
|
4
|
+
"jsx": "react-jsx",
|
|
5
|
+
"jsxImportSource": "mainz",
|
|
6
|
+
"strict": true
|
|
7
|
+
},
|
|
8
|
+
"imports": {
|
|
9
|
+
"@deno/vite-plugin": "npm:@deno/vite-plugin@2.0.2",
|
|
10
|
+
"mainz": "{{mainzSpecifier}}",
|
|
11
|
+
"mainz/": "{{mainzSubpathPrefix}}",
|
|
12
|
+
"vite": "npm:vite@8.0.10"
|
|
13
|
+
},
|
|
14
|
+
"tasks": {
|
|
15
|
+
"dev": "deno run -A --config {{denoConfigPath}} {{mainzCliSpecifier}} dev",
|
|
16
|
+
"build": "deno run -A --config {{denoConfigPath}} {{mainzCliSpecifier}} build",
|
|
17
|
+
"preview": "deno run -A --config {{denoConfigPath}} {{mainzCliSpecifier}} preview",
|
|
18
|
+
"test": "deno run -A --config {{denoConfigPath}} {{mainzCliSpecifier}} test",
|
|
19
|
+
"publish-info": "deno run -A --config {{denoConfigPath}} {{mainzCliSpecifier}} publish-info",
|
|
20
|
+
"diagnose": "deno run -A --config {{denoConfigPath}} {{mainzCliSpecifier}} diagnose"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>{{appTitle}}</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="app"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineApp } from "mainz";
|
|
2
|
+
import { HomePage } from "./pages/Home.page.tsx";
|
|
3
|
+
import { NotFoundPage } from "./pages/NotFound.page.tsx";
|
|
4
|
+
|
|
5
|
+
export const app = defineApp({
|
|
6
|
+
id: "{{appId}}",
|
|
7
|
+
navigation: "{{appNavigation}}",
|
|
8
|
+
pages: [HomePage],
|
|
9
|
+
notFound: NotFoundPage,
|
|
10
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Component } from "mainz";
|
|
2
|
+
|
|
3
|
+
type CounterState = {
|
|
4
|
+
count: number;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export class Counter extends Component<{}, CounterState> {
|
|
8
|
+
protected override initState(): CounterState {
|
|
9
|
+
return { count: 0 };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
private decrement = () => {
|
|
13
|
+
this.setState({ count: this.state.count - 1 });
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
private increment = () => {
|
|
17
|
+
this.setState({ count: this.state.count + 1 });
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
override render() {
|
|
21
|
+
return (
|
|
22
|
+
<section className="counter" aria-label="Counter example">
|
|
23
|
+
<p>Count: {this.state.count}</p>
|
|
24
|
+
<div>
|
|
25
|
+
<button type="button" onClick={this.decrement}>-</button>
|
|
26
|
+
<button type="button" onClick={this.increment}>+</button>
|
|
27
|
+
</div>
|
|
28
|
+
</section>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Page, Route } from "mainz";
|
|
2
|
+
import { Counter } from "../components/Counter.tsx";
|
|
3
|
+
|
|
4
|
+
@Route("/")
|
|
5
|
+
export class HomePage extends Page {
|
|
6
|
+
override head() {
|
|
7
|
+
return {
|
|
8
|
+
title: "{{appTitle}}",
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
override render() {
|
|
13
|
+
return (
|
|
14
|
+
<main>
|
|
15
|
+
<h1>{{ appTitle }}</h1>
|
|
16
|
+
<p>Welcome to your Mainz starter app.</p>
|
|
17
|
+
<Counter />
|
|
18
|
+
</main>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Page } from "mainz";
|
|
2
|
+
|
|
3
|
+
export class NotFoundPage extends Page {
|
|
4
|
+
override head() {
|
|
5
|
+
return {
|
|
6
|
+
title: "404 | {{appTitle}}",
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
override render() {
|
|
11
|
+
return (
|
|
12
|
+
<main>
|
|
13
|
+
<h1>Page not found</h1>
|
|
14
|
+
<a href="/">Go home</a>
|
|
15
|
+
</main>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["dom", "esnext"],
|
|
4
|
+
"jsx": "react-jsx",
|
|
5
|
+
"jsxImportSource": "mainz",
|
|
6
|
+
"strict": true
|
|
7
|
+
},
|
|
8
|
+
"imports": {
|
|
9
|
+
"@deno/vite-plugin": "npm:@deno/vite-plugin@2.0.2",
|
|
10
|
+
"mainz": "{{mainzSpecifier}}",
|
|
11
|
+
"mainz/": "{{mainzSubpathPrefix}}",
|
|
12
|
+
"vite": "npm:vite@8.0.10"
|
|
13
|
+
},
|
|
14
|
+
"tasks": {
|
|
15
|
+
"dev": "deno run -A --config {{denoConfigPath}} {{mainzCliSpecifier}} dev",
|
|
16
|
+
"build": "deno run -A --config {{denoConfigPath}} {{mainzCliSpecifier}} build",
|
|
17
|
+
"preview": "deno run -A --config {{denoConfigPath}} {{mainzCliSpecifier}} preview",
|
|
18
|
+
"test": "deno run -A --config {{denoConfigPath}} {{mainzCliSpecifier}} test",
|
|
19
|
+
"publish-info": "deno run -A --config {{denoConfigPath}} {{mainzCliSpecifier}} publish-info",
|
|
20
|
+
"diagnose": "deno run -A --config {{denoConfigPath}} {{mainzCliSpecifier}} diagnose"
|
|
21
|
+
},
|
|
22
|
+
"workspace": [
|
|
23
|
+
"./app"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineMainzConfig } from "mainz/config";
|
|
2
|
+
|
|
3
|
+
export default defineMainzConfig({
|
|
4
|
+
runtime: "deno",
|
|
5
|
+
targets: [
|
|
6
|
+
{
|
|
7
|
+
name: "{{appName}}",
|
|
8
|
+
rootDir: "{{rootDir}}",
|
|
9
|
+
appFile: "{{rootDir}}/src/app.ts",
|
|
10
|
+
appId: "{{appId}}",
|
|
11
|
+
outDir: "{{outDir}}",
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@jsr:registry=https://npm.jsr.io
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "mainz dev",
|
|
7
|
+
"build": "mainz build",
|
|
8
|
+
"preview": "mainz preview",
|
|
9
|
+
"test": "mainz test",
|
|
10
|
+
"diagnose": "mainz diagnose"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"mainz": "{{mainzSpecifier}}"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"vite": "^8.0.10"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["DOM", "ES2022"],
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"jsxImportSource": "mainz",
|
|
9
|
+
"experimentalDecorators": true,
|
|
10
|
+
"useDefineForClassFields": false,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"types": []
|
|
15
|
+
},
|
|
16
|
+
"include": ["**/*.ts", "**/*.tsx"]
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@jsr:registry=https://npm.jsr.io
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>{{appTitle}}</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="app"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineApp } from "mainz";
|
|
2
|
+
import { HomePage } from "./pages/Home.page.tsx";
|
|
3
|
+
import { NotFoundPage } from "./pages/NotFound.page.tsx";
|
|
4
|
+
|
|
5
|
+
export const app = defineApp({
|
|
6
|
+
id: "{{appId}}",
|
|
7
|
+
navigation: "{{appNavigation}}",
|
|
8
|
+
pages: [HomePage],
|
|
9
|
+
notFound: NotFoundPage,
|
|
10
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Component } from "mainz";
|
|
2
|
+
|
|
3
|
+
type CounterState = {
|
|
4
|
+
count: number;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export class Counter extends Component<{}, CounterState> {
|
|
8
|
+
protected override initState(): CounterState {
|
|
9
|
+
return { count: 0 };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
private decrement = () => {
|
|
13
|
+
this.setState({ count: this.state.count - 1 });
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
private increment = () => {
|
|
17
|
+
this.setState({ count: this.state.count + 1 });
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
override render() {
|
|
21
|
+
return (
|
|
22
|
+
<section className="counter" aria-label="Counter example">
|
|
23
|
+
<p>Count: {this.state.count}</p>
|
|
24
|
+
<div>
|
|
25
|
+
<button type="button" onClick={this.decrement}>-</button>
|
|
26
|
+
<button type="button" onClick={this.increment}>+</button>
|
|
27
|
+
</div>
|
|
28
|
+
</section>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Page, Route } from "mainz";
|
|
2
|
+
import { Counter } from "../components/Counter.tsx";
|
|
3
|
+
|
|
4
|
+
@Route("/")
|
|
5
|
+
export class HomePage extends Page {
|
|
6
|
+
override head() {
|
|
7
|
+
return {
|
|
8
|
+
title: "{{appTitle}}",
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
override render() {
|
|
13
|
+
return (
|
|
14
|
+
<main>
|
|
15
|
+
<h1>{{ appTitle }}</h1>
|
|
16
|
+
<p>Welcome to your Mainz starter app.</p>
|
|
17
|
+
<Counter />
|
|
18
|
+
</main>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Page } from "mainz";
|
|
2
|
+
|
|
3
|
+
export class NotFoundPage extends Page {
|
|
4
|
+
override head() {
|
|
5
|
+
return {
|
|
6
|
+
title: "404 | {{appTitle}}",
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
override render() {
|
|
11
|
+
return (
|
|
12
|
+
<main>
|
|
13
|
+
<h1>Page not found</h1>
|
|
14
|
+
<a href="/">Go home</a>
|
|
15
|
+
</main>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineMainzConfig } from "mainz/config";
|
|
2
|
+
|
|
3
|
+
export default defineMainzConfig({
|
|
4
|
+
runtime: "node",
|
|
5
|
+
targets: [
|
|
6
|
+
{
|
|
7
|
+
name: "{{appName}}",
|
|
8
|
+
rootDir: "{{rootDir}}",
|
|
9
|
+
appFile: "{{rootDir}}/src/app.ts",
|
|
10
|
+
appId: "{{appId}}",
|
|
11
|
+
outDir: "{{outDir}}",
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "mainz dev",
|
|
7
|
+
"build": "mainz build",
|
|
8
|
+
"preview": "mainz preview",
|
|
9
|
+
"test": "mainz test",
|
|
10
|
+
"diagnose": "mainz diagnose"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"mainz": "{{mainzSpecifier}}"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"vite": "^8.0.10"
|
|
17
|
+
},
|
|
18
|
+
"workspaces": [
|
|
19
|
+
"app"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["DOM", "ES2022"],
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"jsxImportSource": "mainz",
|
|
9
|
+
"experimentalDecorators": true,
|
|
10
|
+
"useDefineForClassFields": false,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"types": []
|
|
15
|
+
},
|
|
16
|
+
"include": ["**/*.ts", "**/*.tsx"]
|
|
17
|
+
}
|