@sveltesentio/core 0.2.0 → 0.2.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.1](https://github.com/golusoris/sveltesentio/compare/core-v0.2.0...core-v0.2.1) (2026-06-19)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **core:** restore eslint.config.ts + chart-a11y rule (prior commit's git add aborted) ([a978cf6](https://github.com/golusoris/sveltesentio/commit/a978cf6f4a362b1fb83eabdc426e6b04276607c3))
9
+
3
10
  ## [0.2.0](https://github.com/golusoris/sveltesentio/compare/core-v0.1.0...core-v0.2.0) (2026-06-19)
4
11
 
5
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltesentio/core",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Vite plugin, env schema, error types, id/clock utils — sveltesentio core",
5
5
  "type": "module",
6
6
  "private": false,
@@ -58,15 +58,16 @@
58
58
  }
59
59
  },
60
60
  "devDependencies": {
61
- "@sveltejs/kit": "^2.65.1",
61
+ "@sveltejs/kit": "^2.65.2",
62
62
  "@types/node": "^24.13.2",
63
63
  "esm-env": "^1.2.2",
64
64
  "openapi-fetch": "^0.17.0",
65
65
  "svelte": "^5.56.3",
66
66
  "typescript": "^6.0.3",
67
67
  "uuid": "^14.0.0",
68
- "vitest": "^4.1.8",
69
- "zod": "^4.4.3"
68
+ "vitest": "^4.1.9",
69
+ "zod": "^4.4.3",
70
+ "svelte-eslint-parser": "^1.8.0"
70
71
  },
71
72
  "keywords": [
72
73
  "sveltesentio",
package/src/eslint.ts CHANGED
@@ -1,14 +1,18 @@
1
1
  import type { Rule } from 'eslint';
2
2
 
3
3
  /**
4
- * Flat-config ESLint plugin enforcing the core "no direct time reads" invariant
5
- * (see AGENTS.md §Invariants + docs/principles.md §2.1): time must flow through
6
- * the injected {@link Clock} (`useClock` / `getClock` from `@sveltesentio/core`)
7
- * so it is deterministic and testable. Banned forms:
4
+ * Flat-config ESLint plugin bundling sveltesentio's two cross-package
5
+ * invariants:
8
6
  *
9
- * - `Date.now()`
10
- * - `new Date()` (zero-argument `new Date(serverMs)` is explicit + allowed)
11
- * - `performance.now()`
7
+ * - `no-direct-time` — time must flow through the injected {@link Clock}
8
+ * (`useClock` / `getClock` from `@sveltesentio/core`) so it is deterministic
9
+ * and testable (AGENTS.md §Invariants + docs/principles.md §2.1). Banned forms:
10
+ * `Date.now()`, zero-argument `new Date()`, `performance.now()`.
11
+ * - `chart-a11y-wrapper` — every chart visual rendered from `layerchart` /
12
+ * `uplot` must go through `@sveltesentio/charts`' `<ChartFigure>` so the WCAG
13
+ * 2.2 SC 1.1.1 text alternative cannot be skipped (charts/AGENTS.md §Invariants,
14
+ * ADR-0013). A bare `<LineChart>` / `<Chart>` / uPlot element with no
15
+ * `<ChartFigure>` ancestor is an error.
12
16
  *
13
17
  * Register in a flat `eslint.config.js`:
14
18
  *
@@ -20,6 +24,11 @@ import type { Rule } from 'eslint';
20
24
  * plugins: { '@sveltesentio': sentio },
21
25
  * rules: { '@sveltesentio/no-direct-time': 'error' },
22
26
  * },
27
+ * {
28
+ * files: ['src/**\/*.svelte'],
29
+ * plugins: { '@sveltesentio': sentio },
30
+ * rules: { '@sveltesentio/chart-a11y-wrapper': 'error' },
31
+ * },
23
32
  * ];
24
33
  * ```
25
34
  */
@@ -29,6 +38,7 @@ import type { Rule } from 'eslint';
29
38
  type Node = Rule.Node;
30
39
  type CallExpressionNode = Extract<Node, { type: 'CallExpression' }>;
31
40
  type NewExpressionNode = Extract<Node, { type: 'NewExpression' }>;
41
+ type ImportDeclarationNode = Extract<Node, { type: 'ImportDeclaration' }>;
32
42
  type Callee = CallExpressionNode['callee'];
33
43
 
34
44
  const CLOCK_HINT =
@@ -92,16 +102,108 @@ const noDirectTime: Rule.RuleModule = {
92
102
  },
93
103
  };
94
104
 
