@stacksjs/stx 0.2.171 → 0.2.173
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +925 -834
- package/dist/codemod.d.ts +1 -24
- package/dist/codemod.js +56 -9
- package/dist/craft.js +264 -185
- package/dist/production-builder.js +1 -0
- package/dist/pwa.js +210 -131
- package/dist/render.js +5 -18
- package/dist/script-validation.d.ts +0 -7
- package/dist/script-validation.js +6 -4
- package/dist/server-bindings.js +45 -1
- package/dist/signals.js +93 -15
- package/dist/ssg.js +223 -144
- package/dist/stx-virtual-ts.d.ts +13 -0
- package/dist/stx-virtual-ts.js +36 -7
- package/dist/typecheck.d.ts +1 -13
- package/dist/typecheck.js +98 -4
- package/dist/variable-extractor.js +1 -1
- package/package.json +5 -5
package/dist/codemod.d.ts
CHANGED
|
@@ -18,31 +18,8 @@ export declare interface CodemodOptions {
|
|
|
18
18
|
rules?: CodemodRule[]
|
|
19
19
|
file?: string
|
|
20
20
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Codemods for adopting stx primitives an app hand-rolled around (#1843).
|
|
23
|
-
*
|
|
24
|
-
* Two production apps built on stx reached past it into vanilla JS for things
|
|
25
|
-
* it already ships. The audit found the cause was discovery, not absence: one
|
|
26
|
-
* app called bare `confirm()` in five places and used `title=` thirteen times
|
|
27
|
-
* while `stxConfirm` and `x-tooltip` were both delivered and used zero times.
|
|
28
|
-
* A native fallback that WORKS is the hardest kind of gap to notice — nothing
|
|
29
|
-
* fails, so nothing prompts you to look.
|
|
30
|
-
*
|
|
31
|
-
* #1843 proposes both rewrites as "mechanical and safe". Neither is, and the
|
|
32
|
-
* difference matters enough that this tool reports by default and only edits
|
|
33
|
-
* when asked:
|
|
34
|
-
*
|
|
35
|
-
* - `confirm()` returns a boolean synchronously; `stxConfirm()` returns a
|
|
36
|
-
* Promise. `if (confirm(m))` rewritten to `if (stxConfirm(m))` is ALWAYS
|
|
37
|
-
* TRUE, because a Promise is truthy. A blind rewrite turns every "Delete
|
|
38
|
-
* this?" into an unconditional yes.
|
|
39
|
-
*
|
|
40
|
-
* - `title=` is announced by screen readers. `x-tooltip` sets no `role` and
|
|
41
|
-
* no `aria-*` — it is a visual tooltip driven by mouseover/focusin.
|
|
42
|
-
* Replacing one with the other removes the accessible description. So the
|
|
43
|
-
* tooltip rule ADDS `x-tooltip` and keeps `title`.
|
|
44
|
-
*/
|
|
45
21
|
export type CodemodRule = | 'confirm'
|
|
22
|
+
| 'alert'
|
|
46
23
|
| 'tooltip'
|
|
47
24
|
| 'route-params'
|
|
48
25
|
| 'search-params'
|
package/dist/codemod.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stripCommentsAndLiterals } from "./strip-literals.js";
|
|
1
2
|
function contextAfter(source, index, length = 240) {
|
|
2
3
|
return source.slice(index, index + length);
|
|
3
4
|
}
|
|
@@ -8,11 +9,25 @@ const REPORT_RULES = [
|
|
|
8
9
|
primitive: "useRoute() / useRouteParams() / useRouteParam(name)",
|
|
9
10
|
why: "parsing the URL by hand re-implements the router and does not update on SPA navigation"
|
|
10
11
|
},
|
|
12
|
+
{
|
|
13
|
+
rule: "alert",
|
|
14
|
+
pattern: /(?<![.#\w$])(?:(?:window|globalThis)\s*\.\s*)?alert\s*\(/g,
|
|
15
|
+
primitive: "stxAlert(message)",
|
|
16
|
+
why: "themed, focus-trapped and non-blocking \u2014 but `await` it if the code after it " + "depended on the native dialog blocking until dismissal",
|
|
17
|
+
reject: (source) => /\b(?:async\s+)?function\s+alert\s*\(/.test(source) || /\b(?:const|let|var)\s+alert\s*=/.test(source)
|
|
18
|
+
},
|
|
11
19
|
{
|
|
12
20
|
rule: "search-params",
|
|
13
21
|
pattern: /new\s+URLSearchParams\s*\(/g,
|
|
14
22
|
primitive: "useSearchParams()",
|
|
15
|
-
why: "reactive, and writes back through the router instead of history.replaceState"
|
|
23
|
+
why: "reactive, and writes back through the router instead of history.replaceState",
|
|
24
|
+
reject: (source, index) => {
|
|
25
|
+
const open = source.indexOf("(", index);
|
|
26
|
+
if (open === -1)
|
|
27
|
+
return !0;
|
|
28
|
+
const arg = source.slice(open + 1, source.indexOf(")", open) + 1);
|
|
29
|
+
return !/\b(?:location|search|href|url)\b/i.test(arg);
|
|
30
|
+
}
|
|
16
31
|
},
|
|
17
32
|
{
|
|
18
33
|
rule: "navigate",
|
|
@@ -93,9 +108,9 @@ function declaresOwnConfirm(source) {
|
|
|
93
108
|
function applyConfirmRule(source, file, findings) {
|
|
94
109
|
if (declaresOwnConfirm(source))
|
|
95
110
|
return source;
|
|
96
|
-
const pattern = /(^|[^\w$.])confirm\s*\(/g;
|
|
111
|
+
const pattern = /(^|[^\w$.])(?:(?:window|globalThis)\s*\.\s*)?confirm\s*\(/g, searchable = searchableSource(source);
|
|
97
112
|
let out = "", last = 0, match;
|
|
98
|
-
while ((match = pattern.exec(
|
|
113
|
+
while ((match = pattern.exec(searchable)) !== null) {
|
|
99
114
|
const callAt = match.index + match[1].length, line = lineOf(source, callAt), snippet = snippetAt(source, callAt);
|
|
100
115
|
if (/stxConfirm\s*\($/.test(source.slice(0, callAt + 8)))
|
|
101
116
|
continue;
|
|
@@ -110,16 +125,27 @@ function applyConfirmRule(source, file, findings) {
|
|
|
110
125
|
});
|
|
111
126
|
if (safe) {
|
|
112
127
|
out += source.slice(last, callAt) + "await stxConfirm(";
|
|
113
|
-
last = callAt +
|
|
128
|
+
last = callAt + (match[0].length - match[1].length);
|
|
114
129
|
}
|
|
115
130
|
}
|
|
116
131
|
return out + source.slice(last);
|
|
117
132
|
}
|
|
118
133
|
function applyTooltipRule(source, file, findings) {
|
|
119
|
-
const pattern = /(<[a-z][\w-]*\b[^>]*?)\stitle=(["'])(.*?)\2/gis;
|
|
134
|
+
const pattern = /(<[a-z][\w-]*\b[^>]*?)\stitle=(["'])(.*?)\2/gis, searchable = markupSearchable(source);
|
|
120
135
|
let out = "", last = 0, match;
|
|
121
|
-
while ((match = pattern.exec(
|
|
122
|
-
const [whole, openTag, quote, value] = match, line = lineOf(source, match.index), snippet = snippetAt(source, match.index);
|
|
136
|
+
while ((match = pattern.exec(searchable)) !== null) {
|
|
137
|
+
const [whole, openTag, quote, value] = match, line = lineOf(source, match.index), snippet = snippetAt(source, match.index), tagName = openTag.match(/^<([A-Za-z][\w-]*)/)?.[1] ?? "";
|
|
138
|
+
if (/^[A-Z]/.test(tagName)) {
|
|
139
|
+
findings.push({
|
|
140
|
+
file,
|
|
141
|
+
line,
|
|
142
|
+
rule: "tooltip",
|
|
143
|
+
applied: !1,
|
|
144
|
+
snippet,
|
|
145
|
+
reason: `\`title\` on <${tagName}> is a component prop, not an HTML attribute \u2014 ` + "adding x-tooltip here would put a tooltip on a component and may duplicate text it already renders."
|
|
146
|
+
});
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
123
149
|
if (/\bx-tooltip[=\s]/.test(openTag) || value.trim() === "") {
|
|
124
150
|
findings.push({
|
|
125
151
|
file,
|
|
@@ -137,7 +163,19 @@ function applyTooltipRule(source, file, findings) {
|
|
|
137
163
|
}
|
|
138
164
|
return out + source.slice(last);
|
|
139
165
|
}
|
|
166
|
+
function markupSearchable(source) {
|
|
167
|
+
return source.replace(/<!--[\s\S]*?-->/g, (match) => match.replace(/[^\n]/g, " ")).replace(/\{\{--[\s\S]*?--\}\}/g, (match) => match.replace(/[^\n]/g, " "));
|
|
168
|
+
}
|
|
169
|
+
function blankJsComments(source) {
|
|
170
|
+
const blank = (match) => match.replace(/[^\n]/g, " ");
|
|
171
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, blank).replace(/\/\/[^\n]*/g, blank);
|
|
172
|
+
}
|
|
173
|
+
function searchableSource(source, keepStrings = !1) {
|
|
174
|
+
const markupOnly = markupSearchable(source);
|
|
175
|
+
return (keepStrings ? blankJsComments(markupOnly) : stripCommentsAndLiterals(markupOnly)).replace(/<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi, (match, attrs) => /\bclient\b/i.test(attrs) ? match : match.replace(/[^\n]/g, " "));
|
|
176
|
+
}
|
|
140
177
|
function applyReportRules(source, file, rules, findings) {
|
|
178
|
+
const codeView = searchableSource(source), stringView = searchableSource(source, !0);
|
|
141
179
|
for (const entry of REPORT_RULES) {
|
|
142
180
|
if (!rules.includes(entry.rule))
|
|
143
181
|
continue;
|
|
@@ -145,7 +183,9 @@ function applyReportRules(source, file, rules, findings) {
|
|
|
145
183
|
continue;
|
|
146
184
|
entry.pattern.lastIndex = 0;
|
|
147
185
|
let match;
|
|
148
|
-
|
|
186
|
+
const searchable = /['"]/.test(entry.pattern.source) ? stringView : codeView;
|
|
187
|
+
entry.pattern.lastIndex = 0;
|
|
188
|
+
while ((match = entry.pattern.exec(searchable)) !== null) {
|
|
149
189
|
if (entry.reject?.(source, match.index))
|
|
150
190
|
continue;
|
|
151
191
|
findings.push({
|
|
@@ -168,7 +208,14 @@ export function codemodSource(source, options = {}) {
|
|
|
168
208
|
if (rules.includes("tooltip"))
|
|
169
209
|
code = applyTooltipRule(code, file, findings);
|
|
170
210
|
findings.sort((a, b) => a.line - b.line);
|
|
171
|
-
|
|
211
|
+
const seen = new Set, unique = findings.filter((finding) => {
|
|
212
|
+
const key = `${finding.rule}:${finding.line}`;
|
|
213
|
+
if (seen.has(key))
|
|
214
|
+
return !1;
|
|
215
|
+
seen.add(key);
|
|
216
|
+
return !0;
|
|
217
|
+
});
|
|
218
|
+
return { code, findings: unique };
|
|
172
219
|
}
|
|
173
220
|
const REWRITABLE = new Set(["confirm", "tooltip"]);
|
|
174
221
|
export function formatCodemodFindings(findings, wrote) {
|