htm-transform 0.1.3 → 0.1.4

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/index.d.ts ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @typedef {Object} ElementNode
3
+ * @property {'element'} type
4
+ * @property {boolean | {type: 'expression', expr: import('acorn').Node}} tag
5
+ * @property {Object<string, string | boolean | {type: 'expression', expr: import('acorn').Node}>} props
6
+ * @property {Array<ElementNode | TextNode | ExpressionNode>} children
7
+ */
8
+ /**
9
+ * @typedef {object} TextNode
10
+ * @property {'text'} type
11
+ * @property {string} value
12
+ */
13
+ /**
14
+ * @typedef {object} ExpressionNode
15
+ * @property {'expression'} type
16
+ * @property {string} value
17
+ * @property {import('acorn').Node} [expr]
18
+ */
19
+ /**
20
+ * @typedef {object} Token
21
+ * @property {'openTag' | 'closeTag' | 'text' | 'expression'} type
22
+ * @property {string} [tag]
23
+ * @property {Record<string, string | boolean>} [props]
24
+ * @property {boolean} [selfClosing]
25
+ * @property {string} [value]
26
+ */
27
+ /**
28
+ * Transforms htm tagged templates into h function calls
29
+ * @param {string} code - JavaScript code containing htm tagged templates
30
+ * @param {object} [options] - Transform options
31
+ * @param {string} [options.pragma] - The h function name (default: 'h')
32
+ * @param {string} [options.tag] - The tag name to look for (default: 'html')
33
+ * @param {object} [options.import] - Import configuration
34
+ * @param {string} options.import.from - Module to import from (e.g., 'preact', 'react')
35
+ * @param {string} options.import.name - Export name to import (e.g., 'h', 'createElement')
36
+ * @returns {string} - Transformed code
37
+ */
38
+ export default function transform(code: string, options?: {
39
+ pragma?: string;
40
+ tag?: string;
41
+ import?: {
42
+ from: string;
43
+ name: string;
44
+ };
45
+ }): string;
46
+ export type ElementNode = {
47
+ type: "element";
48
+ tag: boolean | {
49
+ type: "expression";
50
+ expr: import("acorn").Node;
51
+ };
52
+ props: {
53
+ [x: string]: string | boolean | {
54
+ type: "expression";
55
+ expr: import("acorn").Node;
56
+ };
57
+ };
58
+ children: Array<ElementNode | TextNode | ExpressionNode>;
59
+ };
60
+ export type TextNode = {
61
+ type: "text";
62
+ value: string;
63
+ };
64
+ export type ExpressionNode = {
65
+ type: "expression";
66
+ value: string;
67
+ expr?: import("acorn").Node;
68
+ };
69
+ export type Token = {
70
+ type: "openTag" | "closeTag" | "text" | "expression";
71
+ tag?: string;
72
+ props?: Record<string, string | boolean>;
73
+ selfClosing?: boolean;
74
+ value?: string;
75
+ };
package/index.js CHANGED
@@ -11,23 +11,23 @@ import { simple } from "acorn-walk";
11
11
  */
12
12
 
13
13
  /**
14
- * @typedef {Object} TextNode
14
+ * @typedef {object} TextNode
15
15
  * @property {'text'} type
16
16
  * @property {string} value
17
17
  */
18
18
 
19
19
  /**
20
- * @typedef {Object} ExpressionNode
20
+ * @typedef {object} ExpressionNode
21
21
  * @property {'expression'} type
22
22
  * @property {string} value
23
23
  * @property {import('acorn').Node} [expr]
24
24
  */
25
25
 
26
26
  /**
27
- * @typedef {Object} Token
27
+ * @typedef {object} Token
28
28
  * @property {'openTag' | 'closeTag' | 'text' | 'expression'} type
29
29
  * @property {string} [tag]
30
- * @property {Object<string, string | boolean>} [props]
30
+ * @property {Record<string, string | boolean>} [props]
31
31
  * @property {boolean} [selfClosing]
32
32
  * @property {string} [value]
33
33
  */
