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