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