@monimesl/dot-compiler 0.1.0-alpha.8 → 1.0.0-beta.0

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