@effindomv2/create-fui-as-app 0.1.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/LICENSE.md ADDED
@@ -0,0 +1,6 @@
1
+ `@effindomv2/create-fui-as-app` is licensed under the MIT License.
2
+
3
+ See:
4
+
5
+ - `../../LICENSE.md`
6
+ - `../../LICENSES/MIT.md`
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "./scaffold.js";
3
+ const code = runCli(process.argv.slice(2), process.cwd(), console);
4
+ if (code !== 0) {
5
+ process.exit(code);
6
+ }
@@ -0,0 +1,10 @@
1
+ export interface ScaffoldOptions {
2
+ readonly targetDirectory: string;
3
+ readonly projectName: string;
4
+ }
5
+ export interface LoggerLike {
6
+ log(message: string): void;
7
+ error(message: string): void;
8
+ }
9
+ export declare function createProject(options: ScaffoldOptions): void;
10
+ export declare function runCli(argv: readonly string[], cwd: string, logger: LoggerLike): number;
@@ -0,0 +1,55 @@
1
+ import { mkdirSync, readdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { createTemplateFiles } from "./templates.js";
4
+ function normalizePackageName(value) {
5
+ return value
6
+ .trim()
7
+ .toLowerCase()
8
+ .replace(/[^a-z0-9._-]+/g, "-")
9
+ .replace(/^-+/, "")
10
+ .replace(/-+$/, "") || "fui-as-app";
11
+ }
12
+ function assertDirectoryIsEmpty(targetDirectory) {
13
+ mkdirSync(targetDirectory, { recursive: true });
14
+ const entries = readdirSync(targetDirectory);
15
+ if (entries.length > 0) {
16
+ throw new Error(`Target directory is not empty: ${targetDirectory}`);
17
+ }
18
+ }
19
+ export function createProject(options) {
20
+ assertDirectoryIsEmpty(options.targetDirectory);
21
+ const templateFiles = createTemplateFiles({
22
+ projectName: options.projectName,
23
+ packageName: normalizePackageName(options.projectName),
24
+ });
25
+ for (const [relativePath, contents] of templateFiles) {
26
+ const absolutePath = resolve(options.targetDirectory, relativePath);
27
+ mkdirSync(dirname(absolutePath), { recursive: true });
28
+ writeFileSync(absolutePath, contents, "utf8");
29
+ }
30
+ }
31
+ 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>");
35
+ return 1;
36
+ }
37
+ const targetDirectory = resolve(cwd, requestedPath);
38
+ const projectName = requestedPath === "." ? "fui-as-app" : requestedPath;
39
+ try {
40
+ createProject({
41
+ targetDirectory,
42
+ projectName,
43
+ });
44
+ }
45
+ catch (error) {
46
+ logger.error(error instanceof Error ? error.message : String(error));
47
+ return 1;
48
+ }
49
+ logger.log(`Created ${projectName} at ${targetDirectory}`);
50
+ logger.log("Next steps:");
51
+ logger.log(` cd ${requestedPath}`);
52
+ logger.log(" npm install");
53
+ logger.log(" npm run dev");
54
+ return 0;
55
+ }
@@ -0,0 +1,5 @@
1
+ export interface TemplateContext {
2
+ readonly projectName: string;
3
+ readonly packageName: string;
4
+ }
5
+ export declare function createTemplateFiles(context: TemplateContext): Map<string, string>;
@@ -0,0 +1,230 @@
1
+ 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.1",
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"
31
+ }
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 src="./effindom-runtime-config.js"></script>
153
+ <script src="./runtime/dist/bridge.js"></script>
154
+ <script type="module" src="./harness.js"></script>
155
+ </body>
156
+ </html>
157
+ `;
158
+ const asconfigJson = `{
159
+ "targets": {
160
+ "debug": {
161
+ "debug": true,
162
+ "exportRuntime": true,
163
+ "bindings": "esm",
164
+ "outFile": "public/app.wasm",
165
+ "sourceMap": true,
166
+ "textFile": "public/app.wat"
167
+ },
168
+ "release": {
169
+ "exportRuntime": true,
170
+ "bindings": "esm",
171
+ "optimizeLevel": 3,
172
+ "outFile": "public/app.wasm",
173
+ "shrinkLevel": 1,
174
+ "sourceMap": false,
175
+ "textFile": "public/app.wat"
176
+ }
177
+ },
178
+ "options": {
179
+ "runtime": "stub"
180
+ }
181
+ }
182
+ `;
183
+ const tsconfigJson = `{
184
+ "extends": "assemblyscript/std/assembly.json",
185
+ "compilerOptions": {
186
+ "noEmit": true
187
+ },
188
+ "include": ["src/**/*.ts"]
189
+ }
190
+ `;
191
+ const prepareRuntimeMjs = `import { copyFileSync, cpSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
192
+
193
+ const outputDir = "public";
194
+ rmSync(outputDir, { recursive: true, force: true });
195
+ mkdirSync(\`\${outputDir}/runtime\`, { recursive: true });
196
+
197
+ cpSync("node_modules/@effindomv2/runtime/dist", \`\${outputDir}/runtime/dist\`, { recursive: true });
198
+ copyFileSync("index.html", \`\${outputDir}/index.html\`);
199
+
200
+ const runtimeConfig = \`window.__effindomRuntime = Object.assign({}, window.__effindomRuntime, {\\\\n manifestUrl: "./runtime/dist/effindom.v2.manifest.json",\\\\n});\\\\n\`;
201
+ writeFileSync(\`\${outputDir}/effindom-runtime-config.js\`, runtimeConfig, "utf8");
202
+ `;
203
+ const smokeMjs = `import { accessSync } from "node:fs";
204
+
205
+ const expectedFiles = [
206
+ "public/index.html",
207
+ "public/harness.js",
208
+ "public/app.wasm",
209
+ "public/effindom-runtime-config.js",
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
+ ]);
230
+ }
@@ -0,0 +1,2 @@
1
+ export declare const FUI_AS_VERSION = "0.1.1";
2
+ export declare const RUNTIME_VERSION = "0.1.0";
@@ -0,0 +1,2 @@
1
+ export const FUI_AS_VERSION = "0.1.1";
2
+ export const RUNTIME_VERSION = "0.1.0";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import test from "node:test";
6
+ import { createProject } from "../src/scaffold.js";
7
+ test("createProject writes hello-world scaffold including AssemblyScript tsconfig", () => {
8
+ const root = mkdtempSync(join(tmpdir(), "create-fui-as-app-"));
9
+ const target = join(root, "my-app");
10
+ try {
11
+ createProject({
12
+ targetDirectory: target,
13
+ projectName: "my-app",
14
+ });
15
+ const tsconfig = JSON.parse(readFileSync(join(target, "tsconfig.json"), "utf8"));
16
+ assert.equal(tsconfig.extends, "assemblyscript/std/assembly.json");
17
+ assert.deepEqual(tsconfig.include, ["src/**/*.ts"]);
18
+ const packageJson = JSON.parse(readFileSync(join(target, "package.json"), "utf8"));
19
+ assert.equal(typeof packageJson.scripts.dev, "string");
20
+ assert.equal(typeof packageJson.scripts.build, "string");
21
+ assert.equal(typeof packageJson.scripts.test, "string");
22
+ }
23
+ finally {
24
+ rmSync(root, { recursive: true, force: true });
25
+ }
26
+ });
27
+ test("createProject fails on non-empty target directory", () => {
28
+ const root = mkdtempSync(join(tmpdir(), "create-fui-as-app-"));
29
+ const target = join(root, "existing-app");
30
+ try {
31
+ createProject({
32
+ targetDirectory: target,
33
+ projectName: "existing-app",
34
+ });
35
+ writeFileSync(join(target, "README.txt"), "occupied", "utf8");
36
+ assert.throws(() => createProject({
37
+ targetDirectory: target,
38
+ projectName: "existing-app",
39
+ }));
40
+ }
41
+ finally {
42
+ rmSync(root, { recursive: true, force: true });
43
+ }
44
+ });
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@effindomv2/create-fui-as-app",
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "license": "MIT",
6
+ "description": "Scaffold a minimal EffinDom v2 FUI-AS app",
7
+ "type": "module",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "engines": {
12
+ "node": ">=18"
13
+ },
14
+ "bin": {
15
+ "create-fui-as-app": "./dist/src/cli.js"
16
+ },
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/src/scaffold.d.ts",
20
+ "default": "./dist/src/scaffold.js"
21
+ }
22
+ },
23
+ "main": "./dist/src/scaffold.js",
24
+ "types": "./dist/src/scaffold.d.ts",
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "scripts": {
29
+ "build": "tsc -p tsconfig.json",
30
+ "typecheck": "tsc -p tsconfig.json --noEmit",
31
+ "test": "npm run build && node --test dist/tests/**/*.test.js",
32
+ "prepack": "npm run build"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^24.10.1",
36
+ "typescript": "^5.9.3"
37
+ }
38
+ }