@siyahi/tools 0.1.9-next.32 → 0.1.9-next.33
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/README.md +8 -0
- package/lint/index.mjs +99 -12
- package/package.json +1 -1
- package/types/lint/index.d.mts +1 -1
package/README.md
CHANGED
|
@@ -49,6 +49,14 @@ flex-col gap-4">`, `<TableCell><span className="font-mono">`,
|
|
|
49
49
|
A Vite+ or ESLint config can spread every rule at once:
|
|
50
50
|
`import { recommended } from "@siyahi/tools/lint"`.
|
|
51
51
|
|
|
52
|
+
One more rule is for a component library's own types rather than for pages, so
|
|
53
|
+
it runs in every file it is turned on for and is not in `recommended`:
|
|
54
|
+
`siyahi/optional-accepts-undefined` rejects an optional property that does not
|
|
55
|
+
take undefined, such as `tone?: Tone`, and `--fix` writes
|
|
56
|
+
`tone?: Tone | undefined`. Under TypeScript's `exactOptionalPropertyTypes` a
|
|
57
|
+
page can only pass `tone: loss ? "negative" : undefined` to the second.
|
|
58
|
+
Siyahi runs it on its own package.
|
|
59
|
+
|
|
52
60
|
## Font preload
|
|
53
61
|
|
|
54
62
|
A Vite plugin that adds `<link rel="preload" as="font">` tags to the built
|
package/lint/index.mjs
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// Oxlint (and ESLint) plugin `siyahi`. Every rule runs only in files that
|
|
3
3
|
// import `@siyahi/ui`, so pages written before Siyahi stay outside the rules
|
|
4
4
|
// without any configuration. Three rules come from @shadcn/lint; the other
|
|
5
|
-
// four cover what it does not check.
|
|
5
|
+
// four cover what it does not check. One more, optional-accepts-undefined, is
|
|
6
|
+
// for a component library's own types: unscoped, and not in `recommended`.
|
|
6
7
|
import { plugin as shadcn } from "@shadcn/lint";
|
|
7
8
|
|
|
8
9
|
/** @typedef {{ sourceCode: { text: string }, report: (problem: { node: unknown, message: string }) => void }} Context */
|
|
@@ -138,6 +139,88 @@ const noChartThemeMap = {
|
|
|
138
139
|
}),
|
|
139
140
|
};
|
|
140
141
|
|
|
142
|
+
/** Types that already hold undefined, so they need no `| undefined`. */
|
|
143
|
+
const HOLDS_UNDEFINED = new Set(["TSUndefinedKeyword", "TSAnyKeyword", "TSUnknownKeyword", "TSVoidKeyword"]);
|
|
144
|
+
/** Types that bind looser than `|`, so a union with undefined wraps them in brackets. */
|
|
145
|
+
const LOOSE = new Set(["TSFunctionType", "TSConstructorType", "TSConditionalType"]);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {{ type: string, types?: { type: string }[], typeName?: { type: string, name?: string, right?: { name?: string } } }} type
|
|
149
|
+
* @returns {boolean}
|
|
150
|
+
*/
|
|
151
|
+
function holdsUndefined(type) {
|
|
152
|
+
if (HOLDS_UNDEFINED.has(type.type)) return true;
|
|
153
|
+
if (type.type === "TSUnionType") return (type.types ?? []).some(holdsUndefined);
|
|
154
|
+
// React's ReactNode includes undefined.
|
|
155
|
+
const name = type.typeName?.type === "TSQualifiedName" ? type.typeName.right?.name : type.typeName?.name;
|
|
156
|
+
return type.type === "TSTypeReference" && name === "ReactNode";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Whether a node sits in the pattern a conditional type matches against, `T extends { x?: infer U } ? U : never`,
|
|
161
|
+
* where adding undefined would change what matches and what `infer` takes.
|
|
162
|
+
* @param {any} node
|
|
163
|
+
* @returns {boolean}
|
|
164
|
+
*/
|
|
165
|
+
function inPattern(node) {
|
|
166
|
+
for (let child = node; child.parent; child = child.parent) {
|
|
167
|
+
if (child.parent.type === "TSConditionalType" && child.parent.extendsType === child) return true;
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @type {Rule} */
|
|
173
|
+
const optionalAcceptsUndefined = {
|
|
174
|
+
meta: {
|
|
175
|
+
type: "problem",
|
|
176
|
+
fixable: "code",
|
|
177
|
+
docs: { description: "Require `| undefined` on an optional property, for pages under exactOptionalPropertyTypes." },
|
|
178
|
+
},
|
|
179
|
+
// Under exactOptionalPropertyTypes, `tone?: Tone` rejects `tone: loss ? "negative" : undefined`, the way a page
|
|
180
|
+
// writes a prop it sets only sometimes; `tone?: Tone | undefined` takes it.
|
|
181
|
+
create: (context) => {
|
|
182
|
+
const message =
|
|
183
|
+
"An optional property must also accept undefined: add `| undefined`, for pages under exactOptionalPropertyTypes.";
|
|
184
|
+
/**
|
|
185
|
+
* @param {any} node a property signature, class field or mapped type
|
|
186
|
+
* @param {any} type its type
|
|
187
|
+
*/
|
|
188
|
+
function check(node, type) {
|
|
189
|
+
if (!type || holdsUndefined(type) || inPattern(node)) return;
|
|
190
|
+
context.report({
|
|
191
|
+
node,
|
|
192
|
+
message,
|
|
193
|
+
fix: (/** @type {any} */ fixer) =>
|
|
194
|
+
LOOSE.has(type.type)
|
|
195
|
+
? [fixer.insertTextBefore(type, "("), fixer.insertTextAfter(type, ") | undefined")]
|
|
196
|
+
: fixer.insertTextAfter(type, " | undefined"),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
/** @param {any} node */
|
|
201
|
+
TSPropertySignature(node) {
|
|
202
|
+
if (node.optional) check(node, node.typeAnnotation?.typeAnnotation);
|
|
203
|
+
},
|
|
204
|
+
/** @param {any} node */
|
|
205
|
+
PropertyDefinition(node) {
|
|
206
|
+
if (node.optional) check(node, node.typeAnnotation?.typeAnnotation);
|
|
207
|
+
},
|
|
208
|
+
/** @param {any} node `[K in keyof T]?: T[K]` */
|
|
209
|
+
TSMappedType(node) {
|
|
210
|
+
if (node.optional === true || node.optional === "+") check(node, node.typeAnnotation);
|
|
211
|
+
},
|
|
212
|
+
/** @param {any} node `onSelect?(): void`, which has no type to add undefined to */
|
|
213
|
+
TSMethodSignature(node) {
|
|
214
|
+
if (!node.optional || inPattern(node)) return;
|
|
215
|
+
context.report({
|
|
216
|
+
node,
|
|
217
|
+
message: `${message} Write it as a property: \`onSelect?: (() => void) | undefined\`.`,
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
141
224
|
/**
|
|
142
225
|
* Options for shadcn's no-restyle, so every app gets the same policy from
|
|
143
226
|
* `"siyahi/no-restyle": "error"` alone. Pages may place a component (margin,
|
|
@@ -163,24 +246,28 @@ function withOptions(rule, options) {
|
|
|
163
246
|
};
|
|
164
247
|
}
|
|
165
248
|
|
|
249
|
+
/** Rules for pages that use Siyahi. */
|
|
250
|
+
const pageRules = {
|
|
251
|
+
"no-color-literal": scoped(noColorLiteral),
|
|
252
|
+
"no-scheme-variant": scoped(noSchemeVariant),
|
|
253
|
+
"no-numeric-font-size": scoped(noNumericFontSize),
|
|
254
|
+
"no-chart-theme-map": scoped(noChartThemeMap),
|
|
255
|
+
"no-raw-colors": scoped(shadcn.rules["no-raw-colors"]),
|
|
256
|
+
"no-arbitrary-values": scoped(shadcn.rules["no-arbitrary-values"]),
|
|
257
|
+
"no-restyle": scoped(withOptions(shadcn.rules["no-restyle"], RESTYLE)),
|
|
258
|
+
};
|
|
259
|
+
|
|
166
260
|
/** @type {{ meta: { name: string }, rules: Record<string, Rule> }} */
|
|
167
261
|
const plugin = {
|
|
168
262
|
meta: { name: "siyahi" },
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"no-scheme-variant": scoped(noSchemeVariant),
|
|
172
|
-
"no-numeric-font-size": scoped(noNumericFontSize),
|
|
173
|
-
"no-chart-theme-map": scoped(noChartThemeMap),
|
|
174
|
-
"no-raw-colors": scoped(shadcn.rules["no-raw-colors"]),
|
|
175
|
-
"no-arbitrary-values": scoped(shadcn.rules["no-arbitrary-values"]),
|
|
176
|
-
"no-restyle": scoped(withOptions(shadcn.rules["no-restyle"], RESTYLE)),
|
|
177
|
-
},
|
|
263
|
+
// optional-accepts-undefined is for a component library's own types, not for pages, which only use them.
|
|
264
|
+
rules: { ...pageRules, "optional-accepts-undefined": optionalAcceptsUndefined },
|
|
178
265
|
};
|
|
179
266
|
|
|
180
267
|
/**
|
|
181
|
-
* Every rule at "error", for a Vite+ or ESLint config to spread.
|
|
268
|
+
* Every page rule at "error", for a Vite+ or ESLint config to spread.
|
|
182
269
|
* @type {Record<string, "error">}
|
|
183
270
|
*/
|
|
184
|
-
export const recommended = Object.fromEntries(Object.keys(
|
|
271
|
+
export const recommended = Object.fromEntries(Object.keys(pageRules).map((name) => [`siyahi/${name}`, "error"]));
|
|
185
272
|
|
|
186
273
|
export default plugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siyahi/tools",
|
|
3
|
-
"version": "0.1.9-next.
|
|
3
|
+
"version": "0.1.9-next.33",
|
|
4
4
|
"description": "Checks and build helpers for Siyahi pages: a lint plugin, a font preload plugin, a theme contrast check, and screenshot and layout shift scripts.",
|
|
5
5
|
"homepage": "https://siyahi.jatinbansal.com",
|
|
6
6
|
"license": "MIT",
|
package/types/lint/index.d.mts
CHANGED
|
@@ -19,7 +19,7 @@ declare const plugin: {
|
|
|
19
19
|
rules: Record<string, Rule>;
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
|
-
* Every rule at "error", for a Vite+ or ESLint config to spread.
|
|
22
|
+
* Every page rule at "error", for a Vite+ or ESLint config to spread.
|
|
23
23
|
* @type {Record<string, "error">}
|
|
24
24
|
*/
|
|
25
25
|
export declare const recommended: Record<string, "error">;
|