@monimesl/dot-compiler 0.1.0-alpha.9 → 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,73 +18,43 @@ 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 {
@@ -92,46 +62,42 @@ async function readDotJson(file) {
92
62
  } catch (error) {
93
63
  throw new CompileError(`This isn't valid JSON: ${error.message}`, file);
94
64
  }
95
- if (!isObject(dotJson)) throw new CompileError("dot.json must be an object, such as { \"id\": \"com.example.shop\", \"name\": \"Shop\", \"version\": \"1.0.0\" }", 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);
96
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\". Use lowercase letters, digits and hyphens, with dots between words. 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);
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);
103
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 text, and not empty", 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;
@@ -150,14 +116,6 @@ function readNetworkAccess(network, file) {
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,7 +162,7 @@ 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
  }
@@ -235,50 +190,51 @@ async function loadProject(root) {
235
190
  const dotJsonFile = join(root, DOT_JSON_FILE);
236
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
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,18 @@ 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
254
  function uiEntry(project, components) {
299
- const renderer = components ? [`import { createReactRenderer, ${components.join(", ")} } from ${literal(RUNTIME.renderer)};`, `const createRenderer = () => createReactRenderer({ components: { ${components.join(", ")} } });`] : [`import createRenderer from ${literal(RUNTIME.renderer)};`];
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};`];
300
261
  return [
301
- `import { runWebview } from ${literal(RUNTIME.ui)};`,
262
+ `import { runWebview } from ${literal(RUNTIME_MODULES.ui)};`,
302
263
  ...renderer,
303
264
  ...project.pages.map((page, index) => `import view${index} from ${literal(viewModuleId(page.id))};`),
304
265
  "",
@@ -309,11 +270,14 @@ function uiEntry(project, components) {
309
270
  ""
310
271
  ].join("\n");
311
272
  }
312
- function logicEntry(project, { strict }) {
273
+ function logicEntry(project, { strict, dashMinApiVersion }) {
313
274
  const options = [" dot,", ` pages: ${pageTable(project, "page")},`];
314
- if (strict) options.push(" strict: true,");
275
+ if (strict) {
276
+ options.push(" strict: true,");
277
+ options.push(` dashMinApiVersion: ${literal(dashMinApiVersion)},`);
278
+ }
315
279
  return [
316
- `import { runService } from ${literal(RUNTIME.logic)};`,
280
+ `import { runService } from ${literal(RUNTIME_MODULES.logic)};`,
317
281
  `import dot from ${literal(project.dot)};`,
318
282
  ...project.pages.map((page, index) => `import page${index} from ${literal(page.controller)};`),
319
283
  "",
@@ -329,7 +293,7 @@ function envModule(env) {
329
293
  //#endregion
330
294
  //#region src/globals.ts
331
295
  const DOT_GLOBALS = {
332
- "@monimesl/dot-runtime": [
296
+ [RUNTIME_PACKAGE]: [
333
297
  "definePage",
334
298
  "defineDot",
335
299
  "createEvent",
@@ -337,17 +301,18 @@ const DOT_GLOBALS = {
337
301
  ],
338
302
  [ENV_MODULE]: ["env"]
339
303
  };
340
- const SCRIPT = /\.[cm]?[jt]sx?$/;
304
+ const SCRIPT_FILE = /\.[cm]?[jt]sx?$/;
305
+ const ANSWERS_AT_ONCE = /* @__PURE__ */ new Set(["utils", "supports"]);
341
306
  function dotGlobals(root) {
342
307
  const realRoot = realpathSync(root);
343
- 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);
344
309
  const isPageModule = (id) => id.startsWith(join(realRoot, PAGES_DIR) + sep);
345
310
  return {
346
311
  name: "dot:globals",
347
312
  transform(code, id) {
348
313
  if (!isDotModule(id)) return null;
349
314
  const used = new Set(unboundReferences(code, Object.values(DOT_GLOBALS).flat(), id));
350
- if (isPageModule(id) && used.has("dash")) 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.`);
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.`);
351
316
  const imports = [];
352
317
  for (const [source, names] of Object.entries(DOT_GLOBALS)) {
353
318
  const imported = names.filter((name) => used.has(name));
@@ -361,9 +326,12 @@ function dotGlobals(root) {
361
326
  }
362
327
  };
363
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
+ }
364
333
  //#endregion
365
334
  //#region src/build/bundle.ts
366
- /** Bundles one entry with rolldown, replacing `outDir`. Returns the written files. */
367
335
  async function bundle(options) {
368
336
  await rm(options.outDir, {
369
337
  recursive: true,
@@ -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 {
@@ -443,7 +410,7 @@ function requirePackage(fromDir, name, installedIn) {
443
410
  try {
444
411
  packageFile = require.resolve(`${name}/package.json`);
445
412
  } catch {
446
- 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);
447
414
  }
448
415
  const { version } = JSON.parse(readFileSync(packageFile, "utf8"));
449
416
  return {
@@ -463,8 +430,12 @@ function printExpr(expr, scope) {
463
430
  switch (expr.type) {
464
431
  case "Literal": return JSON.stringify(expr.value);
465
432
  case "Identifier": return scope.get(expr.name) ?? `d.${expr.name}`;
466
- case "Member": return expr.property.type === "Literal" && typeof expr.property.value === "string" ? `${print(expr.object)}?.${expr.property.value}` : `${print(expr.object)}?.[${print(expr.property)}]`;
467
- 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)})`;
468
439
  case "Binary": return `(${print(expr.left)} ${expr.operator} ${print(expr.right)})`;