@@ -35,10 +35,10 @@ import { simple } from "acorn-walk";
35
35
  /**
36
36
  * Transforms htm tagged templates into h function calls
37
37
  * @param {string} code - JavaScript code containing htm tagged templates
38
- * @param {Object} options - Transform options
39
- * @param {string} options.pragma - The h function name (default: 'h')
40
- * @param {string} options.tag - The tag name to look for (default: 'html')
41
- * @param {Object} options.import - Import configuration
38
+ * @param {object} [options] - Transform options
39
+ * @param {string} [options.pragma] - The h function name (default: 'h')
40
+ * @param {string} [options.tag] - The tag name to look for (default: 'html')
41
+ * @param {object} [options.import] - Import configuration
42
42
  * @param {string} options.import.from - Module to import from (e.g., 'preact', 'react')
43
43
  * @param {string} options.import.name - Export name to import (e.g., 'h', 'createElement')
44
44
  * @returns {string} - Transformed code
@@ -46,22 +46,28 @@ import { simple } from "acorn-walk";
46
46
  export default function transform(code, options = {}) {
47
47
  const { pragma = "h", tag: tagName = "html", import: importConfig } = options;
48
48
 
49
- const ast = acorn.parse(code, {
50
- ecmaVersion: "latest",
51
- sourceType: "module",
52
- });
49
+ const ast = /** @type {import('acorn').Program} */ (
50
+ acorn.parse(code, {
51
+ ecmaVersion: "latest",
52
+ sourceType: "module",
53
+ })
54
+ );
53
55
 
54
56
  let hasTransformation = false;
55
57
 