105
+ // --- chart-a11y-wrapper ------------------------------------------------------
106
+
107
+ const CHART_LIBS = ['layerchart', 'uplot'] as const;
108
+
109
+ /** The a11y wrappers from `@sveltesentio/charts` that satisfy the invariant. */
110
+ const A11Y_WRAPPER_ELEMENTS = new Set(['ChartFigure']);
111
+
112
+ const CHART_HINT =
113
+ 'render it inside `<ChartFigure>` from @sveltesentio/charts so the chart ' +
114
+ 'ships the required visually-hidden data table (WCAG 2.2 SC 1.1.1, ADR-0013)';
115
+
116
+ /**
117
+ * Structural read of a `svelte-eslint-parser` `SvelteElement` name without
118
+ * depending on the parser's types: a component element exposes
119
+ * `name: { type: 'Identifier' | 'SvelteName', name: string }`. Anything else
120
+ * (HTML tags, member-expression names) is not a chart-library binding and is
121
+ * intentionally ignored.
122
+ */
123
+ function svelteElementName(node: unknown): string | undefined {
124
+ if (typeof node !== 'object' || node === null) return undefined;
125
+ const name = (node as { name?: unknown }).name;
126
+ if (typeof name !== 'object' || name === null) return undefined;
127
+ const raw = (name as { name?: unknown }).name;
128
+ return typeof raw === 'string' ? raw : undefined;
129
+ }
130
+
131
+ /** Reads the source string of a (validated) `ImportDeclaration`. */
132
+ function importSource(node: ImportDeclarationNode): string {
133
+ const value = node.source.value;
134
+ return typeof value === 'string' ? value : '';
135
+ }
136
+
137
+ /** True for `layerchart`, `layerchart/...`, `uplot`, `uplot/...`. */
138
+ function isChartLibSource(source: string): boolean {
139
+ return CHART_LIBS.some(
140
+ (lib) => source === lib || source.startsWith(`${lib}/`),
141
+ );
142
+ }
143
+
144
+ const chartA11yWrapper: Rule.RuleModule = {
145
+ meta: {
146
+ type: 'problem',
147
+ docs: {
148
+ description:
149
+ 'require chart visuals from layerchart / uplot to be wrapped in <ChartFigure>',
150
+ recommended: true,
151
+ },
152
+ schema: [],
153
+ messages: {
154
+ bareChart: `Bare \`<{{name}}>\` from \`{{source}}\` bypasses the a11y wrapper — ${CHART_HINT}.`,
155
+ },
156
+ },
157
+
158
+ create(context: Rule.RuleContext): Rule.RuleListener {
159
+ // Local binding names imported from a chart library → the source they
160
+ // came from, so the message can name it.
161
+ const chartBindings = new Map<string, string>();
162
+
163
+ return {
164
+ ImportDeclaration(node: ImportDeclarationNode): void {
165
+ const source = importSource(node);
166
+ if (!isChartLibSource(source)) return;
167
+ for (const spec of node.specifiers) {
168
+ chartBindings.set(spec.local.name, source);
169
+ }
170
+ },
171
+
172
+ // `SvelteElement` is the svelte-eslint-parser node for any element;
173
+ // not part of ESLint's core `NodeListener`, so it rides the
174
+ // RuleListener index signature.
175
+ SvelteElement(node: Rule.Node): void {
176
+ const name = svelteElementName(node);
177
+ if (name === undefined) return;
178
+ const source = chartBindings.get(name);
179
+ if (source === undefined) return;
180
+ // Allowed when nested under a sanctioned a11y wrapper element.
181
+ const ancestors = context.sourceCode.getAncestors(node);
182
+ const wrapped = ancestors.some((ancestor) =>
183
+ A11Y_WRAPPER_ELEMENTS.has(svelteElementName(ancestor) ?? ''),
184
+ );
185
+ if (wrapped) return;
186
+ context.report({
187
+ node,
188
+ messageId: 'bareChart',
189
+ data: { name, source },
190
+ });
191
+ },
192
+ };
193
+ },
194
+ };
195
+
95
196
  /** The flat-config plugin object (`plugins: { '@sveltesentio': sentioEslint }`). */
96
197
  const sentioEslint = {
97
- meta: { name: '@sveltesentio/core', version: '0.1.0' },
198
+ meta: { name: '@sveltesentio/core', version: '0.2.0' },
98
199
  rules: {
99
200
  'no-direct-time': noDirectTime,
201
+ 'chart-a11y-wrapper': chartA11yWrapper,
100
202
  },
101
203
  } satisfies {
102
204
  meta: { name: string; version: string };
103
205
  rules: Record<string, Rule.RuleModule>;
104
206
  };
105
207
 
106
- export { noDirectTime, sentioEslint };
208
+ export { noDirectTime, chartA11yWrapper, sentioEslint };
107
209
  export default sentioEslint;