469
440
  case "Conditional": return `(${print(expr.test)} ? ${print(expr.consequent)} : ${print(expr.alternate)})`;
470
441
  }
@@ -529,15 +500,17 @@ function compileLoop(compiler, node, parent, scope) {
529
500
  checkVariableName("item", item);
530
501
  checkVariableName("index", index);
531
502
  if (item === index) compiler.fail("<For> `item` and `index` need different names", node.loc);
532
- const itemVar = "$" + item;
533
- const indexVar = "$" + index;
534
- const inner = new Map(scope).set(item, itemVar).set(index, indexVar);
535
- const args = [list, `(${itemVar}, ${indexVar}) => [${compiler.compile(node.children, parent, inner).join(", ")}]`];
536
- if (key === void 0) compiler.warn("<For> has no `key`, so items are matched by position. Add key=\"<property>\" (or \"*this\") so rows stay right when items move, or key=\"*index\" to keep matching by position.", node.loc);
537
- else if (key === "*index") {} else if (key === "*this") args.push(`(${itemVar}) => ${itemVar}`);
538
- else if (isIdentifier(key)) args.push(`(${itemVar}) => ${itemVar}?.${key}`);
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}`);
539
512
  else compiler.fail(`<For> \`key\` must be a property name, *this or *index, got "${key}"`, node.loc);
540
- return `each(${args.join(", ")})`;
513
+ return `each(${eachArguments.join(", ")})`;
541
514
  }
542
515
  //#endregion
543
516
  //#region src/view/compiler.ts
