@monimesl/dot-compiler 0.1.0-alpha.4 → 0.1.0-alpha.41

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