htm-transform 0.1.6 → 0.1.7
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.js +36 -5
- package/package.json +1 -1
- package/test.js +71 -19
- package/HostCard.js +0 -283
package/index.js
CHANGED
|
@@ -68,7 +68,7 @@ export default function transform(code, options = {}) {
|
|
|
68
68
|
* @param {any} loc - The loc object with line/column info
|
|
69
69
|
*/
|
|
70
70
|
function setLocationRecursive(node, start, end, loc) {
|
|
71
|
-
if (!node || typeof node !==
|
|
71
|
+
if (!node || typeof node !== "object" || !node.type) return;
|
|
72
72
|
|
|
73
73
|
node.start = start;
|
|
74
74
|
node.end = end;
|
|
@@ -81,17 +81,24 @@ export default function transform(code, options = {}) {
|
|
|
81
81
|
const value = node[key];
|
|
82
82
|
if (Array.isArray(value)) {
|
|
83
83
|
for (const child of value) {
|
|
84
|
-
if (child && typeof child ===
|
|
84
|
+
if (child && typeof child === "object" && child.type) {
|
|
85
85
|
setLocationRecursive(child, start, end, loc);
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
-
} else if (value && typeof value ===
|
|
88
|
+
} else if (value && typeof value === "object" && value.type) {
|
|
89
89
|
setLocationRecursive(value, start, end, loc);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
const traveler = makeTraveler({
|
|
95
|
+
PropertyDefinition(node, state) {
|
|
96
|
+
// Skip private fields with null values (astravel can't handle them)
|
|
97
|
+
if (node.value === null) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.super.PropertyDefinition.call(this, node, state);
|
|
101
|
+
},
|
|
95
102
|
TaggedTemplateExpression(node, state) {
|
|
96
103
|
if (node.tag.type === "Identifier" && node.tag.name === tagName) {
|
|
97
104
|
hasTransformation = true;
|
|
@@ -112,8 +119,12 @@ export default function transform(code, options = {}) {
|
|
|
112
119
|
}
|
|
113
120
|
Object.assign(node, transformed);
|
|
114
121
|
|
|
115
|
-
//
|
|
116
|
-
|
|
122
|
+
// Manually traverse child nodes for nested html templates
|
|
123
|
+
// We use the default traversal for the new node type
|
|
124
|
+
const newNodeType = transformed.type;
|
|
125
|
+
if (this.super[newNodeType]) {
|
|
126
|
+
this.super[newNodeType].call(this, node, state);
|
|
127
|
+
}
|
|
117
128
|
return;
|
|
118
129
|
}
|
|
119
130
|
// Continue traversal for non-matching nodes
|
|
@@ -129,8 +140,28 @@ export default function transform(code, options = {}) {
|
|
|
129
140
|
}
|
|
130
141
|
|
|
131
142
|
// Attach comments to AST nodes
|
|
143
|
+
// Workaround: attachComments can't handle private fields with null values
|
|
144
|
+
// Temporarily mark them so we can restore them after
|
|
132
145
|
if (comments.length > 0) {
|
|
146
|
+
const nullFields = [];
|
|
147
|
+
const markNullFields = makeTraveler({
|
|
148
|
+
PropertyDefinition(node) {
|
|
149
|
+
if (node.value === null) {
|
|
150
|
+
nullFields.push(node);
|
|
151
|
+
// Create a placeholder literal so astravel can traverse it
|
|
152
|
+
node.value = /** @type {any} */ ({ type: 'Literal', value: null, start: node.start, end: node.end, loc: node.loc });
|
|
153
|
+
}
|
|
154
|
+
this.super.PropertyDefinition.call(this, node);
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
markNullFields.go(ast);
|
|
158
|
+
|
|
133
159
|
attachComments(ast, comments);
|
|
160
|
+
|
|
161
|
+
// Restore null values
|
|
162
|
+
for (const node of nullFields) {
|
|
163
|
+
node.value = null;
|
|
164
|
+
}
|
|
134
165
|
}
|
|
135
166
|
|
|
136
167
|
return generate(ast, { comments: true });
|
package/package.json
CHANGED
package/test.js
CHANGED
|
@@ -420,28 +420,80 @@ test("handles empty closing tag shorthand", () => {
|
|
|
420
420
|
assert.strictEqual(normalize(output), normalize(expected));
|
|
421
421
|
});
|
|
422
422
|
|
|
423
|
-
test("
|
|
423
|
+
test("preserves location info when html template is in array with inline comment", () => {
|
|
424
|
+
const input = `const arr = [{ foo: html\`<span class=\${"hello"}>bar</span>\` }];
|
|
425
|
+
arr.push({
|
|
426
|
+
foo: "bar", // inline comment
|
|
427
|
+
});
|
|
428
|
+
`;
|
|
429
|
+
const output = transform(input);
|
|
430
|
+
const expected = `const arr = [{
|
|
431
|
+
foo: h("span", {
|
|
432
|
+
class: "hello"
|
|
433
|
+
}, "bar")
|
|
434
|
+
}];
|
|
435
|
+
arr.push({
|
|
436
|
+
// inline comment
|
|
437
|
+
foo: "bar"
|
|
438
|
+
});`;
|
|
439
|
+
|
|
440
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test("handles private class fields with html templates", () => {
|
|
444
|
+
const input = `class Alert {
|
|
445
|
+
#timeout;
|
|
446
|
+
|
|
447
|
+
render() {
|
|
448
|
+
return html\`<div>Test</div>\`;
|
|
449
|
+
}
|
|
450
|
+
}`;
|
|
451
|
+
const output = transform(input);
|
|
452
|
+
const expected = `class Alert {
|
|
453
|
+
#timeout;
|
|
454
|
+
|
|
455
|
+
render() {
|
|
456
|
+
return h("div", null, "Test");
|
|
457
|
+
}
|
|
458
|
+
}`;
|
|
459
|
+
|
|
460
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test("transforms html template in private field initializer", () => {
|
|
424
464
|
const input = `class Foo {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
465
|
+
#element = html\`<div>Test</div>\`;
|
|
466
|
+
}`;
|
|
467
|
+
const output = transform(input);
|
|
468
|
+
const expected = `class Foo {
|
|
469
|
+
#element = h("div", null, "Test");
|
|
470
|
+
}`;
|
|
471
|
+
|
|
472
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("handles multiple private fields with JSDoc comments", () => {
|
|
476
|
+
const input = `class Foo {
|
|
477
|
+
/** @type {string | undefined} */
|
|
478
|
+
#timeout;
|
|
479
|
+
/** @type {number | undefined} */
|
|
480
|
+
#count;
|
|
481
|
+
|
|
482
|
+
render() {
|
|
483
|
+
return html\`<div>Test</div>\`;
|
|
439
484
|
}
|
|
440
485
|
}`;
|
|
486
|
+
|
|
441
487
|
const output = transform(input);
|
|
488
|
+
const expected = `class Foo {
|
|
489
|
+
/** @type {string | undefined}*/
|
|
490
|
+
#timeout;
|
|
491
|
+
/** @type {number | undefined}*/
|
|
492
|
+
#count;
|
|
493
|
+
render() {
|
|
494
|
+
return h("div", null, "Test");
|
|
495
|
+
}
|
|
496
|
+
}`;
|
|
442
497
|
|
|
443
|
-
|
|
444
|
-
assert.ok(output.includes('h("span", null, "Test")'));
|
|
445
|
-
assert.ok(output.includes("/** @type"));
|
|
446
|
-
assert.ok(output.includes("// comment on same line"));
|
|
498
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
447
499
|
});
|
package/HostCard.js
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
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
|
-
}
|