@@ -550,6 +523,7 @@ const ROOT_DEFAULTS = {
550
523
  const SEGMENT_BINDING_REGEXP = /{{([\s\S]*?)}}/g;
551
524
  const DATA_PREFIX = "data-";
552
525
  const DATA_ATTRIBUTE = /^data-[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
526
+ const THEME_COLOR_NAME = /^[a-z][a-zA-Z0-9]*$/;
553
527
  var ViewCompiler = class {
554
528
  #context;
555
529
  #components;
@@ -559,63 +533,72 @@ var ViewCompiler = class {
559
533
  for (const name of Object.keys(CONTROL_FLOW_TAGS)) if (name in this.#components) this.fail(`<${name}> is reserved for control flow`);
560
534
  }
561
535
  compile(nodes, parent, scope) {
562
- const out = [];
563
- for (let i = 0; i < nodes.length; i++) {
564
- const node = nodes[i];
536
+ const compiled = [];
537
+ for (let index = 0; index < nodes.length; index++) {
538
+ const node = nodes[index];
565
539
  if (node.type === "Text") {
566
- out.push(...this.#compileText(node.content, parent, scope, node.loc));
540
+ compiled.push(...this.#compileText(node.content, parent, scope, node.loc));
567
541
  continue;
568
542
  }
569
543
  switch (node.tag) {
570
544
  case "If": {
571
- const branches = ifChain(node, nodes.slice(i + 1));
572
- out.push(compileConditional(this, branches, parent, scope));
573
- 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;
574
548
  break;
575
549
  }
576
550
  case "ElseIf":
577
551
  case "Else": return this.fail(`<${node.tag}> must come right after <If> or <ElseIf>`, node.loc);
578
552
  case "For":
579
- out.push(compileLoop(this, node, parent, scope));
553
+ compiled.push(compileLoop(this, node, parent, scope));
580
554
  break;
581
- default: out.push(`h(${this.#compileElementArgs(node, parent, scope).join(", ")})`);
555
+ default: compiled.push(`h(${this.#compileElementArgs(node, parent, scope).join(", ")})`);
582
556
  }
583
557
  }
584
- return out;
558
+ return compiled;
585
559
  }
586
560
  #compileText(content, parent, scope, loc) {
587
561
  if (parent?.children !== "text") {
588
- const wrappers = Object.values(this.#components).filter((component) => component.inline && component.children === "text").sort((a, b) => Number(b.name === "Text") - Number(a.name === "Text")).map((component) => `<${component.name}>`).join(" or ");
589
- this.fail(`'${content}' must be inside ${wrappers || "an inline component"}`, loc);
562
+ if (content.trim() === "") return [];
563
+ this.fail(`'${content}' must be inside ${this.#textWrapperNames()}`, loc);
590
564
  }
591
565
  return this.#compileAsSegments(content, scope, loc).map(printSegment);
592
566
  }
567
+ #textWrapperNames() {
568
+ const textWrappers = Object.values(this.#components).filter((component) => {
569
+ return component.inline === true && component.children === "text";
570
+ });
571
+ const textFirst = [...textWrappers.filter((component) => component.name === "Text"), ...textWrappers.filter((component) => component.name !== "Text")];
572
+ if (textFirst.length === 0) return "an inline component";
573
+ return textFirst.map((component) => `<${component.name}>`).join(" or ");
574
+ }
593
575
  compileRoot(nodes) {
594
576
  if (!this.#components["View"]) this.fail(`The components must include <${ROOT_TAG}>, which is the root of every page`);
595
577
  const [root, outside] = nodes;
596
578
  if (root?.type !== "Element" || root.tag !== "View") return this.fail(`A page must have one <${ROOT_TAG}> around everything else`, root?.loc);
597
- if (outside) {
598
- const name = outside.type === "Text" ? `'${outside.content}'` : `<${outside.tag}>`;
599
- this.fail(`${name} must be inside the page's <${ROOT_TAG}>`, outside.loc);
600
- }
579
+ if (outside) this.fail(`${describeNode(outside)} must be inside the page's <${ROOT_TAG}>`, outside.loc);
601
580
  return this.#compileElementArgs(root, null, EMPTY_SCOPE, ROOT_DEFAULTS);
602
581
  }
603
582
  #compileElementArgs(node, parent, scope, defaults = {}) {
604
583
  const component = this.#components[node.tag];
605
- if (!component) return this.fail(`Unknown component <${node.tag}>.`, node.loc);
584
+ if (!component) return this.fail(`Unknown component <${node.tag}>`, node.loc);
606
585
  this.#context.onComponent?.(component.name);
607
586
  if (parent && !canHaveChildren(parent, component)) this.fail(`<${node.tag}> can't go inside <${parent.name}>`, node.loc);
608
- const attributes = Object.entries(node.attributes);
609
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);
610
- const data = this.#compileData(attributes.filter(([name]) => name.startsWith(DATA_PREFIX)), scope, node.loc);
611
- const props = [
612
- ...attributes.filter(([name]) => !name.startsWith(DATA_PREFIX)).map(([name, value]) => this.#compileProp(component, name, value, scope, node.loc)),
613
- ...Object.entries(node.events).map(([name, handler]) => this.#compileEvent(component, name, handler, data, node.loc)),
614
- ...Object.entries(defaults).filter(([name]) => !(name in node.attributes)).map(([name, value]) => `${name}: ${JSON.stringify(value)}`)
615
- ];
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(", ")} }`;
616
599
  return [
617
600
  JSON.stringify(component.name),
618
- props.length ? `{ ${props.join(", ")} }` : "null",
601
+ propsCode,
619
602
  ...this.compile(node.children, component, scope)
620
603
  ];
621
604
  }
@@ -633,12 +616,12 @@ var ViewCompiler = class {
633
616
  const kind = typeof type === "string" ? `a ${type}` : "an enum";
634
617
  this.fail(`\`${name}\` is ${kind}, so set it with one {{ }} binding`, loc);
635
618
  }
636
- return `${name}: ${segments.map(printSegment).join(" + ")}`;
619
+ return `${name}: ${concatenateSegments(segments)}`;
637
620
  }
638
621
  #compileData(attributes, scope, loc) {
639
622
  if (attributes.length === 0) return null;
640
623
  return `{ ${attributes.map(([name, value]) => {
641
- if (!DATA_ATTRIBUTE.test(name)) this.fail(`\`${name}\` must start with data- and use 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);
642
625
  return `${dataKey(name)}: ${this.#compileDataValue(value, scope, loc)}`;
643
626
  }).join(", ")} }`;
644
627
  }
@@ -647,19 +630,21 @@ var ViewCompiler = class {
647
630
  const bindingCode = singleBindingCode(segments);
648
631
  if (bindingCode !== null) return bindingCode;
649
632
  if (segments.length === 0) return "\"\"";
650
- return segments.map(printSegment).join(" + ");
633
+ return concatenateSegments(segments);
651
634
  }
652
635
  #checkStaticsReference(value, loc) {
653
636
  const { statics } = this.#context;
654
- 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);
655
638
  }
656
639
  #compileEvent(component, attribute, handler, data, loc) {
657
640
  const event = eventName(attribute);
658
641
  if (!component.events.includes(event)) this.fail(`<${component.name}> has no \`${attribute}\` event`, loc);
