@sveltesentio/core 0.2.0 → 0.3.0
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 +14 -0
- package/package.json +9 -4
- package/sentio.d.ts +23 -0
- package/src/eslint.ts +111 -9
- package/src/index.ts +8 -0
- package/src/sentio-config.ts +48 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0](https://github.com/golusoris/sveltesentio/compare/core-v0.2.1...core-v0.3.0) (2026-06-20)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **core:** typed $sentio virtual-module config schema ([75e9f32](https://github.com/golusoris/sveltesentio/commit/75e9f32d378fe0d5457549cb793524317395a134))
|
|
9
|
+
|
|
10
|
+
## [0.2.1](https://github.com/golusoris/sveltesentio/compare/core-v0.2.0...core-v0.2.1) (2026-06-19)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **core:** restore eslint.config.ts + chart-a11y rule (prior commit's git add aborted) ([a978cf6](https://github.com/golusoris/sveltesentio/commit/a978cf6f4a362b1fb83eabdc426e6b04276607c3))
|
|
16
|
+
|
|
3
17
|
## [0.2.0](https://github.com/golusoris/sveltesentio/compare/core-v0.1.0...core-v0.2.0) (2026-06-19)
|
|
4
18
|
|
|
5
19
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sveltesentio/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Vite plugin, env schema, error types, id/clock utils — sveltesentio core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -41,6 +41,9 @@
|
|
|
41
41
|
"./eslint": {
|
|
42
42
|
"import": "./src/eslint.ts",
|
|
43
43
|
"types": "./src/eslint.ts"
|
|
44
|
+
},
|
|
45
|
+
"./sentio": {
|
|
46
|
+
"types": "./sentio.d.ts"
|
|
44
47
|
}
|
|
45
48
|
},
|
|
46
49
|
"peerDependencies": {
|
|
@@ -58,15 +61,16 @@
|
|
|
58
61
|
}
|
|
59
62
|
},
|
|
60
63
|
"devDependencies": {
|
|
61
|
-
"@sveltejs/kit": "^2.65.
|
|
64
|
+
"@sveltejs/kit": "^2.65.2",
|
|
62
65
|
"@types/node": "^24.13.2",
|
|
63
66
|
"esm-env": "^1.2.2",
|
|
64
67
|
"openapi-fetch": "^0.17.0",
|
|
65
68
|
"svelte": "^5.56.3",
|
|
66
69
|
"typescript": "^6.0.3",
|
|
67
70
|
"uuid": "^14.0.0",
|
|
68
|
-
"vitest": "^4.1.
|
|
69
|
-
"zod": "^4.4.3"
|
|
71
|
+
"vitest": "^4.1.9",
|
|
72
|
+
"zod": "^4.4.3",
|
|
73
|
+
"svelte-eslint-parser": "^1.8.0"
|
|
70
74
|
},
|
|
71
75
|
"keywords": [
|
|
72
76
|
"sveltesentio",
|
|
@@ -78,6 +82,7 @@
|
|
|
78
82
|
},
|
|
79
83
|
"files": [
|
|
80
84
|
"src",
|
|
85
|
+
"sentio.d.ts",
|
|
81
86
|
"CHANGELOG.md"
|
|
82
87
|
],
|
|
83
88
|
"scripts": {
|
package/sentio.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient declaration for the `$sentio` virtual module emitted by
|
|
3
|
+
* `sentioPlugin` (see `@sveltesentio/core/vite`). Make it visible to your app
|
|
4
|
+
* by adding `"@sveltesentio/core/sentio"` to `compilerOptions.types`, or with
|
|
5
|
+
* `/// <reference types="@sveltesentio/core/sentio" />`.
|
|
6
|
+
*
|
|
7
|
+
* The exported shape matches `SentioConfig` from `defineSentioConfig`.
|
|
8
|
+
*/
|
|
9
|
+
declare module '$sentio' {
|
|
10
|
+
import type { SentioConfig, InterfaceType } from '@sveltesentio/core';
|
|
11
|
+
|
|
12
|
+
/** Build-time app version. */
|
|
13
|
+
export const version: string;
|
|
14
|
+
/** Default interface-type preset used before client-side classification. */
|
|
15
|
+
export const interfaceType: InterfaceType;
|
|
16
|
+
/** Static build-time feature flags (flag name → enabled). */
|
|
17
|
+
export const features: Readonly<Record<string, boolean>>;
|
|
18
|
+
/** Active theme preset name. */
|
|
19
|
+
export const theme: string;
|
|
20
|
+
|
|
21
|
+
const config: Readonly<SentioConfig>;
|
|
22
|
+
export default config;
|
|
23
|
+
}
|
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
|
|
5
|
-
*
|
|
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
|
-
* - `
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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.
|
|
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;
|
package/src/index.ts
CHANGED
|
@@ -47,4 +47,12 @@ export type {
|
|
|
47
47
|
} from './vite.js';
|
|
48
48
|
export { checkBundleBudget, sentioPlugin } from './vite.js';
|
|
49
49
|
|
|
50
|
+
export type { InterfaceType, SentioConfig } from './sentio-config.js';
|
|
51
|
+
export {
|
|
52
|
+
SentioConfigError,
|
|
53
|
+
defineSentioConfig,
|
|
54
|
+
interfaceTypeSchema,
|
|
55
|
+
sentioConfigSchema,
|
|
56
|
+
} from './sentio-config.js';
|
|
57
|
+
|
|
50
58
|
export { noDirectTime, sentioEslint } from './eslint.js';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/** Interface-type preset (§2.6): desktop pointer, 10-foot TV, handheld touch. */
|
|
4
|
+
export const interfaceTypeSchema = z.enum(['desktop', 'tenfoot', 'handheld']);
|
|
5
|
+
export type InterfaceType = z.infer<typeof interfaceTypeSchema>;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Schema for the typed `$sentio` virtual module — build-time configuration
|
|
9
|
+
* surfaced to client + server code via `import { ... } from '$sentio'`. Feed
|
|
10
|
+
* the validated result of {@link defineSentioConfig} to the Vite plugin as
|
|
11
|
+
* `sentioPlugin({ virtualModule })`.
|
|
12
|
+
*/
|
|
13
|
+
export const sentioConfigSchema = z.object({
|
|
14
|
+
/** Build-time app version surfaced to client code. */
|
|
15
|
+
version: z.string().min(1).default('0.0.0'),
|
|
16
|
+
/** Default interface-type preset used before client-side classification. */
|
|
17
|
+
interfaceType: interfaceTypeSchema.default('desktop'),
|
|
18
|
+
/** Static feature flags resolved at build time (flag name → enabled). */
|
|
19
|
+
features: z.record(z.string(), z.boolean()).default({}),
|
|
20
|
+
/** Active theme preset name. */
|
|
21
|
+
theme: z.string().min(1).default('default'),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export type SentioConfig = z.infer<typeof sentioConfigSchema>;
|
|
25
|
+
|
|
26
|
+
/** Thrown by {@link defineSentioConfig} when the input fails validation. */
|
|
27
|
+
export class SentioConfigError extends Error {
|
|
28
|
+
constructor(message: string) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'SentioConfigError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate + normalise a `$sentio` config. Fills defaults for omitted fields
|
|
36
|
+
* and throws {@link SentioConfigError} with a readable summary on invalid
|
|
37
|
+
* input, so misconfiguration fails the build instead of reaching the client.
|
|
38
|
+
*/
|
|
39
|
+
export function defineSentioConfig(input: unknown = {}): SentioConfig {
|
|
40
|
+
const result = sentioConfigSchema.safeParse(input);
|
|
41
|
+
if (!result.success) {
|
|
42
|
+
const detail = result.error.issues
|
|
43
|
+
.map((issue) => ` - ${issue.path.join('.') || '(root)'}: ${issue.message}`)
|
|
44
|
+
.join('\n');
|
|
45
|
+
throw new SentioConfigError(`[sentio] Invalid $sentio config:\n${detail}`);
|
|
46
|
+
}
|
|
47
|
+
return result.data;
|
|
48
|
+
}
|