htm-transform 0.1.4 → 0.1.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.
- package/HostCard.js +283 -0
- package/index.js +70 -7
- package/package.json +2 -2
- package/test.js +300 -308
- package/tsconfig.json +1 -1
package/HostCard.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { Component, createRef } from "preact";
|
|
2
|
+
/** @import { RefObject } from "preact"; */
|
|
3
|
+
|
|
4
|
+
import html from "~/util/preact/html.js";
|
|
5
|
+
|
|
6
|
+
/** @import Core from "~/app/lib/core/core.js" */
|
|
7
|
+
/** @import { Host } from "~/lib/types.js"; */
|
|
8
|
+
|
|
9
|
+
import Button from "~/component/Button.js";
|
|
10
|
+
import Icon from "~/component/Icon.js";
|
|
11
|
+
|
|
12
|
+
import { signal } from "@preact/signals";
|
|
13
|
+
import { ApiError, deleteMachine } from "~/lib/preact/api.js";
|
|
14
|
+
import { alert } from "~/util/preact/alert.js";
|
|
15
|
+
import { persist } from "~/util/preact/persist.js";
|
|
16
|
+
import { PromiseSignal } from "~/util/preact/promise.js";
|
|
17
|
+
import Dropdown from "~/component/Dropdown.js";
|
|
18
|
+
/** @import { DropdownItem } from "~/component/Dropdown.js"; */
|
|
19
|
+
import Modal from "~/component/Modal.js";
|
|
20
|
+
import HelpModal, { promptOnConnection } from "~/app/component/HelpModal.js";
|
|
21
|
+
import Tooltip from "~/component/Tooltip.js";
|
|
22
|
+
/** @import { TriggerProps } from "~/component/Tooltip.js" */
|
|
23
|
+
|
|
24
|
+
import button from "~/style/button.js";
|
|
25
|
+
import util from "~/style/util.js";
|
|
26
|
+
import { adopt } from "~/util/css.js";
|
|
27
|
+
import sheet from "./HostCard.module.css" with { type: "css" };
|
|
28
|
+
import { hosts, me } from "~/lib/preact/queries.js";
|
|
29
|
+
|
|
30
|
+
const css = adopt(sheet);
|
|
31
|
+
|
|
32
|
+
const lastConnected = signal(/** @type {Record<string, number>} */ ({}));
|
|
33
|
+
persist(lastConnected, "last_connected");
|
|
34
|
+
|
|
35
|
+
const dt = new Intl.DateTimeFormat("en-us", {
|
|
36
|
+
dateStyle: "long",
|
|
37
|
+
timeStyle: "short",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {object} Props
|
|
42
|
+
* @prop {Core} core
|
|
43
|
+
* @prop {Host} host
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/** @extends {Component<Props>} */
|
|
47
|
+
export default class HostCard extends Component {
|
|
48
|
+
me = me();
|
|
49
|
+
hosts = hosts();
|
|
50
|
+
deleting = signal(false);
|
|
51
|
+
|
|
52
|
+
/** @type {RefObject<Modal>} */
|
|
53
|
+
modalRef = createRef();
|
|
54
|
+
|
|
55
|
+
onConnect() {
|
|
56
|
+
if (promptOnConnection.value) {
|
|
57
|
+
this.modalRef.current?.open();
|
|
58
|
+
} else {
|
|
59
|
+
this.connect();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
connect() {
|
|
64
|
+
this.props.core.connect(this.props.host);
|
|
65
|
+
lastConnected.value = {
|
|
66
|
+
...lastConnected.value,
|
|
67
|
+
[this.props.host.id]: Date.now(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get lastConnectedAt() {
|
|
72
|
+
const lastConnectedAt = lastConnected.value[this.props.host.id];
|
|
73
|
+
if (lastConnectedAt) return `Last connected on ${dt.format(new Date(lastConnectedAt))}`;
|
|
74
|
+
return "Never connected";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get actions() {
|
|
78
|
+
const host = this.props.host;
|
|
79
|
+
|
|
80
|
+
/** @type {DropdownItem[]} */
|
|
81
|
+
const actions = [
|
|
82
|
+
{
|
|
83
|
+
text: html`Copy ID: <span class=${util.textId}>${host.id.slice(0, 24)}</span>`,
|
|
84
|
+
onSelect: () => {
|
|
85
|
+
navigator.clipboard.writeText(host.id);
|
|
86
|
+
alert(`${host.name} ID copied to the clipboard!`);
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
// we only show the delete button for your own computers
|
|
92
|
+
const canDelete = this.me.data?.id === host.user_id;
|
|
93
|
+
if (canDelete) {
|
|
94
|
+
actions.push({
|
|
95
|
+
text: "Delete",
|
|
96
|
+
icon: "trash",
|
|
97
|
+
disabled: host.is_online, // we only allow you to delete if it's offline
|
|
98
|
+
onSelect: () => {
|
|
99
|
+
this.deleting.value = true;
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return actions;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
render() {
|
|
108
|
+
const { host, core } = this.props;
|
|
109
|
+
const showBuild = core.debug && host.channel !== "" && !host.channel.startsWith("release");
|
|
110
|
+
|
|
111
|
+
return html`
|
|
112
|
+
<div class=${css.hostCard} data-id=${host.id}>
|
|
113
|
+
<span class=${css.icon}>
|
|
114
|
+
<${Icon} name=${host.is_online ? "windows-online" : "windows-offline"} size=${42} />
|
|
115
|
+
</span>
|
|
116
|
+
<div class=${css.info}>
|
|
117
|
+
<div class=${css.title}>
|
|
118
|
+
<h2>${host.name || html`<span style="opacity:0.5;">Unnamed Computer</span>`}</h2>
|
|
119
|
+
<span class=${css.status} data-online=${host.is_online}>
|
|
120
|
+
${host.is_online ? "Online" : "Offline"}
|
|
121
|
+
</span>
|
|
122
|
+
</div>
|
|
123
|
+
<div class=${css.hostinfo}>
|
|
124
|
+
<span>${this.lastConnectedAt}</span>
|
|
125
|
+
${showBuild && html`<code>${host.channel}/${host.version}</code>`}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
<div class=${css.right}>
|
|
129
|
+
${this.renderConnectButton()}
|
|
130
|
+
<${Dropdown}
|
|
131
|
+
id="host-dropdown-${host.id}"
|
|
132
|
+
placement="bottom-end"
|
|
133
|
+
trigger=${html`
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
class=${button.button}
|
|
137
|
+
data-level="tertiary"
|
|
138
|
+
data-size="small"
|
|
139
|
+
popovertarget="host-dropdown-${host.id}"
|
|
140
|
+
>
|
|
141
|
+
<${Icon} name="menu" label="menu" size=${12} />
|
|
142
|
+
</button>
|
|
143
|
+
`}
|
|
144
|
+
items=${this.actions}
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
<${HelpModal} modalRef=${this.modalRef} displayHideCheckbox onClose=${() => this.connect()} />
|
|
149
|
+
<${DeleteModal}
|
|
150
|
+
id=${host.id}
|
|
151
|
+
name=${host.name}
|
|
152
|
+
open=${this.deleting.value}
|
|
153
|
+
onClose=${() => {
|
|
154
|
+
this.deleting.value = false;
|
|
155
|
+
}}
|
|
156
|
+
onDelete=${() => this.hosts.fetch()}
|
|
157
|
+
/>
|
|
158
|
+
`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
renderConnectButton() {
|
|
162
|
+
const { host, core } = this.props;
|
|
163
|
+
if (!host.is_online) return null;
|
|
164
|
+
|
|
165
|
+
const clients = core.clients;
|
|
166
|
+
const connectingToThisHost = clients[host.peer_id]?.status === "connecting";
|
|
167
|
+
|
|
168
|
+
if (connectingToThisHost) {
|
|
169
|
+
return html`
|
|
170
|
+
<${Button}
|
|
171
|
+
level="primary"
|
|
172
|
+
kind="negative"
|
|
173
|
+
onClick=${() => this.props.core.clientWithdrawOffer(host.peer_id)}
|
|
174
|
+
>
|
|
175
|
+
<${Icon} name="loader" />
|
|
176
|
+
Cancel
|
|
177
|
+
<//>
|
|
178
|
+
`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ensure vanity and breaking versions match on both client and host
|
|
182
|
+
const re = /(\d+)\.(\d+)\.\d+(?:-.+)?/;
|
|
183
|
+
const [, clientVanity, clientBreaking] = core.info.version.match(re) ?? [];
|
|
184
|
+
const [, hostVanity, hostBreaking] = host.version.match(re) ?? [];
|
|
185
|
+
const versionsMatch = clientVanity === hostVanity && clientBreaking === hostBreaking;
|
|
186
|
+
|
|
187
|
+
const connectingToAnyHost = Object.values(clients).some((c) => c.status === "connecting");
|
|
188
|
+
const hosting = core.hostGuests.length > 0 && !core.connectWhileHosting;
|
|
189
|
+
|
|
190
|
+
const disabled = !core.connected || hosting || connectingToAnyHost || !versionsMatch;
|
|
191
|
+
|
|
192
|
+
let tooltip = "";
|
|
193
|
+
if (hosting) {
|
|
194
|
+
tooltip = "You can't connect to another computer while guests are connected to you.";
|
|
195
|
+
} else if (!versionsMatch) {
|
|
196
|
+
tooltip = `Your app's version is not compatible with ${host.name}.`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return tooltip
|
|
200
|
+
? html`
|
|
201
|
+
<${Tooltip} contents=${tooltip}>
|
|
202
|
+
${(/** @type {TriggerProps} */ { ref, ...trigger }) =>
|
|
203
|
+
html`<${Button} disabled level="primary" innerRef=${ref} ...${trigger}>Connect<//>`}
|
|
204
|
+
<//>
|
|
205
|
+
`
|
|
206
|
+
: html`
|
|
207
|
+
<${Button}
|
|
208
|
+
level="primary"
|
|
209
|
+
loading=${connectingToThisHost}
|
|
210
|
+
disabled=${disabled}
|
|
211
|
+
onClick=${() => this.onConnect()}
|
|
212
|
+
>
|
|
213
|
+
Connect
|
|
214
|
+
<//>
|
|
215
|
+
`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
class DeleteMachineError extends ApiError {
|
|
220
|
+
/** @override */
|
|
221
|
+
get messages() {
|
|
222
|
+
return {
|
|
223
|
+
[ApiError.noField]: {
|
|
224
|
+
not_found: "Couldn't find this computer. It may have already been deleted.",
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* @typedef {object} DeleteModalProps
|
|
232
|
+
* @prop {string} id
|
|
233
|
+
* @prop {string} name
|
|
234
|
+
* @prop {boolean} open
|
|
235
|
+
* @prop {() => void} onClose
|
|
236
|
+
* @prop {() => void} onDelete
|
|
237
|
+
*/
|
|
238
|
+
|
|
239
|
+
/** @extends {Component<DeleteModalProps>} */
|
|
240
|
+
class DeleteModal extends Component {
|
|
241
|
+
deleteMachine = new PromiseSignal(async () => {
|
|
242
|
+
await deleteMachine(this.props.id);
|
|
243
|
+
alert(`Successfully deleted ${this.props.name}`, { type: "success" });
|
|
244
|
+
this.props.onDelete();
|
|
245
|
+
this.props.onClose();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
render() {
|
|
249
|
+
const { name, open, onClose } = this.props;
|
|
250
|
+
|
|
251
|
+
const error = this.deleteMachine.error && DeleteMachineError.from(this.deleteMachine.error);
|
|
252
|
+
|
|
253
|
+
return html`
|
|
254
|
+
<${Modal} title="Delete ${name}?" open=${open} onClose=${onClose}>
|
|
255
|
+
<div class=${css.deleteModal}>
|
|
256
|
+
<p>You'll be able to add ${name} again by opening Phaze on that computer.</p>
|
|
257
|
+
<p class=${css.error}>${error?.message}</p>
|
|
258
|
+
<form
|
|
259
|
+
class=${css.actions}
|
|
260
|
+
onSubmit=${(/** @type {SubmitEvent} */ ev) => {
|
|
261
|
+
if (ev.submitter?.getAttribute("formmethod") === "dialog") return;
|
|
262
|
+
ev.preventDefault();
|
|
263
|
+
this.deleteMachine.invoke();
|
|
264
|
+
}}
|
|
265
|
+
>
|
|
266
|
+
<${Button}
|
|
267
|
+
type="submit"
|
|
268
|
+
level="primary"
|
|
269
|
+
kind="negative"
|
|
270
|
+
value="delete"
|
|
271
|
+
loading=${this.deleteMachine.loading}
|
|
272
|
+
>
|
|
273
|
+
Delete
|
|
274
|
+
<//>
|
|
275
|
+
<${Button} type="submit" formmethod="dialog" disabled=${this.deleteMachine.loading}>
|
|
276
|
+
Cancel
|
|
277
|
+
<//>
|
|
278
|
+
</form>
|
|
279
|
+
</div>
|
|
280
|
+
<//>
|
|
281
|
+
`;
|
|
282
|
+
}
|
|
283
|
+
}
|
package/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as acorn from "acorn";
|
|
2
2
|
import { generate } from "astring";
|
|
3
|
-
import {
|
|
3
|
+
import { attachComments, makeTraveler } from "astravel";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @typedef {Object} ElementNode
|
|
@@ -46,40 +46,94 @@ 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
|
+
/** @type {Array<import('acorn').Comment>} */
|
|
50
|
+
const comments = [];
|
|
51
|
+
|
|
49
52
|
const ast = /** @type {import('acorn').Program} */ (
|
|
50
53
|
acorn.parse(code, {
|
|
51
54
|
ecmaVersion: "latest",
|
|
52
55
|
sourceType: "module",
|
|
56
|
+
locations: true,
|
|
57
|
+
onComment: comments,
|
|
53
58
|
})
|
|
54
59
|
);
|
|
55
60
|
|
|
56
61
|
let hasTransformation = false;
|
|
57
62
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Recursively sets location information on all nodes in an AST subtree
|
|
65
|
+
* @param {any} node - The AST node to process
|
|
66
|
+
* @param {number} start - The start position
|
|
67
|
+
* @param {number} end - The end position
|
|
68
|
+
* @param {any} loc - The loc object with line/column info
|
|
69
|
+
*/
|
|
70
|
+
function setLocationRecursive(node, start, end, loc) {
|
|
71
|
+
if (!node || typeof node !== 'object') return;
|
|
72
|
+
|
|
73
|
+
node.start = start;
|
|
74
|
+
node.end = end;
|
|
75
|
+
if (loc) {
|
|
76
|
+
node.loc = loc;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Recursively set location for all child nodes
|
|
80
|
+
for (const key in node) {
|
|
81
|
+
const value = node[key];
|
|
82
|
+
if (Array.isArray(value)) {
|
|
83
|
+
for (const child of value) {
|
|
84
|
+
if (child && typeof child === 'object' && child.type) {
|
|
85
|
+
setLocationRecursive(child, start, end, loc);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} else if (value && typeof value === 'object' && value.type) {
|
|
89
|
+
setLocationRecursive(value, start, end, loc);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const traveler = makeTraveler({
|
|
95
|
+
TaggedTemplateExpression(node, state) {
|
|
63
96
|
if (node.tag.type === "Identifier" && node.tag.name === tagName) {
|
|
64
97
|
hasTransformation = true;
|
|
65
98
|
const transformed = transformTaggedTemplate(node, pragma);
|
|
66
99
|
|
|
100
|
+
// Preserve original location information
|
|
101
|
+
const originalStart = node.start;
|
|
102
|
+
const originalEnd = node.end;
|
|
103
|
+
const originalLoc = node.loc;
|
|
104
|
+
|
|
105
|
+
// Set location info recursively on transformed tree
|
|
106
|
+
setLocationRecursive(transformed, originalStart, originalEnd, originalLoc);
|
|
107
|
+
|
|
67
108
|
// Replace the node with the transformed version
|
|
68
109
|
const mutableNode = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (node));
|
|
69
110
|
for (const key in mutableNode) {
|
|
70
111
|
delete mutableNode[key];
|
|
71
112
|
}
|
|
72
113
|
Object.assign(node, transformed);
|
|
114
|
+
|
|
115
|
+
// Continue traversing into the transformed node to handle nested templates
|
|
116
|
+
this.go(node, state);
|
|
117
|
+
return;
|
|
73
118
|
}
|
|
119
|
+
// Continue traversal for non-matching nodes
|
|
120
|
+
this.super.TaggedTemplateExpression.call(this, node, state);
|
|
74
121
|
},
|
|
75
122
|
});
|
|
76
123
|
|
|
124
|
+
traveler.go(ast);
|
|
125
|
+
|
|
77
126
|
// Add import statement if specified and transformation occurred
|
|
78
127
|
if (hasTransformation && importConfig?.from && importConfig?.name) {
|
|
79
128
|
addImportDeclaration(ast, importConfig.from, importConfig.name);
|
|
80
129
|
}
|
|
81
130
|
|
|
82
|
-
|
|
131
|
+
// Attach comments to AST nodes
|
|
132
|
+
if (comments.length > 0) {
|
|
133
|
+
attachComments(ast, comments);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return generate(ast, { comments: true });
|
|
83
137
|
}
|
|
84
138
|
|
|
85
139
|
/**
|
|
@@ -286,6 +340,15 @@ function tokenize(template) {
|
|
|
286
340
|
while (i < template.length) {
|
|
287
341
|
// Check for tag
|
|
288
342
|
if (template[i] === "<") {
|
|
343
|
+
// Check for empty closing tag <//>
|
|
344
|
+
if (template.slice(i, i + 4) === "<//>") {
|
|
345
|
+
/** @type {Token} */
|
|
346
|
+
const closeToken = { type: "closeTag", tag: "" };
|
|
347
|
+
tokens.push(closeToken);
|
|
348
|
+
i += 4;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
289
352
|
const tagMatch = template.slice(i).match(/^<(\/?)([a-zA-Z0-9_${}]+)([^>]*?)(\/?)>/);
|
|
290
353
|
|
|
291
354
|
if (tagMatch) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "htm-transform",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Transform htm tagged templates into h function calls using acorn",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"license": "MPL-2.0",
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"acorn": "^8.11.3",
|
|
29
|
-
"
|
|
29
|
+
"astravel": "^0.6.1",
|
|
30
30
|
"astring": "^1.8.6"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
package/test.js
CHANGED
|
@@ -7,449 +7,441 @@ function normalize(str) {
|
|
|
7
7
|
return str.replace(/\s+/g, " ").trim();
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const expected = `const result = h("h1", {
|
|
10
|
+
test("transforms simple element", () => {
|
|
11
|
+
const input = `const result = html\`<h1 id=hello>Hello world!</h1>\`;`;
|
|
12
|
+
const output = transform(input);
|
|
13
|
+
const expected = `const result = h("h1", {
|
|
15
14
|
id: "hello"
|
|
16
15
|
}, "Hello world!");`;
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
18
|
+
});
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
test("transforms element with dynamic class and content", () => {
|
|
21
|
+
const input = `const result = html\`<div class=\${className}>\${content}</div>\`;`;
|
|
22
|
+
const output = transform(input);
|
|
23
|
+
const expected = `const result = h("div", {
|
|
25
24
|
class: className
|
|
26
25
|
}, content);`;
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
28
|
+
});
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
test("transforms component with props", () => {
|
|
31
|
+
const input = `const result = html\`<\${Header} name="ToDo's" />\`;`;
|
|
32
|
+
const output = transform(input);
|
|
33
|
+
const expected = `const result = h(Header, {
|
|
35
34
|
name: "ToDo's"
|
|
36
35
|
});`;
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
38
|
+
});
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
test("transforms multiple root elements into array", () => {
|
|
41
|
+
const input = `const result = html\`
|
|
43
42
|
<h1 id=hello>Hello</h1>
|
|
44
43
|
<div class=world>World!</div>
|
|
45
44
|
\`;`;
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
const output = transform(input);
|
|
46
|
+
const expected = `const result = [h("h1", {
|
|
48
47
|
id: "hello"
|
|
49
48
|
}, "Hello"), h("div", {
|
|
50
49
|
class: "world"
|
|
51
50
|
}, "World!")];`;
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
53
|
+
});
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
test("transforms nested elements", () => {
|
|
56
|
+
const input = `const result = html\`<div><p>Hello</p><p>World</p></div>\`;`;
|
|
57
|
+
const output = transform(input);
|
|
58
|
+
const expected = `const result = h("div", null, h("p", null, "Hello"), h("p", null, "World"));`;
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
61
|
+
});
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
test("transforms spread props", () => {
|
|
64
|
+
const input = `const result = html\`<div ...\${props}>Content</div>\`;`;
|
|
65
|
+
const output = transform(input);
|
|
66
|
+
const expected = `const result = h("div", props, "Content");`;
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
69
|
+
});
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
test("transforms mixed props and spread", () => {
|
|
72
|
+
const input = `const result = html\`<div class="test" ...\${props}>Content</div>\`;`;
|
|
73
|
+
const output = transform(input);
|
|
74
|
+
const expected = `const result = h("div", {
|
|
76
75
|
class: "test",
|
|
77
76
|
...props
|
|
78
77
|
}, "Content");`;
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
80
|
+
});
|
|
82
81
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
test("transforms self-closing tags", () => {
|
|
83
|
+
const input = `const result = html\`<img src="test.jpg" alt="Test" />\`;`;
|
|
84
|
+
const output = transform(input);
|
|
85
|
+
const expected = `const result = h("img", {
|
|
87
86
|
src: "test.jpg",
|
|
88
87
|
alt: "Test"
|
|
89
88
|
});`;
|
|
90
89
|
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
91
|
+
});
|
|
93
92
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
test("transforms elements with boolean attributes", () => {
|
|
94
|
+
const input = `const result = html\`<input type="checkbox" checked disabled />\`;`;
|
|
95
|
+
const output = transform(input);
|
|
96
|
+
const expected = `const result = h("input", {
|
|
98
97
|
type: "checkbox",
|
|
99
98
|
checked: true,
|
|
100
99
|
disabled: true
|
|
101
100
|
});`;
|
|
102
101
|
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
103
|
+
});
|
|
105
104
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
test("preserves other code unchanged", () => {
|
|
106
|
+
const input = `const foo = 'bar';\nconst result = html\`<div>Test</div>\`;\nconsole.log(foo);`;
|
|
107
|
+
const output = transform(input);
|
|
108
|
+
const expected = `const foo = 'bar';
|
|
110
109
|
const result = h("div", null, "Test");
|
|
111
110
|
console.log(foo);`;
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
test("handles custom pragma option", () => {
|
|
117
|
-
const input = `const result = html\`<div>Test</div>\`;`;
|
|
118
|
-
const output = transform(input, { pragma: "React.createElement" });
|
|
119
|
-
const expected = `const result = React.createElement("div", null, "Test");`;
|
|
112
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
113
|
+
});
|
|
120
114
|
|
|
121
|
-
|
|
122
|
-
|
|
115
|
+
test("handles custom pragma option", () => {
|
|
116
|
+
const input = `const result = html\`<div>Test</div>\`;`;
|
|
117
|
+
const output = transform(input, { pragma: "React.createElement" });
|
|
118
|
+
const expected = `const result = React.createElement("div", null, "Test");`;
|
|
123
119
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const output = transform(input, { tag: "htm" });
|
|
127
|
-
const expected = `const result = h("div", null, "Test");`;
|
|
120
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
121
|
+
});
|
|
128
122
|
|
|
129
|
-
|
|
130
|
-
|
|
123
|
+
test("handles custom tag name option", () => {
|
|
124
|
+
const input = `const result = htm\`<div>Test</div>\`;`;
|
|
125
|
+
const output = transform(input, { tag: "htm" });
|
|
126
|
+
const expected = `const result = h("div", null, "Test");`;
|
|
131
127
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const output = transform(input);
|
|
135
|
-
const expected = `const result = h("ul", null, items.map(i => h("li", null, i)));`;
|
|
128
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
129
|
+
});
|
|
136
130
|
|
|
137
|
-
|
|
138
|
-
})
|
|
131
|
+
test("transforms element with multiple dynamic children", () => {
|
|
132
|
+
const input = `const result = html\`<ul>\${items.map(i => html\`<li>\${i}</li>\`)}</ul>\`;`;
|
|
133
|
+
const output = transform(input);
|
|
134
|
+
const expected = `const result = h("ul", null, items.map(i => h("li", null, i)));`;
|
|
139
135
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const output = transform(input);
|
|
143
|
-
const expected = `const result = h(Wrapper, null, "Content");`;
|
|
136
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
137
|
+
});
|
|
144
138
|
|
|
145
|
-
|
|
146
|
-
}
|
|
139
|
+
test("transforms component with closing tag", () => {
|
|
140
|
+
const input = `const result = html\`<\${Wrapper}>Content</\${Wrapper}>\`;`;
|
|
141
|
+
const output = transform(input);
|
|
142
|
+
const expected = `const result = h(Wrapper, null, "Content");`;
|
|
147
143
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const output = transform(input);
|
|
151
|
-
const expected = `const result = h("div", null);`;
|
|
144
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
145
|
+
});
|
|
152
146
|
|
|
153
|
-
|
|
154
|
-
|
|
147
|
+
test("transforms empty element", () => {
|
|
148
|
+
const input = `const result = html\`<div></div>\`;`;
|
|
149
|
+
const output = transform(input);
|
|
150
|
+
const expected = `const result = h("div", null);`;
|
|
155
151
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const output = transform(input);
|
|
159
|
-
const expected = `const result = h("div", null, "Test");`;
|
|
152
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
153
|
+
});
|
|
160
154
|
|
|
161
|
-
|
|
162
|
-
|
|
155
|
+
test("does not add import when import option is omitted", () => {
|
|
156
|
+
const input = `const result = html\`<div>Test</div>\`;`;
|
|
157
|
+
const output = transform(input);
|
|
158
|
+
const expected = `const result = h("div", null, "Test");`;
|
|
163
159
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const output = transform(input, {
|
|
167
|
-
import: { from: "preact", name: "h" },
|
|
168
|
-
});
|
|
169
|
-
const expected = `import {h} from "preact";
|
|
170
|
-
const result = h("div", null, "Test");`;
|
|
160
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
161
|
+
});
|
|
171
162
|
|
|
172
|
-
|
|
163
|
+
test("adds import statement when import config is specified", () => {
|
|
164
|
+
const input = `const result = html\`<div>Test</div>\`;`;
|
|
165
|
+
const output = transform(input, {
|
|
166
|
+
import: { from: "preact", name: "h" },
|
|
173
167
|
});
|
|
168
|
+
const expected = `import {h} from "preact";
|
|
169
|
+
const result = h("div", null, "Test");`;
|
|
174
170
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const output = transform(input, {
|
|
178
|
-
pragma: "createElement",
|
|
179
|
-
import: { from: "react", name: "createElement" },
|
|
180
|
-
});
|
|
181
|
-
const expected = `import {createElement} from "react";
|
|
182
|
-
const result = createElement("div", null, "Test");`;
|
|
171
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
172
|
+
});
|
|
183
173
|
|
|
184
|
-
|
|
174
|
+
test("adds import for React.createElement", () => {
|
|
175
|
+
const input = `const result = html\`<div>Test</div>\`;`;
|
|
176
|
+
const output = transform(input, {
|
|
177
|
+
pragma: "createElement",
|
|
178
|
+
import: { from: "react", name: "createElement" },
|
|
185
179
|
});
|
|
180
|
+
const expected = `import {createElement} from "react";
|
|
181
|
+
const result = createElement("div", null, "Test");`;
|
|
186
182
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const output = transform(input, {
|
|
190
|
-
import: { from: "preact" },
|
|
191
|
-
});
|
|
192
|
-
const expected = `const result = h("div", null, "Test");`;
|
|
183
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
184
|
+
});
|
|
193
185
|
|
|
194
|
-
|
|
186
|
+
test("does not add import when only from is specified", () => {
|
|
187
|
+
const input = `const result = html\`<div>Test</div>\`;`;
|
|
188
|
+
const output = transform(input, {
|
|
189
|
+
import: { from: "preact" },
|
|
195
190
|
});
|
|
191
|
+
const expected = `const result = h("div", null, "Test");`;
|
|
196
192
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const output = transform(input, {
|
|
200
|
-
import: { name: "h" },
|
|
201
|
-
});
|
|
202
|
-
const expected = `const result = h("div", null, "Test");`;
|
|
193
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
194
|
+
});
|
|
203
195
|
|
|
204
|
-
|
|
196
|
+
test("does not add import when only name is specified", () => {
|
|
197
|
+
const input = `const result = html\`<div>Test</div>\`;`;
|
|
198
|
+
const output = transform(input, {
|
|
199
|
+
import: { name: "h" },
|
|
205
200
|
});
|
|
201
|
+
const expected = `const result = h("div", null, "Test");`;
|
|
202
|
+
|
|
203
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
204
|
+
});
|
|
206
205
|
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
test("does not duplicate existing import", () => {
|
|
207
|
+
const input = `import { h } from 'preact';
|
|
209
208
|
const result = html\`<div>Test</div>\`;`;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
209
|
+
const output = transform(input, {
|
|
210
|
+
import: { from: "preact", name: "h" },
|
|
211
|
+
});
|
|
212
|
+
const expected = `import {h} from 'preact';
|
|
214
213
|
const result = h("div", null, "Test");`;
|
|
215
214
|
|
|
216
|
-
|
|
217
|
-
|
|
215
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
216
|
+
});
|
|
218
217
|
|
|
219
|
-
|
|
220
|
-
|
|
218
|
+
test("adds import before existing imports", () => {
|
|
219
|
+
const input = `import { useState } from 'preact/hooks';
|
|
221
220
|
const result = html\`<div>Test</div>\`;`;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
221
|
+
const output = transform(input, {
|
|
222
|
+
import: { from: "preact", name: "h" },
|
|
223
|
+
});
|
|
224
|
+
const expected = `import {h} from "preact";
|
|
226
225
|
import {useState} from 'preact/hooks';
|
|
227
226
|
const result = h("div", null, "Test");`;
|
|
228
227
|
|
|
229
|
-
|
|
230
|
-
|
|
228
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
229
|
+
});
|
|
231
230
|
|
|
232
|
-
|
|
233
|
-
|
|
231
|
+
test("adds import before code when no imports exist", () => {
|
|
232
|
+
const input = `const foo = 'bar';
|
|
234
233
|
const result = html\`<div>Test</div>\`;`;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
234
|
+
const output = transform(input, {
|
|
235
|
+
import: { from: "preact", name: "h" },
|
|
236
|
+
});
|
|
237
|
+
const expected = `import {h} from "preact";
|
|
239
238
|
const foo = 'bar';
|
|
240
239
|
const result = h("div", null, "Test");`;
|
|
241
240
|
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
242
|
+
});
|
|
244
243
|
|
|
245
|
-
|
|
246
|
-
|
|
244
|
+
test("does not duplicate import when importing same export from same module", () => {
|
|
245
|
+
const input = `import { h } from 'preact';
|
|
247
246
|
import { render } from 'preact';
|
|
248
247
|
const result = html\`<div>Test</div>\`;`;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
248
|
+
const output = transform(input, {
|
|
249
|
+
import: { from: "preact", name: "h" },
|
|
250
|
+
});
|
|
251
|
+
const expected = `import {h} from 'preact';
|
|
253
252
|
import {render} from 'preact';
|
|
254
253
|
const result = h("div", null, "Test");`;
|
|
255
254
|
|
|
256
|
-
|
|
257
|
-
|
|
255
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
256
|
+
});
|
|
258
257
|
|
|
259
|
-
|
|
260
|
-
|
|
258
|
+
test("adds import even if same module is imported with different exports", () => {
|
|
259
|
+
const input = `import { render } from 'preact';
|
|
261
260
|
const result = html\`<div>Test</div>\`;`;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
261
|
+
const output = transform(input, {
|
|
262
|
+
import: { from: "preact", name: "h" },
|
|
263
|
+
});
|
|
264
|
+
const expected = `import {h} from "preact";
|
|
266
265
|
import {render} from 'preact';
|
|
267
266
|
const result = h("div", null, "Test");`;
|
|
268
267
|
|
|
269
|
-
|
|
270
|
-
|
|
268
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
269
|
+
});
|
|
271
270
|
|
|
272
|
-
|
|
273
|
-
|
|
271
|
+
test("handles multiple htm transformations with import", () => {
|
|
272
|
+
const input = `const a = html\`<div>A</div>\`;
|
|
274
273
|
const b = html\`<span>B</span>\`;`;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
274
|
+
const output = transform(input, {
|
|
275
|
+
import: { from: "preact", name: "h" },
|
|
276
|
+
});
|
|
277
|
+
const expected = `import {h} from "preact";
|
|
279
278
|
const a = h("div", null, "A");
|
|
280
279
|
const b = h("span", null, "B");`;
|
|
281
280
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
test("works with different module paths", () => {
|
|
286
|
-
const input = `const result = html\`<div>Test</div>\`;`;
|
|
287
|
-
const output = transform(input, {
|
|
288
|
-
import: { from: "preact/compat", name: "h" },
|
|
289
|
-
});
|
|
290
|
-
const expected = `import {h} from "preact/compat";
|
|
291
|
-
const result = h("div", null, "Test");`;
|
|
281
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
282
|
+
});
|
|
292
283
|
|
|
293
|
-
|
|
284
|
+
test("works with different module paths", () => {
|
|
285
|
+
const input = `const result = html\`<div>Test</div>\`;`;
|
|
286
|
+
const output = transform(input, {
|
|
287
|
+
import: { from: "preact/compat", name: "h" },
|
|
294
288
|
});
|
|
295
|
-
|
|
296
|
-
test("works with scoped packages", () => {
|
|
297
|
-
const input = `const result = html\`<div>Test</div>\`;`;
|
|
298
|
-
const output = transform(input, {
|
|
299
|
-
import: { from: "@preact/signals", name: "h" },
|
|
300
|
-
});
|
|
301
|
-
const expected = `import {h} from "@preact/signals";
|
|
289
|
+
const expected = `import {h} from "preact/compat";
|
|
302
290
|
const result = h("div", null, "Test");`;
|
|
303
291
|
|
|
304
|
-
|
|
292
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("works with scoped packages", () => {
|
|
296
|
+
const input = `const result = html\`<div>Test</div>\`;`;
|
|
297
|
+
const output = transform(input, {
|
|
298
|
+
import: { from: "@preact/signals", name: "h" },
|
|
305
299
|
});
|
|
300
|
+
const expected = `import {h} from "@preact/signals";
|
|
301
|
+
const result = h("div", null, "Test");`;
|
|
306
302
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const output = transform(input, {
|
|
310
|
-
pragma: "myH",
|
|
311
|
-
import: { from: "my-library", name: "myH" },
|
|
312
|
-
});
|
|
313
|
-
const expected = `import {myH} from "my-library";
|
|
314
|
-
const result = myH("div", null, "Test");`;
|
|
303
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
304
|
+
});
|
|
315
305
|
|
|
316
|
-
|
|
306
|
+
test("adds import with custom pragma matching name", () => {
|
|
307
|
+
const input = `const result = html\`<div>Test</div>\`;`;
|
|
308
|
+
const output = transform(input, {
|
|
309
|
+
pragma: "myH",
|
|
310
|
+
import: { from: "my-library", name: "myH" },
|
|
317
311
|
});
|
|
312
|
+
const expected = `import {myH} from "my-library";
|
|
313
|
+
const result = myH("div", null, "Test");`;
|
|
318
314
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const output = transform(input, {
|
|
322
|
-
import: { from: "preact", name: "h" },
|
|
323
|
-
});
|
|
324
|
-
const expected = ``;
|
|
315
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
316
|
+
});
|
|
325
317
|
|
|
326
|
-
|
|
318
|
+
test("handles empty file with import config", () => {
|
|
319
|
+
const input = ``;
|
|
320
|
+
const output = transform(input, {
|
|
321
|
+
import: { from: "preact", name: "h" },
|
|
327
322
|
});
|
|
323
|
+
const expected = ``;
|
|
328
324
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const output = transform(input, {
|
|
332
|
-
import: { from: "preact", name: "h" },
|
|
333
|
-
});
|
|
334
|
-
const expected = `import {useState} from 'preact/hooks';`;
|
|
325
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
326
|
+
});
|
|
335
327
|
|
|
336
|
-
|
|
328
|
+
test("handles file with only imports", () => {
|
|
329
|
+
const input = `import { useState } from 'preact/hooks';`;
|
|
330
|
+
const output = transform(input, {
|
|
331
|
+
import: { from: "preact", name: "h" },
|
|
337
332
|
});
|
|
333
|
+
const expected = `import {useState} from 'preact/hooks';`;
|
|
338
334
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
335
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("spread at beginning means later props override", () => {
|
|
339
|
+
const input = `const result = html\`<div ...\${props} class="test">Content</div>\`;`;
|
|
340
|
+
const output = transform(input);
|
|
341
|
+
const expected = `const result = h("div", {
|
|
343
342
|
...props,
|
|
344
343
|
class: "test"
|
|
345
344
|
}, "Content");`;
|
|
346
345
|
|
|
347
|
-
|
|
348
|
-
|
|
346
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
347
|
+
});
|
|
349
348
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
349
|
+
test("spread between props preserves order", () => {
|
|
350
|
+
const input = `const result = html\`<div id="foo" ...\${props} class="test">Content</div>\`;`;
|
|
351
|
+
const output = transform(input);
|
|
352
|
+
const expected = `const result = h("div", {
|
|
354
353
|
id: "foo",
|
|
355
354
|
...props,
|
|
356
355
|
class: "test"
|
|
357
356
|
}, "Content");`;
|
|
358
357
|
|
|
359
|
-
|
|
360
|
-
|
|
358
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
359
|
+
});
|
|
361
360
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
361
|
+
test("handles attributes with hyphens", () => {
|
|
362
|
+
const input = `const result = html\`<div data-value="test" aria-label="label">Content</div>\`;`;
|
|
363
|
+
const output = transform(input);
|
|
364
|
+
const expected = `const result = h("div", {
|
|
366
365
|
"data-value": "test",
|
|
367
366
|
"aria-label": "label"
|
|
368
367
|
}, "Content");`;
|
|
369
368
|
|
|
370
|
-
|
|
371
|
-
|
|
369
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
370
|
+
});
|
|
372
371
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
372
|
+
test("handles dynamic attributes with hyphens", () => {
|
|
373
|
+
const input = `const result = html\`<div data-id=\${id} aria-label=\${label}>Content</div>\`;`;
|
|
374
|
+
const output = transform(input);
|
|
375
|
+
const expected = `const result = h("div", {
|
|
377
376
|
"data-id": id,
|
|
378
377
|
"aria-label": label
|
|
379
378
|
}, "Content");`;
|
|
380
379
|
|
|
381
|
-
|
|
382
|
-
|
|
380
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
381
|
+
});
|
|
383
382
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
383
|
+
test("handles template literals in attribute values", () => {
|
|
384
|
+
const input = `const result = html\`<div class="\${css.foo} \${css.bar}">Test</div>\`;`;
|
|
385
|
+
const output = transform(input);
|
|
386
|
+
const expected = `const result = h("div", {
|
|
388
387
|
class: \`\${css.foo} \${css.bar}\`
|
|
389
388
|
}, "Test");`;
|
|
390
389
|
|
|
391
|
-
|
|
392
|
-
|
|
390
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
391
|
+
});
|
|
393
392
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
</button>
|
|
449
|
-
\`;
|
|
450
|
-
}
|
|
451
|
-
`;
|
|
452
|
-
|
|
453
|
-
const output = transform(input);
|
|
454
|
-
});
|
|
393
|
+
test("preserves comments", () => {
|
|
394
|
+
const input = `// This is a comment
|
|
395
|
+
const x = html\`<div>Hello</div>\`;
|
|
396
|
+
// Another comment
|
|
397
|
+
const y = 42;`;
|
|
398
|
+
const output = transform(input);
|
|
399
|
+
const expected = `// This is a comment
|
|
400
|
+
const x = h("div", null, "Hello");
|
|
401
|
+
// Another comment
|
|
402
|
+
const y = 42;`;
|
|
403
|
+
|
|
404
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("handles nested html in logical OR expression", () => {
|
|
408
|
+
const input = `const result = html\`<div>\${name || html\`<span>Default</span>\`}</div>\`;`;
|
|
409
|
+
const output = transform(input);
|
|
410
|
+
const expected = `const result = h("div", null, name || h("span", null, "Default"));`;
|
|
411
|
+
|
|
412
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("handles empty closing tag shorthand", () => {
|
|
416
|
+
const input = `const result = html\`<\${Button}>Click<//>\`;`;
|
|
417
|
+
const output = transform(input);
|
|
418
|
+
const expected = `const result = h(Button, null, "Click");`;
|
|
419
|
+
|
|
420
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("handles html template in array with JSDoc comment", () => {
|
|
424
|
+
const input = `class Foo {
|
|
425
|
+
get actions() {
|
|
426
|
+
/** @type {Action[]} */
|
|
427
|
+
const actions = [
|
|
428
|
+
{
|
|
429
|
+
text: html\`<span>Test</span>\`,
|
|
430
|
+
},
|
|
431
|
+
];
|
|
432
|
+
|
|
433
|
+
actions.push({
|
|
434
|
+
text: "Do the Thing",
|
|
435
|
+
disabled: item.foo, // comment on same line
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
return actions;
|
|
439
|
+
}
|
|
440
|
+
}`;
|
|
441
|
+
const output = transform(input);
|
|
442
|
+
|
|
443
|
+
// Should transform the html template without losing location info that causes attachComments to fail
|
|
444
|
+
assert.ok(output.includes('h("span", null, "Test")'));
|
|
445
|
+
assert.ok(output.includes("/** @type"));
|
|
446
|
+
assert.ok(output.includes("// comment on same line"));
|
|
455
447
|
});
|