659
642
  if (!isIdentifier(handler)) this.fail(`\`${attribute}\` must name a mutation or action in controller.ts, got "${handler}"`, loc);
660
643
  const { handlers } = this.#context;
661
- if (handlers && !handlers.includes(handler)) this.fail(`\`${handler}\` is not a mutation or action in controller.ts`, loc);
662
- 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})`;
663
648
  }
664
649
  checkControlFlowElementAttributes(node) {
665
650
  const { attributes: allowed, required } = CONTROL_FLOW_TAGS[node.tag];
@@ -674,9 +659,10 @@ var ViewCompiler = class {
674
659
  return bindingCode;
675
660
  }
676
661
  #compileLiteral(type, value, loc) {
662
+ if (isWordsOr(type)) return this.#compileWordOr(type, value, loc);
677
663
  if (typeof type !== "string") {
678
664
  if (type.includes(value)) return JSON.stringify(value);
679
- 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);
680
666
  }
681
667
  switch (type) {
682
668
  case "boolean":
@@ -684,28 +670,34 @@ var ViewCompiler = class {
684
670
  if (value === "false") return "false";
685
671
  return this.fail(`Expected true or false, got "${value}"`, loc);
686
672
  case "number":
687
- if (value.trim() !== "" && Number.isFinite(Number(value))) return String(Number(value));
673
+ if (isNumberText(value)) return printNumber(value);
688
674
  return this.fail(`Expected a number, got "${value}"`, loc);
689
675
  case "string":
690
676
  case "any": return JSON.stringify(value);
691
677
  }
692
678
  }
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
+ }
693
685
  #compileAsSegments(input, scope, loc) {
694
686
  const segments = [];
695
- let last = 0;
687
+ let staticStart = 0;
696
688
  for (const match of input.matchAll(SEGMENT_BINDING_REGEXP)) {
697
- if (match.index > last) segments.push({
689
+ if (match.index > staticStart) segments.push({
698
690
  kind: "static",
699
- value: input.slice(last, match.index)
691
+ value: input.slice(staticStart, match.index)
700
692
  });
701
- const expr = this.#parseBindingExpression(match[1], scope, loc);
693
+ const expression = this.#parseBindingExpression(match[1], scope, loc);
702
694
  segments.push({
703
695
  kind: "binding",
704
- code: printExpr(expr, scope)
696
+ code: printExpr(expression, scope)
705
697
  });
706
- last = match.index + match[0].length;
698
+ staticStart = match.index + match[0].length;
707
699
  }
708
- const rest = input.slice(last);
700
+ const rest = input.slice(staticStart);
709
701
  if (rest.includes("{{")) this.fail("A binding is missing its closing `}}`", loc);
710
702
  if (rest) segments.push({
711
703
  kind: "static",
@@ -714,22 +706,22 @@ var ViewCompiler = class {
714
706
  return segments;
715
707
  }
716
708
  #parseBindingExpression(source, scope, loc) {
717
- let expr;
709
+ let expression;
718
710
  try {
719
- expr = parseExpression(source);
711
+ expression = parseExpression(source);
720
712
  } catch (error) {
721
713
  if (!(error instanceof ScriptParseError)) throw error;
722
714
  return this.fail(`Invalid binding \`{{${source}}}\`: ${error.reason}`, loc);
723
715
  }
724
716
  const { stateKeys } = this.#context;