56
58
  simple(ast, {
59
+ /**
60
+ * @param {import('acorn').TaggedTemplateExpression} node
61
+ */
57
62
  TaggedTemplateExpression(node) {
58
63
  if (node.tag.type === "Identifier" && node.tag.name === tagName) {
59
64
  hasTransformation = true;
60
65
  const transformed = transformTaggedTemplate(node, pragma);
61
66
 
62
67
  // Replace the node with the transformed version
63
- for (const key in node) {
64
- delete node[key];
68
+ const mutableNode = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (node));
69
+ for (const key in mutableNode) {
70
+ delete mutableNode[key];
65
71
  }
66
72
  Object.assign(node, transformed);
67
73
  }
@@ -78,43 +84,73 @@ export default function transform(code, options = {}) {
78
84
 
79
85
  /**
80
86
  * Adds an import declaration at the top of the AST
81
- * @param {import('acorn').Node} ast - The AST to modify
87
+ * @param {import('acorn').Program} ast - The AST to modify
82
88
  * @param {string} moduleName - The module to import from
83
89
  * @param {string} exportName - The export name to import
84
90
  * @returns {void}
85
91
  */
86
92
  function addImportDeclaration(ast, moduleName, exportName) {
87
93
  const hasImport = ast.body.some(
88
- (node) =>
89
- node.type === "ImportDeclaration" &&
90
- node.source.value === moduleName &&
91
- node.specifiers.some(
92
- (spec) => spec.type === "ImportSpecifier" && spec.imported.name === exportName,
93
- ),
94
+ /**
95
+ * @param {import('acorn').Statement | import('acorn').ModuleDeclaration} node
96
+ * @returns {boolean}
97
+ */
98
+ (node) => {
99
+ if (node.type !== "ImportDeclaration") return false;
100
+ const importNode = /** @type {import('acorn').ImportDeclaration} */ (node);
101
+ return (
102
+ importNode.source.value === moduleName &&
103
+ importNode.specifiers.some(
104
+ /**
105
+ * @param {import('acorn').ImportSpecifier | import('acorn').ImportDefaultSpecifier | import('acorn').ImportNamespaceSpecifier} spec
106
+ * @returns {boolean}
107
+ */
108
+ (spec) => {
109
+ if (spec.type !== "ImportSpecifier") return false;
110
+ const importSpec = /** @type {import('acorn').ImportSpecifier} */ (spec);
111
+ return (
112
+ importSpec.imported.type === "Identifier" && importSpec.imported.name === exportName
113
+ );
114
+ },
115
+ )
116
+ );
117
+ },
94
118
  );
95
119
 
96
120
  if (hasImport) return;
97
121
 
98
122
  // Create import declaration: import { exportName } from 'moduleName';
123
+ /** @type {import('acorn').ImportDeclaration} */
99
124
  const importDeclaration = {
100
125
  type: "ImportDeclaration",
126
+ start: 0,
127
+ end: 0,
101
128
  specifiers: [
102
129
  {
103
130
  type: "ImportSpecifier",
131
+ start: 0,
132
+ end: 0,
104
133
  imported: {
105
134
  type: "Identifier",
135
+ start: 0,
136
+ end: 0,
106
137
  name: exportName,
107
138
  },
108
139
  local: {
109
140
  type: "Identifier",
141
+ start: 0,
142
+ end: 0,
110
143
  name: exportName,
111
144
  },
112
145
  },
113
146
  ],
114
147
  source: {
115
148
  type: "Literal",
149
+ start: 0,
150
+ end: 0,
116
151
  value: moduleName,
117
152
  },
153
+ attributes: [],
118
154
  };
119
155
 
120
156
  // Add to the beginning of the body
@@ -123,9 +159,9 @@ function addImportDeclaration(ast, moduleName, exportName) {
123
159
 
124
160
  /**
125
161
  * Transforms a single tagged template expression
126
- * @param {import('acorn').Node} node - The tagged template expression node
162
+ * @param {import('acorn').TaggedTemplateExpression} node - The tagged template expression node
127
163
  * @param {string} pragma - The pragma function name
128
- * @returns {import('acorn').Node} The transformed AST node
164
+ * @returns {import('acorn').Literal | import('acorn').CallExpression | import('acorn').ArrayExpression | import('acorn').Expression} The transformed AST node
129
165
  */
130
166
  function transformTaggedTemplate(node, pragma) {
131
167
  const quasi = node.quasi;
@@ -149,15 +185,21 @@ function transformTaggedTemplate(node, pragma) {
149
185
 
150
186
  // Convert to h() calls
151
187
  if (elements.length === 0) {
152
- return { type: "Literal", value: null };
188
+ /** @type {import('acorn').Literal} */
189
+ const literal = { type: "Literal", start: 0, end: 0, value: null };
190
+ return literal;
153
191
  } else if (elements.length === 1) {
154
192
  return elementsToAST(elements[0], pragma);
155
193
  } else {
156
194
  // Multiple root elements - return array
157
- return {
195
+ /** @type {import('acorn').ArrayExpression} */
196
+ const arrayExpr = {
158
197
  type: "ArrayExpression",
198
+ start: 0,
199
+ end: 0,
159
200
  elements: elements.map((el) => elementsToAST(el, pragma)),
160
201
  };
202
+ return arrayExpr;
161
203
  }
162
204
  }
163
205
 
@@ -185,15 +227,16 @@ function parseTemplate(template, placeholders) {
185
227
 
186
228
  if (token.type === "openTag") {
187
229
  i++;
230
+ /** @type {ElementNode} */
188
231
  const element = {
189
232
  type: "element",
190
- tag: token.tag,
191
- props: token.props,
233
+ tag: token.tag || "",
234
+ props: token.props || {},
192
235
  children: [],
193
236
  };
194
237
 
195
238
  if (!token.selfClosing) {
196
- element.children = parse(token.tag);
239
+ element.children = parse(token.tag || "");
197
240
  }
198
241
 
199
242
  children.push(element);
@@ -204,13 +247,17 @@ function parseTemplate(template, placeholders) {
204
247
  }
205
248
  i++;
206
249
  } else if (token.type === "text") {
207
- const text = token.value.trim();
250
+ const text = (token.value || "").trim();
208
251
  if (text) {
209
- children.push({ type: "text", value: text });
252
+ /** @type {TextNode} */
253
+ const textNode = { type: "text", value: text };
254
+ children.push(textNode);
210
255
  }
211
256
  i++;
212
257
  } else if (token.type === "expression") {
213
- children.push({ type: "expression", value: token.value });
258
+ /** @type {ExpressionNode} */
259
+ const exprNode = { type: "expression", value: token.value || "" };
260
+ children.push(exprNode);
214
261
  i++;
215
262
  } else {
216
263
  i++;
@@ -232,6 +279,7 @@ function parseTemplate(template, placeholders) {
232
279
  * @returns {Array<Token>} Array of tokens
233
280
  */
234
281
  function tokenize(template) {
282
+ /** @type {Token[]} */
235
283
  const tokens = [];
236
284
  let i = 0;
237
285
 
@@ -244,21 +292,27 @@ function tokenize(template) {
244
292
  const [full, isClosing, tagName, attrsStr, selfClosing] = tagMatch;
245
293
 
246
294
  if (isClosing) {
247
- tokens.push({ type: "closeTag", tag: tagName });
295
+ /** @type {Token} */
296
+ const closeToken = { type: "closeTag", tag: tagName };
297
+ tokens.push(closeToken);
248
298
  } else {
249
299
  const props = parseAttributes(attrsStr);
250
- tokens.push({
300
+ /** @type {Token} */
301
+ const openToken = {
251
302
  type: "openTag",
252
303
  tag: tagName,
253
304
  props,
254
305
  selfClosing: selfClosing === "/",
255
- });
306
+ };
307
+ tokens.push(openToken);
256
308
  }
257
309
 
258
310
  i += full.length;
259
311
  } else {
260
312
  // Not a valid tag, treat as text
261
- tokens.push({ type: "text", value: "<" });
313
+ /** @type {Token} */
314
+ const textToken = { type: "text", value: "<" };
315
+ tokens.push(textToken);
262
316
  i++;
263
317
  }
264
318
  } else {
@@ -272,9 +326,13 @@ function tokenize(template) {
272
326
  // Check if this is an expression placeholder
273
327
  const exprMatch = text.match(/^(__EXPR_\d+__)/);
274
328
  if (exprMatch && text === exprMatch[0]) {
275
- tokens.push({ type: "expression", value: exprMatch[0] });
329
+ /** @type {Token} */
330
+ const exprToken = { type: "expression", value: exprMatch[0] };
331
+ tokens.push(exprToken);
276
332
  } else if (text.trim()) {
277
- tokens.push({ type: "text", value: text });
333
+ /** @type {Token} */
334
+ const textToken = { type: "text", value: text };
335
+ tokens.push(textToken);
278
336
  }
279
337
  }
280
338
  }
@@ -288,6 +346,7 @@ function tokenize(template) {
288
346
  * @returns {Object<string, string | boolean>} Parsed attributes object
289
347
  */
290
348
  function parseAttributes(attrsStr) {
349
+ /** @type {Record<string, string | boolean>} */
291
350
  const props = {};
292
351
  const attrRegex = /([a-zA-Z0-9_:-]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
293
352
  let match;
@@ -319,45 +378,69 @@ function resolvePlaceholders(nodes, placeholders) {
319
378
  if (node.type === "element") {
320
379
  // Resolve tag name if it's a placeholder (component)
321
380
  if (typeof node.tag === "string" && node.tag.startsWith("__EXPR_")) {
322
- node.tag = { type: "expression", expr: placeholderMap.get(node.tag) };
381
+ const expr = placeholderMap.get(node.tag);
382
+ if (expr) {
383
+ node.tag = { type: "expression", expr };
384
+ }
323
385
  }
324
386
 
325
387
  // Resolve props, preserving order
388
+ /** @type {Record<string, string | boolean | {type: 'expression', expr: import('acorn').Node}>} */
326
389
  const newProps = {};
327
390
  for (const [key, value] of Object.entries(node.props)) {
328
391
  if (key.startsWith("__EXPR_")) {
329
392
  // Handle spread props (key is the placeholder)
330
- newProps["...spread"] = { type: "expression", expr: placeholderMap.get(key) };
393
+ const expr = placeholderMap.get(key);
394
+ if (expr) {
395
+ newProps["...spread"] = { type: "expression", expr };
396
+ }
331
397
  } else if (typeof value === "string" && placeholderMap.has(value)) {
332
398
  // Single placeholder value
333
- newProps[key] = { type: "expression", expr: placeholderMap.get(value) };
399
+ const expr = placeholderMap.get(value);
400
+ if (expr) {
401
+ newProps[key] = { type: "expression", expr };
402
+ }
334
403
  } else if (typeof value === "string" && value.includes("__EXPR_")) {
335
404
  // Template string with placeholders - create TemplateLiteral
336
405
  const parts = value.split(/(__EXPR_\d+__)/);
406
+ /** @type {Array<import('acorn').TemplateElement>} */
337
407
  const quasis = [];
408
+ /** @type {Array<import('acorn').Expression>} */
338
409
  const expressions = [];
339
410
 
340
411
  for (let i = 0; i < parts.length; i++) {
341
412
  if (i % 2 === 0) {
342
413
  // Static string part
343
- quasis.push({
414
+ /** @type {import('acorn').TemplateElement} */
415
+ const templateElement = {
344
416
  type: "TemplateElement",
417
+ start: 0,
418
+ end: 0,
345
419
  value: { raw: parts[i], cooked: parts[i] },
346
420
  tail: i === parts.length - 1,
347
- });
421
+ };
422
+ quasis.push(templateElement);
348
423
  } else {
349
424
  // Expression placeholder
350
- expressions.push(placeholderMap.get(parts[i]));
425
+ const expr = placeholderMap.get(parts[i]);
426
+ if (expr) {
427
+ expressions.push(/** @type {import('acorn').Expression} */ (expr));
428
+ }
351
429
  }
352
430
  }
353
431
 
432
+ /** @type {import('acorn').TemplateLiteral} */
433
+ const templateLiteral = {
434
+ type: "TemplateLiteral",
435
+ start: 0,
436
+ end: 0,
437
+ quasis,
438
+ expressions,
439
+ };
440
+
354
441
  newProps[key] = {
355
442
  type: "expression",
356
- expr: {
357
- type: "TemplateLiteral",
358
- quasis,
359
- expressions,
360
- },
443
+ expr: templateLiteral,
361
444
  };
362
445
  } else {
363
446
  newProps[key] = value;
@@ -366,7 +449,9 @@ function resolvePlaceholders(nodes, placeholders) {
366
449
  node.props = newProps;
367
450
 
368
451
  // Resolve children
369
- node.children = node.children.map(resolve);
452
+ node.children = /** @type {Array<ElementNode | TextNode | ExpressionNode>} */ (
453
+ node.children.map(resolve).flat()
454
+ );
370
455
  } else if (node.type === "expression") {
371
456
  const expr = placeholderMap.get(node.value);
372
457
  if (expr) {
@@ -376,21 +461,30 @@ function resolvePlaceholders(nodes, placeholders) {
376
461
  // Check if text contains placeholders
377
462
  const parts = node.value.split(/(__EXPR_\d+__)/);
378
463
  if (parts.length > 1) {
379
- return parts
464
+ /** @type {Array<ElementNode | TextNode | ExpressionNode>} */
465
+ const result = parts
380
466
  .filter((p) => p)
381
467
  .map((p) => {
382
468
  if (p.startsWith("__EXPR_")) {
383
- return { type: "expression", expr: placeholderMap.get(p) };
469
+ const expr = placeholderMap.get(p);
470
+ if (expr) {
471
+ /** @type {ExpressionNode} */
472
+ const exprNode = { type: "expression", value: p, expr };
473
+ return exprNode;
474
+ }
384
475
  }
385
- return { type: "text", value: p };
476
+ /** @type {TextNode} */
477
+ const textNode = { type: "text", value: p };
478
+ return textNode;
386
479
  });
480
+ return result;
387
481
  }
388
482
  }
389
483
 
390
484
  return node;
391
485
  }
392
486
 
393
- return nodes.map(resolve).flat();
487
+ return /** @type {Array<ElementNode | TextNode | ExpressionNode>} */ (nodes.map(resolve).flat());
394
488
  }
395
489
 
396
490
  /**
@@ -407,26 +501,36 @@ function needsQuoting(key) {
407
501
  * Converts element nodes to AST nodes representing h() calls
408
502
  * @param {ElementNode | TextNode | ExpressionNode} node - The node to convert
409
503
  * @param {string} pragma - The pragma function name
410
- * @returns {import('acorn').Node} The AST node
504
+ * @returns {import('acorn').Literal | import('acorn').CallExpression | import('acorn').Expression} The AST node
411
505
  */
412
506
  function elementsToAST(node, pragma) {
413
507
  if (node.type === "text") {
414
- return { type: "Literal", value: node.value };
508
+ /** @type {import('acorn').Literal} */
509
+ const literal = { type: "Literal", start: 0, end: 0, value: node.value };
510
+ return literal;
415
511
  }
416
512
 
417
513
  if (node.type === "expression") {
418
- return node.expr;
514
+ return /** @type {import('acorn').Expression} */ (node.expr);
419
515
  }
420
516
 
421
517
  if (node.type === "element") {
422
518
  // Build h(tag, props, ...children)
519
+ /** @type {Array<import('acorn').Expression | import('acorn').SpreadElement>} */
423
520
  const args = [];
424
521
 
425
522
  // Tag argument
426
- if (node.tag.type === "expression") {
427
- args.push(node.tag.expr);
523
+ if (typeof node.tag === "object" && node.tag.type === "expression") {
524
+ args.push(/** @type {import('acorn').Expression} */ (node.tag.expr));
428
525
  } else {
429
- args.push({ type: "Literal", value: node.tag });
526
+ /** @type {import('acorn').Literal} */
527
+ const tagLiteral = {
528
+ type: "Literal",
529
+ start: 0,
530
+ end: 0,
531
+ value: /** @type {string} */ (node.tag),
532
+ };
533
+ args.push(tagLiteral);
430
534
  }
431
535
 
432
536
  // Props argument
@@ -434,54 +538,122 @@ function elementsToAST(node, pragma) {
434
538
  if (propsEntries.length > 0) {
435
539
  const spreadProp = node.props["...spread"];
436
540
 
437
- if (spreadProp && propsEntries.length === 1) {
541
+ if (
542
+ spreadProp &&
543
+ typeof spreadProp === "object" &&
544
+ spreadProp.type === "expression" &&
545
+ propsEntries.length === 1
546
+ ) {
438
547
  // Just spread props
439
- args.push(spreadProp.expr);
440
- } else if (spreadProp) {
548
+ args.push(/** @type {import('acorn').Expression} */ (spreadProp.expr));
549
+ } else if (spreadProp && typeof spreadProp === "object" && spreadProp.type === "expression") {
441
550
  // Merge spread with other props, preserving order
551
+ /** @type {Array<import('acorn').Property | import('acorn').SpreadElement>} */
442
552
  const properties = propsEntries.map(([key, value]) => {
443
- if (key === "...spread") {
444
- return {
553
+ if (key === "...spread" && typeof value === "object" && value.type === "expression") {
554
+ /** @type {import('acorn').SpreadElement} */
555
+ const spread = {
445
556
  type: "SpreadElement",
446
- argument: value.expr,
557
+ start: 0,
558
+ end: 0,
559
+ argument: /** @type {import('acorn').Expression} */ (value.expr),
447
560
  };
561
+ return spread;
448
562
  }
449
- return {
563
+ /** @type {import('acorn').Property} */
564
+ const prop = {
450
565
  type: "Property",
566
+ start: 0,
567
+ end: 0,
451
568
  key: needsQuoting(key)
452
- ? { type: "Literal", value: key }
453
- : { type: "Identifier", name: key },
454
- value: value.type === "expression" ? value.expr : { type: "Literal", value },
569
+ ? /** @type {import('acorn').Literal} */ ({
570
+ type: "Literal",
571
+ start: 0,
572
+ end: 0,
573
+ value: key,
574
+ })
575
+ : /** @type {import('acorn').Identifier} */ ({
576
+ type: "Identifier",
577
+ start: 0,
578
+ end: 0,
579
+ name: key,
580
+ }),
581
+ value:
582
+ typeof value === "object" && value.type === "expression"
583
+ ? /** @type {import('acorn').Expression} */ (value.expr)
584
+ : /** @type {import('acorn').Literal} */ ({
585
+ type: "Literal",
586
+ start: 0,
587
+ end: 0,
588
+ value: /** @type {string | boolean} */ (value),
589
+ }),
455
590
  kind: "init",
456
591
  method: false,
457
592
  shorthand: false,
458
593
  computed: false,
459
594
  };
595
+ return prop;
460
596
  });
461
597
 
462
- args.push({
598
+ /** @type {import('acorn').ObjectExpression} */
599
+ const objExpr = {
463
600
  type: "ObjectExpression",
601
+ start: 0,
602
+ end: 0,
464
603
  properties,
465
- });
604
+ };
605
+ args.push(objExpr);
466
606
  } else {
467
607
  // Normal props
468
- args.push({
469
- type: "ObjectExpression",
470
- properties: propsEntries.map(([key, value]) => ({
608
+ /** @type {Array<import('acorn').Property | import('acorn').SpreadElement>} */
609
+ const properties = propsEntries.map(([key, value]) => {
610
+ /** @type {import('acorn').Property} */
611
+ const prop = {
471
612
  type: "Property",
613
+ start: 0,
614
+ end: 0,
472
615
  key: needsQuoting(key)
473
- ? { type: "Literal", value: key }
474
- : { type: "Identifier", name: key },
475
- value: value.type === "expression" ? value.expr : { type: "Literal", value },
616
+ ? /** @type {import('acorn').Literal} */ ({
617
+ type: "Literal",
618
+ start: 0,
619
+ end: 0,
620
+ value: key,
621
+ })
622
+ : /** @type {import('acorn').Identifier} */ ({
623
+ type: "Identifier",
624
+ start: 0,
625
+ end: 0,
626
+ name: key,
627
+ }),
628
+ value:
629
+ typeof value === "object" && value.type === "expression"
630
+ ? /** @type {import('acorn').Expression} */ (value.expr)
631
+ : /** @type {import('acorn').Literal} */ ({
632
+ type: "Literal",
633
+ start: 0,
634
+ end: 0,
635
+ value: /** @type {string | boolean} */ (value),
636
+ }),
476
637
  kind: "init",
477
638
  method: false,
478
639
  shorthand: false,
479
640
  computed: false,
480
- })),
641
+ };
642
+ return prop;
481
643
  });
644
+ /** @type {import('acorn').ObjectExpression} */
645
+ const objExpr = {
646
+ type: "ObjectExpression",
647
+ start: 0,
648
+ end: 0,
649
+ properties,
650
+ };
651
+ args.push(objExpr);
482
652
  }
483
653
  } else {
484
- args.push({ type: "Literal", value: null });
654
+ /** @type {import('acorn').Literal} */
655
+ const nullLiteral = { type: "Literal", start: 0, end: 0, value: null };
656
+ args.push(nullLiteral);
485
657
  }
486
658
 
487
659
  // Children arguments
@@ -489,12 +661,24 @@ function elementsToAST(node, pragma) {
489
661
  args.push(elementsToAST(child, pragma));
490
662
  }
491
663
 
492
- return {
664
+ /** @type {import('acorn').CallExpression} */
665
+ const callExpr = {
493
666
  type: "CallExpression",
494
- callee: { type: "Identifier", name: pragma },
667
+ start: 0,
668
+ end: 0,
669
+ callee: /** @type {import('acorn').Identifier} */ ({
670
+ type: "Identifier",
671
+ start: 0,
672
+ end: 0,
673
+ name: pragma,
674
+ }),
495
675
  arguments: args,
676
+ optional: false,
496
677
  };
678
+ return callExpr;
497
679
  }
498
680
 
499
- return { type: "Literal", value: null };
681
+ /** @type {import('acorn').Literal} */
682
+ const nullLiteral = { type: "Literal", start: 0, end: 0, value: null };
683
+ return nullLiteral;
500
684
  }
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "htm-transform",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Transform htm tagged templates into h function calls using acorn",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
- "types": "index.js",
7
+ "types": "index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "types": "./index.js",
10
+ "types": "./index.d.ts",
11
11
  "default": "./index.js"
12
12
  }
13
13
  },
14
14
  "scripts": {
15
15
  "test": "node --test test.js",
16
- "test:watch": "node --test --watch test.js"
16
+ "dts": "tsc"
17
17
  },
18
18
  "keywords": [
19
19
  "htm",
@@ -28,5 +28,8 @@
28
28
  "acorn": "^8.11.3",
29
29
  "acorn-walk": "^8.3.2",
30
30
  "astring": "^1.8.6"
31
+ },
32
+ "devDependencies": {
33
+ "typescript": "^5.9.3"
31
34
  }
32
35
  }
package/test.js CHANGED
@@ -390,4 +390,66 @@ const result = myH("div", null, "Test");`;
390
390
 
391
391
  assert.strictEqual(normalize(output), normalize(expected));
392
392
  });
393
+
394
+ test("todo", () => {
395
+ const input = `import html from "~/util/preact/html.js";
396
+ /** @import { JSX } from "preact" */
397
+
398
+ import button from "~/style/button.js";
399
+ import Icon from "./Icon.js";
400
+
401
+ import { adopt } from "~/util/css.js";
402
+ import sheet from "./Button.module.css" with { type: "css" };
403
+ const css = adopt(sheet);
404
+
405
+ /**
406
+ * @typedef {object} Props
407
+ * @property {"primary" | "secondary" | "tertiary"} [level = "secondary"]
408
+ * @property {"neutral" | "positive" | "negative"} [kind = "neutral"]
409
+ * @property {"small" | "base" | "large"} [size = "base"]
410
+ * @property {boolean} [loading]
411
+ * @property {string} [popovertarget]
412
+ * @property {(el: HTMLButtonElement) => void} [innerRef]
413
+ */
414
+
415
+ /** @param {Props & JSX.ButtonHTMLAttributes<HTMLButtonElement>} props */
416
+ export default function Button(props) {
417
+ const {
418
+ level,
419
+ kind = "neutral",
420
+ size = "base",
421
+ loading,
422
+ disabled,
423
+ type = "button",
424
+ children,
425
+ innerRef: ref,
426
+ ...rest
427
+ } = props;
428
+
429
+ return html\`
430
+ <button
431
+ class="\${css.wrapper} \${button.button}"
432
+ data-level=\${level}
433
+ data-kind=\${kind}
434
+ data-size=\${size}
435
+ type=\${type}
436
+ disabled=\${disabled || loading}
437
+ ref=\${ref}
438
+ ...\${rest}
439
+ >
440
+ <span class="\${css.layer} \${css.contents}">\${children}</span>
441
+ \${loading
442
+ ? html\`
443
+ <span class="\${css.layer} \${css.loader}">
444
+ <\${Icon} class="\${css.loader}" name="loader" />
445
+ </span>
446
+ \`
447
+ : null}
448
+ </button>
449
+ \`;
450
+ }
451
+ `;
452
+
453
+ const output = transform(input);
454
+ });
393
455
  });
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "esnext",
4
+ "moduleResolution": "bundler",
5
+ "target": "es2022",
6
+ "lib": ["es2022"],
7
+ "declaration": true,
8
+ "emitDeclarationOnly": true,
9
+ "allowJs": true,
10
+ "checkJs": true,
11
+ "skipLibCheck": true,
12
+ },
13
+ "include": ["index.js"],
14
+ "exclude": ["node_modules", "test.js"],
15
+ }
package/jsconfig.json DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "module": "ESNext",
4
- "moduleResolution": "bundler",
5
- "target": "ES2022",
6
- "checkJs": true,
7
- "strict": true,
8
- "lib": ["ES2022"]
9
- },
10
- "include": ["index.js", "test.js"],
11
- "exclude": ["node_modules"]
12
- }