@nativele/ts-dom 0.4.0 → 0.4.6

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.
@@ -227,6 +227,14 @@ function rootElementType(html) {
227
227
  const el = elements[0];
228
228
  return el.namespaceURI === HTML_NS ? `El<${JSON.stringify(el.tagName)}>` : "SVGElement";
229
229
  }
230
+ /** The whitespace mode an existing generated module was written with; "preserve" when there is none. */
231
+ function recordedWhitespace(out) {
232
+ try {
233
+ return readFileSync(out, "utf8").split("\n", 3).includes(COLLAPSE_MARK) ? "collapse" : "preserve";
234
+ } catch {
235
+ return "preserve";
236
+ }
237
+ }
230
238
  function globToRegExp(glob) {
231
239
  let re = "";
232
240
  for (let i = 0; i < glob.length; i++) {
@@ -302,8 +310,9 @@ function generateModule(html, options = {}, imports = [], defs = []) {
302
310
  events: ${union(a.events)};
303
311
  }`;
304
312
  const needsEl = type.includes("El<");
305
- return `// Generated by ts-dom. Do not edit.
306
- ${imports.map((i) => `import ${q(i)};\n`).join("")}import { template } from ${q(runtime)};
313
+ const side = imports.map((i) => `import ${q(i)};\n`).join("");
314
+ return `// Generated by ts-dom. Do not edit.${options.whitespace === "collapse" ? `\n${COLLAPSE_MARK}` : ""}
315
+ ${side}import { template } from ${q(runtime)};
307
316
  ${needsEl ? `import type { El } from ${q(runtime)};\n` : ""}export default template<${type}>(${JSON.stringify(toNodes(parseFragment(html), options.whitespace === "collapse"))});
308
317
  `;
309
318
  }
@@ -320,21 +329,31 @@ function formatDiagnostics(file, ds) {
320
329
  return ds.map((d) => `${file}:${d.line}:${d.col} ${d.severity} [${d.rule}] ${d.message}`).join("\n");
321
330
  }
322
331
  var CodegenError = class extends Error {};
323
- /**
324
- * Generate a single `x.html.ts`; throws CodegenError (and writes nothing) when there are error diagnostics.
325
- * `defs` is either a directory to scan or a precomputed scan result: custom elements used by the template
326
- * are imported automatically from the modules that define them.
327
- */
328
- function generateFile(htmlPath, options = {}, defs) {
332
+ /** What `generateFile` would write for a template, without writing it; throws CodegenError when the template has error diagnostics. */
333
+ function renderFile(htmlPath, options = {}, defs) {
329
334
  const html = readFileSync(htmlPath, "utf8");
335
+ if (options.whitespace === void 0) options = {
336
+ ...options,
337
+ whitespace: recordedWhitespace(modulePathFor(htmlPath))
338
+ };
330
339
  const analysis = analyze(html);
331
340
  const errors = analysis.diagnostics.filter((d) => d.severity === "error");
332
341
  if (errors.length) throw new CodegenError(formatDiagnostics(htmlPath, errors));
333
342
  const known = typeof defs === "string" ? scanDefines(defs) : defs ?? [];
334
343
  const here = dirname(htmlPath);
335
344
  const imports = [...new Set(known.filter((d) => analysis.customTags.has(d.tag)).map((d) => relImport(here, d.file)))].sort();
336
- const out = modulePathFor(htmlPath);
337
- const next = generateModule(html, options, imports, known);
345
+ return {
346
+ out: modulePathFor(htmlPath),
347
+ next: generateModule(html, options, imports, known)
348
+ };
349
+ }
350
+ /**
351
+ * Generate a single `x.html.ts`; throws CodegenError (and writes nothing) when there are error diagnostics.
352
+ * `defs` is either a directory to scan or a precomputed scan result: custom elements used by the template
353
+ * are imported automatically from the modules that define them.
354
+ */
355
+ function generateFile(htmlPath, options = {}, defs) {
356
+ const { out, next } = renderFile(htmlPath, options, defs);
338
357
  let prev = "";
339
358
  try {
340
359
  prev = readFileSync(out, "utf8");
@@ -345,7 +364,7 @@ function generateFile(htmlPath, options = {}, defs) {
345
364
  function listHtml(dir) {
346
365
  const out = [];
347
366
  for (const name of readdirSync(dir)) {
348
- if (name === "node_modules") continue;
367
+ if (name === "node_modules" || name === "dist") continue;
349
368
  const full = join(dir, name);
350
369
  if (statSync(full).isDirectory()) out.push(...listHtml(full));
351
370
  else if (name.endsWith(".html") && !isFullPage(readFileSync(full, "utf8"))) out.push(full);
@@ -354,6 +373,7 @@ function listHtml(dir) {
354
373
  }
355
374
  const TAGS_FILE = "ts-dom-tags.ts";
356
375
  const TAGS_HEADER = "// Generated by ts-dom. Do not edit.";
376
+ const COLLAPSE_MARK = "// whitespace: collapse";
357
377
  function listTs(dir) {
358
378
  const out = [];
359
379
  for (const name of readdirSync(dir)) {
@@ -431,6 +451,20 @@ function scanTags(dir) {
431
451
  * so that `refs.x` for `<tag data-ref="x">` in a template is fully typed (props, methods and emitted events).
432
452
  */
433
453
  function generateTags(dir) {
454
+ const { out, next } = renderTags(dir);
455
+ if (next === null) {
456
+ if (existsSync(out) && readFileSync(out, "utf8").startsWith(TAGS_HEADER)) unlinkSync(out);
457
+ return null;
458
+ }
459
+ let prev = "";
460
+ try {
461
+ prev = readFileSync(out, "utf8");
462
+ } catch {}
463
+ if (prev !== next) writeFileSync(out, next);
464
+ return out;
465
+ }
466
+ /** What `generateTags` would write, without writing it; `next` is null when the directory defines no exported component. */
467
+ function renderTags(dir) {
434
468
  const defs = scanTags(dir);
435
469
  const out = join(dir, TAGS_FILE);
436
470
  const seen = /* @__PURE__ */ new Map();
@@ -439,19 +473,86 @@ function generateTags(dir) {
439
473
  if (prev && (prev.from !== d.from || prev.className !== d.className)) throw new CodegenError(`${dir}: [duplicate-define] the tag <${d.tag}> is defined more than once (${prev.from} and ${d.from}). Fix: each custom element name can be defined only once; rename one of them.`);
440
474
  seen.set(d.tag, d);
441
475
  }
442
- if (!defs.length) {
443
- if (existsSync(out) && readFileSync(out, "utf8").startsWith(TAGS_HEADER)) unlinkSync(out);
444
- return null;
445
- }
476
+ if (!defs.length) return {
477
+ out,
478
+ next: null
479
+ };
446
480
  const alias = (d) => `Tag${[...seen.keys()].indexOf(d.tag)}`;
447
481
  const imports = [...seen.values()].map((d) => `import type { ${d.isDefault ? "default" : d.className} as ${alias(d)} } from ${JSON.stringify(d.from)};`);
448
482
  const entries = [...seen.values()].map((d) => ` ${JSON.stringify(d.tag)}: ${alias(d)};`);
449
- const next = `${TAGS_HEADER}\n${imports.join("\n")}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n${entries.join("\n")}\n }\n}\n\nexport {};\n`;
450
- let prev = "";
483
+ return {
484
+ out,
485
+ next: `${TAGS_HEADER}\n${imports.join("\n")}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n${entries.join("\n")}\n }\n}\n\nexport {};\n`
486
+ };
487
+ }
488
+ /**
489
+ * Everything `generateAll` would write for a directory, in memory: generated path to content, or null for a file that should not exist.
490
+ * Templates with error diagnostics are left out and their messages returned in `errors`.
491
+ */
492
+ function planGenerated(dir, options = {}) {
493
+ const files = /* @__PURE__ */ new Map();
494
+ const errors = [];
495
+ const defs = scanDefines(dir);
496
+ for (const f of listHtml(dir)) {
497
+ if (!isSelected(dir, f, options)) continue;
498
+ try {
499
+ const { out, next } = renderFile(f, options, defs);
500
+ files.set(out, next);
501
+ } catch (e) {
502
+ if (e instanceof CodegenError) errors.push(e.message);
503
+ else throw e;
504
+ }
505
+ }
451
506
  try {
452
- prev = readFileSync(out, "utf8");
453
- } catch {}
454
- if (prev !== next) writeFileSync(out, next);
507
+ const tags = renderTags(dir);
508
+ files.set(tags.out, tags.next);
509
+ } catch (e) {
510
+ if (e instanceof CodegenError) errors.push(e.message);
511
+ else throw e;
512
+ }
513
+ for (const f of listGenerated(dir)) if (!files.has(f) && !existsSync(f.slice(0, -3))) files.set(f, null);
514
+ return {
515
+ files,
516
+ errors
517
+ };
518
+ }
519
+ /** Generated files under `dir` that differ from what `ts-dom gen` would write now. */
520
+ function staleGenerated(dir, options = {}) {
521
+ return checkGenerated(dir, options).stale;
522
+ }
523
+ /** `staleGenerated`, plus the errors that would make `ts-dom gen` fail (template diagnostics, a tag defined twice). */
524
+ function checkGenerated(dir, options = {}) {
525
+ const plan = planGenerated(dir, options);
526
+ const out = [];
527
+ for (const [file, next] of plan.files) {
528
+ const prev = existsSync(file) ? readFileSync(file, "utf8") : null;
529
+ if (next === null) {
530
+ if (prev !== null && prev.startsWith(TAGS_HEADER)) out.push({
531
+ file,
532
+ reason: file.endsWith(".html.ts") ? "orphan" : "outdated"
533
+ });
534
+ } else if (prev === null) out.push({
535
+ file,
536
+ reason: "missing"
537
+ });
538
+ else if (prev !== next) out.push({
539
+ file,
540
+ reason: "outdated"
541
+ });
542
+ }
543
+ return {
544
+ stale: out.sort((a, b) => a.file.localeCompare(b.file)),
545
+ errors: plan.errors
546
+ };
547
+ }
548
+ function listGenerated(dir) {
549
+ const out = [];
550
+ for (const name of readdirSync(dir)) {
551
+ if (name === "node_modules") continue;
552
+ const full = join(dir, name);
553
+ if (statSync(full).isDirectory()) out.push(...listGenerated(full));
554
+ else if (name.endsWith(".html.ts") && readFileSync(full, "utf8").startsWith(TAGS_HEADER)) out.push(full);
555
+ }
455
556
  return out;
456
557
  }
457
558
  /** Generate every `x.html.ts` (and the tag map) under a directory; collects all errors and throws them together. */
@@ -475,6 +576,7 @@ function generateAll(dir, options = {}) {
475
576
  if (e instanceof CodegenError) errors.push(e.message);
476
577
  else throw e;
477
578
  }
579
+ for (const f of listGenerated(dir)) if (!existsSync(f.slice(0, -3))) unlinkSync(f);
478
580
  if (errors.length) throw new CodegenError(errors.join("\n"));
479
581
  return written;
480
582
  }
@@ -712,4 +814,4 @@ function checkA11y(html, ignore = []) {
712
814
  for (const { id, el } of labelFor) if (!ids.has(id)) report("label-for", el, `<label for="${id}"> points to an id that does not exist in this template. Fix: add id="${id}" to the control, or wrap the control in the label.`);
713
815
  return out.sort((a, b) => a.line - b.line || a.col - b.col);
714
816
  }
715
- export { analyze as a, generateFile as c, listHtml as d, scanDefines as f, CodegenError as i, isFullPage as l, checkA11y as n, formatDiagnostics as o, watchAll as p, CODEGEN_RULES as r, generateAll as s, A11Y_RULES as t, isSelected as u };
817
+ export { watchAll as _, analyze as a, generateAll as c, isSelected as d, listHtml as f, staleGenerated as g, scanDefines as h, CodegenError as i, generateFile as l, renderTags as m, checkA11y as n, checkGenerated as o, listTs as p, CODEGEN_RULES as r, formatDiagnostics as s, A11Y_RULES as t, isFullPage as u };