725
717
  if (stateKeys) {
726
- const undeclared = stateReads(expr, scope).find((name) => !stateKeys.includes(name));
718
+ const undeclared = stateReads(expression, scope).find((name) => !stateKeys.includes(name));
727
719
  if (undeclared) {
728
720
  const declared = stateKeys.join(", ") || "none";
729
721
  this.fail(`\`${undeclared}\` isn't in state.ts. It has: ${declared}`, loc);
730
722
  }
731
723
  }
732
- return expr;
724
+ return expression;
733
725
  }
734
726
  warn(message, loc) {
735
727
  this.#context.onWarning?.(new CompileWarning(message, this.#context.file, loc));
@@ -741,8 +733,9 @@ var ViewCompiler = class {
741
733
  function ifChain(ifNode, nextSiblings) {
742
734
  const branches = [ifNode];
743
735
  for (const sibling of nextSiblings) {
744
- if (branches.at(-1).tag === "Else" || !continuesIf(sibling)) break;
736
+ if (!continuesIf(sibling)) break;
745
737
  branches.push(sibling);
738
+ if (sibling.tag === "Else") break;
746
739
  }
747
740
  return branches;
748
741
  }
@@ -755,11 +748,21 @@ function singleBindingCode(segments) {
755
748
  return null;
756
749
  }
757
750
  function dataKey(attribute) {
758
- const [first = "", ...rest] = attribute.slice(5).split("-");
759
- 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}>`;
760
759
  }
761
760
  function printSegment(segment) {
762
- 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(" + ");
763
766
  }
764
767
  function canHaveChildren(parent, child) {
765
768
  switch (parent.children) {
@@ -768,17 +771,23 @@ function canHaveChildren(parent, child) {
768
771
  case "elements": return true;
769
772
  }
770
773
  }
774
+ function isNumberText(value) {
775
+ return value.trim() !== "" && Number.isFinite(Number(value));
776
+ }
777
+ function printNumber(value) {
778
+ return String(Number(value));
779
+ }
771
780
  //#endregion
772
781
  //#region src/view/compile.ts
773
782
  function compileView(source, context) {
774
783
  const nodes = parseDmlToIR(source, {
775
784
  locations: true,
776
785
  file: context.file,
777
- whitespace: "collapse"
786
+ whitespace: "inline"
778
787
  });
779
788
  return [
780
789
  "export default function render(d, { h, on, text, each }) {",
781
- ` 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 );`,
782
791
  "}",
783
792
  ""
784
793
  ].join("\n");
@@ -838,13 +847,13 @@ function addVariables(env, variables, file) {
838
847
  //#endregion
839
848
  //#region src/build/build.ts
840
849
  const MANIFEST_FILE = "manifest.json";
841
- const ICON_FILE = "icon.png";
842
850
  const STYLES_FILE = "dot.css";
843
851
  const ARCHIVE_DATE = new Date(1980, 0, 1);
844
852
  /** Compiles and bundles the dot into `outDir` for hosts, and refreshes the types in `.dot/`. */
845
853
  async function build(options) {
846
854
  const project = await loadProject(options.root);
847
855
  const mode = options.mode ?? "production";
856
+ if (mode === "production") refuseLocalNetworkHosts(project);
848
857
  const env = await loadPublicEnv(project.root, mode);
849
858
  const runtime = installedRuntime(project.root);
850
859
  await writePageTypes(project);
@@ -861,20 +870,16 @@ async function build(options) {
861
870
  fileName: "dot.js",
862
871
  format,
863
872
  mode,
864
- minify: mode === "production",
865
873
  conditions: options.conditions ?? [],
866
874
  external
867
875
  });
868
- const files = [
869
- ...await bundleLayer(ENTRY.ui, uiDir, "esm", Object.keys(REACT_HOST_MODULES)),
870
- ...await bundleLayer(ENTRY.logic, join(outDir, "logic"), "iife", []),
871
- await copyRendererStyles(runtime, uiDir),
872
- await writeIndexHtml(project, uiDir, runtime.react.version),
873
- ...await copyStatics(project, uiDir),
874
- await writeManifest(project, outDir, runtime)
875
- ];
876
- const icon = await copyIcon(project, outDir);
877
- if (icon) files.push(icon);
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));
878
883
  if (mode === "production") files.push(await writeArchive(project, outDir, files));
879
884
  return {
880
885
  outDir,
@@ -882,6 +887,9 @@ async function build(options) {
882
887
  warnings
883
888
  };
884
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
+ }
885
893
  async function copyRendererStyles(runtime, uiDir) {
886
894
  const file = join(uiDir, STYLES_FILE);
887
895
  await mkdir(uiDir, { recursive: true });
@@ -891,25 +899,30 @@ async function copyRendererStyles(runtime, uiDir) {
891
899
  async function writeArchive(project, outDir, files) {
892
900
  const entries = {};
893
901
  for (const file of files) if (!file.endsWith(".map")) {
894
- const name = relative(outDir, file).split(sep).join("/");
902
+ const name = relativeSlashPath(outDir, file);
895
903
  entries[name] = [await readFile(file), { mtime: ARCHIVE_DATE }];
896
904
  }
897
- const { id, version } = project.dotJson;
898
- for (const name of await readdir(outDir)) if (name.startsWith(`${id}-`) && name.endsWith(".zip")) await rm(join(outDir, name));
899
- 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`);
900
908
  await writeFile(file, zipSync(entries, { level: 9 }));
