@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 +6 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +6 -0
- package/dist/src/scaffold.d.ts +10 -0
- package/dist/src/scaffold.js +55 -0
- package/dist/src/templates.d.ts +5 -0
- package/dist/src/templates.js +230 -0
- package/dist/src/versions.d.ts +2 -0
- package/dist/src/versions.js +2 -0
- package/dist/tests/scaffold.test.d.ts +1 -0
- package/dist/tests/scaffold.test.js +44 -0
- package/package.json +38 -0
package/LICENSE.md
ADDED
package/dist/src/cli.js
ADDED
|
@@ -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,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 @@
|
|
|
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
|
+
}
|