@effindomv2/create-fui-as-app 0.1.2 → 0.1.4

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 (54) hide show
  1. package/build/templates/hello/asconfig.json +24 -0
  2. package/build/templates/hello/harness.ts +9 -0
  3. package/build/templates/hello/index.html +98 -0
  4. package/build/templates/hello/package.json +32 -0
  5. package/build/templates/hello/scripts/prepare-runtime.ts +18 -0
  6. package/build/templates/hello/scripts/smoke.ts +15 -0
  7. package/build/templates/hello/src/App.ts +7 -0
  8. package/build/templates/hello/src/Fui.ts +1 -0
  9. package/build/templates/hello/src/FuiBrowser.ts +1 -0
  10. package/build/templates/hello/src/FuiExports.ts +1 -0
  11. package/build/templates/hello/src/FuiPrimitives.ts +1 -0
  12. package/build/templates/hello/src/HelloWorld.ts +105 -0
  13. package/build/templates/hello/src/generated/HostEvents.ts +20 -0
  14. package/build/templates/hello/src/generated/HostServices.ts +8 -0
  15. package/build/templates/hello/src/host/host-events.ts +22 -0
  16. package/build/templates/hello/src/host/host-services.ts +18 -0
  17. package/build/templates/hello/tsconfig.json +9 -0
  18. package/build/templates/mvc/asconfig.json +24 -0
  19. package/build/templates/mvc/harness.ts +40 -0
  20. package/build/templates/mvc/index.html +11 -0
  21. package/build/templates/mvc/package.json +34 -0
  22. package/build/templates/mvc/route-shell.html +56 -0
  23. package/build/templates/mvc/scripts/prepare-runtime.ts +22 -0
  24. package/build/templates/mvc/scripts/smoke.ts +18 -0
  25. package/build/templates/mvc/src/Fui.ts +1 -0
  26. package/build/templates/mvc/src/FuiBrowser.ts +1 -0
  27. package/build/templates/mvc/src/FuiExports.ts +1 -0
  28. package/build/templates/mvc/src/FuiPrimitives.ts +1 -0
  29. package/build/templates/mvc/src/generated/HostEvents.ts +20 -0
  30. package/build/templates/mvc/src/generated/HostServices.ts +8 -0
  31. package/build/templates/mvc/src/host/host-events.ts +22 -0
  32. package/build/templates/mvc/src/host/host-services.ts +17 -0
  33. package/build/templates/mvc/src/routes/mvc/pages/home/HomeController.ts +55 -0
  34. package/build/templates/mvc/src/routes/mvc/pages/home/HomeModel.ts +8 -0
  35. package/build/templates/mvc/src/routes/mvc/pages/home/HomeView.ts +76 -0
  36. package/build/templates/mvc/src/routes/mvc/pages/settings/SettingsController.ts +27 -0
  37. package/build/templates/mvc/src/routes/mvc/pages/settings/SettingsModel.ts +6 -0
  38. package/build/templates/mvc/src/routes/mvc/pages/settings/SettingsView.ts +61 -0
  39. package/build/templates/mvc/src/routes/mvc/shared/design-system/MvcNavPill.ts +42 -0
  40. package/build/templates/mvc/src/routes/mvc/shared/design-system/MvcPrimaryButton.ts +19 -0
  41. package/build/templates/mvc/src/routes/mvc/shared/routes.ts +22 -0
  42. package/build/templates/mvc/src/routes/mvc_home.ts +20 -0
  43. package/build/templates/mvc/src/routes/mvc_settings.ts +20 -0
  44. package/build/templates/mvc/tsconfig.json +9 -0
  45. package/dist/scripts/sync-templates.d.ts +1 -0
  46. package/dist/scripts/sync-templates.js +297 -0
  47. package/dist/src/scaffold.d.ts +2 -0
  48. package/dist/src/scaffold.js +50 -5
  49. package/dist/src/templates.d.ts +2 -1
  50. package/dist/src/templates.js +39 -226
  51. package/dist/src/versions.d.ts +1 -1
  52. package/dist/src/versions.js +1 -1
  53. package/dist/tests/scaffold.test.js +26 -0
  54. package/package.json +6 -3
