@monimesl/dot-compiler 0.1.0-alpha.3 → 0.1.0-alpha.31

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