@monimesl/dot-compiler 0.1.0-alpha.3 → 0.1.0-alpha.31
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 +28 -30
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +311 -299
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -2,9 +2,11 @@ import { createRequire } from "node:module";
|
|
|
2
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
|
-
import { ScriptParseError, parseDotModule, parseExpression, parseLogicModule, parseStateModule, unboundReferences } from "@monimesl/dot-script-parser";
|
|
6
|
-
import { PERMISSIONS, REACT_HOST_MODULES, SourceError, builtinComponents, builtinRegistry, componentRegistry, eventName, isNetworkHost, isPermission, isSemanticVersion, reactHostUrl } from "@monimesl/dot-core";
|
|
5
|
+
import { ScriptParseError, globalMemberUses, parseDotModule, parseExpression, parseLogicModule, parseStateModule, unboundReferences } from "@monimesl/dot-script-parser";
|
|
6
|
+
import { DEFAULT_DASH_API_VERSION, PERMISSIONS, REACT_HOST_MODULES, SourceError, builtinComponents, builtinRegistry, componentRegistry, eventName, isDashApiVersion, 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,118 +18,84 @@ var CompileWarning = class extends SourceError {
|
|
|
16
18
|
name = "CompileWarning";
|
|
17
19
|
};
|
|
18
20
|
//#endregion
|
|
19
|
-
//#region src/
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
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(", ");
|
|
24
|
+
}
|
|
25
|
+
function relativeSlashPath(from, to) {
|
|
26
|
+
return relative(from, to).split(sep).join("/");
|
|
61
27
|
}
|
|
62
28
|
//#endregion
|
|
63
29
|
//#region src/project/dot-json.ts
|
|
64
30
|
const DOT_JSON_FILE = "dot.json";
|
|
65
31
|
const DOT_JSON_FIELDS = [
|
|
66
|
-
"$schema",
|
|
67
32
|
"id",
|
|
33
|
+
"$schema",
|
|
34
|
+
"package",
|
|
68
35
|
"name",
|
|
69
36
|
"version",
|
|
70
|
-
"description",
|
|
71
|
-
"categories",
|
|
72
|
-
"icon",
|
|
73
37
|
"permissions",
|
|
74
38
|
"network",
|
|
75
39
|
"dash"
|
|
76
40
|
];
|
|
77
41
|
const REQUIRED_DOT_JSON_FIELDS = [
|
|
78
|
-
"
|
|
42
|
+
"package",
|
|
79
43
|
"name",
|
|
80
44
|
"version"
|
|
81
45
|
];
|
|
82
|
-
const DASH_FIELDS = ["minVersion"];
|
|
83
46
|
const NETWORK_FIELDS = ["hosts"];
|
|
84
|
-
const
|
|
85
|
-
const
|
|
47
|
+
const DASH_FIELDS = ["minApiVersion"];
|
|
48
|
+
const LISTING_FIELDS = [
|
|
49
|
+
"description",
|
|
50
|
+
"categories",
|
|
51
|
+
"icon"
|
|
52
|
+
];
|
|
53
|
+
const PACKAGE_WORD = "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?";
|
|
54
|
+
const DOT_PACKAGE_PATTERN = new RegExp(`^(?=[a-z])${PACKAGE_WORD}(?:\\.${PACKAGE_WORD})+$`);
|
|
55
|
+
function isDotPackage(name) {
|
|
56
|
+
return name.length <= 128 && DOT_PACKAGE_PATTERN.test(name);
|
|
57
|
+
}
|
|
86
58
|
async function readDotJson(file) {
|
|
87
59
|
let dotJson;
|
|
88
60
|
try {
|
|
89
61
|
dotJson = JSON.parse(await readFile(file, "utf8"));
|
|
90
62
|
} catch (error) {
|
|
91
|
-
throw new CompileError(`
|
|
63
|
+
throw new CompileError(`This isn't valid JSON: ${error.message}`, file);
|
|
92
64
|
}
|
|
93
|
-
if (!isObject(dotJson)) throw new CompileError("
|
|
94
|
-
if ("pages" in dotJson) throw new CompileError("
|
|
65
|
+
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);
|
|
66
|
+
if ("pages" in dotJson) throw new CompileError("List pages in dot.ts, not here: defineDot({ pages: [...] })", file);
|
|
67
|
+
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);
|
|
95
68
|
rejectUnknownFields(dotJson, DOT_JSON_FIELDS, file);
|
|
69
|
+
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);
|
|
96
70
|
for (const field of REQUIRED_DOT_JSON_FIELDS) if (dotJson[field] === void 0) throw new CompileError(`"${field}" is required`, file);
|
|
97
|
-
const { $schema, id, name, version,
|
|
71
|
+
const { $schema, package: packageName, id, name, version, permissions, network, dash } = dotJson;
|
|
98
72
|
if ($schema !== void 0 && typeof $schema !== "string") throw new CompileError("\"$schema\" must be a string", file);
|
|
99
|
-
if (typeof
|
|
100
|
-
if (
|
|
101
|
-
if (!isNonEmptyString(
|
|
73
|
+
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);
|
|
74
|
+
if (packageName.length > 128) throw new CompileError(`"package" must be at most 128 characters`, file);
|
|
75
|
+
if (id !== void 0 && !isNonEmptyString(id)) throw new CompileError("\"id\" must be the ID Monime gave the dot, as text", file);
|
|
76
|
+
if (!isNonEmptyString(name)) throw new CompileError("\"name\" must be text, and not empty", file);
|
|
102
77
|
if (!isSemanticVersion(version)) throw new CompileError("\"version\" must be a semantic version, such as \"1.0.0\"", file);
|
|
103
|
-
if (description !== void 0 && !isNonEmptyString(description)) throw new CompileError("\"description\" must be a non-empty string", file);
|
|
104
|
-
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);
|
|
105
78
|
return {
|
|
106
79
|
id,
|
|
80
|
+
package: packageName,
|
|
107
81
|
name,
|
|
108
82
|
version,
|
|
109
|
-
description,
|
|
110
|
-
categories: readCategories(categories, file),
|
|
111
|
-
icon,
|
|
112
83
|
permissions: readPermissions(permissions, file),
|
|
113
84
|
network: readNetworkAccess(network, file),
|
|
114
|
-
dash:
|
|
85
|
+
dash: readDashNeeds(dash, file)
|
|
115
86
|
};
|
|
116
87
|
}
|
|
117
|
-
function
|
|
118
|
-
if (
|
|
119
|
-
if (!
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
listed.push(category);
|
|
125
|
-
}
|
|
126
|
-
return listed;
|
|
88
|
+
function readDashNeeds(dash, file) {
|
|
89
|
+
if (dash === void 0) return;
|
|
90
|
+
if (!isObject(dash)) throw new CompileError("\"dash\" must be an object, such as { \"minApiVersion\": \"1.0\" }", file);
|
|
91
|
+
rejectUnknownFields(dash, DASH_FIELDS, file, "dash");
|
|
92
|
+
const { minApiVersion = DEFAULT_DASH_API_VERSION } = dash;
|
|
93
|
+
if (!isDashApiVersion(minApiVersion)) throw new CompileError("\"dash.minApiVersion\" must be a Dash API version: a major and a minor, such as \"1.0\"", file);
|
|
94
|
+
return { minApiVersion };
|
|
127
95
|
}
|
|
128
96
|
function readPermissions(permissions, file) {
|
|
129
97
|
if (permissions === void 0) return;
|
|
130
|
-
if (!isObject(permissions)) throw new CompileError("\"permissions\" must be an object, such as { \"
|
|
98
|
+
if (!isObject(permissions)) throw new CompileError("\"permissions\" must be an object, such as { \"camera\": \"Scan tickets at the door\" }", file);
|
|
131
99
|
for (const [permission, reason] of Object.entries(permissions)) {
|
|
132
100
|
if (!isPermission(permission)) throw new CompileError(`Unknown permission "${permission}". The permissions are ${quotedList(PERMISSIONS)}`, file);
|
|
133
101
|
if (!isNonEmptyString(reason)) throw new CompileError(`"permissions.${permission}" must say why the dot needs it, such as "Show your name on receipts"`, file);
|
|
@@ -142,20 +110,12 @@ function readNetworkAccess(network, file) {
|
|
|
142
110
|
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);
|
|
143
111
|
const listed = [];
|
|
144
112
|
for (const host of hosts) {
|
|
145
|
-
if (typeof host !== "string" || !isNetworkHost(host)) throw new CompileError(`"${String(host)}" is not a host
|
|
113
|
+
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);
|
|
146
114
|
if (listed.includes(host)) throw new CompileError(`"network.hosts" lists "${host}" more than once`, file);
|
|
147
115
|
listed.push(host);
|
|
148
116
|
}
|
|
149
117
|
return { hosts: listed };
|
|
150
118
|
}
|
|
151
|
-
function readDashRequirements(dash, file) {
|
|
152
|
-
if (dash === void 0) return;
|
|
153
|
-
if (!isObject(dash)) throw new CompileError("\"dash\" must be an object, such as { \"minVersion\": \"2.4.0\" }", file);
|
|
154
|
-
rejectUnknownFields(dash, DASH_FIELDS, file, "dash");
|
|
155
|
-
const { minVersion } = dash;
|
|
156
|
-
if (minVersion !== void 0 && !isSemanticVersion(minVersion)) throw new CompileError("\"dash.minVersion\" must be a semantic version, such as \"2.4.0\"", file);
|
|
157
|
-
return { minVersion };
|
|
158
|
-
}
|
|
159
119
|
function rejectUnknownFields(object, allowed, file, parent) {
|
|
160
120
|
for (const field of Object.keys(object)) {
|
|
161
121
|
if (allowed.includes(field)) continue;
|
|
@@ -163,9 +123,6 @@ function rejectUnknownFields(object, allowed, file, parent) {
|
|
|
163
123
|
throw new CompileError(`Unknown field "${parent}.${field}". The fields allowed in "${parent}" are ${quotedList(allowed)}`, file);
|
|
164
124
|
}
|
|
165
125
|
}
|
|
166
|
-
function quotedList(values) {
|
|
167
|
-
return values.map((value) => `"${value}"`).join(", ");
|
|
168
|
-
}
|
|
169
126
|
function isObject(value) {
|
|
170
127
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
171
128
|
}
|
|
@@ -205,16 +162,16 @@ async function listStatics(root) {
|
|
|
205
162
|
if (!entry.isFile() || entry.name.startsWith(".")) continue;
|
|
206
163
|
const file = join(entry.parentPath, entry.name);
|
|
207
164
|
await checkImage(file, extname(entry.name).toLowerCase());
|
|
208
|
-
statics.push(`${STATICS_DIR}/${
|
|
165
|
+
statics.push(`${STATICS_DIR}/${relativeSlashPath(staticsDir, file)}`);
|
|
209
166
|
}
|
|
210
167
|
return statics.sort();
|
|
211
168
|
}
|
|
212
169
|
async function checkImage(file, extension) {
|
|
213
170
|
const isFormat = IMAGE_FORMATS[extension];
|
|
214
|
-
if (!isFormat) throw new CompileError(
|
|
171
|
+
if (!isFormat) throw new CompileError(`${STATICS_DIR}/ can only hold images: ${STATIC_ASSET_EXTENSIONS.join(", ")}`, file);
|
|
215
172
|
const { size } = await stat(file);
|
|
216
|
-
if (size > 1048576) throw new CompileError(`The image is ${(size / MAX_STATIC_ASSET_BYTES).toFixed(1)} MB
|
|
217
|
-
if (!isFormat(await readFile(file))) throw new CompileError(`The file
|
|
173
|
+
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);
|
|
174
|
+
if (!isFormat(await readFile(file))) throw new CompileError(`The file isn't really a ${extension.slice(1)} image`, file);
|
|
218
175
|
}
|
|
219
176
|
function isJpeg(header) {
|
|
220
177
|
return header[0] === 255 && header[1] === 216 && header[2] === 255;
|
|
@@ -231,52 +188,53 @@ const PAGE_FILES = {
|
|
|
231
188
|
async function loadProject(root) {
|
|
232
189
|
root = resolve(root);
|
|
233
190
|
const dotJsonFile = join(root, DOT_JSON_FILE);
|
|
234
|
-
if (!existsSync(dotJsonFile)) throw new CompileError(`
|
|
191
|
+
if (!existsSync(dotJsonFile)) throw new CompileError(`There is no ${DOT_JSON_FILE} in this folder`, root);
|
|
235
192
|
const dotJson = await readDotJson(dotJsonFile);
|
|
236
|
-
if (dotJson.icon !== void 0 && !existsSync(join(root, dotJson.icon))) throw new CompileError(`The icon "${dotJson.icon}" is missing`, dotJsonFile);
|
|
237
193
|
const dotFile = join(root, DOT_FILE);
|
|
238
|
-
if (!existsSync(dotFile)) throw new CompileError(`
|
|
194
|
+
if (!existsSync(dotFile)) throw new CompileError(`There is no ${DOT_FILE}. Add one: export default defineDot({ pages: [...] })`, root);
|
|
239
195
|
const { pages } = parseDotModule(await readFile(dotFile, "utf8"), dotFile);
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
for (const page of
|
|
243
|
-
const
|
|
244
|
-
if (
|
|
196
|
+
const pageSources = await Promise.all(pages.map((path) => loadPage(root, path, dotFile)));
|
|
197
|
+
const pagesByLowercaseId = /* @__PURE__ */ new Map();
|
|
198
|
+
for (const page of pageSources) {
|
|
199
|
+
const earlierPage = pagesByLowercaseId.get(page.id.toLowerCase());
|
|
200
|
+
if (earlierPage) {
|
|
245
201
|
let file = dotFile;
|
|
246
202
|
if (page.id !== page.path) file = page.controller;
|
|
247
|
-
else if (
|
|
248
|
-
throw new CompileError(`Page id "${page.id}" (${page.path}) is already used by ${
|
|
203
|
+
else if (earlierPage.id !== earlierPage.path) file = earlierPage.controller;
|
|
204
|
+
throw new CompileError(`Page id "${page.id}" (${page.path}) is already used by ${earlierPage.path}`, file);
|
|
249
205
|
}
|
|
250
|
-
|
|
206
|
+
pagesByLowercaseId.set(page.id.toLowerCase(), page);
|
|
251
207
|
}
|
|
252
208
|
return {
|
|
253
209
|
root,
|
|
254
210
|
dotJson,
|
|
255
211
|
dot: dotFile,
|
|
256
|
-
pages:
|
|
212
|
+
pages: pageSources,
|
|
257
213
|
statics: await listStatics(root)
|
|
258
214
|
};
|
|
259
215
|
}
|
|
260
216
|
async function loadPage(root, path, dotFile) {
|
|
261
|
-
const
|
|
217
|
+
const pageDir = join(root, PAGES_DIR, path);
|
|
262
218
|
const files = {
|
|
263
|
-
view: join(
|
|
264
|
-
state: join(
|
|
265
|
-
controller: join(
|
|
219
|
+
view: join(pageDir, PAGE_FILES.view),
|
|
220
|
+
state: join(pageDir, PAGE_FILES.state),
|
|
221
|
+
controller: join(pageDir, PAGE_FILES.controller)
|
|
266
222
|
};
|
|
267
223
|
const missing = Object.values(files).filter((file) => !existsSync(file));
|
|
268
224
|
if (missing.length > 0) throw new CompileError(`Page "${path}" is missing ${missing.join(", ")}`, dotFile);
|
|
269
|
-
const
|
|
225
|
+
const controller = await readFile(files.controller, "utf8");
|
|
226
|
+
const { id, share } = parseLogicModule(controller, files.controller);
|
|
270
227
|
return {
|
|
271
228
|
path,
|
|
272
229
|
id: id ?? path,
|
|
230
|
+
share,
|
|
273
231
|
...files
|
|
274
232
|
};
|
|
275
233
|
}
|
|
276
234
|
//#endregion
|
|
277
235
|
//#region src/entries.ts
|
|
278
236
|
const RUNTIME_PACKAGE = "@monimesl/dot-runtime";
|
|
279
|
-
const
|
|
237
|
+
const RUNTIME_MODULES = {
|
|
280
238
|
ui: `${RUNTIME_PACKAGE}/ui`,
|
|
281
239
|
logic: `${RUNTIME_PACKAGE}/logic`,
|
|
282
240
|
renderer: `${RUNTIME_PACKAGE}/react`
|
|
@@ -290,37 +248,40 @@ const viewModuleId = (id) => `dot:view/${id}`;
|
|
|
290
248
|
function literal(value) {
|
|
291
249
|
return JSON.stringify(value);
|
|
292
250
|
}
|
|
293
|
-
function pageTable(project,
|
|
294
|
-
return `{${project.pages.map((page, index) => `\n ${literal(page.id)}: ${
|
|
251
|
+
function pageTable(project, importName) {
|
|
252
|
+
return `{${project.pages.map((page, index) => `\n ${literal(page.id)}: ${importName}${index},`).join("")}\n }`;
|
|
295
253
|
}
|
|
296
|
-
function uiEntry(project) {
|
|
254
|
+
function uiEntry(project, components) {
|
|
255
|
+
const rendererModule = literal(RUNTIME_MODULES.renderer);
|
|
256
|
+
let renderer;
|
|
257
|
+
if (components) {
|
|
258
|
+
const componentList = components.join(", ");
|
|
259
|
+
renderer = [`import { createReactRenderer, ${componentList} } from ${rendererModule};`, `const createRenderer = () => createReactRenderer({ components: { ${componentList} } });`];
|
|
260
|
+
} else renderer = [`import createRenderer from ${rendererModule};`];
|
|
297
261
|
return [
|
|
298
|
-
`import {
|
|
299
|
-
|
|
262
|
+
`import { runWebview } from ${literal(RUNTIME_MODULES.ui)};`,
|
|
263
|
+
...renderer,
|
|
300
264
|
...project.pages.map((page, index) => `import view${index} from ${literal(viewModuleId(page.id))};`),
|
|
301
265
|
"",
|
|
302
|
-
"
|
|
303
|
-
"
|
|
304
|
-
" transport: hostTransport(),",
|
|
305
|
-
" renderer: createRenderer(),",
|
|
266
|
+
"runWebview({",
|
|
267
|
+
" renderer: createRenderer,",
|
|
306
268
|
` views: ${pageTable(project, "view")},`,
|
|
307
269
|
"});",
|
|
308
270
|
""
|
|
309
271
|
].join("\n");
|
|
310
272
|
}
|
|
311
|
-
function logicEntry(project, { strict }) {
|
|
312
|
-
const options = [
|
|
313
|
-
|
|
314
|
-
"
|
|
315
|
-
`
|
|
316
|
-
|
|
317
|
-
if (strict) options.push(" strict: true,");
|
|
273
|
+
function logicEntry(project, { strict, dashMinApiVersion }) {
|
|
274
|
+
const options = [" dot,", ` pages: ${pageTable(project, "page")},`];
|
|
275
|
+
if (strict) {
|
|
276
|
+
options.push(" strict: true,");
|
|
277
|
+
options.push(` dashMinApiVersion: ${literal(dashMinApiVersion)},`);
|
|
278
|
+
}
|
|
318
279
|
return [
|
|
319
|
-
`import {
|
|
280
|
+
`import { runService } from ${literal(RUNTIME_MODULES.logic)};`,
|
|
320
281
|
`import dot from ${literal(project.dot)};`,
|
|
321
282
|
...project.pages.map((page, index) => `import page${index} from ${literal(page.controller)};`),
|
|
322
283
|
"",
|
|
323
|
-
"
|
|
284
|
+
"runService({",
|
|
324
285
|
...options,
|
|
325
286
|
"});",
|
|
326
287
|
""
|
|
@@ -332,7 +293,7 @@ function envModule(env) {
|
|
|
332
293
|
//#endregion
|
|
333
294
|
//#region src/globals.ts
|
|
334
295
|
const DOT_GLOBALS = {
|
|
335
|
-
|
|
296
|
+
[RUNTIME_PACKAGE]: [
|
|
336
297
|
"definePage",
|
|
337
298
|
"defineDot",
|
|
338
299
|
"createEvent",
|
|
@@ -340,17 +301,18 @@ const DOT_GLOBALS = {
|
|
|
340
301
|
],
|
|
341
302
|
[ENV_MODULE]: ["env"]
|
|
342
303
|
};
|
|
343
|
-
const
|
|
304
|
+
const SCRIPT_FILE = /\.[cm]?[jt]sx?$/;
|
|
305
|
+
const ANSWERS_AT_ONCE = /* @__PURE__ */ new Set(["utils", "supports"]);
|
|
344
306
|
function dotGlobals(root) {
|
|
345
307
|
const realRoot = realpathSync(root);
|
|
346
|
-
const isDotModule = (id) => id.startsWith(realRoot + sep) && !id.includes(`${sep}node_modules${sep}`) &&
|
|
308
|
+
const isDotModule = (id) => id.startsWith(realRoot + sep) && !id.includes(`${sep}node_modules${sep}`) && SCRIPT_FILE.test(id);
|
|
347
309
|
const isPageModule = (id) => id.startsWith(join(realRoot, PAGES_DIR) + sep);
|
|
348
310
|
return {
|
|
349
311
|
name: "dot:globals",
|
|
350
312
|
transform(code, id) {
|
|
351
313
|
if (!isDotModule(id)) return null;
|
|
352
314
|
const used = new Set(unboundReferences(code, Object.values(DOT_GLOBALS).flat(), id));
|
|
353
|
-
if (isPageModule(id) && used.has("dash")) this.error(`${relative(realRoot, id)} uses the global \`dash\`. In a page,
|
|
315
|
+
if (isPageModule(id) && used.has("dash") && makesDashCalls(code, id)) 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. Only dash.utils and dash.supports, which answer at once, work anywhere.`);
|
|
354
316
|
const imports = [];
|
|
355
317
|
for (const [source, names] of Object.entries(DOT_GLOBALS)) {
|
|
356
318
|
const imported = names.filter((name) => used.has(name));
|
|
@@ -364,20 +326,25 @@ function dotGlobals(root) {
|
|
|
364
326
|
}
|
|
365
327
|
};
|
|
366
328
|
}
|
|
329
|
+
function makesDashCalls(code, id) {
|
|
330
|
+
for (const member of globalMemberUses(code, "dash", id)) if (!ANSWERS_AT_ONCE.has(member)) return true;
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
367
333
|
//#endregion
|
|
368
334
|
//#region src/build/bundle.ts
|
|
369
|
-
/** Bundles one entry with rolldown, replacing `outDir`. Returns the written files. */
|
|
370
335
|
async function bundle(options) {
|
|
371
336
|
await rm(options.outDir, {
|
|
372
337
|
recursive: true,
|
|
373
338
|
force: true
|
|
374
339
|
});
|
|
340
|
+
const plugins = [];
|
|
341
|
+
if (options.external.length > 0) plugins.push(esmExternalRequirePlugin({ external: [...options.external] }));
|
|
342
|
+
plugins.push(generatedModules(options.root, options.modules), dotGlobals(options.root));
|
|
375
343
|
const bundler = await rolldown({
|
|
376
344
|
input: options.entry,
|
|
377
345
|
cwd: options.root,
|
|
378
|
-
external: [...options.external],
|
|
379
346
|
platform: "browser",
|
|
380
|
-
plugins
|
|
347
|
+
plugins,
|
|
381
348
|
resolve: { conditionNames: [
|
|
382
349
|
...options.conditions,
|
|
383
350
|
"browser",
|
|
@@ -385,22 +352,25 @@ async function bundle(options) {
|
|
|
385
352
|
"require",
|
|
386
353
|
"default"
|
|
387
354
|
] },
|
|
388
|
-
transform: { define: { "process.env.NODE_ENV": JSON.stringify(options.mode) } }
|
|
355
|
+
transform: { define: { "process.env.NODE_ENV": JSON.stringify(options.mode) } },
|
|
356
|
+
onLog(level, log, defaultHandler) {
|
|
357
|
+
if (log.code !== "MODULE_LEVEL_DIRECTIVE") defaultHandler(level, log);
|
|
358
|
+
}
|
|
389
359
|
});
|
|
390
360
|
try {
|
|
391
361
|
const { output } = await bundler.write({
|
|
392
362
|
dir: options.outDir,
|
|
393
363
|
format: options.format,
|
|
394
364
|
entryFileNames: options.fileName,
|
|
395
|
-
sourcemap:
|
|
396
|
-
|
|
365
|
+
sourcemap: options.mode === "development",
|
|
366
|
+
sourcemapExcludeSources: options.mode === "production",
|
|
367
|
+
minify: options.mode === "production"
|
|
397
368
|
});
|
|
398
369
|
return output.map((chunk) => join(options.outDir, chunk.fileName));
|
|
399
370
|
} finally {
|
|
400
371
|
await bundler.close();
|
|
401
372
|
}
|
|
402
373
|
}
|
|
403
|
-
/** Serves generated modules and resolves their imports as if they lived in the project root. */
|
|
404
374
|
function generatedModules(root, modules) {
|
|
405
375
|
const importer = join(root, DOT_JSON_FILE);
|
|
406
376
|
return {
|
|
@@ -409,11 +379,12 @@ function generatedModules(root, modules) {
|
|
|
409
379
|
if (modules.has(source)) return `\0${source}`;
|
|
410
380
|
if (!from?.startsWith("\0dot:")) return null;
|
|
411
381
|
const resolved = await this.resolve(source, importer, { skipSelf: true });
|
|
412
|
-
if (!resolved) this.error(`
|
|
382
|
+
if (!resolved) this.error(`Can't find "${source}" from ${root}. Is it installed?`);
|
|
413
383
|
return resolved;
|
|
414
384
|
},
|
|
415
385
|
load(id) {
|
|
416
|
-
|
|
386
|
+
if (!id.startsWith("\0")) return null;
|
|
387
|
+
return modules.get(id.slice(1)) ?? null;
|
|
417
388
|
}
|
|
418
389
|
};
|
|
419
390
|
}
|
|
@@ -429,7 +400,8 @@ function installedRuntime(root) {
|
|
|
429
400
|
react: {
|
|
430
401
|
version: react.version,
|
|
431
402
|
dir: join(renderer.dir, "dist", "react-host")
|
|
432
|
-
}
|
|
403
|
+
},
|
|
404
|
+
stylesheet: join(renderer.dir, "dist", "styles.css")
|
|
433
405
|
};
|
|
434
406
|
}
|
|
435
407
|
function requirePackage(fromDir, name, installedIn) {
|
|
@@ -438,7 +410,7 @@ function requirePackage(fromDir, name, installedIn) {
|
|
|
438
410
|
try {
|
|
439
411
|
packageFile = require.resolve(`${name}/package.json`);
|
|
440
412
|
} catch {
|
|
441
|
-
throw new CompileError(`${name}
|
|
413
|
+
throw new CompileError(`${name} isn't installed in ${installedIn}. Install the project's dependencies, then try again`, fromDir);
|
|
442
414
|
}
|
|
443
415
|
const { version } = JSON.parse(readFileSync(packageFile, "utf8"));
|
|
444
416
|
return {
|
|
@@ -453,17 +425,17 @@ const EMPTY_SCOPE = /* @__PURE__ */ new Map();
|
|
|
453
425
|
function isIdentifier(name) {
|
|
454
426
|
return IDENTIFIER.test(name);
|
|
455
427
|
}
|
|
456
|
-
/**
|
|
457
|
-
* Prints a binding as JS. Loop variables read their local; everything else reads page data from
|
|
458
|
-
* `d`. Member access never throws on nullish values.
|
|
459
|
-
*/
|
|
460
428
|
function printExpr(expr, scope) {
|
|
461
429
|
const print = (node) => printExpr(node, scope);
|
|
462
430
|
switch (expr.type) {
|
|
463
431
|
case "Literal": return JSON.stringify(expr.value);
|
|
464
432
|
case "Identifier": return scope.get(expr.name) ?? `d.${expr.name}`;
|
|
465
|
-
case "Member":
|
|
466
|
-
|
|
433
|
+
case "Member":
|
|
434
|
+
if (expr.property.type === "Literal" && typeof expr.property.value === "string") return `${print(expr.object)}?.${expr.property.value}`;
|
|
435
|
+
return `${print(expr.object)}?.[${print(expr.property)}]`;
|
|
436
|
+
case "Unary":
|
|
437
|
+
if (expr.operator === "typeof") return `(typeof ${print(expr.argument)})`;
|
|
438
|
+
return `(${expr.operator}${print(expr.argument)})`;
|
|
467
439
|
case "Binary": return `(${print(expr.left)} ${expr.operator} ${print(expr.right)})`;
|
|
468
440
|
case "Conditional": return `(${print(expr.test)} ? ${print(expr.consequent)} : ${print(expr.alternate)})`;
|
|
469
441
|
}
|
|
@@ -508,11 +480,6 @@ const CONTROL_FLOW_TAGS = {
|
|
|
508
480
|
required: ["each"]
|
|
509
481
|
}
|
|
510
482
|
};
|
|
511
|
-
const CONTROL_FLOW = new Set(Object.keys(CONTROL_FLOW_TAGS));
|
|
512
|
-
/**
|
|
513
|
-
* `<If condition>`, then any `<ElseIf condition>`, then an optional `<Else>`, as consecutive
|
|
514
|
-
* siblings compile to a spread conditional: `...(a ? [...]: b ? [...]: [...])`.
|
|
515
|
-
*/
|
|
516
483
|
function compileConditional(compiler, branches, parent, scope) {
|
|
517
484
|
let code = "[]";
|
|
518
485
|
for (const branch of [...branches].reverse()) {
|
|
@@ -523,12 +490,6 @@ function compileConditional(compiler, branches, parent, scope) {
|
|
|
523
490
|
}
|
|
524
491
|
return `...(${code})`;
|
|
525
492
|
}
|
|
526
|
-
/**
|
|
527
|
-
* `<For each="{{ list }}" item="item" index="index" key="id">`. `item` and `index` default to those
|
|
528
|
-
* names. `key` is a property of each item, `*this` for the item itself, or `*index` to match items
|
|
529
|
-
* by position; it is optional, but a warning is logged when missing. Compiles to
|
|
530
|
-
* `each(list, (item, index) => [...], key?)`.
|
|
531
|
-
*/
|
|
532
493
|
function compileLoop(compiler, node, parent, scope) {
|
|
533
494
|
compiler.checkControlFlowElementAttributes(node);
|
|
534
495
|
const list = compiler.compileSingleBinding(node, "each", scope);
|
|
@@ -538,16 +499,18 @@ function compileLoop(compiler, node, parent, scope) {
|
|
|
538
499
|
};
|
|
539
500
|
checkVariableName("item", item);
|
|
540
501
|
checkVariableName("index", index);
|
|
541
|
-
if (item === index) compiler.fail("<For> `item` and `index`
|
|
542
|
-
const
|
|
543
|
-
const
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
502
|
+
if (item === index) compiler.fail("<For> `item` and `index` need different names", node.loc);
|
|
503
|
+
const itemVariable = "$" + item;
|
|
504
|
+
const indexVariable = "$" + index;
|
|
505
|
+
const loopScope = new Map(scope);
|
|
506
|
+
loopScope.set(item, itemVariable);
|
|
507
|
+
loopScope.set(index, indexVariable);
|
|
508
|
+
const eachArguments = [list, `(${itemVariable}, ${indexVariable}) => [${compiler.compile(node.children, parent, loopScope).join(", ")}]`];
|
|
509
|
+
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);
|
|
510
|
+
else if (key === "*index") {} else if (key === "*this") eachArguments.push(`(${itemVariable}) => ${itemVariable}`);
|
|
511
|
+
else if (isIdentifier(key)) eachArguments.push(`(${itemVariable}) => ${itemVariable}?.${key}`);
|
|
549
512
|
else compiler.fail(`<For> \`key\` must be a property name, *this or *index, got "${key}"`, node.loc);
|
|
550
|
-
return `each(${
|
|
513
|
+
return `each(${eachArguments.join(", ")})`;
|
|
551
514
|
}
|
|
552
515
|
//#endregion
|
|
553
516
|
//#region src/view/compiler.ts
|
|
@@ -560,191 +523,205 @@ const ROOT_DEFAULTS = {
|
|
|
560
523
|
const SEGMENT_BINDING_REGEXP = /{{([\s\S]*?)}}/g;
|
|
561
524
|
const DATA_PREFIX = "data-";
|
|
562
525
|
const DATA_ATTRIBUTE = /^data-[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
526
|
+
const THEME_COLOR_NAME = /^[a-z][a-zA-Z0-9]*$/;
|
|
563
527
|
var ViewCompiler = class {
|
|
564
528
|
#context;
|
|
565
529
|
#components;
|
|
566
530
|
constructor(context) {
|
|
567
531
|
this.#context = context;
|
|
568
532
|
this.#components = context.components ?? builtinRegistry;
|
|
569
|
-
for (const name of
|
|
533
|
+
for (const name of Object.keys(CONTROL_FLOW_TAGS)) if (name in this.#components) this.fail(`<${name}> is reserved for control flow`);
|
|
570
534
|
}
|
|
571
535
|
compile(nodes, parent, scope) {
|
|
572
|
-
const
|
|
573
|
-
for (let
|
|
574
|
-
const node = nodes[
|
|
536
|
+
const compiled = [];
|
|
537
|
+
for (let index = 0; index < nodes.length; index++) {
|
|
538
|
+
const node = nodes[index];
|
|
575
539
|
if (node.type === "Text") {
|
|
576
|
-
|
|
540
|
+
compiled.push(...this.#compileText(node.content, parent, scope, node.loc));
|
|
577
541
|
continue;
|
|
578
542
|
}
|
|
579
543
|
switch (node.tag) {
|
|
580
544
|
case "If": {
|
|
581
|
-
const branches = ifChain(node, nodes.slice(
|
|
582
|
-
|
|
583
|
-
|
|
545
|
+
const branches = ifChain(node, nodes.slice(index + 1));
|
|
546
|
+
compiled.push(compileConditional(this, branches, parent, scope));
|
|
547
|
+
index += branches.length - 1;
|
|
584
548
|
break;
|
|
585
549
|
}
|
|
586
550
|
case "ElseIf":
|
|
587
|
-
case "Else": return this.fail(`<${node.tag}> must
|
|
551
|
+
case "Else": return this.fail(`<${node.tag}> must come right after <If> or <ElseIf>`, node.loc);
|
|
588
552
|
case "For":
|
|
589
|
-
|
|
553
|
+
compiled.push(compileLoop(this, node, parent, scope));
|
|
590
554
|
break;
|
|
591
|
-
default:
|
|
555
|
+
default: compiled.push(`h(${this.#compileElementArgs(node, parent, scope).join(", ")})`);
|
|
592
556
|
}
|
|
593
557
|
}
|
|
594
|
-
return
|
|
558
|
+
return compiled;
|
|
595
559
|
}
|
|
596
|
-
compileText(content, parent, scope, loc) {
|
|
560
|
+
#compileText(content, parent, scope, loc) {
|
|
597
561
|
if (parent?.children !== "text") {
|
|
598
|
-
|
|
599
|
-
this.fail(`'${content}' must be
|
|
562
|
+
if (content.trim() === "") return [];
|
|
563
|
+
this.fail(`'${content}' must be inside ${this.#textWrapperNames()}`, loc);
|
|
600
564
|
}
|
|
601
|
-
return this
|
|
565
|
+
return this.#compileAsSegments(content, scope, loc).map(printSegment);
|
|
602
566
|
}
|
|
603
|
-
|
|
604
|
-
|
|
567
|
+
#textWrapperNames() {
|
|
568
|
+
const textWrappers = Object.values(this.#components).filter((component) => {
|
|
569
|
+
return component.inline === true && component.children === "text";
|
|
570
|
+
});
|
|
571
|
+
const textFirst = [...textWrappers.filter((component) => component.name === "Text"), ...textWrappers.filter((component) => component.name !== "Text")];
|
|
572
|
+
if (textFirst.length === 0) return "an inline component";
|
|
573
|
+
return textFirst.map((component) => `<${component.name}>`).join(" or ");
|
|
605
574
|
}
|
|
606
|
-
/**
|
|
607
|
-
* Compiles a page's markup, which must be a single <View> with everything else inside it, to the
|
|
608
|
-
* arguments of that root's `h` call.
|
|
609
|
-
*/
|
|
610
575
|
compileRoot(nodes) {
|
|
611
576
|
if (!this.#components["View"]) this.fail(`The components must include <${ROOT_TAG}>, which is the root of every page`);
|
|
612
577
|
const [root, outside] = nodes;
|
|
613
|
-
if (root?.type !== "Element" || root.tag !== "View") return this.fail(`A page
|
|
614
|
-
if (outside) {
|
|
615
|
-
const name = outside.type === "Text" ? `'${outside.content}'` : `<${outside.tag}>`;
|
|
616
|
-
this.fail(`${name} must be inside the root <${ROOT_TAG}>`, outside.loc);
|
|
617
|
-
}
|
|
578
|
+
if (root?.type !== "Element" || root.tag !== "View") return this.fail(`A page must have one <${ROOT_TAG}> around everything else`, root?.loc);
|
|
579
|
+
if (outside) this.fail(`${describeNode(outside)} must be inside the page's <${ROOT_TAG}>`, outside.loc);
|
|
618
580
|
return this.#compileElementArgs(root, null, EMPTY_SCOPE, ROOT_DEFAULTS);
|
|
619
581
|
}
|
|
620
582
|
#compileElementArgs(node, parent, scope, defaults = {}) {
|
|
621
583
|
const component = this.#components[node.tag];
|
|
622
|
-
if (!component) return this.fail(`Unknown component <${node.tag}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
584
|
+
if (!component) return this.fail(`Unknown component <${node.tag}>`, node.loc);
|
|
585
|
+
this.#context.onComponent?.(component.name);
|
|
586
|
+
if (parent && !canHaveChildren(parent, component)) this.fail(`<${node.tag}> can't go inside <${parent.name}>`, node.loc);
|
|
587
|
+
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);
|
|
588
|
+
const dataAttributes = [];
|
|
589
|
+
const propAttributes = [];
|
|
590
|
+
for (const [name, value] of Object.entries(node.attributes)) if (name.startsWith("data-")) dataAttributes.push([name, value]);
|
|
591
|
+
else propAttributes.push([name, value]);
|
|
592
|
+
const data = this.#compileData(dataAttributes, scope, node.loc);
|
|
593
|
+
const props = [];
|
|
594
|
+
for (const [name, value] of propAttributes) props.push(this.#compileProp(component, name, value, scope, node.loc));
|
|
595
|
+
for (const [name, handler] of Object.entries(node.events)) props.push(this.#compileEvent(component, name, handler, data, node.loc));
|
|
596
|
+
for (const [name, value] of Object.entries(defaults)) if (!(name in node.attributes)) props.push(`${name}: ${JSON.stringify(value)}`);
|
|
597
|
+
let propsCode = "null";
|
|
598
|
+
if (props.length > 0) propsCode = `{ ${props.join(", ")} }`;
|
|
631
599
|
return [
|
|
632
600
|
JSON.stringify(component.name),
|
|
633
|
-
|
|
601
|
+
propsCode,
|
|
634
602
|
...this.compile(node.children, component, scope)
|
|
635
603
|
];
|
|
636
604
|
}
|
|
637
|
-
compileProp(component, name, value, scope, loc) {
|
|
605
|
+
#compileProp(component, name, value, scope, loc) {
|
|
638
606
|
const type = component.props[name];
|
|
639
|
-
if (!type) return this.fail(`<${component.name}>
|
|
640
|
-
const segments = this
|
|
607
|
+
if (!type) return this.fail(`<${component.name}> has no \`${name}\` attribute`, loc);
|
|
608
|
+
const segments = this.#compileAsSegments(value, scope, loc);
|
|
641
609
|
const bindingCode = singleBindingCode(segments);
|
|
642
610
|
if (bindingCode !== null) return `${name}: ${bindingCode}`;
|
|
643
611
|
if (segments.every((segment) => segment.kind === "static")) {
|
|
644
612
|
this.#checkStaticsReference(value, loc);
|
|
645
|
-
return `${name}: ${this
|
|
613
|
+
return `${name}: ${this.#compileLiteral(type, value, loc)}`;
|
|
646
614
|
}
|
|
647
615
|
if (type !== "string" && type !== "any") {
|
|
648
616
|
const kind = typeof type === "string" ? `a ${type}` : "an enum";
|
|
649
|
-
this.fail(`\`${name}\` is ${kind}
|
|
617
|
+
this.fail(`\`${name}\` is ${kind}, so set it with one {{ }} binding`, loc);
|
|
650
618
|
}
|
|
651
|
-
return `${name}: ${segments
|
|
619
|
+
return `${name}: ${concatenateSegments(segments)}`;
|
|
652
620
|
}
|
|
653
621
|
#compileData(attributes, scope, loc) {
|
|
654
622
|
if (attributes.length === 0) return null;
|
|
655
623
|
return `{ ${attributes.map(([name, value]) => {
|
|
656
|
-
if (!DATA_ATTRIBUTE.test(name)) this.fail(`\`${name}\` must be
|
|
624
|
+
if (!DATA_ATTRIBUTE.test(name)) this.fail(`\`${name}\` must be lowercase words joined by -, such as data-account-id`, loc);
|
|
657
625
|
return `${dataKey(name)}: ${this.#compileDataValue(value, scope, loc)}`;
|
|
658
626
|
}).join(", ")} }`;
|
|
659
627
|
}
|
|
660
628
|
#compileDataValue(value, scope, loc) {
|
|
661
|
-
const segments = this
|
|
629
|
+
const segments = this.#compileAsSegments(value, scope, loc);
|
|
662
630
|
const bindingCode = singleBindingCode(segments);
|
|
663
631
|
if (bindingCode !== null) return bindingCode;
|
|
664
632
|
if (segments.length === 0) return "\"\"";
|
|
665
|
-
return segments
|
|
633
|
+
return concatenateSegments(segments);
|
|
666
634
|
}
|
|
667
635
|
#checkStaticsReference(value, loc) {
|
|
668
636
|
const { statics } = this.#context;
|
|
669
|
-
if (statics && value.startsWith(`statics/`) && !statics.includes(value)) this.fail(`"${value}"
|
|
637
|
+
if (statics && value.startsWith(`statics/`) && !statics.includes(value)) this.fail(`"${value}" isn't in ${STATICS_DIR}/`, loc);
|
|
670
638
|
}
|
|
671
|
-
compileEvent(component, attribute, handler, data, loc) {
|
|
639
|
+
#compileEvent(component, attribute, handler, data, loc) {
|
|
672
640
|
const event = eventName(attribute);
|
|
673
|
-
if (!component.events.includes(event)) this.fail(`<${component.name}> has no
|
|
641
|
+
if (!component.events.includes(event)) this.fail(`<${component.name}> has no \`${attribute}\` event`, loc);
|
|
674
642
|
if (!isIdentifier(handler)) this.fail(`\`${attribute}\` must name a mutation or action in controller.ts, got "${handler}"`, loc);
|
|
675
643
|
const { handlers } = this.#context;
|
|
676
|
-
if (handlers && !handlers.includes(handler)) this.fail(`\`${handler}\`
|
|
677
|
-
|
|
644
|
+
if (handlers && !handlers.includes(handler)) this.fail(`\`${handler}\` isn't a mutation or action in controller.ts`, loc);
|
|
645
|
+
let onArguments = JSON.stringify(handler);
|
|
646
|
+
if (data) onArguments += `, ${data}`;
|
|
647
|
+
return `${attribute}: on(${onArguments})`;
|
|
678
648
|
}
|
|
679
649
|
checkControlFlowElementAttributes(node) {
|
|
680
650
|
const { attributes: allowed, required } = CONTROL_FLOW_TAGS[node.tag];
|
|
681
651
|
const [event] = Object.keys(node.events);
|
|
682
|
-
if (event) this.fail(`<${node.tag}>
|
|
683
|
-
for (const name of Object.keys(node.attributes)) if (!allowed.includes(name)) this.fail(`<${node.tag}>
|
|
652
|
+
if (event) this.fail(`<${node.tag}> can't handle events such as \`${event}\`. Put the handler on a component inside it.`, node.loc);
|
|
653
|
+
for (const name of Object.keys(node.attributes)) if (!allowed.includes(name)) this.fail(`<${node.tag}> has no \`${name}\` attribute`, node.loc);
|
|
684
654
|
for (const name of required) if (!(name in node.attributes)) this.fail(`<${node.tag}> requires \`${name}\``, node.loc);
|
|
685
655
|
}
|
|
686
656
|
compileSingleBinding(node, attribute, scope) {
|
|
687
|
-
const bindingCode = singleBindingCode(this
|
|
688
|
-
if (bindingCode === null) return this.fail(`<${node.tag}> \`${attribute}\` must be
|
|
657
|
+
const bindingCode = singleBindingCode(this.#compileAsSegments(node.attributes[attribute] ?? "", scope, node.loc));
|
|
658
|
+
if (bindingCode === null) return this.fail(`<${node.tag}> \`${attribute}\` must be one {{ }} binding`, node.loc);
|
|
689
659
|
return bindingCode;
|
|
690
660
|
}
|
|
691
|
-
compileLiteral(type, value, loc) {
|
|
661
|
+
#compileLiteral(type, value, loc) {
|
|
662
|
+
if (isWordsOr(type)) return this.#compileWordOr(type, value, loc);
|
|
692
663
|
if (typeof type !== "string") {
|
|
693
664
|
if (type.includes(value)) return JSON.stringify(value);
|
|
694
|
-
return this.fail(`Expected one of ${type
|
|
665
|
+
return this.fail(`Expected one of ${quotedList(type)}, got "${value}"`, loc);
|
|
695
666
|
}
|
|
696
667
|
switch (type) {
|
|
697
668
|
case "boolean":
|
|
698
669
|
if (value === "" || value === "true") return "true";
|
|
699
670
|
if (value === "false") return "false";
|
|
700
|
-
return this.fail(`Expected
|
|
671
|
+
return this.fail(`Expected true or false, got "${value}"`, loc);
|
|
701
672
|
case "number":
|
|
702
|
-
if (
|
|
673
|
+
if (isNumberText(value)) return printNumber(value);
|
|
703
674
|
return this.fail(`Expected a number, got "${value}"`, loc);
|
|
704
675
|
case "string":
|
|
705
676
|
case "any": return JSON.stringify(value);
|
|
706
677
|
}
|
|
707
678
|
}
|
|
708
|
-
|
|
679
|
+
#compileWordOr(type, value, loc) {
|
|
680
|
+
if (type.words.includes(value)) return JSON.stringify(value);
|
|
681
|
+
if (type.or === "number" && isNumberText(value)) return printNumber(value);
|
|
682
|
+
if (type.or === "themeColor" && THEME_COLOR_NAME.test(value)) return JSON.stringify(value);
|
|
683
|
+
return this.fail(`Expected one of ${quotedList(type.words)}, or ${otherThanWords(type)}, got "${value}"`, loc);
|
|
684
|
+
}
|
|
685
|
+
#compileAsSegments(input, scope, loc) {
|
|
709
686
|
const segments = [];
|
|
710
|
-
let
|
|
687
|
+
let staticStart = 0;
|
|
711
688
|
for (const match of input.matchAll(SEGMENT_BINDING_REGEXP)) {
|
|
712
|
-
if (match.index >
|
|
689
|
+
if (match.index > staticStart) segments.push({
|
|
713
690
|
kind: "static",
|
|
714
|
-
value: input.slice(
|
|
691
|
+
value: input.slice(staticStart, match.index)
|
|
715
692
|
});
|
|
716
|
-
const
|
|
693
|
+
const expression = this.#parseBindingExpression(match[1], scope, loc);
|
|
717
694
|
segments.push({
|
|
718
695
|
kind: "binding",
|
|
719
|
-
code: printExpr(
|
|
696
|
+
code: printExpr(expression, scope)
|
|
720
697
|
});
|
|
721
|
-
|
|
698
|
+
staticStart = match.index + match[0].length;
|
|
722
699
|
}
|
|
723
|
-
const rest = input.slice(
|
|
724
|
-
if (rest.includes("{{")) this.fail("
|
|
700
|
+
const rest = input.slice(staticStart);
|
|
701
|
+
if (rest.includes("{{")) this.fail("A binding is missing its closing `}}`", loc);
|
|
725
702
|
if (rest) segments.push({
|
|
726
703
|
kind: "static",
|
|
727
704
|
value: rest
|
|
728
705
|
});
|
|
729
706
|
return segments;
|
|
730
707
|
}
|
|
731
|
-
parseBindingExpression(source, scope, loc) {
|
|
732
|
-
let
|
|
708
|
+
#parseBindingExpression(source, scope, loc) {
|
|
709
|
+
let expression;
|
|
733
710
|
try {
|
|
734
|
-
|
|
711
|
+
expression = parseExpression(source);
|
|
735
712
|
} catch (error) {
|
|
736
713
|
if (!(error instanceof ScriptParseError)) throw error;
|
|
737
714
|
return this.fail(`Invalid binding \`{{${source}}}\`: ${error.reason}`, loc);
|
|
738
715
|
}
|
|
739
716
|
const { stateKeys } = this.#context;
|
|
740
717
|
if (stateKeys) {
|
|
741
|
-
const undeclared = stateReads(
|
|
718
|
+
const undeclared = stateReads(expression, scope).find((name) => !stateKeys.includes(name));
|
|
742
719
|
if (undeclared) {
|
|
743
720
|
const declared = stateKeys.join(", ") || "none";
|
|
744
|
-
this.fail(`\`${undeclared}\`
|
|
721
|
+
this.fail(`\`${undeclared}\` isn't in state.ts. It has: ${declared}`, loc);
|
|
745
722
|
}
|
|
746
723
|
}
|
|
747
|
-
return
|
|
724
|
+
return expression;
|
|
748
725
|
}
|
|
749
726
|
warn(message, loc) {
|
|
750
727
|
this.#context.onWarning?.(new CompileWarning(message, this.#context.file, loc));
|
|
@@ -756,8 +733,9 @@ var ViewCompiler = class {
|
|
|
756
733
|
function ifChain(ifNode, nextSiblings) {
|
|
757
734
|
const branches = [ifNode];
|
|
758
735
|
for (const sibling of nextSiblings) {
|
|
759
|
-
if (
|
|
736
|
+
if (!continuesIf(sibling)) break;
|
|
760
737
|
branches.push(sibling);
|
|
738
|
+
if (sibling.tag === "Else") break;
|
|
761
739
|
}
|
|
762
740
|
return branches;
|
|
763
741
|
}
|
|
@@ -770,11 +748,21 @@ function singleBindingCode(segments) {
|
|
|
770
748
|
return null;
|
|
771
749
|
}
|
|
772
750
|
function dataKey(attribute) {
|
|
773
|
-
const
|
|
774
|
-
|
|
751
|
+
const words = attribute.slice(5).split("-");
|
|
752
|
+
let key = words[0] ?? "";
|
|
753
|
+
for (const word of words.slice(1)) key += word.charAt(0).toUpperCase() + word.slice(1);
|
|
754
|
+
return key;
|
|
755
|
+
}
|
|
756
|
+
function describeNode(node) {
|
|
757
|
+
if (node.type === "Text") return `'${node.content}'`;
|
|
758
|
+
return `<${node.tag}>`;
|
|
775
759
|
}
|
|
776
760
|
function printSegment(segment) {
|
|
777
|
-
|
|
761
|
+
if (segment.kind === "static") return JSON.stringify(segment.value);
|
|
762
|
+
return `text(${segment.code})`;
|
|
763
|
+
}
|
|
764
|
+
function concatenateSegments(segments) {
|
|
765
|
+
return segments.map(printSegment).join(" + ");
|
|
778
766
|
}
|
|
779
767
|
function canHaveChildren(parent, child) {
|
|
780
768
|
switch (parent.children) {
|
|
@@ -783,24 +771,23 @@ function canHaveChildren(parent, child) {
|
|
|
783
771
|
case "elements": return true;
|
|
784
772
|
}
|
|
785
773
|
}
|
|
774
|
+
function isNumberText(value) {
|
|
775
|
+
return value.trim() !== "" && Number.isFinite(Number(value));
|
|
776
|
+
}
|
|
777
|
+
function printNumber(value) {
|
|
778
|
+
return String(Number(value));
|
|
779
|
+
}
|
|
786
780
|
//#endregion
|
|
787
781
|
//#region src/view/compile.ts
|
|
788
|
-
/**
|
|
789
|
-
* Compiles `.dml` markup into a renderer-agnostic ES module:
|
|
790
|
-
* `export default function render(d, { h, on, text, each }) { return h("View", ...) }`.
|
|
791
|
-
*
|
|
792
|
-
* The 'h' will be a function from the renderer to create the virtual element for a component. The
|
|
793
|
-
* function returns the page's root element, the <View>.
|
|
794
|
-
*/
|
|
795
782
|
function compileView(source, context) {
|
|
796
783
|
const nodes = parseDmlToIR(source, {
|
|
797
784
|
locations: true,
|
|
798
785
|
file: context.file,
|
|
799
|
-
whitespace: "
|
|
786
|
+
whitespace: "inline"
|
|
800
787
|
});
|
|
801
788
|
return [
|
|
802
789
|
"export default function render(d, { h, on, text, each }) {",
|
|
803
|
-
` return h(${new ViewCompiler(context).compileRoot(nodes).map((
|
|
790
|
+
` return h(${new ViewCompiler(context).compileRoot(nodes).map((argument) => `\n ${argument},`).join("")}\n );`,
|
|
804
791
|
"}",
|
|
805
792
|
""
|
|
806
793
|
].join("\n");
|
|
@@ -860,7 +847,8 @@ function addVariables(env, variables, file) {
|
|
|
860
847
|
//#endregion
|
|
861
848
|
//#region src/build/build.ts
|
|
862
849
|
const MANIFEST_FILE = "manifest.json";
|
|
863
|
-
const
|
|
850
|
+
const STYLES_FILE = "dot.css";
|
|
851
|
+
const ARCHIVE_DATE = new Date(1980, 0, 1);
|
|
864
852
|
/** Compiles and bundles the dot into `outDir` for hosts, and refreshes the types in `.dot/`. */
|
|
865
853
|
async function build(options) {
|
|
866
854
|
const project = await loadProject(options.root);
|
|
@@ -873,43 +861,64 @@ async function build(options) {
|
|
|
873
861
|
const modules = await compileModules(project, env, componentRegistry([...Object.values(builtinComponents), ...options.components ?? []]), mode, warnings);
|
|
874
862
|
const outDir = resolve(project.root, options.outDir ?? "dist");
|
|
875
863
|
const uiDir = join(outDir, "ui");
|
|
876
|
-
const bundleLayer = (entry, layerDir, format, external) => {
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
await writeManifest(project, outDir, runtime)
|
|
896
|
-
];
|
|
897
|
-
const icon = await copyIcon(project, outDir);
|
|
898
|
-
if (icon) files.push(icon);
|
|
864
|
+
const bundleLayer = (entry, layerDir, format, external) => bundle({
|
|
865
|
+
root: project.root,
|
|
866
|
+
modules,
|
|
867
|
+
entry,
|
|
868
|
+
outDir: layerDir,
|
|
869
|
+
fileName: "dot.js",
|
|
870
|
+
format,
|
|
871
|
+
mode,
|
|
872
|
+
conditions: options.conditions ?? [],
|
|
873
|
+
external
|
|
874
|
+
});
|
|
875
|
+
const files = [];
|
|
876
|
+
files.push(...await bundleLayer(ENTRY.ui, uiDir, "esm", Object.keys(REACT_HOST_MODULES)));
|
|
877
|
+
files.push(...await bundleLayer(ENTRY.logic, join(outDir, "logic"), "iife", []));
|
|
878
|
+
files.push(await copyRendererStyles(runtime, uiDir));
|
|
879
|
+
files.push(await writeIndexHtml(project, uiDir, runtime.react.version));
|
|
880
|
+
files.push(...await copyStatics(project, uiDir));
|
|
881
|
+
files.push(await writeManifest(project, outDir, runtime));
|
|
882
|
+
if (mode === "production") files.push(await writeArchive(project, outDir, files));
|
|
899
883
|
return {
|
|
900
884
|
outDir,
|
|
901
885
|
files,
|
|
902
886
|
warnings
|
|
903
887
|
};
|
|
904
888
|
}
|
|
889
|
+
async function copyRendererStyles(runtime, uiDir) {
|
|
890
|
+
const file = join(uiDir, STYLES_FILE);
|
|
891
|
+
await mkdir(uiDir, { recursive: true });
|
|
892
|
+
await copyFile(runtime.stylesheet, file);
|
|
893
|
+
return file;
|
|
894
|
+
}
|
|
895
|
+
async function writeArchive(project, outDir, files) {
|
|
896
|
+
const entries = {};
|
|
897
|
+
for (const file of files) if (!file.endsWith(".map")) {
|
|
898
|
+
const name = relativeSlashPath(outDir, file);
|
|
899
|
+
entries[name] = [await readFile(file), { mtime: ARCHIVE_DATE }];
|
|
900
|
+
}
|
|
901
|
+
const { package: packageName, version } = project.dotJson;
|
|
902
|
+
for (const name of await readdir(outDir)) if (name.startsWith(`${packageName}-`) && name.endsWith(".zip")) await rm(join(outDir, name));
|
|
903
|
+
const file = join(outDir, `${packageName}-${version}.zip`);
|
|
904
|
+
await writeFile(file, zipSync(entries, { level: 9 }));
|
|
905
|
+
return file;
|
|
906
|
+
}
|
|
905
907
|
async function compileModules(project, env, components, mode, warnings) {
|
|
906
908
|
const modules = /* @__PURE__ */ new Map();
|
|
909
|
+
const usedComponents = /* @__PURE__ */ new Set();
|
|
907
910
|
for (const page of project.pages) {
|
|
908
|
-
const view = await compilePage(page, components, project.statics, warnings);
|
|
911
|
+
const view = await compilePage(page, components, project.statics, warnings, usedComponents);
|
|
909
912
|
modules.set(viewModuleId(page.id), view);
|
|
910
913
|
}
|
|
911
|
-
|
|
912
|
-
|
|
914
|
+
let rendererComponents;
|
|
915
|
+
if (mode === "production") rendererComponents = [...usedComponents].filter((name) => name in builtinComponents).sort();
|
|
916
|
+
modules.set(ENTRY.ui, uiEntry(project, rendererComponents));
|
|
917
|
+
const logicOptions = {
|
|
918
|
+
strict: mode === "development",
|
|
919
|
+
dashMinApiVersion: dashMinApiVersion(project)
|
|
920
|
+
};
|
|
921
|
+
modules.set(ENTRY.logic, logicEntry(project, logicOptions));
|
|
913
922
|
modules.set(ENV_MODULE, envModule(env));
|
|
914
923
|
return modules;
|
|
915
924
|
}
|
|
@@ -935,32 +944,30 @@ async function writeManifest(project, outDir, runtime) {
|
|
|
935
944
|
await writeFile(file, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
936
945
|
return file;
|
|
937
946
|
}
|
|
938
|
-
async function copyIcon(project, outDir) {
|
|
939
|
-
if (project.dotJson.icon === void 0) return null;
|
|
940
|
-
const file = join(outDir, ICON_FILE);
|
|
941
|
-
await copyFile(join(project.root, project.dotJson.icon), file);
|
|
942
|
-
return file;
|
|
943
|
-
}
|
|
944
947
|
function dotManifest(project, runtime) {
|
|
945
|
-
const { id,
|
|
948
|
+
const { id, package: packageName, name, version, permissions, network } = project.dotJson;
|
|
946
949
|
const pages = project.pages.map((page) => page.id);
|
|
950
|
+
const share = {};
|
|
951
|
+
for (const page of project.pages) if (page.share) share[page.id] = page.share;
|
|
947
952
|
return {
|
|
948
953
|
id,
|
|
954
|
+
package: packageName,
|
|
949
955
|
name,
|
|
950
956
|
version,
|
|
951
957
|
runtime: runtime.version,
|
|
952
958
|
react: runtime.react.version,
|
|
953
|
-
description,
|
|
954
|
-
categories,
|
|
955
|
-
icon: project.dotJson.icon === void 0 ? void 0 : ICON_FILE,
|
|
956
959
|
permissions,
|
|
957
960
|
network,
|
|
958
|
-
dash,
|
|
961
|
+
dash: { minApiVersion: dashMinApiVersion(project) },
|
|
959
962
|
entry: pages[0],
|
|
960
|
-
pages
|
|
963
|
+
pages,
|
|
964
|
+
share
|
|
961
965
|
};
|
|
962
966
|
}
|
|
963
|
-
|
|
967
|
+
function dashMinApiVersion(project) {
|
|
968
|
+
return project.dotJson.dash?.minApiVersion ?? DEFAULT_DASH_API_VERSION;
|
|
969
|
+
}
|
|
970
|
+
async function compilePage(page, components, statics, warnings, usedComponents) {
|
|
964
971
|
const [view, state, controller] = await Promise.all([
|
|
965
972
|
readFile(page.view, "utf8"),
|
|
966
973
|
readFile(page.state, "utf8"),
|
|
@@ -970,7 +977,8 @@ async function compilePage(page, components, statics, warnings) {
|
|
|
970
977
|
file: page.view,
|
|
971
978
|
components,
|
|
972
979
|
statics,
|
|
973
|
-
onWarning: (warning) => warnings.push(warning)
|
|
980
|
+
onWarning: (warning) => warnings.push(warning),
|
|
981
|
+
onComponent: (name) => usedComponents.add(name)
|
|
974
982
|
};
|
|
975
983
|
const { keys } = parseStateModule(state, page.state);
|
|
976
984
|
if (keys) context.stateKeys = keys;
|
|
@@ -986,8 +994,12 @@ function indexHtml(title, reactVersion) {
|
|
|
986
994
|
<html lang="en">
|
|
987
995
|
<head>
|
|
988
996
|
<meta charset="UTF-8" />
|
|
989
|
-
<meta
|
|
997
|
+
<meta
|
|
998
|
+
name="viewport"
|
|
999
|
+
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
|
1000
|
+
/>
|
|
990
1001
|
<title>${escaped}</title>
|
|
1002
|
+
<link rel="stylesheet" href="./${STYLES_FILE}" />
|
|
991
1003
|
<style>
|
|
992
1004
|
body {
|
|
993
1005
|
margin: 0;
|
|
@@ -1003,6 +1015,6 @@ function indexHtml(title, reactVersion) {
|
|
|
1003
1015
|
`;
|
|
1004
1016
|
}
|
|
1005
1017
|
//#endregion
|
|
1006
|
-
export { CONTROL_FLOW_TAGS, CompileError, CompileWarning, DATA_PREFIX, ROOT_TAG, STATICS_DIR, build, canHaveChildren, compileView, dataKey, installedRuntime, listStatics, loadProject };
|
|
1018
|
+
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 };
|
|
1007
1019
|
|
|
1008
1020
|
//# sourceMappingURL=index.js.map
|