901
909
  return file;
902
910
  }
903
911
  async function compileModules(project, env, components, mode, warnings) {
904
912
  const modules = /* @__PURE__ */ new Map();
905
- const used = /* @__PURE__ */ new Set();
913
+ const usedComponents = /* @__PURE__ */ new Set();
906
914
  for (const page of project.pages) {
907
- const view = await compilePage(page, components, project.statics, warnings, used);
915
+ const view = await compilePage(page, components, project.statics, warnings, usedComponents);
908
916
  modules.set(viewModuleId(page.id), view);
909
917
  }
910
- const builtins = [...used].filter((name) => name in builtinComponents).sort();
911
- modules.set(ENTRY.ui, uiEntry(project, mode === "production" ? builtins : void 0));
912
- 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));
913
926
  modules.set(ENV_MODULE, envModule(env));
914
927
  return modules;
915
928
  }
@@ -935,32 +948,30 @@ async function writeManifest(project, outDir, runtime) {
935
948
  await writeFile(file, `${JSON.stringify(manifest, null, 2)}\n`);
936
949
  return file;
937
950
  }
938
- async function copyIcon(project, outDir) {
939
- if (project.dotJson.icon === void 0) return null;
940
- const file = join(outDir, ICON_FILE);
941
- await copyFile(join(project.root, project.dotJson.icon), file);
942
- return file;
943
- }
944
951
  function dotManifest(project, runtime) {
945
- const { id, name, version, description, categories, permissions, network, dash } = project.dotJson;
952
+ const { id, package: packageName, name, version, permissions, network } = project.dotJson;
946
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;
947
956
  return {
948
957
  id,
958
+ package: packageName,
949
959
  name,
950
960
  version,
951
961
  runtime: runtime.version,
952
962
  react: runtime.react.version,
953
- description,
954
- categories,
955
- icon: project.dotJson.icon === void 0 ? void 0 : ICON_FILE,
956
963
  permissions,
957
964
  network,
958
- dash,
965
+ dash: { minApiVersion: dashMinApiVersion(project) },
959
966
  entry: pages[0],
960
- pages
967
+ pages,
968
+ share
961
969
  };
962
970
  }
963
- async function compilePage(page, components, statics, warnings, used) {
971
+ function dashMinApiVersion(project) {
972
+ return project.dotJson.dash?.minApiVersion ?? DEFAULT_DASH_API_VERSION;
973
+ }
974
+ async function compilePage(page, components, statics, warnings, usedComponents) {
964
975
  const [view, state, controller] = await Promise.all([
965
976
  readFile(page.view, "utf8"),
966
977
  readFile(page.state, "utf8"),
@@ -971,7 +982,7 @@ async function compilePage(page, components, statics, warnings, used) {
971
982
  components,
972
983
  statics,
973
984
  onWarning: (warning) => warnings.push(warning),
974
- onComponent: (name) => used.add(name)
985
+ onComponent: (name) => usedComponents.add(name)
975
986
  };
976
987
  const { keys } = parseStateModule(state, page.state);
977
988
  if (keys) context.stateKeys = keys;
@@ -989,7 +1000,7 @@ function indexHtml(title, reactVersion) {
989
1000
  <meta charset="UTF-8" />
990
1001
  <meta
991
1002
  name="viewport"
992
- content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
1003
+ content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
993
1004
  />
994
1005
  <title>${escaped}</title>
995
1006
  <link rel="stylesheet" href="./${STYLES_FILE}" />
@@ -1008,6 +1019,6 @@ function indexHtml(title, reactVersion) {
1008
1019
  `;
1009
1020
  }
1010
1021
  //#endregion
1011
- 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 };
1012
1023
 
1013
1024
  //# sourceMappingURL=index.js.map