@monimesl/dot-compiler 0.1.0-alpha.2 → 0.1.0-alpha.21
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/dist/index.d.ts +35 -28
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +305 -268
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import { dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
5
5
|
import { ScriptParseError, parseDotModule, parseExpression, parseLogicModule, parseStateModule, unboundReferences } from "@monimesl/dot-script-parser";
|
|
6
|
-
import { PERMISSIONS, REACT_HOST_MODULES, SourceError, builtinComponents, builtinRegistry, componentRegistry, eventName, isPermission, isSemanticVersion, reactHostUrl } from "@monimesl/dot-core";
|
|
6
|
+
import { PERMISSIONS, REACT_HOST_MODULES, SourceError, builtinComponents, builtinRegistry, componentRegistry, eventName, isNetworkHost, isPermission, isSemanticVersion, isWordsOr, otherThanWords, reactHostUrl } from "@monimesl/dot-core";
|
|
7
|
+
import { zipSync } from "fflate";
|
|
7
8
|
import { rolldown } from "rolldown";
|
|
9
|
+
import { esmExternalRequirePlugin } from "rolldown/plugins";
|
|
8
10
|
import { parseDmlToIR } from "@monimesl/dot-dml-parser";
|
|
9
11
|
import { parseEnv } from "node:util";
|
|
10
12
|
//#region src/errors.ts
|
|
@@ -16,112 +18,68 @@ var CompileWarning = class extends SourceError {
|
|
|
16
18
|
name = "CompileWarning";
|
|
17
19
|
};
|
|
18
20
|
//#endregion
|
|
19
|
-
//#region src/
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"asset_finance",
|
|
23
|
-
"donations",
|
|
24
|
-
"education",
|
|
25
|
-
"electricity",
|
|
26
|
-
"entertainment",
|
|
27
|
-
"events",
|
|
28
|
-
"finance",
|
|
29
|
-
"food_and_drink",
|
|
30
|
-
"gambling",
|
|
31
|
-
"games",
|
|
32
|
-
"government",
|
|
33
|
-
"health",
|
|
34
|
-
"insurance",
|
|
35
|
-
"internet_service_provider",
|
|
36
|
-
"investments",
|
|
37
|
-
"jobs",
|
|
38
|
-
"lifestyle",
|
|
39
|
-
"loans",
|
|
40
|
-
"micro_credit",
|
|
41
|
-
"microfinance",
|
|
42
|
-
"news",
|
|
43
|
-
"other",
|
|
44
|
-
"productivity",
|
|
45
|
-
"real_estate",
|
|
46
|
-
"religion",
|
|
47
|
-
"remittances",
|
|
48
|
-
"savings",
|
|
49
|
-
"shopping",
|
|
50
|
-
"social",
|
|
51
|
-
"sports",
|
|
52
|
-
"telecom",
|
|
53
|
-
"television",
|
|
54
|
-
"transport",
|
|
55
|
-
"travel",
|
|
56
|
-
"utilities",
|
|
57
|
-
"water"
|
|
58
|
-
];
|
|
59
|
-
function isCategory(value) {
|
|
60
|
-
return CATEGORIES.includes(value);
|
|
21
|
+
//#region src/utils.ts
|
|
22
|
+
function quotedList(values) {
|
|
23
|
+
return values.map((value) => `"${value}"`).join(", ");
|
|
61
24
|
}
|
|
62
25
|
//#endregion
|
|
63
26
|
//#region src/project/dot-json.ts
|
|
64
27
|
const DOT_JSON_FILE = "dot.json";
|
|
65
28
|
const DOT_JSON_FIELDS = [
|
|
66
|
-
"$schema",
|
|
67
29
|
"id",
|
|
30
|
+
"$schema",
|
|
31
|
+
"package",
|
|
68
32
|
"name",
|
|
69
33
|
"version",
|
|
70
|
-
"description",
|
|
71
|
-
"categories",
|
|
72
|
-
"icon",
|
|
73
34
|
"permissions",
|
|
74
|
-
"
|
|
35
|
+
"network"
|
|
75
36
|
];
|
|
76
37
|
const REQUIRED_DOT_JSON_FIELDS = [
|
|
77
|
-
"
|
|
38
|
+
"package",
|
|
78
39
|
"name",
|
|
79
40
|
"version"
|
|
80
41
|
];
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
42
|
+
const NETWORK_FIELDS = ["hosts"];
|
|
43
|
+
const LISTING_FIELDS = [
|
|
44
|
+
"description",
|
|
45
|
+
"categories",
|
|
46
|
+
"icon"
|
|
47
|
+
];
|
|
48
|
+
const PACKAGE_WORD = "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?";
|
|
49
|
+
const DOT_PACKAGE_PATTERN = new RegExp(`^(?=[a-z])${PACKAGE_WORD}(?:\\.${PACKAGE_WORD})+$`);
|
|
50
|
+
function isDotPackage(name) {
|
|
51
|
+
return name.length <= 128 && DOT_PACKAGE_PATTERN.test(name);
|
|
52
|
+
}
|
|
84
53
|
async function readDotJson(file) {
|
|
85
54
|
let dotJson;
|
|
86
55
|
try {
|
|
87
56
|
dotJson = JSON.parse(await readFile(file, "utf8"));
|
|
88
57
|
} catch (error) {
|
|
89
|
-
throw new CompileError(`
|
|
58
|
+
throw new CompileError(`This isn't valid JSON: ${error.message}`, file);
|
|
90
59
|
}
|
|
91
|
-
if (!isObject(dotJson)) throw new CompileError("
|
|
92
|
-
if ("pages" in dotJson) throw new CompileError("
|
|
60
|
+
if (!isObject(dotJson)) throw new CompileError("dot.json must be an object, such as { \"package\": \"com.mysalone.thing\", \"name\": \"My Salone Thing\", \"version\": \"1.0.0\" }", file);
|
|
61
|
+
if ("pages" in dotJson) throw new CompileError("List pages in dot.ts, not here: defineDot({ pages: [...] })", file);
|
|
62
|
+
for (const field of LISTING_FIELDS) if (field in dotJson) throw new CompileError(`Remove "${field}". A dot's description, categories and icon are set where it's published.`, file);
|
|
63
|
+
if ("dash" in dotJson) throw new CompileError("Remove \"dash\". Dash now checks the Dot runtime version a dot is built with.", file);
|
|
93
64
|
rejectUnknownFields(dotJson, DOT_JSON_FIELDS, file);
|
|
65
|
+
if (dotJson.package === void 0 && typeof dotJson.id === "string" && DOT_PACKAGE_PATTERN.test(dotJson.id)) throw new CompileError(`"${dotJson.id}" is a package name. Rename "id" to "package".`, file);
|
|
94
66
|
for (const field of REQUIRED_DOT_JSON_FIELDS) if (dotJson[field] === void 0) throw new CompileError(`"${field}" is required`, file);
|
|
95
|
-
const { $schema, id, name, version,
|
|
67
|
+
const { $schema, package: packageName, id, name, version, permissions, network } = dotJson;
|
|
96
68
|
if ($schema !== void 0 && typeof $schema !== "string") throw new CompileError("\"$schema\" must be a string", file);
|
|
97
|
-
if (typeof
|
|
98
|
-
if (
|
|
99
|
-
if (!isNonEmptyString(
|
|
69
|
+
if (typeof packageName !== "string" || !DOT_PACKAGE_PATTERN.test(packageName)) throw new CompileError("\"package\" must be a reverse domain name. Use lowercase words joined by dots, such as \"com.mysalone.thing\".", file);
|
|
70
|
+
if (packageName.length > 128) throw new CompileError(`"package" must be at most 128 characters`, file);
|
|
71
|
+
if (id !== void 0 && !isNonEmptyString(id)) throw new CompileError("\"id\" must be the ID Monime gave the dot, as text", file);
|
|
72
|
+
if (!isNonEmptyString(name)) throw new CompileError("\"name\" must be text, and not empty", file);
|
|
100
73
|
if (!isSemanticVersion(version)) throw new CompileError("\"version\" must be a semantic version, such as \"1.0.0\"", file);
|
|
101
|
-
if (description !== void 0 && !isNonEmptyString(description)) throw new CompileError("\"description\" must be a non-empty string", file);
|
|
102
|
-
if (icon !== void 0 && !(isNonEmptyString(icon) && icon.endsWith(".png"))) throw new CompileError("\"icon\" must be the path to a PNG file, such as \"icon.png\"", file);
|
|
103
74
|
return {
|
|
104
75
|
id,
|
|
76
|
+
package: packageName,
|
|
105
77
|
name,
|
|
106
78
|
version,
|
|
107
|
-
description,
|
|
108
|
-
categories: readCategories(categories, file),
|
|
109
|
-
icon,
|
|
110
79
|
permissions: readPermissions(permissions, file),
|
|
111
|
-
|
|
80
|
+
network: readNetworkAccess(network, file)
|
|
112
81
|
};
|
|
113
82
|
}
|
|
114
|
-
function readCategories(categories, file) {
|
|
115
|
-
if (categories === void 0) return;
|
|
116
|
-
if (!Array.isArray(categories) || categories.length === 0) throw new CompileError("\"categories\" must list one or more categories, such as [\"finance\", \"savings\"]", file);
|
|
117
|
-
const listed = [];
|
|
118
|
-
for (const category of categories) {
|
|
119
|
-
if (!isCategory(category)) throw new CompileError(`"${String(category)}" is not a category. The categories are ${quotedList(CATEGORIES)}`, file);
|
|
120
|
-
if (listed.includes(category)) throw new CompileError(`"categories" lists "${category}" more than once`, file);
|
|
121
|
-
listed.push(category);
|
|
122
|
-
}
|
|
123
|
-
return listed;
|
|
124
|
-
}
|
|
125
83
|
function readPermissions(permissions, file) {
|
|
126
84
|
if (permissions === void 0) return;
|
|
127
85
|
if (!isObject(permissions)) throw new CompileError("\"permissions\" must be an object, such as { \"account\": \"Show your name on receipts\" }", file);
|
|
@@ -131,13 +89,19 @@ function readPermissions(permissions, file) {
|
|
|
131
89
|
}
|
|
132
90
|
return permissions;
|
|
133
91
|
}
|
|
134
|
-
function
|
|
135
|
-
if (
|
|
136
|
-
if (!isObject(
|
|
137
|
-
rejectUnknownFields(
|
|
138
|
-
const {
|
|
139
|
-
if (
|
|
140
|
-
|
|
92
|
+
function readNetworkAccess(network, file) {
|
|
93
|
+
if (network === void 0) return;
|
|
94
|
+
if (!isObject(network)) throw new CompileError("\"network\" must be an object, such as { \"hosts\": [\"api.example.com\"] }", file);
|
|
95
|
+
rejectUnknownFields(network, NETWORK_FIELDS, file, "network");
|
|
96
|
+
const { hosts } = network;
|
|
97
|
+
if (!Array.isArray(hosts) || hosts.length === 0) throw new CompileError("\"network.hosts\" must list the hosts the dot sends requests to, such as [\"api.example.com\"]", file);
|
|
98
|
+
const listed = [];
|
|
99
|
+
for (const host of hosts) {
|
|
100
|
+
if (typeof host !== "string" || !isNetworkHost(host)) throw new CompileError(`"${String(host)}" is not a host. Use a lowercase host name, such as "api.example.com", or "*.example.com" for all its subdomains. Leave out https://, the port and the path.`, file);
|
|
101
|
+
if (listed.includes(host)) throw new CompileError(`"network.hosts" lists "${host}" more than once`, file);
|
|
102
|
+
listed.push(host);
|
|
103
|
+
}
|
|
104
|
+
return { hosts: listed };
|
|
141
105
|
}
|
|
142
106
|
function rejectUnknownFields(object, allowed, file, parent) {
|
|
143
107
|
for (const field of Object.keys(object)) {
|
|
@@ -146,9 +110,6 @@ function rejectUnknownFields(object, allowed, file, parent) {
|
|
|
146
110
|
throw new CompileError(`Unknown field "${parent}.${field}". The fields allowed in "${parent}" are ${quotedList(allowed)}`, file);
|
|
147
111
|
}
|
|
148
112
|
}
|
|
149
|
-
function quotedList(values) {
|
|
150
|
-
return values.map((value) => `"${value}"`).join(", ");
|
|
151
|
-
}
|
|
152
113
|
function isObject(value) {
|
|
153
114
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
154
115
|
}
|
|
@@ -194,10 +155,10 @@ async function listStatics(root) {
|
|
|
194
155
|
}
|
|
195
156
|
async function checkImage(file, extension) {
|
|
196
157
|
const isFormat = IMAGE_FORMATS[extension];
|
|
197
|
-
if (!isFormat) throw new CompileError(
|
|
158
|
+
if (!isFormat) throw new CompileError(`${STATICS_DIR}/ can only hold images: ${STATIC_ASSET_EXTENSIONS.join(", ")}`, file);
|
|
198
159
|
const { size } = await stat(file);
|
|
199
|
-
if (size > 1048576) throw new CompileError(`The image is ${(size / MAX_STATIC_ASSET_BYTES).toFixed(1)} MB
|
|
200
|
-
if (!isFormat(await readFile(file))) throw new CompileError(`The file
|
|
160
|
+
if (size > 1048576) throw new CompileError(`The image is ${(size / MAX_STATIC_ASSET_BYTES).toFixed(1)} MB. Images must be 1 MB or smaller`, file);
|
|
161
|
+
if (!isFormat(await readFile(file))) throw new CompileError(`The file isn't really a ${extension.slice(1)} image`, file);
|
|
201
162
|
}
|
|
202
163
|
function isJpeg(header) {
|
|
203
164
|
return header[0] === 255 && header[1] === 216 && header[2] === 255;
|
|
@@ -214,23 +175,22 @@ const PAGE_FILES = {
|
|
|
214
175
|
async function loadProject(root) {
|
|
215
176
|
root = resolve(root);
|
|
216
177
|
const dotJsonFile = join(root, DOT_JSON_FILE);
|
|
217
|
-
if (!existsSync(dotJsonFile)) throw new CompileError(`
|
|
178
|
+
if (!existsSync(dotJsonFile)) throw new CompileError(`There is no ${DOT_JSON_FILE} in this folder`, root);
|
|
218
179
|
const dotJson = await readDotJson(dotJsonFile);
|
|
219
|
-
if (dotJson.icon !== void 0 && !existsSync(join(root, dotJson.icon))) throw new CompileError(`The icon "${dotJson.icon}" is missing`, dotJsonFile);
|
|
220
180
|
const dotFile = join(root, DOT_FILE);
|
|
221
|
-
if (!existsSync(dotFile)) throw new CompileError(`
|
|
181
|
+
if (!existsSync(dotFile)) throw new CompileError(`There is no ${DOT_FILE}. Add one: export default defineDot({ pages: [...] })`, root);
|
|
222
182
|
const { pages } = parseDotModule(await readFile(dotFile, "utf8"), dotFile);
|
|
223
183
|
const sources = await Promise.all(pages.map((path) => loadPage(root, path, dotFile)));
|
|
224
|
-
const
|
|
184
|
+
const pagesByLowercaseId = /* @__PURE__ */ new Map();
|
|
225
185
|
for (const page of sources) {
|
|
226
|
-
const other =
|
|
186
|
+
const other = pagesByLowercaseId.get(page.id.toLowerCase());
|
|
227
187
|
if (other) {
|
|
228
188
|
let file = dotFile;
|
|
229
189
|
if (page.id !== page.path) file = page.controller;
|
|
230
190
|
else if (other.id !== other.path) file = other.controller;
|
|
231
191
|
throw new CompileError(`Page id "${page.id}" (${page.path}) is already used by ${other.path}`, file);
|
|
232
192
|
}
|
|
233
|
-
|
|
193
|
+
pagesByLowercaseId.set(page.id.toLowerCase(), page);
|
|
234
194
|
}
|
|
235
195
|
return {
|
|
236
196
|
root,
|
|
@@ -241,11 +201,11 @@ async function loadProject(root) {
|
|
|
241
201
|
};
|
|
242
202
|
}
|
|
243
203
|
async function loadPage(root, path, dotFile) {
|
|
244
|
-
const
|
|
204
|
+
const pageDir = join(root, PAGES_DIR, path);
|
|
245
205
|
const files = {
|
|
246
|
-
view: join(
|
|
247
|
-
state: join(
|
|
248
|
-
controller: join(
|
|
206
|
+
view: join(pageDir, PAGE_FILES.view),
|
|
207
|
+
state: join(pageDir, PAGE_FILES.state),
|
|
208
|
+
controller: join(pageDir, PAGE_FILES.controller)
|
|
249
209
|
};
|
|
250
210
|
const missing = Object.values(files).filter((file) => !existsSync(file));
|
|
251
211
|
if (missing.length > 0) throw new CompileError(`Page "${path}" is missing ${missing.join(", ")}`, dotFile);
|
|
@@ -259,7 +219,7 @@ async function loadPage(root, path, dotFile) {
|
|
|
259
219
|
//#endregion
|
|
260
220
|
//#region src/entries.ts
|
|
261
221
|
const RUNTIME_PACKAGE = "@monimesl/dot-runtime";
|
|
262
|
-
const
|
|
222
|
+
const RUNTIME_MODULES = {
|
|
263
223
|
ui: `${RUNTIME_PACKAGE}/ui`,
|
|
264
224
|
logic: `${RUNTIME_PACKAGE}/logic`,
|
|
265
225
|
renderer: `${RUNTIME_PACKAGE}/react`
|
|
@@ -273,37 +233,37 @@ const viewModuleId = (id) => `dot:view/${id}`;
|
|
|
273
233
|
function literal(value) {
|
|
274
234
|
return JSON.stringify(value);
|
|
275
235
|
}
|
|
276
|
-
function pageTable(project,
|
|
277
|
-
return `{${project.pages.map((page, index) => `\n ${literal(page.id)}: ${
|
|
236
|
+
function pageTable(project, importName) {
|
|
237
|
+
return `{${project.pages.map((page, index) => `\n ${literal(page.id)}: ${importName}${index},`).join("")}\n }`;
|
|
278
238
|
}
|
|
279
|
-
function uiEntry(project) {
|
|
239
|
+
function uiEntry(project, components) {
|
|
240
|
+
const rendererModule = literal(RUNTIME_MODULES.renderer);
|
|
241
|
+
let renderer;
|
|
242
|
+
if (components) {
|
|
243
|
+
const componentList = components.join(", ");
|
|
244
|
+
renderer = [`import { createReactRenderer, ${componentList} } from ${rendererModule};`, `const createRenderer = () => createReactRenderer({ components: { ${componentList} } });`];
|
|
245
|
+
} else renderer = [`import createRenderer from ${rendererModule};`];
|
|
280
246
|
return [
|
|
281
|
-
`import {
|
|
282
|
-
|
|
247
|
+
`import { runWebview } from ${literal(RUNTIME_MODULES.ui)};`,
|
|
248
|
+
...renderer,
|
|
283
249
|
...project.pages.map((page, index) => `import view${index} from ${literal(viewModuleId(page.id))};`),
|
|
284
250
|
"",
|
|
285
|
-
"
|
|
286
|
-
"
|
|
287
|
-
" transport: hostTransport(),",
|
|
288
|
-
" renderer: createRenderer(),",
|
|
251
|
+
"runWebview({",
|
|
252
|
+
" renderer: createRenderer,",
|
|
289
253
|
` views: ${pageTable(project, "view")},`,
|
|
290
254
|
"});",
|
|
291
255
|
""
|
|
292
256
|
].join("\n");
|
|
293
257
|
}
|
|
294
258
|
function logicEntry(project, { strict }) {
|
|
295
|
-
const options = [
|
|
296
|
-
" transport: hostTransport(),",
|
|
297
|
-
" dot,",
|
|
298
|
-
` pages: ${pageTable(project, "page")},`
|
|
299
|
-
];
|
|
259
|
+
const options = [" dot,", ` pages: ${pageTable(project, "page")},`];
|
|
300
260
|
if (strict) options.push(" strict: true,");
|
|
301
261
|
return [
|
|
302
|
-
`import {
|
|
262
|
+
`import { runService } from ${literal(RUNTIME_MODULES.logic)};`,
|
|
303
263
|
`import dot from ${literal(project.dot)};`,
|
|
304
264
|
...project.pages.map((page, index) => `import page${index} from ${literal(page.controller)};`),
|
|
305
265
|
"",
|
|
306
|
-
"
|
|
266
|
+
"runService({",
|
|
307
267
|
...options,
|
|
308
268
|
"});",
|
|
309
269
|
""
|
|
@@ -323,17 +283,21 @@ const DOT_GLOBALS = {
|
|
|
323
283
|
],
|
|
324
284
|
[ENV_MODULE]: ["env"]
|
|
325
285
|
};
|
|
326
|
-
const
|
|
286
|
+
const SCRIPT_FILE = /\.[cm]?[jt]sx?$/;
|
|
327
287
|
function dotGlobals(root) {
|
|
328
|
-
const
|
|
288
|
+
const realRoot = realpathSync(root);
|
|
289
|
+
const isDotModule = (id) => id.startsWith(realRoot + sep) && !id.includes(`${sep}node_modules${sep}`) && SCRIPT_FILE.test(id);
|
|
290
|
+
const isPageModule = (id) => id.startsWith(join(realRoot, PAGES_DIR) + sep);
|
|
329
291
|
return {
|
|
330
292
|
name: "dot:globals",
|
|
331
293
|
transform(code, id) {
|
|
332
294
|
if (!isDotModule(id)) return null;
|
|
295
|
+
const used = new Set(unboundReferences(code, Object.values(DOT_GLOBALS).flat(), id));
|
|
296
|
+
if (isPageModule(id) && used.has("dash")) this.error(`${relative(realRoot, id)} uses the global \`dash\`. In a page, use \`this.dash\` in actions and hooks, so its calls stop when the page closes.`);
|
|
333
297
|
const imports = [];
|
|
334
298
|
for (const [source, names] of Object.entries(DOT_GLOBALS)) {
|
|
335
|
-
const
|
|
336
|
-
if (
|
|
299
|
+
const imported = names.filter((name) => used.has(name));
|
|
300
|
+
if (imported.length > 0) imports.push(`import { ${imported.join(", ")} } from ${JSON.stringify(source)};`);
|
|
337
301
|
}
|
|
338
302
|
if (imports.length === 0) return null;
|
|
339
303
|
return {
|
|
@@ -345,18 +309,19 @@ function dotGlobals(root) {
|
|
|
345
309
|
}
|
|
346
310
|
//#endregion
|
|
347
311
|
//#region src/build/bundle.ts
|
|
348
|
-
/** Bundles one entry with rolldown, replacing `outDir`. Returns the written files. */
|
|
349
312
|
async function bundle(options) {
|
|
350
313
|
await rm(options.outDir, {
|
|
351
314
|
recursive: true,
|
|
352
315
|
force: true
|
|
353
316
|
});
|
|
317
|
+
const plugins = [];
|
|
318
|
+
if (options.external.length > 0) plugins.push(esmExternalRequirePlugin({ external: [...options.external] }));
|
|
319
|
+
plugins.push(generatedModules(options.root, options.modules), dotGlobals(options.root));
|
|
354
320
|
const bundler = await rolldown({
|
|
355
321
|
input: options.entry,
|
|
356
322
|
cwd: options.root,
|
|
357
|
-
external: [...options.external],
|
|
358
323
|
platform: "browser",
|
|
359
|
-
plugins
|
|
324
|
+
plugins,
|
|
360
325
|
resolve: { conditionNames: [
|
|
361
326
|
...options.conditions,
|
|
362
327
|
"browser",
|
|
@@ -364,14 +329,18 @@ async function bundle(options) {
|
|
|
364
329
|
"require",
|
|
365
330
|
"default"
|
|
366
331
|
] },
|
|
367
|
-
transform: { define: { "process.env.NODE_ENV": JSON.stringify(options.mode) } }
|
|
332
|
+
transform: { define: { "process.env.NODE_ENV": JSON.stringify(options.mode) } },
|
|
333
|
+
onLog(level, log, defaultHandler) {
|
|
334
|
+
if (log.code !== "MODULE_LEVEL_DIRECTIVE") defaultHandler(level, log);
|
|
335
|
+
}
|
|
368
336
|
});
|
|
369
337
|
try {
|
|
370
338
|
const { output } = await bundler.write({
|
|
371
339
|
dir: options.outDir,
|
|
372
340
|
format: options.format,
|
|
373
341
|
entryFileNames: options.fileName,
|
|
374
|
-
sourcemap:
|
|
342
|
+
sourcemap: options.mode === "development",
|
|
343
|
+
sourcemapExcludeSources: options.mode === "production",
|
|
375
344
|
minify: options.minify
|
|
376
345
|
});
|
|
377
346
|
return output.map((chunk) => join(options.outDir, chunk.fileName));
|
|
@@ -379,7 +348,6 @@ async function bundle(options) {
|
|
|
379
348
|
await bundler.close();
|
|
380
349
|
}
|
|
381
350
|
}
|
|
382
|
-
/** Serves generated modules and resolves their imports as if they lived in the project root. */
|
|
383
351
|
function generatedModules(root, modules) {
|
|
384
352
|
const importer = join(root, DOT_JSON_FILE);
|
|
385
353
|
return {
|
|
@@ -388,11 +356,12 @@ function generatedModules(root, modules) {
|
|
|
388
356
|
if (modules.has(source)) return `\0${source}`;
|
|
389
357
|
if (!from?.startsWith("\0dot:")) return null;
|
|
390
358
|
const resolved = await this.resolve(source, importer, { skipSelf: true });
|
|
391
|
-
if (!resolved) this.error(`
|
|
359
|
+
if (!resolved) this.error(`Can't find "${source}" from ${root}. Is it installed?`);
|
|
392
360
|
return resolved;
|
|
393
361
|
},
|
|
394
362
|
load(id) {
|
|
395
|
-
|
|
363
|
+
if (!id.startsWith("\0")) return null;
|
|
364
|
+
return modules.get(id.slice(1)) ?? null;
|
|
396
365
|
}
|
|
397
366
|
};
|
|
398
367
|
}
|
|
@@ -408,7 +377,8 @@ function installedRuntime(root) {
|
|
|
408
377
|
react: {
|
|
409
378
|
version: react.version,
|
|
410
379
|
dir: join(renderer.dir, "dist", "react-host")
|
|
411
|
-
}
|
|
380
|
+
},
|
|
381
|
+
stylesheet: join(renderer.dir, "dist", "styles.css")
|
|
412
382
|
};
|
|
413
383
|
}
|
|
414
384
|
function requirePackage(fromDir, name, installedIn) {
|
|
@@ -417,7 +387,7 @@ function requirePackage(fromDir, name, installedIn) {
|
|
|
417
387
|
try {
|
|
418
388
|
packageFile = require.resolve(`${name}/package.json`);
|
|
419
389
|
} catch {
|
|
420
|
-
throw new CompileError(`${name}
|
|
390
|
+
throw new CompileError(`${name} isn't installed in ${installedIn}. Install the project's dependencies, then try again`, fromDir);
|
|
421
391
|
}
|
|
422
392
|
const { version } = JSON.parse(readFileSync(packageFile, "utf8"));
|
|
423
393
|
return {
|
|
@@ -432,17 +402,17 @@ const EMPTY_SCOPE = /* @__PURE__ */ new Map();
|
|
|
432
402
|
function isIdentifier(name) {
|
|
433
403
|
return IDENTIFIER.test(name);
|
|
434
404
|
}
|
|
435
|
-
/**
|
|
436
|
-
* Prints a binding as JS. Loop variables read their local; everything else reads page data from
|
|
437
|
-
* `d`. Member access never throws on nullish values.
|
|
438
|
-
*/
|
|
439
405
|
function printExpr(expr, scope) {
|
|
440
406
|
const print = (node) => printExpr(node, scope);
|
|
441
407
|
switch (expr.type) {
|
|
442
408
|
case "Literal": return JSON.stringify(expr.value);
|
|
443
409
|
case "Identifier": return scope.get(expr.name) ?? `d.${expr.name}`;
|
|
444
|
-
case "Member":
|
|
445
|
-
|
|
410
|
+
case "Member":
|
|
411
|
+
if (expr.property.type === "Literal" && typeof expr.property.value === "string") return `${print(expr.object)}?.${expr.property.value}`;
|
|
412
|
+
return `${print(expr.object)}?.[${print(expr.property)}]`;
|
|
413
|
+
case "Unary":
|
|
414
|
+
if (expr.operator === "typeof") return `(typeof ${print(expr.argument)})`;
|
|
415
|
+
return `(${expr.operator}${print(expr.argument)})`;
|
|
446
416
|
case "Binary": return `(${print(expr.left)} ${expr.operator} ${print(expr.right)})`;
|
|
447
417
|
case "Conditional": return `(${print(expr.test)} ? ${print(expr.consequent)} : ${print(expr.alternate)})`;
|
|
448
418
|
}
|
|
@@ -464,43 +434,41 @@ function stateReads(expr, scope) {
|
|
|
464
434
|
}
|
|
465
435
|
//#endregion
|
|
466
436
|
//#region src/view/control-flow.ts
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
437
|
+
const CONTROL_FLOW_TAGS = {
|
|
438
|
+
If: {
|
|
439
|
+
attributes: ["condition"],
|
|
440
|
+
required: ["condition"]
|
|
441
|
+
},
|
|
442
|
+
ElseIf: {
|
|
443
|
+
attributes: ["condition"],
|
|
444
|
+
required: ["condition"]
|
|
445
|
+
},
|
|
446
|
+
Else: {
|
|
447
|
+
attributes: [],
|
|
448
|
+
required: []
|
|
449
|
+
},
|
|
450
|
+
For: {
|
|
451
|
+
attributes: [
|
|
452
|
+
"each",
|
|
453
|
+
"item",
|
|
454
|
+
"index",
|
|
455
|
+
"key"
|
|
456
|
+
],
|
|
457
|
+
required: ["each"]
|
|
458
|
+
}
|
|
459
|
+
};
|
|
477
460
|
function compileConditional(compiler, branches, parent, scope) {
|
|
478
461
|
let code = "[]";
|
|
479
462
|
for (const branch of [...branches].reverse()) {
|
|
480
463
|
const body = `[${compiler.compile(branch.children, parent, scope).join(", ")}]`;
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
} else {
|
|
485
|
-
compiler.checkControlFlowElementAttributes(branch, ["condition"], ["condition"]);
|
|
486
|
-
code = `${compiler.compileSingleBinding(branch, "condition", scope)} ? ${body} : ${code}`;
|
|
487
|
-
}
|
|
464
|
+
compiler.checkControlFlowElementAttributes(branch);
|
|
465
|
+
if (branch.tag === "Else") code = body;
|
|
466
|
+
else code = `${compiler.compileSingleBinding(branch, "condition", scope)} ? ${body} : ${code}`;
|
|
488
467
|
}
|
|
489
468
|
return `...(${code})`;
|
|
490
469
|
}
|
|
491
|
-
/**
|
|
492
|
-
* `<For each="{{ list }}" item="item" index="index" key="id">`. `item` and `index` default to those
|
|
493
|
-
* names. `key` is a property of each item, `*this` for the item itself, or `*index` to match items
|
|
494
|
-
* by position; it is optional, but a warning is logged when missing. Compiles to
|
|
495
|
-
* `each(list, (item, index) => [...], key?)`.
|
|
496
|
-
*/
|
|
497
470
|
function compileLoop(compiler, node, parent, scope) {
|
|
498
|
-
compiler.checkControlFlowElementAttributes(node
|
|
499
|
-
"each",
|
|
500
|
-
"item",
|
|
501
|
-
"index",
|
|
502
|
-
"key"
|
|
503
|
-
], ["each"]);
|
|
471
|
+
compiler.checkControlFlowElementAttributes(node);
|
|
504
472
|
const list = compiler.compileSingleBinding(node, "each", scope);
|
|
505
473
|
const { item = "item", index = "index", key } = node.attributes;
|
|
506
474
|
const checkVariableName = (attribute, name) => {
|
|
@@ -508,128 +476,182 @@ function compileLoop(compiler, node, parent, scope) {
|
|
|
508
476
|
};
|
|
509
477
|
checkVariableName("item", item);
|
|
510
478
|
checkVariableName("index", index);
|
|
511
|
-
if (item === index) compiler.fail("<For> `item` and `index`
|
|
512
|
-
const
|
|
513
|
-
const
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
479
|
+
if (item === index) compiler.fail("<For> `item` and `index` need different names", node.loc);
|
|
480
|
+
const itemVariable = "$" + item;
|
|
481
|
+
const indexVariable = "$" + index;
|
|
482
|
+
const loopScope = new Map(scope);
|
|
483
|
+
loopScope.set(item, itemVariable);
|
|
484
|
+
loopScope.set(index, indexVariable);
|
|
485
|
+
const eachArguments = [list, `(${itemVariable}, ${indexVariable}) => [${compiler.compile(node.children, parent, loopScope).join(", ")}]`];
|
|
486
|
+
if (key === void 0) compiler.warn("<For> has no `key`, so items are matched by position. If items can move, add key=\"<property>\" or key=\"*this\". To keep matching by position, add key=\"*index\".", node.loc);
|
|
487
|
+
else if (key === "*index") {} else if (key === "*this") eachArguments.push(`(${itemVariable}) => ${itemVariable}`);
|
|
488
|
+
else if (isIdentifier(key)) eachArguments.push(`(${itemVariable}) => ${itemVariable}?.${key}`);
|
|
519
489
|
else compiler.fail(`<For> \`key\` must be a property name, *this or *index, got "${key}"`, node.loc);
|
|
520
|
-
return `each(${
|
|
490
|
+
return `each(${eachArguments.join(", ")})`;
|
|
521
491
|
}
|
|
522
492
|
//#endregion
|
|
523
493
|
//#region src/view/compiler.ts
|
|
494
|
+
const ROOT_TAG = "View";
|
|
495
|
+
const ROOT_DEFAULTS = {
|
|
496
|
+
padding: "large",
|
|
497
|
+
gap: "large",
|
|
498
|
+
grow: true
|
|
499
|
+
};
|
|
524
500
|
const SEGMENT_BINDING_REGEXP = /{{([\s\S]*?)}}/g;
|
|
501
|
+
const DATA_PREFIX = "data-";
|
|
502
|
+
const DATA_ATTRIBUTE = /^data-[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
503
|
+
const THEME_COLOR_NAME = /^[a-z][a-zA-Z0-9]*$/;
|
|
525
504
|
var ViewCompiler = class {
|
|
526
505
|
#context;
|
|
527
506
|
#components;
|
|
528
507
|
constructor(context) {
|
|
529
508
|
this.#context = context;
|
|
530
509
|
this.#components = context.components ?? builtinRegistry;
|
|
531
|
-
for (const name of
|
|
510
|
+
for (const name of Object.keys(CONTROL_FLOW_TAGS)) if (name in this.#components) this.fail(`<${name}> is reserved for control flow`);
|
|
532
511
|
}
|
|
533
512
|
compile(nodes, parent, scope) {
|
|
534
|
-
const
|
|
513
|
+
const compiled = [];
|
|
535
514
|
for (let i = 0; i < nodes.length; i++) {
|
|
536
515
|
const node = nodes[i];
|
|
537
516
|
if (node.type === "Text") {
|
|
538
|
-
|
|
517
|
+
compiled.push(...this.#compileText(node.content, parent, scope, node.loc));
|
|
539
518
|
continue;
|
|
540
519
|
}
|
|
541
520
|
switch (node.tag) {
|
|
542
521
|
case "If": {
|
|
543
522
|
const branches = ifChain(node, nodes.slice(i + 1));
|
|
544
|
-
|
|
523
|
+
compiled.push(compileConditional(this, branches, parent, scope));
|
|
545
524
|
i += branches.length - 1;
|
|
546
525
|
break;
|
|
547
526
|
}
|
|
548
527
|
case "ElseIf":
|
|
549
|
-
case "Else": return this.fail(`<${node.tag}> must
|
|
528
|
+
case "Else": return this.fail(`<${node.tag}> must come right after <If> or <ElseIf>`, node.loc);
|
|
550
529
|
case "For":
|
|
551
|
-
|
|
530
|
+
compiled.push(compileLoop(this, node, parent, scope));
|
|
552
531
|
break;
|
|
553
|
-
default:
|
|
532
|
+
default: compiled.push(`h(${this.#compileElementArgs(node, parent, scope).join(", ")})`);
|
|
554
533
|
}
|
|
555
534
|
}
|
|
556
|
-
return
|
|
535
|
+
return compiled;
|
|
557
536
|
}
|
|
558
|
-
compileText(content, parent, scope, loc) {
|
|
537
|
+
#compileText(content, parent, scope, loc) {
|
|
559
538
|
if (parent?.children !== "text") {
|
|
560
|
-
|
|
561
|
-
this.
|
|
539
|
+
if (content.trim() === "") return [];
|
|
540
|
+
const wrappers = Object.values(this.#components).filter((component) => component.inline && component.children === "text").sort((a, b) => Number(b.name === "Text") - Number(a.name === "Text")).map((component) => `<${component.name}>`).join(" or ");
|
|
541
|
+
this.fail(`'${content}' must be inside ${wrappers || "an inline component"}`, loc);
|
|
542
|
+
}
|
|
543
|
+
return this.#compileAsSegments(content, scope, loc).map(printSegment);
|
|
544
|
+
}
|
|
545
|
+
compileRoot(nodes) {
|
|
546
|
+
if (!this.#components["View"]) this.fail(`The components must include <${ROOT_TAG}>, which is the root of every page`);
|
|
547
|
+
const [root, outside] = nodes;
|
|
548
|
+
if (root?.type !== "Element" || root.tag !== "View") return this.fail(`A page must have one <${ROOT_TAG}> around everything else`, root?.loc);
|
|
549
|
+
if (outside) {
|
|
550
|
+
const name = outside.type === "Text" ? `'${outside.content}'` : `<${outside.tag}>`;
|
|
551
|
+
this.fail(`${name} must be inside the page's <${ROOT_TAG}>`, outside.loc);
|
|
562
552
|
}
|
|
563
|
-
return this
|
|
553
|
+
return this.#compileElementArgs(root, null, EMPTY_SCOPE, ROOT_DEFAULTS);
|
|
564
554
|
}
|
|
565
|
-
|
|
555
|
+
#compileElementArgs(node, parent, scope, defaults = {}) {
|
|
566
556
|
const component = this.#components[node.tag];
|
|
567
|
-
if (!component) return this.fail(`Unknown component <${node.tag}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
557
|
+
if (!component) return this.fail(`Unknown component <${node.tag}>`, node.loc);
|
|
558
|
+
this.#context.onComponent?.(component.name);
|
|
559
|
+
if (parent && !canHaveChildren(parent, component)) this.fail(`<${node.tag}> can't go inside <${parent.name}>`, node.loc);
|
|
560
|
+
for (const { prop, ignores, because } of component.overrides ?? []) if (prop in node.attributes && ignores in node.attributes) this.warn(`<${component.name}> ignores \`${ignores}\` with \`${prop}\`: ${because}`, node.loc);
|
|
561
|
+
const dataAttributes = [];
|
|
562
|
+
const propAttributes = [];
|
|
563
|
+
for (const [name, value] of Object.entries(node.attributes)) if (name.startsWith("data-")) dataAttributes.push([name, value]);
|
|
564
|
+
else propAttributes.push([name, value]);
|
|
565
|
+
const data = this.#compileData(dataAttributes, scope, node.loc);
|
|
566
|
+
const props = [];
|
|
567
|
+
for (const [name, value] of propAttributes) props.push(this.#compileProp(component, name, value, scope, node.loc));
|
|
568
|
+
for (const [name, handler] of Object.entries(node.events)) props.push(this.#compileEvent(component, name, handler, data, node.loc));
|
|
569
|
+
for (const [name, value] of Object.entries(defaults)) if (!(name in node.attributes)) props.push(`${name}: ${JSON.stringify(value)}`);
|
|
570
|
+
return [
|
|
571
571
|
JSON.stringify(component.name),
|
|
572
572
|
props.length ? `{ ${props.join(", ")} }` : "null",
|
|
573
573
|
...this.compile(node.children, component, scope)
|
|
574
|
-
]
|
|
574
|
+
];
|
|
575
575
|
}
|
|
576
|
-
compileProp(component, name, value, scope, loc) {
|
|
576
|
+
#compileProp(component, name, value, scope, loc) {
|
|
577
577
|
const type = component.props[name];
|
|
578
|
-
if (!type) return this.fail(`<${component.name}>
|
|
579
|
-
const segments = this
|
|
578
|
+
if (!type) return this.fail(`<${component.name}> has no \`${name}\` attribute`, loc);
|
|
579
|
+
const segments = this.#compileAsSegments(value, scope, loc);
|
|
580
580
|
const bindingCode = singleBindingCode(segments);
|
|
581
581
|
if (bindingCode !== null) return `${name}: ${bindingCode}`;
|
|
582
582
|
if (segments.every((segment) => segment.kind === "static")) {
|
|
583
583
|
this.#checkStaticsReference(value, loc);
|
|
584
|
-
return `${name}: ${this
|
|
584
|
+
return `${name}: ${this.#compileLiteral(type, value, loc)}`;
|
|
585
585
|
}
|
|
586
586
|
if (type !== "string" && type !== "any") {
|
|
587
587
|
const kind = typeof type === "string" ? `a ${type}` : "an enum";
|
|
588
|
-
this.fail(`\`${name}\` is ${kind}
|
|
588
|
+
this.fail(`\`${name}\` is ${kind}, so set it with one {{ }} binding`, loc);
|
|
589
589
|
}
|
|
590
590
|
return `${name}: ${segments.map(printSegment).join(" + ")}`;
|
|
591
591
|
}
|
|
592
|
+
#compileData(attributes, scope, loc) {
|
|
593
|
+
if (attributes.length === 0) return null;
|
|
594
|
+
return `{ ${attributes.map(([name, value]) => {
|
|
595
|
+
if (!DATA_ATTRIBUTE.test(name)) this.fail(`\`${name}\` must be lowercase words joined by -, such as data-account-id`, loc);
|
|
596
|
+
return `${dataKey(name)}: ${this.#compileDataValue(value, scope, loc)}`;
|
|
597
|
+
}).join(", ")} }`;
|
|
598
|
+
}
|
|
599
|
+
#compileDataValue(value, scope, loc) {
|
|
600
|
+
const segments = this.#compileAsSegments(value, scope, loc);
|
|
601
|
+
const bindingCode = singleBindingCode(segments);
|
|
602
|
+
if (bindingCode !== null) return bindingCode;
|
|
603
|
+
if (segments.length === 0) return "\"\"";
|
|
604
|
+
return segments.map(printSegment).join(" + ");
|
|
605
|
+
}
|
|
592
606
|
#checkStaticsReference(value, loc) {
|
|
593
607
|
const { statics } = this.#context;
|
|
594
|
-
if (statics && value.startsWith(`statics/`) && !statics.includes(value)) this.fail(`"${value}"
|
|
608
|
+
if (statics && value.startsWith(`statics/`) && !statics.includes(value)) this.fail(`"${value}" isn't in ${STATICS_DIR}/`, loc);
|
|
595
609
|
}
|
|
596
|
-
compileEvent(component, attribute, handler, loc) {
|
|
610
|
+
#compileEvent(component, attribute, handler, data, loc) {
|
|
597
611
|
const event = eventName(attribute);
|
|
598
|
-
if (!component.events.includes(event)) this.fail(`<${component.name}> has no
|
|
612
|
+
if (!component.events.includes(event)) this.fail(`<${component.name}> has no \`${attribute}\` event`, loc);
|
|
599
613
|
if (!isIdentifier(handler)) this.fail(`\`${attribute}\` must name a mutation or action in controller.ts, got "${handler}"`, loc);
|
|
600
614
|
const { handlers } = this.#context;
|
|
601
|
-
if (handlers && !handlers.includes(handler)) this.fail(`\`${handler}\`
|
|
602
|
-
return `${attribute}: on(${JSON.stringify(handler)})`;
|
|
615
|
+
if (handlers && !handlers.includes(handler)) this.fail(`\`${handler}\` isn't a mutation or action in controller.ts`, loc);
|
|
616
|
+
return `${attribute}: on(${data ? `${JSON.stringify(handler)}, ${data}` : JSON.stringify(handler)})`;
|
|
603
617
|
}
|
|
604
|
-
checkControlFlowElementAttributes(node
|
|
618
|
+
checkControlFlowElementAttributes(node) {
|
|
619
|
+
const { attributes: allowed, required } = CONTROL_FLOW_TAGS[node.tag];
|
|
605
620
|
const [event] = Object.keys(node.events);
|
|
606
|
-
if (event) this.fail(`<${node.tag}>
|
|
607
|
-
for (const name of Object.keys(node.attributes)) if (!allowed.includes(name)) this.fail(`<${node.tag}>
|
|
621
|
+
if (event) this.fail(`<${node.tag}> can't handle events such as \`${event}\`. Put the handler on a component inside it.`, node.loc);
|
|
622
|
+
for (const name of Object.keys(node.attributes)) if (!allowed.includes(name)) this.fail(`<${node.tag}> has no \`${name}\` attribute`, node.loc);
|
|
608
623
|
for (const name of required) if (!(name in node.attributes)) this.fail(`<${node.tag}> requires \`${name}\``, node.loc);
|
|
609
624
|
}
|
|
610
625
|
compileSingleBinding(node, attribute, scope) {
|
|
611
|
-
const bindingCode = singleBindingCode(this
|
|
612
|
-
if (bindingCode === null) return this.fail(`<${node.tag}> \`${attribute}\` must be
|
|
626
|
+
const bindingCode = singleBindingCode(this.#compileAsSegments(node.attributes[attribute] ?? "", scope, node.loc));
|
|
627
|
+
if (bindingCode === null) return this.fail(`<${node.tag}> \`${attribute}\` must be one {{ }} binding`, node.loc);
|
|
613
628
|
return bindingCode;
|
|
614
629
|
}
|
|
615
|
-
compileLiteral(type, value, loc) {
|
|
630
|
+
#compileLiteral(type, value, loc) {
|
|
631
|
+
if (isWordsOr(type)) return this.#compileWordOr(type, value, loc);
|
|
616
632
|
if (typeof type !== "string") {
|
|
617
633
|
if (type.includes(value)) return JSON.stringify(value);
|
|
618
|
-
return this.fail(`Expected one of ${type
|
|
634
|
+
return this.fail(`Expected one of ${quotedList(type)}, got "${value}"`, loc);
|
|
619
635
|
}
|
|
620
636
|
switch (type) {
|
|
621
637
|
case "boolean":
|
|
622
638
|
if (value === "" || value === "true") return "true";
|
|
623
639
|
if (value === "false") return "false";
|
|
624
|
-
return this.fail(`Expected
|
|
640
|
+
return this.fail(`Expected true or false, got "${value}"`, loc);
|
|
625
641
|
case "number":
|
|
626
|
-
if (
|
|
642
|
+
if (isNumberText(value)) return String(Number(value));
|
|
627
643
|
return this.fail(`Expected a number, got "${value}"`, loc);
|
|
628
644
|
case "string":
|
|
629
645
|
case "any": return JSON.stringify(value);
|
|
630
646
|
}
|
|
631
647
|
}
|
|
632
|
-
|
|
648
|
+
#compileWordOr(type, value, loc) {
|
|
649
|
+
if (type.words.includes(value)) return JSON.stringify(value);
|
|
650
|
+
if (type.or === "number" && isNumberText(value)) return String(Number(value));
|
|
651
|
+
if (type.or === "themeColor" && THEME_COLOR_NAME.test(value)) return JSON.stringify(value);
|
|
652
|
+
return this.fail(`Expected one of ${quotedList(type.words)}, or ${otherThanWords(type)}, got "${value}"`, loc);
|
|
653
|
+
}
|
|
654
|
+
#compileAsSegments(input, scope, loc) {
|
|
633
655
|
const segments = [];
|
|
634
656
|
let last = 0;
|
|
635
657
|
for (const match of input.matchAll(SEGMENT_BINDING_REGEXP)) {
|
|
@@ -637,7 +659,7 @@ var ViewCompiler = class {
|
|
|
637
659
|
kind: "static",
|
|
638
660
|
value: input.slice(last, match.index)
|
|
639
661
|
});
|
|
640
|
-
const expr = this
|
|
662
|
+
const expr = this.#parseBindingExpression(match[1], scope, loc);
|
|
641
663
|
segments.push({
|
|
642
664
|
kind: "binding",
|
|
643
665
|
code: printExpr(expr, scope)
|
|
@@ -645,14 +667,14 @@ var ViewCompiler = class {
|
|
|
645
667
|
last = match.index + match[0].length;
|
|
646
668
|
}
|
|
647
669
|
const rest = input.slice(last);
|
|
648
|
-
if (rest.includes("{{")) this.fail("
|
|
670
|
+
if (rest.includes("{{")) this.fail("A binding is missing its closing `}}`", loc);
|
|
649
671
|
if (rest) segments.push({
|
|
650
672
|
kind: "static",
|
|
651
673
|
value: rest
|
|
652
674
|
});
|
|
653
675
|
return segments;
|
|
654
676
|
}
|
|
655
|
-
parseBindingExpression(source, scope, loc) {
|
|
677
|
+
#parseBindingExpression(source, scope, loc) {
|
|
656
678
|
let expr;
|
|
657
679
|
try {
|
|
658
680
|
expr = parseExpression(source);
|
|
@@ -665,7 +687,7 @@ var ViewCompiler = class {
|
|
|
665
687
|
const undeclared = stateReads(expr, scope).find((name) => !stateKeys.includes(name));
|
|
666
688
|
if (undeclared) {
|
|
667
689
|
const declared = stateKeys.join(", ") || "none";
|
|
668
|
-
this.fail(`\`${undeclared}\`
|
|
690
|
+
this.fail(`\`${undeclared}\` isn't in state.ts. It has: ${declared}`, loc);
|
|
669
691
|
}
|
|
670
692
|
}
|
|
671
693
|
return expr;
|
|
@@ -693,6 +715,10 @@ function singleBindingCode(segments) {
|
|
|
693
715
|
if (segments.length === 1 && first?.kind === "binding") return first.code;
|
|
694
716
|
return null;
|
|
695
717
|
}
|
|
718
|
+
function dataKey(attribute) {
|
|
719
|
+
const [first = "", ...rest] = attribute.slice(5).split("-");
|
|
720
|
+
return first + rest.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
721
|
+
}
|
|
696
722
|
function printSegment(segment) {
|
|
697
723
|
return segment.kind === "static" ? JSON.stringify(segment.value) : `text(${segment.code})`;
|
|
698
724
|
}
|
|
@@ -703,23 +729,20 @@ function canHaveChildren(parent, child) {
|
|
|
703
729
|
case "elements": return true;
|
|
704
730
|
}
|
|
705
731
|
}
|
|
732
|
+
function isNumberText(value) {
|
|
733
|
+
return value.trim() !== "" && Number.isFinite(Number(value));
|
|
734
|
+
}
|
|
706
735
|
//#endregion
|
|
707
736
|
//#region src/view/compile.ts
|
|
708
|
-
/**
|
|
709
|
-
* Compiles `.dml` markup into a renderer-agnostic ES module:
|
|
710
|
-
* `export default function render(d, { h, on, text, each }) { return [...] }`.
|
|
711
|
-
*
|
|
712
|
-
* The 'h' will be a function from the renderer to create the virtual element for a component.
|
|
713
|
-
*/
|
|
714
737
|
function compileView(source, context) {
|
|
715
738
|
const nodes = parseDmlToIR(source, {
|
|
716
739
|
locations: true,
|
|
717
740
|
file: context.file,
|
|
718
|
-
whitespace: "
|
|
741
|
+
whitespace: "inline"
|
|
719
742
|
});
|
|
720
743
|
return [
|
|
721
744
|
"export default function render(d, { h, on, text, each }) {",
|
|
722
|
-
` return
|
|
745
|
+
` return h(${new ViewCompiler(context).compileRoot(nodes).map((argument) => `\n ${argument},`).join("")}\n );`,
|
|
723
746
|
"}",
|
|
724
747
|
""
|
|
725
748
|
].join("\n");
|
|
@@ -734,7 +757,7 @@ async function writePageTypes(project) {
|
|
|
734
757
|
}
|
|
735
758
|
async function writeEnvTypes(project, env) {
|
|
736
759
|
const members = Object.keys(env).map((name) => `readonly ${name}: string;`);
|
|
737
|
-
return writeIfChanged(join(project.root, ENV_TYPES_FILE), globalInterface("the
|
|
760
|
+
return writeIfChanged(join(project.root, ENV_TYPES_FILE), globalInterface("the variables in .env files", "DotPublicEnv", members));
|
|
738
761
|
}
|
|
739
762
|
function globalInterface(source, name, members) {
|
|
740
763
|
return [
|
|
@@ -758,30 +781,29 @@ async function writeIfChanged(file, content) {
|
|
|
758
781
|
}
|
|
759
782
|
//#endregion
|
|
760
783
|
//#region src/project/env.ts
|
|
761
|
-
const
|
|
762
|
-
const PUBLIC_ENV_NAME = /^DOT_PUBLIC_[A-Z0-9_]+$/;
|
|
784
|
+
const PUBLIC_ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
763
785
|
async function loadPublicEnv(root, mode, shellEnv = process.env) {
|
|
764
786
|
const env = {};
|
|
765
787
|
for (const fileName of [".env", `.env.${mode}`]) {
|
|
766
788
|
const file = join(root, fileName);
|
|
767
|
-
if (existsSync(file))
|
|
789
|
+
if (existsSync(file)) addVariables(env, parseEnv(await readFile(file, "utf8")), file);
|
|
768
790
|
}
|
|
769
|
-
addPublicVariables(env, shellEnv, "the shell environment");
|
|
770
791
|
const sorted = {};
|
|
771
|
-
for (const name of Object.keys(env).sort()) sorted[name] = env[name];
|
|
792
|
+
for (const name of Object.keys(env).sort()) sorted[name] = shellEnv[name] ?? env[name];
|
|
772
793
|
return sorted;
|
|
773
794
|
}
|
|
774
|
-
function
|
|
795
|
+
function addVariables(env, variables, file) {
|
|
775
796
|
for (const [name, value] of Object.entries(variables)) {
|
|
776
|
-
if (
|
|
777
|
-
if (!PUBLIC_ENV_NAME.test(name)) throw new CompileError(`"${name}" must
|
|
797
|
+
if (value === void 0) continue;
|
|
798
|
+
if (!PUBLIC_ENV_NAME.test(name)) throw new CompileError(`"${name}" must be capital letters, digits and underscores, starting with a letter`, file);
|
|
778
799
|
env[name] = value;
|
|
779
800
|
}
|
|
780
801
|
}
|
|
781
802
|
//#endregion
|
|
782
803
|
//#region src/build/build.ts
|
|
783
804
|
const MANIFEST_FILE = "manifest.json";
|
|
784
|
-
const
|
|
805
|
+
const STYLES_FILE = "dot.css";
|
|
806
|
+
const ARCHIVE_DATE = new Date(1980, 0, 1);
|
|
785
807
|
/** Compiles and bundles the dot into `outDir` for hosts, and refreshes the types in `.dot/`. */
|
|
786
808
|
async function build(options) {
|
|
787
809
|
const project = await loadProject(options.root);
|
|
@@ -794,42 +816,60 @@ async function build(options) {
|
|
|
794
816
|
const modules = await compileModules(project, env, componentRegistry([...Object.values(builtinComponents), ...options.components ?? []]), mode, warnings);
|
|
795
817
|
const outDir = resolve(project.root, options.outDir ?? "dist");
|
|
796
818
|
const uiDir = join(outDir, "ui");
|
|
797
|
-
const bundleLayer = (entry, layerDir, format, external) => {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
});
|
|
810
|
-
};
|
|
819
|
+
const bundleLayer = (entry, layerDir, format, external) => bundle({
|
|
820
|
+
root: project.root,
|
|
821
|
+
modules,
|
|
822
|
+
entry,
|
|
823
|
+
outDir: layerDir,
|
|
824
|
+
fileName: "dot.js",
|
|
825
|
+
format,
|
|
826
|
+
mode,
|
|
827
|
+
minify: mode === "production",
|
|
828
|
+
conditions: options.conditions ?? [],
|
|
829
|
+
external
|
|
830
|
+
});
|
|
811
831
|
const files = [
|
|
812
832
|
...await bundleLayer(ENTRY.ui, uiDir, "esm", Object.keys(REACT_HOST_MODULES)),
|
|
813
833
|
...await bundleLayer(ENTRY.logic, join(outDir, "logic"), "iife", []),
|
|
834
|
+
await copyRendererStyles(runtime, uiDir),
|
|
814
835
|
await writeIndexHtml(project, uiDir, runtime.react.version),
|
|
815
836
|
...await copyStatics(project, uiDir),
|
|
816
837
|
await writeManifest(project, outDir, runtime)
|
|
817
838
|
];
|
|
818
|
-
|
|
819
|
-
if (icon) files.push(icon);
|
|
839
|
+
if (mode === "production") files.push(await writeArchive(project, outDir, files));
|
|
820
840
|
return {
|
|
821
841
|
outDir,
|
|
822
842
|
files,
|
|
823
843
|
warnings
|
|
824
844
|
};
|
|
825
845
|
}
|
|
846
|
+
async function copyRendererStyles(runtime, uiDir) {
|
|
847
|
+
const file = join(uiDir, STYLES_FILE);
|
|
848
|
+
await mkdir(uiDir, { recursive: true });
|
|
849
|
+
await copyFile(runtime.stylesheet, file);
|
|
850
|
+
return file;
|
|
851
|
+
}
|
|
852
|
+
async function writeArchive(project, outDir, files) {
|
|
853
|
+
const entries = {};
|
|
854
|
+
for (const file of files) if (!file.endsWith(".map")) {
|
|
855
|
+
const name = relative(outDir, file).split(sep).join("/");
|
|
856
|
+
entries[name] = [await readFile(file), { mtime: ARCHIVE_DATE }];
|
|
857
|
+
}
|
|
858
|
+
const { package: packageName, version } = project.dotJson;
|
|
859
|
+
for (const name of await readdir(outDir)) if (name.startsWith(`${packageName}-`) && name.endsWith(".zip")) await rm(join(outDir, name));
|
|
860
|
+
const file = join(outDir, `${packageName}-${version}.zip`);
|
|
861
|
+
await writeFile(file, zipSync(entries, { level: 9 }));
|
|
862
|
+
return file;
|
|
863
|
+
}
|
|
826
864
|
async function compileModules(project, env, components, mode, warnings) {
|
|
827
865
|
const modules = /* @__PURE__ */ new Map();
|
|
866
|
+
const usedComponents = /* @__PURE__ */ new Set();
|
|
828
867
|
for (const page of project.pages) {
|
|
829
|
-
const view = await compilePage(page, components, project.statics, warnings);
|
|
868
|
+
const view = await compilePage(page, components, project.statics, warnings, usedComponents);
|
|
830
869
|
modules.set(viewModuleId(page.id), view);
|
|
831
870
|
}
|
|
832
|
-
|
|
871
|
+
const usedBuiltins = [...usedComponents].filter((name) => name in builtinComponents).sort();
|
|
872
|
+
modules.set(ENTRY.ui, uiEntry(project, mode === "production" ? usedBuiltins : void 0));
|
|
833
873
|
modules.set(ENTRY.logic, logicEntry(project, { strict: mode === "development" }));
|
|
834
874
|
modules.set(ENV_MODULE, envModule(env));
|
|
835
875
|
return modules;
|
|
@@ -856,31 +896,23 @@ async function writeManifest(project, outDir, runtime) {
|
|
|
856
896
|
await writeFile(file, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
857
897
|
return file;
|
|
858
898
|
}
|
|
859
|
-
async function copyIcon(project, outDir) {
|
|
860
|
-
if (project.dotJson.icon === void 0) return null;
|
|
861
|
-
const file = join(outDir, ICON_FILE);
|
|
862
|
-
await copyFile(join(project.root, project.dotJson.icon), file);
|
|
863
|
-
return file;
|
|
864
|
-
}
|
|
865
899
|
function dotManifest(project, runtime) {
|
|
866
|
-
const { id,
|
|
900
|
+
const { id, package: packageName, name, version, permissions, network } = project.dotJson;
|
|
867
901
|
const pages = project.pages.map((page) => page.id);
|
|
868
902
|
return {
|
|
869
903
|
id,
|
|
904
|
+
package: packageName,
|
|
870
905
|
name,
|
|
871
906
|
version,
|
|
872
907
|
runtime: runtime.version,
|
|
873
908
|
react: runtime.react.version,
|
|
874
|
-
description,
|
|
875
|
-
categories,
|
|
876
|
-
icon: project.dotJson.icon === void 0 ? void 0 : ICON_FILE,
|
|
877
909
|
permissions,
|
|
878
|
-
|
|
910
|
+
network,
|
|
879
911
|
entry: pages[0],
|
|
880
912
|
pages
|
|
881
913
|
};
|
|
882
914
|
}
|
|
883
|
-
async function compilePage(page, components, statics, warnings) {
|
|
915
|
+
async function compilePage(page, components, statics, warnings, usedComponents) {
|
|
884
916
|
const [view, state, controller] = await Promise.all([
|
|
885
917
|
readFile(page.view, "utf8"),
|
|
886
918
|
readFile(page.state, "utf8"),
|
|
@@ -890,7 +922,8 @@ async function compilePage(page, components, statics, warnings) {
|
|
|
890
922
|
file: page.view,
|
|
891
923
|
components,
|
|
892
924
|
statics,
|
|
893
|
-
onWarning: (warning) => warnings.push(warning)
|
|
925
|
+
onWarning: (warning) => warnings.push(warning),
|
|
926
|
+
onComponent: (name) => usedComponents.add(name)
|
|
894
927
|
};
|
|
895
928
|
const { keys } = parseStateModule(state, page.state);
|
|
896
929
|
if (keys) context.stateKeys = keys;
|
|
@@ -906,8 +939,12 @@ function indexHtml(title, reactVersion) {
|
|
|
906
939
|
<html lang="en">
|
|
907
940
|
<head>
|
|
908
941
|
<meta charset="UTF-8" />
|
|
909
|
-
<meta
|
|
942
|
+
<meta
|
|
943
|
+
name="viewport"
|
|
944
|
+
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
|
945
|
+
/>
|
|
910
946
|
<title>${escaped}</title>
|
|
947
|
+
<link rel="stylesheet" href="./${STYLES_FILE}" />
|
|
911
948
|
<style>
|
|
912
949
|
body {
|
|
913
950
|
margin: 0;
|
|
@@ -923,6 +960,6 @@ function indexHtml(title, reactVersion) {
|
|
|
923
960
|
`;
|
|
924
961
|
}
|
|
925
962
|
//#endregion
|
|
926
|
-
export { CompileError, CompileWarning, build, compileView, installedRuntime, loadProject };
|
|
963
|
+
export { CONTROL_FLOW_TAGS, CompileError, CompileWarning, DATA_PREFIX, DOT_FILE, DOT_JSON_FILE, PAGES_DIR, ROOT_TAG, STATICS_DIR, build, canHaveChildren, compileView, dataKey, installedRuntime, isDotPackage, listStatics, loadProject };
|
|
927
964
|
|
|
928
965
|
//# sourceMappingURL=index.js.map
|