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