@@ -0,0 +1,297 @@
1
+ import { cpSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, relative, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
5
+ const REPO_ROOT = resolve(PACKAGE_ROOT, "..", "..");
6
+ const FUI_AS_ROOT = join(REPO_ROOT, "v2", "fui-as");
7
+ const FUI_AS_TEMPLATES_ROOT = join(FUI_AS_ROOT, "templates");
8
+ const TEMPLATES_ROOT = join(PACKAGE_ROOT, "build", "templates");
9
+ const LEGACY_TEMPLATES_ROOT = join(PACKAGE_ROOT, "templates");
10
+ function writeTextFile(filePath, contents) {
11
+ mkdirSync(dirname(filePath), { recursive: true });
12
+ writeFileSync(filePath, contents, "utf8");
13
+ }
14
+ function posixRelativeImport(fromFilePath, toFilePath) {
15
+ const fromDirectory = dirname(fromFilePath);
16
+ let specifier = relative(fromDirectory, toFilePath).replaceAll("\\", "/");
17
+ if (specifier.endsWith(".ts")) {
18
+ specifier = specifier.slice(0, -3);
19
+ }
20
+ if (!specifier.startsWith(".")) {
21
+ specifier = `./${specifier}`;
22
+ }
23
+ return specifier;
24
+ }
25
+ function rewriteSdkImportsInFile(filePath, templateSrcRoot) {
26
+ const original = readFileSync(filePath, "utf8");
27
+ const rewritten = original
28
+ .replace(/(['"])(?:\.\.\/)+src\/(Fui|FuiExports|FuiPrimitives|FuiBrowser)\1/g, (_full, quote, symbolName) => {
29
+ const targetPath = join(templateSrcRoot, `${symbolName}.ts`);
30
+ const relativeSpecifier = posixRelativeImport(filePath, targetPath);
31
+ return `${quote}${relativeSpecifier}${quote}`;
32
+ })
33
+ .replace(/(['"])(?:\.\.\/)+browser\/src\/(?:host-events|host-services)\1/g, '"@effindomv2/fui-as/browser"');
34
+ if (rewritten !== original) {
35
+ writeFileSync(filePath, rewritten, "utf8");
36
+ }
37
+ }
38
+ function walkFiles(root, callback) {
39
+ for (const entry of readdirSync(root)) {
40
+ const absolutePath = join(root, entry);
41
+ const stats = statSync(absolutePath);
42
+ if (stats.isDirectory()) {
43
+ walkFiles(absolutePath, callback);
44
+ continue;
45
+ }
46
+ callback(absolutePath);
47
+ }
48
+ }
49
+ function writeSharedSdkShims(templateRoot) {
50
+ const srcRoot = join(templateRoot, "src");
51
+ writeTextFile(join(srcRoot, "Fui.ts"), 'export * from "../node_modules/@effindomv2/fui-as/src/Fui";\n');
52
+ writeTextFile(join(srcRoot, "FuiExports.ts"), 'export * from "../node_modules/@effindomv2/fui-as/src/FuiExports";\n');
53
+ writeTextFile(join(srcRoot, "FuiPrimitives.ts"), 'export * from "../node_modules/@effindomv2/fui-as/src/FuiPrimitives";\n');
54
+ writeTextFile(join(srcRoot, "FuiBrowser.ts"), 'export * from "../node_modules/@effindomv2/fui-as/browser/src/index";\n');
55
+ }
56
+ function writeAsconfig(templateRoot) {
57
+ writeTextFile(join(templateRoot, "asconfig.json"), JSON.stringify({
58
+ targets: {
59
+ debug: {
60
+ debug: true,
61
+ exportRuntime: true,
62
+ bindings: "esm",
63
+ outFile: "public/app.wasm",
64
+ sourceMap: true,
65
+ textFile: "public/app.wat",
66
+ },
67
+ release: {
68
+ exportRuntime: true,
69
+ bindings: "esm",
70
+ optimizeLevel: 3,
71
+ outFile: "public/app.wasm",
72
+ shrinkLevel: 1,
73
+ sourceMap: false,
74
+ textFile: "public/app.wat",
75
+ },
76
+ },
77
+ options: {
78
+ runtime: "stub",
79
+ },
80
+ }, null, 2) + "\n");
81
+ }
82
+ function writeTsconfig(templateRoot) {
83
+ writeTextFile(join(templateRoot, "tsconfig.json"), JSON.stringify({
84
+ extends: "assemblyscript/std/assembly.json",
85
+ compilerOptions: {
86
+ noEmit: true,
87
+ },
88
+ include: ["src/**/*.ts"],
89
+ }, null, 2) + "\n");
90
+ }
91
+ function writePackageJsonTemplate(templateRoot, scripts, description) {
92
+ writeTextFile(join(templateRoot, "package.json"), JSON.stringify({
93
+ name: "__PACKAGE_NAME__",
94
+ version: "0.1.0",
95
+ private: true,
96
+ type: "module",
97
+ description,
98
+ scripts,
99
+ dependencies: {
100
+ "@effindomv2/fui-as": "__FUI_AS_VERSION__",
101
+ "@effindomv2/runtime": "__RUNTIME_VERSION__",
102
+ },
103
+ devDependencies: {
104
+ assemblyscript: "0.28.17",
105
+ "chokidar-cli": "^3.0.0",
106
+ concurrently: "^9.2.1",
107
+ esbuild: "^0.27.7",
108
+ "http-server": "^14.1.1",
109
+ tsx: "^4.20.6",
110
+ },
111
+ }, null, 2) + "\n");
112
+ }
113
+ function writeHelloSupportFiles(templateRoot) {
114
+ writeTextFile(join(templateRoot, "scripts", "prepare-runtime.ts"), `import { copyFileSync, cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
115
+
116
+ const outputDir = "public";
117
+ rmSync(outputDir, { recursive: true, force: true });
118
+ mkdirSync(\`\${outputDir}/runtime\`, { recursive: true });
119
+
120
+ cpSync("node_modules/@effindomv2/runtime/dist", \`\${outputDir}/runtime/dist\`, { recursive: true });
121
+ cpSync("node_modules/@effindomv2/runtime/dist/fonts", \`\${outputDir}/runtime/fonts\`, { recursive: true });
122
+ copyFileSync("node_modules/@effindomv2/runtime/dist/bridge.js", \`\${outputDir}/bridge.js\`);
123
+ if (existsSync("node_modules/@effindomv2/runtime/dist/bridge.js.map")) {
124
+ copyFileSync("node_modules/@effindomv2/runtime/dist/bridge.js.map", \`\${outputDir}/bridge.js.map\`);
125
+ }
126
+ writeFileSync(
127
+ \`\${outputDir}/effindom-runtime-config.js\`,
128
+ 'window.__effindomRuntime = Object.assign({}, window.__effindomRuntime, { manifestUrl: "./runtime/dist/effindom.v2.manifest.json" });\\n',
129
+ "utf8",
130
+ );
131
+ copyFileSync("index.html", \`\${outputDir}/index.html\`);
132
+ `);
133
+ writeTextFile(join(templateRoot, "scripts", "smoke.ts"), `import { accessSync } from "node:fs";
134
+
135
+ const expectedFiles = [
136
+ "public/index.html",
137
+ "public/harness.js",
138
+ "public/app.wasm",
139
+ "public/bridge.js",
140
+ "public/effindom-runtime-config.js",
141
+ "public/runtime/dist/effindom.v2.manifest.json",
142
+ "public/runtime/fonts/NotoSans-Regular.ttf",
143
+ ];
144
+
145
+ for (const filePath of expectedFiles) {
146
+ accessSync(filePath);
147
+ }
148
+ `);
149
+ writePackageJsonTemplate(templateRoot, {
150
+ build: "npm run generate:host && npm run build:assets && npm run build:wasm && npm run build:harness",
151
+ "build:assets": "tsx scripts/prepare-runtime.ts",
152
+ "build:wasm": "asc src/App.ts --config asconfig.json --target release",
153
+ "build:harness": "esbuild harness.ts --bundle --format=esm --platform=browser --outfile=public/harness.js",
154
+ "generate:host-services": "tsx ./node_modules/@effindomv2/fui-as/scripts/generate-host-services.ts src/host/host-services.ts appHostServices src/generated/HostServices.ts ../FuiPrimitives",
155
+ "generate:host-events": "tsx ./node_modules/@effindomv2/fui-as/scripts/generate-host-events.ts src/host/host-events.ts appHostEvents src/generated/HostEvents.ts ../FuiPrimitives",
156
+ "generate:host": "npm run generate:host-services && npm run generate:host-events",
157
+ watch: 'chokidar "src/**/*.ts" "harness.ts" "index.html" "asconfig.json" -c "npm run build"',
158
+ serve: "http-server public -p 8080 -c-1",
159
+ dev: 'npm run build && concurrently -k -n watch,serve "npm run watch" "npm run serve"',
160
+ test: "npm run build && tsx scripts/smoke.ts",
161
+ }, "Scaffolded FUI-AS hello-world app");
162
+ }
163
+ function writeMvcSupportFiles(templateRoot) {
164
+ writeTextFile(join(templateRoot, "index.html"), `<!doctype html>
165
+ <html lang="en">
166
+ <head>
167
+ <meta charset="utf-8" />
168
+ <meta http-equiv="refresh" content="0; url=/mvc-home/" />
169
+ <title>FUI-AS MVC App</title>
170
+ </head>
171
+ <body>
172
+ <p>Redirecting to <a href="/mvc-home/">/mvc-home/</a>…</p>
173
+ </body>
174
+ </html>
175
+ `);
176
+ writeTextFile(join(templateRoot, "scripts", "prepare-runtime.ts"), `import { copyFileSync, cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
177
+
178
+ const outputDir = "public";
179
+ rmSync(outputDir, { recursive: true, force: true });
180
+ mkdirSync(\`\${outputDir}/runtime\`, { recursive: true });
181
+ mkdirSync(\`\${outputDir}/mvc-home\`, { recursive: true });
182
+ mkdirSync(\`\${outputDir}/mvc-settings\`, { recursive: true });
183
+
184
+ cpSync("node_modules/@effindomv2/runtime/dist", \`\${outputDir}/runtime/dist\`, { recursive: true });
185
+ cpSync("node_modules/@effindomv2/runtime/dist/fonts", \`\${outputDir}/runtime/fonts\`, { recursive: true });
186
+ copyFileSync("node_modules/@effindomv2/runtime/dist/bridge.js", \`\${outputDir}/bridge.js\`);
187
+ if (existsSync("node_modules/@effindomv2/runtime/dist/bridge.js.map")) {
188
+ copyFileSync("node_modules/@effindomv2/runtime/dist/bridge.js.map", \`\${outputDir}/bridge.js.map\`);
189
+ }
190
+ writeFileSync(
191
+ \`\${outputDir}/effindom-runtime-config.js\`,
192
+ 'window.__effindomRuntime = Object.assign({}, window.__effindomRuntime, { manifestUrl: "./runtime/dist/effindom.v2.manifest.json" });\\n',
193
+ "utf8",
194
+ );
195
+ copyFileSync("index.html", \`\${outputDir}/index.html\`);
196
+ copyFileSync("route-shell.html", \`\${outputDir}/mvc-home/index.html\`);
197
+ copyFileSync("route-shell.html", \`\${outputDir}/mvc-settings/index.html\`);
198
+ `);
199
+ writeTextFile(join(templateRoot, "scripts", "smoke.ts"), `import { accessSync } from "node:fs";
200
+
201
+ const expectedFiles = [
202
+ "public/index.html",
203
+ "public/harness.js",
204
+ "public/mvc-home/index.html",
205
+ "public/mvc-settings/index.html",
206
+ "public/mvc-home.wasm",
207
+ "public/mvc-settings.wasm",
208
+ "public/bridge.js",
209
+ "public/effindom-runtime-config.js",
210
+ "public/runtime/dist/effindom.v2.manifest.json",
211
+ "public/runtime/fonts/NotoSans-Regular.ttf",
212
+ ];
213
+
214
+ for (const filePath of expectedFiles) {
215
+ accessSync(filePath);
216
+ }
217
+ `);
218
+ writePackageJsonTemplate(templateRoot, {
219
+ build: "npm run generate:host && npm run build:assets && npm run build:wasm && npm run build:harness",
220
+ "build:assets": "tsx scripts/prepare-runtime.ts",
221
+ "build:wasm": "npm run build:wasm:home && npm run build:wasm:settings",
222
+ "build:wasm:home": "asc src/routes/mvc_home.ts --config asconfig.json --target release --outFile public/mvc-home.wasm",
223
+ "build:wasm:settings": "asc src/routes/mvc_settings.ts --config asconfig.json --target release --outFile public/mvc-settings.wasm",
224
+ "build:harness": "esbuild harness.ts --bundle --format=esm --platform=browser --outfile=public/harness.js",
225
+ "generate:host-services": "tsx ./node_modules/@effindomv2/fui-as/scripts/generate-host-services.ts src/host/host-services.ts appHostServices src/generated/HostServices.ts ../FuiPrimitives",
226
+ "generate:host-events": "tsx ./node_modules/@effindomv2/fui-as/scripts/generate-host-events.ts src/host/host-events.ts appHostEvents src/generated/HostEvents.ts ../FuiPrimitives",
227
+ "generate:host": "npm run generate:host-services && npm run generate:host-events",
228
+ watch: 'chokidar "src/**/*.ts" "harness.ts" "route-shell.html" "index.html" "asconfig.json" -c "npm run build"',
229
+ serve: "http-server public -p 8080 -c-1",
230
+ dev: 'npm run build && concurrently -k -n watch,serve "npm run watch" "npm run serve"',
231
+ test: "npm run build && tsx scripts/smoke.ts",
232
+ }, "Scaffolded FUI-AS MVC app");
233
+ }
234
+ function rewriteHarnessImports(filePath) {
235
+ const original = readFileSync(filePath, "utf8");
236
+ const rewritten = original
237
+ .replace(/(['"])(?:\.\.\/)+browser\/src\/common-harness\1/g, '"@effindomv2/fui-as/browser"')
238
+ .replace(/(['"])(?:\.\.\/)+browser\/src\/routed-harness\1/g, '"@effindomv2/fui-as/browser"')
239
+ .replaceAll("?v=midnight-6", "");
240
+ if (rewritten !== original) {
241
+ writeFileSync(filePath, rewritten, "utf8");
242
+ }
243
+ }
244
+ function normalizeGeneratedHeader(filePath, label) {
245
+ const original = readFileSync(filePath, "utf8");
246
+ const rewritten = original.replace(/^\/\/ Generated by .*$/m, `// Generated from ${label}.`);
247
+ if (rewritten !== original) {
248
+ writeFileSync(filePath, rewritten, "utf8");
249
+ }
250
+ }
251
+ function syncHelloTemplate() {
252
+ const templateRoot = join(TEMPLATES_ROOT, "hello");
253
+ const templateSrcRoot = join(templateRoot, "src");
254
+ rmSync(templateRoot, { recursive: true, force: true });
255
+ const sourceRoot = join(FUI_AS_TEMPLATES_ROOT, "demo-hello-world");
256
+ cpSync(join(sourceRoot, "src"), templateSrcRoot, { recursive: true });
257
+ cpSync(join(sourceRoot, "harness.ts"), join(templateRoot, "harness.ts"));
258
+ cpSync(join(sourceRoot, "index.html"), join(templateRoot, "index.html"));
259
+ writeSharedSdkShims(templateRoot);
260
+ writeAsconfig(templateRoot);
261
+ writeTsconfig(templateRoot);
262
+ writeHelloSupportFiles(templateRoot);
263
+ walkFiles(templateSrcRoot, (filePath) => {
264
+ if (filePath.endsWith(".ts")) {
265
+ rewriteSdkImportsInFile(filePath, templateSrcRoot);
266
+ }
267
+ });
268
+ rewriteHarnessImports(join(templateRoot, "harness.ts"));
269
+ normalizeGeneratedHeader(join(templateSrcRoot, "generated", "HostEvents.ts"), "the scaffold host-events definition");
270
+ normalizeGeneratedHeader(join(templateSrcRoot, "generated", "HostServices.ts"), "the scaffold host-services definition");
271
+ }
272
+ function syncMvcTemplate() {
273
+ const templateRoot = join(TEMPLATES_ROOT, "mvc");
274
+ const templateSrcRoot = join(templateRoot, "src");
275
+ rmSync(templateRoot, { recursive: true, force: true });
276
+ const sourceRoot = join(FUI_AS_TEMPLATES_ROOT, "demo-mvc");
277
+ cpSync(join(sourceRoot, "src"), templateSrcRoot, { recursive: true });
278
+ cpSync(join(sourceRoot, "harness.ts"), join(templateRoot, "harness.ts"));
279
+ cpSync(join(sourceRoot, "route-shell.html"), join(templateRoot, "route-shell.html"));
280
+ writeSharedSdkShims(templateRoot);
281
+ writeAsconfig(templateRoot);
282
+ writeTsconfig(templateRoot);
283
+ writeMvcSupportFiles(templateRoot);
284
+ walkFiles(templateSrcRoot, (filePath) => {
285
+ if (filePath.endsWith(".ts")) {
286
+ rewriteSdkImportsInFile(filePath, templateSrcRoot);
287
+ }
288
+ });
289
+ rewriteHarnessImports(join(templateRoot, "harness.ts"));
290
+ normalizeGeneratedHeader(join(templateSrcRoot, "generated", "HostEvents.ts"), "the scaffold host-events definition");
291
+ normalizeGeneratedHeader(join(templateSrcRoot, "generated", "HostServices.ts"), "the scaffold host-services definition");
292
+ }
293
+ rmSync(TEMPLATES_ROOT, { recursive: true, force: true });
294
+ rmSync(LEGACY_TEMPLATES_ROOT, { recursive: true, force: true });
295
+ mkdirSync(TEMPLATES_ROOT, { recursive: true });
296
+ syncHelloTemplate();
297
+ syncMvcTemplate();
@@ -1,6 +1,8 @@
1
+ import { type TemplateName } from "./templates.js";
1
2
  export interface ScaffoldOptions {
2
3
  readonly targetDirectory: string;
3
4
  readonly projectName: string;
5
+ readonly template?: TemplateName;
4
6
  }
5
7
  export interface LoggerLike {
6
8
  log(message: string): void;
@@ -18,7 +18,7 @@ function assertDirectoryIsEmpty(targetDirectory) {
18
18
  }
19
19
  export function createProject(options) {
20
20
  assertDirectoryIsEmpty(options.targetDirectory);
21
- const templateFiles = createTemplateFiles({
21
+ const templateFiles = createTemplateFiles(options.template ?? "hello", {
22
22
  projectName: options.projectName,
23
23
  packageName: normalizePackageName(options.projectName),
24
24
  });
@@ -28,10 +28,54 @@ export function createProject(options) {
28
28
  writeFileSync(absolutePath, contents, "utf8");
29
29
  }
30
30
  }
31
+ function parseCliOptions(argv) {
32
+ let requestedPath = null;
33
+ let template = "hello";
34
+ for (let index = 0; index < argv.length; index += 1) {
35
+ const argument = argv[index];
36
+ if (argument === "--template") {
37
+ const value = argv.at(index + 1);
38
+ if (value === undefined) {
39
+ return { requestedPath, template, error: "--template requires a value (hello or mvc)." };
40
+ }
41
+ if (value !== "hello" && value !== "mvc") {
42
+ return { requestedPath, template, error: `Unsupported template: ${value}` };
43
+ }
44
+ template = value;
45
+ index += 1;
46
+ continue;
47
+ }
48
+ if (argument.startsWith("--template=")) {
49
+ const value = argument.slice("--template=".length);
50
+ if (value !== "hello" && value !== "mvc") {
51
+ return { requestedPath, template, error: `Unsupported template: ${value}` };
52
+ }
53
+ template = value;
54
+ continue;
55
+ }
56
+ if (argument.startsWith("-")) {
57
+ return { requestedPath, template, error: `Unknown option: ${argument}` };
58
+ }
59
+ if (requestedPath !== null) {
60
+ return { requestedPath, template, error: `Unexpected argument: ${argument}` };
61
+ }
62
+ requestedPath = argument;
63
+ }
64
+ return { requestedPath, template, error: null };
65
+ }
66
+ function printUsage(logger) {
67
+ logger.error("Usage: create-fui-as-app <project-directory> [--template hello|mvc]");
68
+ }
31
69
  export function runCli(argv, cwd, logger) {
32
- const requestedPath = argv[0];
33
- if (requestedPath === undefined || requestedPath.length === 0) {
34
- logger.error("Usage: create-fui-as-app <project-directory>");
70
+ const parsed = parseCliOptions(argv);
71
+ if (parsed.error !== null) {
72
+ logger.error(parsed.error);
73
+ printUsage(logger);
74
+ return 1;
75
+ }
76
+ const requestedPath = parsed.requestedPath;
77
+ if (requestedPath === null || requestedPath.length === 0) {
78
+ printUsage(logger);
35
79
  return 1;
36
80
  }
37
81
  const targetDirectory = resolve(cwd, requestedPath);
@@ -40,13 +84,14 @@ export function runCli(argv, cwd, logger) {
40
84
  createProject({
41
85
  targetDirectory,
42
86
  projectName,
87
+ template: parsed.template,
43
88
  });
44
89
  }
45
90
  catch (error) {
46
91
  logger.error(error instanceof Error ? error.message : String(error));
47
92
  return 1;
48
93
  }
49
- logger.log(`Created ${projectName} at ${targetDirectory}`);
94
+ logger.log(`Created ${projectName} (${parsed.template} template) at ${targetDirectory}`);
50
95
  logger.log("Next steps:");
51
96
  logger.log(` cd ${requestedPath}`);
52
97
  logger.log(" npm install");
@@ -1,5 +1,6 @@
1
+ export type TemplateName = "hello" | "mvc";
1
2
  export interface TemplateContext {
2
3
  readonly projectName: string;
3
4
  readonly packageName: string;
4
5
  }
5
- export declare function createTemplateFiles(context: TemplateContext): Map<string, string>;
6
+ export declare function createTemplateFiles(template: TemplateName, context: TemplateContext): Map<string, string>;
@@ -1,230 +1,43 @@
1
+ import { readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
1
4
  import { FUI_AS_VERSION, RUNTIME_VERSION } from "./versions.js";
2
- function formatJson(value) {
3
- return `${JSON.stringify(value, null, 2)}\n`;
4
- }
5
- function renderPackageJson(context) {
6
- return formatJson({
7
- name: context.packageName,
8
- version: "0.1.2",
9
- private: true,
10
- type: "module",
11
- scripts: {
12
- build: "npm run build:assets && npm run build:wasm && npm run build:harness",
13
- "build:assets": "node scripts/prepare-runtime.mjs",
14
- "build:wasm": "asc src/App.ts --config asconfig.json --target release",
15
- "build:harness": "esbuild harness.ts --bundle --format=esm --platform=browser --outfile=public/harness.js",
16
- watch: "chokidar \"src/**/*.ts\" \"harness.ts\" \"index.html\" \"asconfig.json\" -c \"npm run build\"",
17
- serve: "http-server public -p 8080 -c-1",
18
- dev: "npm run build && concurrently -k -n watch,serve \"npm run watch\" \"npm run serve\"",
19
- test: "npm run build && node scripts/smoke.mjs"
20
- },
21
- dependencies: {
22
- "@effindomv2/fui-as": FUI_AS_VERSION,
23
- "@effindomv2/runtime": RUNTIME_VERSION
24
- },
25
- devDependencies: {
26
- assemblyscript: "0.28.17",
27
- "chokidar-cli": "^3.0.0",
28
- concurrently: "^9.2.1",
29
- esbuild: "^0.27.7",
30
- "http-server": "^14.1.1"
5
+ const TEMPLATE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "build", "templates");
6
+ function collectTemplateFiles(root, relativePath = "") {
7
+ const absolutePath = resolve(root, relativePath);
8
+ const stats = statSync(absolutePath);
9
+ if (stats.isFile()) {
10
+ return new Map([[relativePath, readFileSync(absolutePath, "utf8")]]);
11
+ }
12
+ const output = new Map();
13
+ for (const entry of readdirSync(absolutePath)) {
14
+ const nestedRelativePath = relativePath.length === 0 ? entry : `${relativePath}/${entry}`;
15
+ const nestedAbsolutePath = resolve(root, nestedRelativePath);
16
+ const nestedStats = statSync(nestedAbsolutePath);
17
+ if (nestedStats.isDirectory()) {
18
+ const nestedFiles = collectTemplateFiles(root, nestedRelativePath);
19
+ for (const [filePath, contents] of nestedFiles) {
20
+ output.set(filePath, contents);
21
+ }
22
+ continue;
31
23
  }
32
- });
33
- }
34
- const appTs = `import { Application } from "../node_modules/@effindomv2/fui-as/src/Fui";
35
- export * from "../node_modules/@effindomv2/fui-as/src/FuiExports";
36
-
37
- import { createHelloWorldPage } from "./HelloWorld";
38
-
39
- Application.register((app) => app.page(createHelloWorldPage));
40
- `;
41
- const helloWorldTs = `import {
42
- AlignItems,
43
- Button,
44
- Column,
45
- JustifyContent,
46
- rgb,
47
- SelectionArea,
48
- Text,
49
- TextAlign,
50
- Unit,
51
- } from "../node_modules/@effindomv2/fui-as/src/Fui";
52
-
53
- class HelloWorld {
54
- private clickCount: i32 = 0;
55
- private readonly counterLabel: Text = new Text("Clicked 0 times");
56
-
57
- constructor() {
58
- this.counterLabel.fontSize(20.0);
59
- }
60
-
61
- buildPage(): SelectionArea {
62
- const title = new Text("Hello world from FUI-AS")
63
- .fontSize(36.0)
64
- .textAlign(TextAlign.Center)
65
- .width(100.0, Unit.Percent);
66
-
67
- const subtitle = new Text("A tiny app in two files: App.ts + HelloWorld.ts")
68
- .fontSize(16.0)
69
- .textAlign(TextAlign.Center)
70
- .width(100.0, Unit.Percent);
71
-
72
- const button = new Button("Click me")
73
- .margin(0.0, 18.0, 0.0, 12.0)
74
- .onClickWith<HelloWorld>(this, (owner) => {
75
- owner.clickCount += 1;
76
- owner.counterLabel.text(
77
- "Clicked " + owner.clickCount.toString() + " time" + (owner.clickCount == 1 ? "" : "s"),
78
- );
79
- });
80
-
81
- const note = new Text(
82
- "For production apps, move to an explicit MVC structure once screens, state, or host integration grows.",
83
- )
84
- .fontSize(14.0)
85
- .textAlign(TextAlign.Center)
86
- .width(680.0, Unit.Pixel);
87
-
88
- return new SelectionArea()
89
- .fillWidth()
90
- .fillHeight()
91
- .child(
92
- Column(
93
- title,
94
- subtitle,
95
- button,
96
- this.counterLabel,
97
- note,
98
- )
99
- .fillWidth()
100
- .fillHeight()
101
- .padding(24.0, 24.0, 24.0, 24.0)
102
- .justifyContent(JustifyContent.Center)
103
- .alignItems(AlignItems.Center),
104
- )
105
- .bgColor(rgb(0, 0, 0)) as SelectionArea;
106
- }
107
- }
108
-
109
- export function createHelloWorldPage(): SelectionArea {
110
- return new HelloWorld().buildPage();
111
- }
112
- `;
113
- const harnessTs = `import { startHarness } from "@effindomv2/fui-as/browser";
114
-
115
- startHarness({
116
- wasmPath: "./app.wasm",
117
- });
118
- `;
119
- const indexHtml = `<!doctype html>
120
- <html lang="en">
121
- <head>
122
- <meta charset="utf-8" />
123
- <meta name="viewport" content="width=device-width, initial-scale=1" />
124
- <title>FUI-AS Hello World</title>
125
- <style>
126
- html,
127
- body {
128
- width: 100%;
129
- height: 100%;
130
- margin: 0;
131
- padding: 0;
132
- overflow: hidden;
133
- background: #0f172a;
134
- }
135
-
136
- [data-effindom-canvas-size-source] {
137
- position: fixed;
138
- inset: 0;
139
- }
140
-
141
- #fui-canvas {
142
- display: block;
143
- width: 100%;
144
- height: 100%;
145
- }
146
- </style>
147
- </head>
148
- <body>
149
- <main data-effindom-canvas-size-source>
150
- <canvas id="fui-canvas"></canvas>
151
- </main>
152
- <script>
153
- window.__effindomRuntime = Object.assign({}, window.__effindomRuntime, {
154
- manifestUrl: "./runtime/dist/effindom.v2.manifest.json",
155
- });
156
- </script>
157
- <script src="./runtime/dist/bridge.js"></script>
158
- <script type="module" src="./harness.js"></script>
159
- </body>
160
- </html>
161
- `;
162
- const asconfigJson = `{
163
- "targets": {
164
- "debug": {
165
- "debug": true,
166
- "exportRuntime": true,
167
- "bindings": "esm",
168
- "outFile": "public/app.wasm",
169
- "sourceMap": true,
170
- "textFile": "public/app.wat"
171
- },
172
- "release": {
173
- "exportRuntime": true,
174
- "bindings": "esm",
175
- "optimizeLevel": 3,
176
- "outFile": "public/app.wasm",
177
- "shrinkLevel": 1,
178
- "sourceMap": false,
179
- "textFile": "public/app.wat"
24
+ output.set(nestedRelativePath, readFileSync(nestedAbsolutePath, "utf8"));
180
25
  }
181
- },
182
- "options": {
183
- "runtime": "stub"
184
- }
185
- }
186
- `;
187
- const tsconfigJson = `{
188
- "extends": "assemblyscript/std/assembly.json",
189
- "compilerOptions": {
190
- "noEmit": true
191
- },
192
- "include": ["src/**/*.ts"]
193
- }
194
- `;
195
- const prepareRuntimeMjs = `import { copyFileSync, cpSync, mkdirSync, rmSync } from "node:fs";
196
-
197
- const outputDir = "public";
198
- rmSync(outputDir, { recursive: true, force: true });
199
- mkdirSync(\`\${outputDir}/runtime\`, { recursive: true });
200
-
201
- cpSync("node_modules/@effindomv2/runtime/dist", \`\${outputDir}/runtime/dist\`, { recursive: true });
202
- copyFileSync("index.html", \`\${outputDir}/index.html\`);
203
- `;
204
- const smokeMjs = `import { accessSync } from "node:fs";
205
-
206
- const expectedFiles = [
207
- "public/index.html",
208
- "public/harness.js",
209
- "public/app.wasm",
210
- "public/runtime/dist/bridge.js",
211
- "public/runtime/dist/effindom.v2.manifest.json",
212
- ];
213
-
214
- for (const filePath of expectedFiles) {
215
- accessSync(filePath);
216
- }
217
- `;
218
- export function createTemplateFiles(context) {
219
- return new Map([
220
- ["package.json", renderPackageJson(context)],
221
- ["index.html", indexHtml],
222
- ["asconfig.json", asconfigJson],
223
- ["tsconfig.json", tsconfigJson],
224
- ["harness.ts", harnessTs],
225
- ["src/App.ts", appTs],
226
- ["src/HelloWorld.ts", helloWorldTs],
227
- ["scripts/prepare-runtime.mjs", prepareRuntimeMjs],
228
- ["scripts/smoke.mjs", smokeMjs],
229
- ]);
26
+ return output;
27
+ }
28
+ function replaceTemplateTokens(value, context) {
29
+ return value
30
+ .replaceAll("__PACKAGE_NAME__", context.packageName)
31
+ .replaceAll("__PROJECT_NAME__", context.projectName)
32
+ .replaceAll("__FUI_AS_VERSION__", FUI_AS_VERSION)
33
+ .replaceAll("__RUNTIME_VERSION__", RUNTIME_VERSION);
34
+ }
35
+ export function createTemplateFiles(template, context) {
36
+ const templateDirectory = resolve(TEMPLATE_ROOT, template);
37
+ const files = collectTemplateFiles(templateDirectory);
38
+ const output = new Map();
39
+ for (const [filePath, contents] of files) {
40
+ output.set(filePath, replaceTemplateTokens(contents, context));
41
+ }
42
+ return output;
230
43
  }
@@ -1,2 +1,2 @@
1
- export declare const FUI_AS_VERSION = "0.1.1";
1
+ export declare const FUI_AS_VERSION = "0.1.2";
2
2
  export declare const RUNTIME_VERSION = "0.1.0";