snice 7.3.0 → 7.4.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/.agents/skills/snice/SKILL.md +12 -16
- package/README.md +5 -1
- package/adapters/react/SniceProvider.js +1 -1
- package/adapters/react/SniceRouter.js +1 -1
- package/adapters/react/matchRoute.js +1 -1
- package/adapters/react/useRequestHandler.js +1 -1
- package/bin/analyzer-contracts.js +3 -1
- package/bin/project-analyzer.js +219 -6
- package/bin/snice.js +77 -13
- package/dist/app-context.d.ts +10 -10
- package/dist/cdn/accordion/snice-accordion.js +1 -1
- package/dist/cdn/action-bar/snice-action-bar.js +1 -1
- package/dist/cdn/activity-feed/snice-activity-feed.js +1 -1
- package/dist/cdn/alert/snice-alert.js +1 -1
- package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
- package/dist/cdn/approval-flow/snice-approval-flow.js +1 -1
- package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
- package/dist/cdn/availability/snice-availability.js +1 -1
- package/dist/cdn/avatar/snice-avatar.js +1 -1
- package/dist/cdn/avatar-group/snice-avatar-group.js +1 -1
- package/dist/cdn/badge/snice-badge.js +1 -1
- package/dist/cdn/banner/snice-banner.js +1 -1
- package/dist/cdn/binpack/snice-binpack.js +1 -1
- package/dist/cdn/book/snice-book.js +1 -1
- package/dist/cdn/booking/snice-booking.js +1 -1
- package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +1 -1
- package/dist/cdn/button/snice-button.js +1 -1
- package/dist/cdn/calendar/snice-calendar.js +1 -1
- package/dist/cdn/camera/snice-camera.js +1 -1
- package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
- package/dist/cdn/candlestick/snice-candlestick.js +1 -1
- package/dist/cdn/card/snice-card.js +1 -1
- package/dist/cdn/carousel/snice-carousel.js +1 -1
- package/dist/cdn/cart/snice-cart.js +1 -1
- package/dist/cdn/chart/snice-chart.js +1 -1
- package/dist/cdn/chat/snice-chat.js +1 -1
- package/dist/cdn/checkbox/snice-checkbox.js +1 -1
- package/dist/cdn/chip/snice-chip.js +1 -1
- package/dist/cdn/code-block/snice-code-block.js +1 -1
- package/dist/cdn/color-display/snice-color-display.js +1 -1
- package/dist/cdn/color-picker/snice-color-picker.js +1 -1
- package/dist/cdn/command-palette/snice-command-palette.js +1 -1
- package/dist/cdn/comments/snice-comments.js +1 -1
- package/dist/cdn/countdown/snice-countdown.js +1 -1
- package/dist/cdn/cropper/snice-cropper.js +1 -1
- package/dist/cdn/data-card/snice-data-card.js +1 -1
- package/dist/cdn/date-picker/snice-date-picker.js +1 -1
- package/dist/cdn/date-range-picker/snice-date-range-picker.js +1 -1
- package/dist/cdn/date-time-picker/snice-date-time-picker.js +1 -1
- package/dist/cdn/diff/snice-diff.js +1 -1
- package/dist/cdn/divider/snice-divider.js +1 -1
- package/dist/cdn/doc/snice-doc.js +1 -1
- package/dist/cdn/draw/snice-draw.js +1 -1
- package/dist/cdn/drawer/snice-drawer.js +1 -1
- package/dist/cdn/empty-state/snice-empty-state.js +1 -1
- package/dist/cdn/estimate/snice-estimate.js +1 -1
- package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
- package/dist/cdn/file-upload/snice-file-upload.js +1 -1
- package/dist/cdn/flip-card/snice-flip-card.js +1 -1
- package/dist/cdn/flow/snice-flow.js +1 -1
- package/dist/cdn/form-layout/snice-form-layout.js +1 -1
- package/dist/cdn/funnel/snice-funnel.js +1 -1
- package/dist/cdn/gantt/snice-gantt.js +1 -1
- package/dist/cdn/gauge/snice-gauge.js +1 -1
- package/dist/cdn/grid/snice-grid.js +1 -1
- package/dist/cdn/heatmap/snice-heatmap.js +1 -1
- package/dist/cdn/image/snice-image.js +1 -1
- package/dist/cdn/input/snice-input.js +1 -1
- package/dist/cdn/invoice/snice-invoice.js +1 -1
- package/dist/cdn/kanban/snice-kanban.js +1 -1
- package/dist/cdn/key-value/snice-key-value.js +1 -1
- package/dist/cdn/kpi/snice-kpi.js +1 -1
- package/dist/cdn/layout/snice-layout.js +1 -1
- package/dist/cdn/leaderboard/snice-leaderboard.js +1 -1
- package/dist/cdn/link/snice-link.js +1 -1
- package/dist/cdn/link-preview/snice-link-preview.js +1 -1
- package/dist/cdn/list/snice-list.js +1 -1
- package/dist/cdn/location/snice-location.js +1 -1
- package/dist/cdn/login/snice-login.js +1 -1
- package/dist/cdn/map/snice-map.js +1 -1
- package/dist/cdn/markdown/snice-markdown.js +1 -1
- package/dist/cdn/masonry/snice-masonry.js +1 -1
- package/dist/cdn/menu/snice-menu.js +1 -1
- package/dist/cdn/message-strip/snice-message-strip.js +1 -1
- package/dist/cdn/modal/snice-modal.js +1 -1
- package/dist/cdn/music-player/snice-music-player.js +1 -1
- package/dist/cdn/nav/snice-nav.js +1 -1
- package/dist/cdn/network-graph/snice-network-graph.js +1 -1
- package/dist/cdn/notification-center/snice-notification-center.js +1 -1
- package/dist/cdn/order-tracker/snice-order-tracker.js +1 -1
- package/dist/cdn/org-chart/snice-org-chart.js +1 -1
- package/dist/cdn/pagination/snice-pagination.js +1 -1
- package/dist/cdn/paint/snice-paint.js +1 -1
- package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
- package/dist/cdn/permission-matrix/snice-permission-matrix.js +1 -1
- package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
- package/dist/cdn/popover/snice-popover.js +1 -1
- package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
- package/dist/cdn/product-card/snice-product-card.js +1 -1
- package/dist/cdn/progress/snice-progress.js +1 -1
- package/dist/cdn/progress-ring/snice-progress-ring.js +1 -1
- package/dist/cdn/qr-code/snice-qr-code.js +1 -1
- package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
- package/dist/cdn/radio/snice-radio.js +1 -1
- package/dist/cdn/range-slider/snice-range-slider.js +1 -1
- package/dist/cdn/rating/snice-rating.js +1 -1
- package/dist/cdn/receipt/snice-receipt.js +1 -1
- package/dist/cdn/recipe/snice-recipe.js +1 -1
- package/dist/cdn/runtime/README.md +2 -2
- package/dist/cdn/runtime/snice-runtime.esm.js +309 -188
- package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.esm.min.js +1 -1
- package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.js +309 -187
- package/dist/cdn/runtime/snice-runtime.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.min.js +1 -1
- package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
- package/dist/cdn/sankey/snice-sankey.js +1 -1
- package/dist/cdn/segmented-control/snice-segmented-control.js +1 -1
- package/dist/cdn/select/snice-select.js +1 -1
- package/dist/cdn/skeleton/snice-skeleton.js +1 -1
- package/dist/cdn/slider/snice-slider.js +1 -1
- package/dist/cdn/sortable/snice-sortable.js +1 -1
- package/dist/cdn/sparkline/snice-sparkline.js +1 -1
- package/dist/cdn/spinner/snice-spinner.js +1 -1
- package/dist/cdn/split-button/snice-split-button.js +1 -1
- package/dist/cdn/split-pane/snice-split-pane.js +1 -1
- package/dist/cdn/spotlight/snice-spotlight.js +1 -1
- package/dist/cdn/stat-group/snice-stat-group.js +1 -1
- package/dist/cdn/step-input/snice-step-input.js +1 -1
- package/dist/cdn/stepper/snice-stepper.js +1 -1
- package/dist/cdn/switch/snice-switch.js +1 -1
- package/dist/cdn/table/snice-table.esm.js +1 -1
- package/dist/cdn/table/snice-table.js +1 -1
- package/dist/cdn/tabs/snice-tabs.js +1 -1
- package/dist/cdn/tag/snice-tag.js +1 -1
- package/dist/cdn/tag-input/snice-tag-input.js +1 -1
- package/dist/cdn/terminal/snice-terminal.js +1 -1
- package/dist/cdn/testimonial/snice-testimonial.js +1 -1
- package/dist/cdn/textarea/snice-textarea.js +1 -1
- package/dist/cdn/time-picker/snice-time-picker.js +1 -1
- package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
- package/dist/cdn/timeline/snice-timeline.js +1 -1
- package/dist/cdn/timer/snice-timer.js +1 -1
- package/dist/cdn/toast/snice-toast.js +1 -1
- package/dist/cdn/tooltip/snice-tooltip.js +1 -1
- package/dist/cdn/tree/snice-tree.js +1 -1
- package/dist/cdn/treemap/snice-treemap.js +1 -1
- package/dist/cdn/user-card/snice-user-card.js +1 -1
- package/dist/cdn/video-player/snice-video-player.js +1 -1
- package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
- package/dist/cdn/waterfall/snice-waterfall.js +1 -1
- package/dist/cdn/weather/snice-weather.js +1 -1
- package/dist/cdn/work-order/snice-work-order.js +1 -1
- package/dist/components/pagination/snice-pagination.types.d.ts +5 -0
- package/dist/context-provider.d.ts +11 -1
- package/dist/context.d.ts +5 -6
- package/dist/index.cjs +306 -184
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.esm.js +306 -185
- package/dist/index.esm.js.map +1 -1
- package/dist/index.iife.js +306 -184
- package/dist/index.iife.js.map +1 -1
- package/dist/react/SniceProvider.js +1 -1
- package/dist/react/SniceRouter.js +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/matchRoute.js +1 -1
- package/dist/react/useRequestHandler.js +1 -1
- package/dist/symbols.cjs +1 -1
- package/dist/symbols.esm.js +1 -1
- package/dist/testing-dom.cjs +48 -0
- package/dist/testing-dom.cjs.map +1 -0
- package/dist/testing-dom.d.ts +23 -0
- package/dist/testing-dom.esm.js +45 -0
- package/dist/testing-dom.esm.js.map +1 -0
- package/dist/transitions.cjs +1 -1
- package/dist/transitions.esm.js +1 -1
- package/dist/types/context.d.ts +8 -8
- package/dist/utils.d.ts +3 -1
- package/docs/ai/api.md +12 -1
- package/docs/ai/architecture.md +7 -3
- package/docs/ai/bindings.md +1 -1
- package/docs/ai/cli.md +22 -0
- package/docs/ai/components/badge.md +4 -0
- package/docs/ai/components/pagination.md +4 -2
- package/docs/ai/components/segmented-control.md +5 -1
- package/docs/ai/controllers.md +30 -11
- package/docs/ai/decorators.md +2 -2
- package/docs/ai/events.md +8 -2
- package/docs/ai/fetcher.md +32 -1
- package/docs/ai/lifecycle.md +4 -1
- package/docs/ai/patterns.md +9 -4
- package/docs/ai/request-response.md +20 -6
- package/docs/ai/routing.md +8 -1
- package/docs/ai/testing.md +71 -0
- package/docs/bindings.md +1 -1
- package/docs/cli.md +23 -0
- package/docs/components/badge.md +4 -0
- package/docs/components/pagination.md +8 -5
- package/docs/components/segmented-control.md +5 -1
- package/docs/controllers.md +30 -11
- package/docs/events.md +11 -2
- package/docs/fetcher.md +32 -1
- package/docs/lifecycle.md +5 -1
- package/docs/request-response.md +25 -7
- package/docs/routing.md +10 -1
- package/docs/testing.md +71 -0
- package/package.json +6 -1
|
@@ -12,22 +12,11 @@ reconstruct Snice APIs from Lit or generic web-component conventions.
|
|
|
12
12
|
|
|
13
13
|
Make this decision before creating files or choosing decorators:
|
|
14
14
|
|
|
15
|
-
- **
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
that exist for those elements stay in an `@controller` class.
|
|
21
|
-
- **Element orchestration belongs in the page.** A `@page` composes elements,
|
|
22
|
-
passes properties, handles their events, binds their controllers, and
|
|
23
|
-
coordinates the screen. Routing is one page concern; it is not the definition
|
|
24
|
-
of the page's role.
|
|
25
|
-
|
|
26
|
-
Do not attach a controller to the page host or move orchestration into a “page
|
|
27
|
-
controller.” Do not move visual behavior into a controller merely to make an
|
|
28
|
-
element smaller. A host-free reusable function may stay a plain module wherever
|
|
29
|
-
the project convention puts it. State plus lifecycle belongs in an explicitly
|
|
30
|
-
constructed `@daemon`, not a singleton.
|
|
15
|
+
- **Element:** visual behavior, rendering, internal DOM, focus, keyboard behavior, visual state, and semantic events.
|
|
16
|
+
- **Controller:** application behavior specific to a set of elements, including API/storage access and business rules.
|
|
17
|
+
- **Page:** element orchestration: composition, properties, events, controller binding, and screen coordination. Routing is only one page concern.
|
|
18
|
+
|
|
19
|
+
Do not attach a controller to a page host, create a “page controller,” or move visual behavior into a controller to shrink an element. Host-free reusable functions may stay plain modules. State plus lifecycle belongs in an explicitly constructed `@daemon`, never a singleton.
|
|
31
20
|
|
|
32
21
|
Examples: focus/internal buttons → element; element-specific API behavior → controller;
|
|
33
22
|
coordinating search, results, and dialog elements → page; state plus lifecycle → daemon.
|
|
@@ -76,6 +65,11 @@ type-checks with `npx tsc -p references/tsconfig.kitchen-sink.json` after
|
|
|
76
65
|
- Import React wrappers from `snice/react`; also register each underlying
|
|
77
66
|
custom element through its documented deep side-effect import.
|
|
78
67
|
- Pass objects and arrays through property bindings, not string attributes.
|
|
68
|
+
- In Router-backed controllers, receive `Context` with `@context()` and use
|
|
69
|
+
`ctx.fetch` for middleware-aware transport. Managed decorators activate
|
|
70
|
+
after `attach()`, so context-dependent startup belongs in the `@context()`
|
|
71
|
+
handler. Do not call bare `fetch`, import the Router into the controller, or
|
|
72
|
+
invent another context-injection channel.
|
|
79
73
|
- Preserve native form, keyboard, focus, and accessibility behavior.
|
|
80
74
|
- Use the project compiler configuration for TC39 decorators.
|
|
81
75
|
- Do not edit generated distribution, adapter, metadata, or website output.
|
|
@@ -85,6 +79,8 @@ configuration, import, hallucination, and component-recommendation checks.
|
|
|
85
79
|
Use `npx snice doctor` or `npx snice validate` only when isolating the
|
|
86
80
|
configuration or source half of a failed check.
|
|
87
81
|
|
|
82
|
+
Diagnostics expose stable codes. After reading the referenced AI docs and verifying intentional acceptance, the agent may record the narrowest suppression in project-root `.sniceignore`: `code path:line:column` (one diagnostic), `code path` (one file), or `code` (global). Use global only when the project rejects the rule. Never suppress merely to pass a gate; comment non-obvious reasons.
|
|
83
|
+
|
|
88
84
|
## Verify
|
|
89
85
|
|
|
90
86
|
Run the consuming project's available checks in this order:
|
package/README.md
CHANGED
|
@@ -433,7 +433,11 @@ setValue(val: string) {
|
|
|
433
433
|
|
|
434
434
|
### Global State
|
|
435
435
|
|
|
436
|
-
**`@context(options?)`** - Receive
|
|
436
|
+
**`@context(options?)`** - Receive Router context updates on pages, descendant
|
|
437
|
+
elements, and attached controllers. Controllers can use the same
|
|
438
|
+
middleware-aware `ctx.fetch` as pages; context-dependent controller startup
|
|
439
|
+
belongs in the `@context()` handler because managed decorators activate after
|
|
440
|
+
`attach()`.
|
|
437
441
|
```typescript
|
|
438
442
|
// Method decorator that receives context updates
|
|
439
443
|
@context()
|
|
@@ -21,7 +21,7 @@ export const ANALYZER_CONTRACTS = {
|
|
|
21
21
|
"componentModules": 188,
|
|
22
22
|
"componentUtilityModules": 31,
|
|
23
23
|
"reactWrappers": 193,
|
|
24
|
-
"rootExports":
|
|
24
|
+
"rootExports": 118
|
|
25
25
|
},
|
|
26
26
|
"rootExports": [
|
|
27
27
|
"AdoptedOptions",
|
|
@@ -30,6 +30,7 @@ export const ANALYZER_CONTRACTS = {
|
|
|
30
30
|
"Context",
|
|
31
31
|
"ContextAwareFetcher",
|
|
32
32
|
"ContextOptions",
|
|
33
|
+
"ContextProviderOptions",
|
|
33
34
|
"ControllerClass",
|
|
34
35
|
"CreateRequestHandlerOptions",
|
|
35
36
|
"DaemonMap",
|
|
@@ -99,6 +100,7 @@ export const ANALYZER_CONTRACTS = {
|
|
|
99
100
|
"escapeHtml",
|
|
100
101
|
"getBodyScrollLockCount",
|
|
101
102
|
"getContext",
|
|
103
|
+
"getContextFetch",
|
|
102
104
|
"getController",
|
|
103
105
|
"getSymbol",
|
|
104
106
|
"html",
|
package/bin/project-analyzer.js
CHANGED
|
@@ -47,6 +47,11 @@ const LEGACY_LIT_LIFECYCLE = new Set([
|
|
|
47
47
|
'performUpdate'
|
|
48
48
|
]);
|
|
49
49
|
|
|
50
|
+
const NATIVE_ELEMENT_IDL_MEMBERS = new Set([
|
|
51
|
+
'autofocus', 'dir', 'hidden', 'id', 'inert', 'lang', 'role', 'slot',
|
|
52
|
+
'title', 'translate'
|
|
53
|
+
]);
|
|
54
|
+
|
|
50
55
|
const SNICE_REACT_EVENT_PROPS = new Set([
|
|
51
56
|
'onInputChange', 'onSelectChange', 'onTextareaChange', 'onCheckboxChange',
|
|
52
57
|
'onRadioChange', 'onSliderChange', 'onDateChange', 'onTimeChange',
|
|
@@ -525,6 +530,44 @@ const RULE_DEFINITIONS = [
|
|
|
525
530
|
}
|
|
526
531
|
}
|
|
527
532
|
},
|
|
533
|
+
{
|
|
534
|
+
id: 'snice/imperative-controller-attach',
|
|
535
|
+
severity: 'suggestion',
|
|
536
|
+
category: 'architecture',
|
|
537
|
+
description: 'Prefer declarative controller bindings in application templates.',
|
|
538
|
+
check(context) {
|
|
539
|
+
if (isFrameworkImplementation(context.filename) || isTestFilename(context.filename)) return;
|
|
540
|
+
for (const localName of localsFor(context.provenance.rootBindings, 'attachController')) {
|
|
541
|
+
const pattern = new RegExp(`\\b${escapeRegExp(localName)}\\s*\\(`, 'g');
|
|
542
|
+
for (const match of context.source.matchAll(pattern)) {
|
|
543
|
+
context.report(match.index, {
|
|
544
|
+
message: `${localName}() imperatively attaches a controller in application code even though controller bindings own this lifecycle declaratively.`,
|
|
545
|
+
fix: 'Bind the decorated class in the owning template with controller=${ControllerClass}. Keep imperative attachment for focused framework tests only. Review docs/ai/controllers.md.'
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
id: 'snice/element-member-shadows-native-idl',
|
|
553
|
+
severity: 'warning',
|
|
554
|
+
category: 'properties',
|
|
555
|
+
description: 'Warn when a Snice element redeclares an inherited HTMLElement IDL member.',
|
|
556
|
+
check(context) {
|
|
557
|
+
if (isFrameworkImplementation(context.filename)) return;
|
|
558
|
+
for (const declaration of findSniceElementClasses(context.source)) {
|
|
559
|
+
const members = inspectElementMembers(declaration.body);
|
|
560
|
+
for (const name of new Set([...members.fields, ...members.accessors, ...members.reactive])) {
|
|
561
|
+
if (!NATIVE_ELEMENT_IDL_MEMBERS.has(name)) continue;
|
|
562
|
+
const relativeIndex = members.memberIndices.get(name) ?? 0;
|
|
563
|
+
context.report(declaration.bodyStart + relativeIndex, {
|
|
564
|
+
message: `${declaration.name}.${name} shadows HTMLElement.${name}, so native reflection/accessibility behavior and Snice property binding can conflict.`,
|
|
565
|
+
fix: `Rename the application-owned member. If native ${name} semantics are intended, use the inherited HTMLElement property/attribute without redeclaring it.`
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
},
|
|
528
571
|
{
|
|
529
572
|
id: 'snice/self-ready-await',
|
|
530
573
|
severity: 'error',
|
|
@@ -806,6 +849,51 @@ const RULE_DEFINITIONS = [
|
|
|
806
849
|
}
|
|
807
850
|
}
|
|
808
851
|
},
|
|
852
|
+
{
|
|
853
|
+
id: 'snice/live-control-value-binding',
|
|
854
|
+
severity: 'suggestion',
|
|
855
|
+
category: 'rendering',
|
|
856
|
+
description: 'Recommend live property bindings for controlled values on verified self-mutating Snice controls.',
|
|
857
|
+
check(context) {
|
|
858
|
+
const components = {
|
|
859
|
+
'snice-input': 'input',
|
|
860
|
+
'snice-textarea': 'textarea',
|
|
861
|
+
'snice-segmented-control': 'segmented-control'
|
|
862
|
+
};
|
|
863
|
+
for (const opening of findOpeningTags(context.source, Object.keys(components))) {
|
|
864
|
+
const binding = /(?:^|\s)value\s*=\s*(?:['"])?\$\{/.exec(opening.text);
|
|
865
|
+
if (!binding) continue;
|
|
866
|
+
const valueIndex = opening.index + binding.index + binding[0].indexOf('value');
|
|
867
|
+
context.report(valueIndex, {
|
|
868
|
+
message: `${opening.name} receives a dynamic value attribute, but its live value can diverge after user interaction.`,
|
|
869
|
+
fix: `For a controlled value, import live from 'snice' and use .value=\${live(value)} so an owner render compares with the control's current property. Keep value= only for an authored/reset default. Review docs/ai/components/${components[opening.name]}.md and docs/ai/bindings.md.`
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
id: 'snice/prefer-dispatch-decorator',
|
|
876
|
+
severity: 'suggestion',
|
|
877
|
+
category: 'events',
|
|
878
|
+
description: 'Prefer @dispatch for static CustomEvents emitted from a Snice host.',
|
|
879
|
+
check(context) {
|
|
880
|
+
if (isFrameworkImplementation(context.filename)) return;
|
|
881
|
+
|
|
882
|
+
const declarations = [
|
|
883
|
+
...findSniceElementClasses(context.source).map(declaration => ({ declaration, role: 'element' })),
|
|
884
|
+
...findDecoratedClasses(context.source, 'controller').map(declaration => ({ declaration, role: 'controller' }))
|
|
885
|
+
];
|
|
886
|
+
|
|
887
|
+
for (const { declaration, role } of declarations) {
|
|
888
|
+
for (const dispatch of findManualCustomEventDispatches(declaration.body, role)) {
|
|
889
|
+
context.report(declaration.bodyStart + dispatch.index, {
|
|
890
|
+
message: `Manual host dispatch of "${dispatch.eventName}" duplicates the standard @dispatch CustomEvent path.`,
|
|
891
|
+
fix: `Prefer an emitter method decorated with @dispatch('${dispatch.eventName}') and return the detail payload directly. Keep dispatchEvent() when code needs the Event object, a dynamic event name, or the cancellation boolean. Review docs/ai/events.md.`
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
},
|
|
809
897
|
{
|
|
810
898
|
id: 'snice/custom-event-contract',
|
|
811
899
|
severity: 'error',
|
|
@@ -1424,6 +1512,14 @@ const RULE_DEFINITIONS = [
|
|
|
1424
1512
|
projectOnly: true,
|
|
1425
1513
|
check() {}
|
|
1426
1514
|
},
|
|
1515
|
+
{
|
|
1516
|
+
id: 'snice/shadowed-query-route',
|
|
1517
|
+
severity: 'warning',
|
|
1518
|
+
category: 'router',
|
|
1519
|
+
description: 'Warn when same-page string route order makes a query-parameter variant unreachable.',
|
|
1520
|
+
projectOnly: true,
|
|
1521
|
+
check() {}
|
|
1522
|
+
},
|
|
1427
1523
|
{
|
|
1428
1524
|
id: 'snice/template-reads-nonreactive-field',
|
|
1429
1525
|
severity: 'warning',
|
|
@@ -1465,7 +1561,7 @@ const RULE_DEFINITIONS = [
|
|
|
1465
1561
|
*/
|
|
1466
1562
|
export const PROJECT_ANALYZER_RULES = Object.freeze(
|
|
1467
1563
|
RULE_DEFINITIONS.map(({ id, severity, category, description }) =>
|
|
1468
|
-
Object.freeze({ id, severity, category, description })
|
|
1564
|
+
Object.freeze({ id, code: id, severity, category, description })
|
|
1469
1565
|
)
|
|
1470
1566
|
);
|
|
1471
1567
|
|
|
@@ -1476,6 +1572,7 @@ export const PROJECT_ANALYZER_RULES = Object.freeze(
|
|
|
1476
1572
|
* @param {string} [filename=''] Optional filename included in every diagnostic.
|
|
1477
1573
|
* @returns {Array<{
|
|
1478
1574
|
* severity: 'error'|'warning'|'suggestion',
|
|
1575
|
+
* code: string,
|
|
1479
1576
|
* ruleId: string,
|
|
1480
1577
|
* message: string,
|
|
1481
1578
|
* fix: string,
|
|
@@ -1499,6 +1596,7 @@ export function analyzeSource(source, filename = '') {
|
|
|
1499
1596
|
const location = sourceLocation(source, Math.max(0, nonSource.index));
|
|
1500
1597
|
return [{
|
|
1501
1598
|
severity: 'error',
|
|
1599
|
+
code: 'snice/non-source-content',
|
|
1502
1600
|
ruleId: 'snice/non-source-content',
|
|
1503
1601
|
message: nonSource.message,
|
|
1504
1602
|
fix: nonSource.fix,
|
|
@@ -1526,6 +1624,7 @@ export function analyzeSource(source, filename = '') {
|
|
|
1526
1624
|
seen.add(key);
|
|
1527
1625
|
const diagnostic = {
|
|
1528
1626
|
severity: rule.severity,
|
|
1627
|
+
code: rule.id,
|
|
1529
1628
|
ruleId: rule.id,
|
|
1530
1629
|
message: detail.message,
|
|
1531
1630
|
fix: detail.fix,
|
|
@@ -1868,6 +1967,27 @@ export function analyzeProject(files) {
|
|
|
1868
1967
|
column: location.column
|
|
1869
1968
|
});
|
|
1870
1969
|
}
|
|
1970
|
+
|
|
1971
|
+
const precedingBareRoutes = new Set();
|
|
1972
|
+
for (const route of findPageStringRoutes(page.file.source, page.index)) {
|
|
1973
|
+
const queryIndex = route.spec.indexOf('?');
|
|
1974
|
+
const pathname = queryIndex >= 0 ? route.spec.slice(0, queryIndex) : route.spec;
|
|
1975
|
+
if (queryIndex < 0) {
|
|
1976
|
+
precedingBareRoutes.add(pathname);
|
|
1977
|
+
continue;
|
|
1978
|
+
}
|
|
1979
|
+
if (!precedingBareRoutes.has(pathname)) continue;
|
|
1980
|
+
const location = sourceLocation(page.file.source, route.index);
|
|
1981
|
+
diagnostics.push({
|
|
1982
|
+
severity: 'warning',
|
|
1983
|
+
ruleId: 'snice/shadowed-query-route',
|
|
1984
|
+
message: `${page.name}'s query route "${route.spec}" is shadowed by an earlier "${pathname}" entry with the same specificity, so its query properties never bind.`,
|
|
1985
|
+
fix: `Move "${route.spec}" before "${pathname}" in the routes array, or use route objects with explicit order values when a more complex tie-break is intentional. Review docs/ai/routing.md.`,
|
|
1986
|
+
file: page.file.filename,
|
|
1987
|
+
line: location.line,
|
|
1988
|
+
column: location.column
|
|
1989
|
+
});
|
|
1990
|
+
}
|
|
1871
1991
|
}
|
|
1872
1992
|
for (const [key, declarations] of declarationsByClass) {
|
|
1873
1993
|
const page = declarations.find(declaration => declaration.decorator === 'page');
|
|
@@ -2040,7 +2160,9 @@ export function analyzeProject(files) {
|
|
|
2040
2160
|
}
|
|
2041
2161
|
}
|
|
2042
2162
|
|
|
2043
|
-
return diagnostics
|
|
2163
|
+
return diagnostics
|
|
2164
|
+
.map(diagnostic => diagnostic.code ? diagnostic : { ...diagnostic, code: diagnostic.ruleId })
|
|
2165
|
+
.sort(compareProjectDiagnostics);
|
|
2044
2166
|
}
|
|
2045
2167
|
|
|
2046
2168
|
/**
|
|
@@ -2290,10 +2412,91 @@ function findSniceElementClasses(source) {
|
|
|
2290
2412
|
return declarations;
|
|
2291
2413
|
}
|
|
2292
2414
|
|
|
2415
|
+
/**
|
|
2416
|
+
* Find standalone, static CustomEvent dispatches that @dispatch can express
|
|
2417
|
+
* without changing control flow. Calls whose return value is consumed, whose
|
|
2418
|
+
* event name is dynamic, or whose target is not the Snice host are deliberately
|
|
2419
|
+
* left alone as valid low-level event code.
|
|
2420
|
+
*/
|
|
2421
|
+
function findManualCustomEventDispatches(body, role) {
|
|
2422
|
+
const dispatches = [];
|
|
2423
|
+
const pattern = role === 'controller'
|
|
2424
|
+
? /\bthis\s*\.\s*element\s*(?:\?\.\s*|\.\s*)dispatchEvent\s*\(/g
|
|
2425
|
+
: /\bthis\s*\.\s*dispatchEvent\s*\(/g;
|
|
2426
|
+
|
|
2427
|
+
for (const match of body.matchAll(pattern)) {
|
|
2428
|
+
const lineStart = body.lastIndexOf('\n', match.index) + 1;
|
|
2429
|
+
if (body.slice(lineStart, match.index).trim()) continue;
|
|
2430
|
+
|
|
2431
|
+
const dispatchOpen = body.indexOf('(', match.index);
|
|
2432
|
+
const dispatchClose = findMatchingDelimiter(body, dispatchOpen, '(', ')');
|
|
2433
|
+
if (dispatchOpen < 0 || dispatchClose < 0) continue;
|
|
2434
|
+
|
|
2435
|
+
let afterDispatch = dispatchClose + 1;
|
|
2436
|
+
while (body[afterDispatch] === ' ' || body[afterDispatch] === '\t') afterDispatch++;
|
|
2437
|
+
if (body[afterDispatch] !== ';') continue;
|
|
2438
|
+
const lineEnd = body.indexOf('\n', afterDispatch);
|
|
2439
|
+
const remainder = body.slice(afterDispatch + 1, lineEnd < 0 ? body.length : lineEnd);
|
|
2440
|
+
if (remainder.trim()) continue;
|
|
2441
|
+
|
|
2442
|
+
let cursor = dispatchOpen + 1;
|
|
2443
|
+
while (/\s/.test(body[cursor] ?? '')) cursor++;
|
|
2444
|
+
if (!body.startsWith('new', cursor) || /[\w$]/.test(body[cursor + 3] ?? '')) continue;
|
|
2445
|
+
cursor += 3;
|
|
2446
|
+
while (/\s/.test(body[cursor] ?? '')) cursor++;
|
|
2447
|
+
if (!body.startsWith('CustomEvent', cursor) || /[\w$]/.test(body[cursor + 11] ?? '')) continue;
|
|
2448
|
+
cursor += 11;
|
|
2449
|
+
while (/\s/.test(body[cursor] ?? '')) cursor++;
|
|
2450
|
+
|
|
2451
|
+
if (body[cursor] === '<') {
|
|
2452
|
+
const genericClose = findMatchingDelimiter(body, cursor, '<', '>');
|
|
2453
|
+
if (genericClose < 0) continue;
|
|
2454
|
+
cursor = genericClose + 1;
|
|
2455
|
+
while (/\s/.test(body[cursor] ?? '')) cursor++;
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
if (body[cursor] !== '(') continue;
|
|
2459
|
+
cursor++;
|
|
2460
|
+
while (/\s/.test(body[cursor] ?? '')) cursor++;
|
|
2461
|
+
const eventName = /^(['"])([^'"]+)\1/.exec(body.slice(cursor));
|
|
2462
|
+
if (!eventName) continue;
|
|
2463
|
+
|
|
2464
|
+
dispatches.push({ index: match.index, eventName: eventName[2] });
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
return dispatches;
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2293
2470
|
function findElementDecoratorTag(source, index) {
|
|
2294
2471
|
return /^@element\s*\(\s*['"]([^'"]+)['"]/.exec(source.slice(index, index + 500))?.[1] ?? null;
|
|
2295
2472
|
}
|
|
2296
2473
|
|
|
2474
|
+
function findPageStringRoutes(source, decoratorIndex) {
|
|
2475
|
+
const open = source.indexOf('(', decoratorIndex);
|
|
2476
|
+
if (open < 0) return [];
|
|
2477
|
+
const close = findMatchingDelimiter(source, open, '(', ')');
|
|
2478
|
+
if (close < 0) return [];
|
|
2479
|
+
const argumentsSource = source.slice(open + 1, close);
|
|
2480
|
+
const routesProperty = /\broutes\s*:\s*\[/.exec(argumentsSource);
|
|
2481
|
+
if (!routesProperty) return [];
|
|
2482
|
+
const arrayOpen = open + 1 + routesProperty.index + routesProperty[0].lastIndexOf('[');
|
|
2483
|
+
const arrayClose = findMatchingDelimiter(source, arrayOpen, '[', ']');
|
|
2484
|
+
if (arrayClose < 0 || arrayClose > close) return [];
|
|
2485
|
+
|
|
2486
|
+
const body = source.slice(arrayOpen + 1, arrayClose);
|
|
2487
|
+
const routes = [];
|
|
2488
|
+
let searchFrom = 0;
|
|
2489
|
+
for (const item of splitTopLevelArguments(body)) {
|
|
2490
|
+
const relative = body.indexOf(item, searchFrom);
|
|
2491
|
+
if (relative < 0) continue;
|
|
2492
|
+
searchFrom = relative + item.length;
|
|
2493
|
+
const literal = /^(['"])([^'"]+)\1$/.exec(item);
|
|
2494
|
+
if (!literal) continue;
|
|
2495
|
+
routes.push({ spec: literal[2], index: arrayOpen + 1 + relative });
|
|
2496
|
+
}
|
|
2497
|
+
return routes;
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2297
2500
|
function inspectElementMembers(body) {
|
|
2298
2501
|
const code = blankStringContents(maskComments(body));
|
|
2299
2502
|
const depths = new Uint16Array(code.length + 1);
|
|
@@ -2309,11 +2512,13 @@ function inspectElementMembers(body) {
|
|
|
2309
2512
|
const readonly = new Set();
|
|
2310
2513
|
const functionFields = new Set();
|
|
2311
2514
|
const reactive = new Set();
|
|
2312
|
-
const
|
|
2515
|
+
const memberIndices = new Map();
|
|
2516
|
+
const fieldPattern = /(?:^|\n)\s*((?:(?:public|private|protected|static|readonly|override|declare|abstract)\s+)*)([A-Za-z_$][\w$]*)\s*[!?]?\s*(?::[^=\n;]+)?\s*(?==|;)/g;
|
|
2313
2517
|
for (const match of code.matchAll(fieldPattern)) {
|
|
2314
2518
|
const nameIndex = match.index + match[0].lastIndexOf(match[2]);
|
|
2315
2519
|
if (depths[nameIndex] !== 0) continue;
|
|
2316
2520
|
fields.add(match[2]);
|
|
2521
|
+
if (!memberIndices.has(match[2])) memberIndices.set(match[2], nameIndex);
|
|
2317
2522
|
if (/\breadonly\b/.test(match[1])) readonly.add(match[2]);
|
|
2318
2523
|
}
|
|
2319
2524
|
const functionFieldPattern = /(?:^|\n)\s*(?:(?:public|private|protected|static|readonly|override|declare|abstract)\s+)*([A-Za-z_$][\w$]*)\s*[!?]?\s*(?::[^=\n;{]+)?\s*=\s*(?:async\s*)?(?:\([^\n)]*\)|[A-Za-z_$][\w$]*)\s*=>/g;
|
|
@@ -2324,7 +2529,10 @@ function inspectElementMembers(body) {
|
|
|
2324
2529
|
const accessorPattern = /(?:^|\n)\s*(?:(?:public|private|protected|static|readonly|override|declare|abstract)\s+)*(?:get|set|accessor)\s+([A-Za-z_$][\w$]*)\b/g;
|
|
2325
2530
|
for (const match of code.matchAll(accessorPattern)) {
|
|
2326
2531
|
const nameIndex = match.index + match[0].lastIndexOf(match[1]);
|
|
2327
|
-
if (depths[nameIndex] === 0)
|
|
2532
|
+
if (depths[nameIndex] === 0) {
|
|
2533
|
+
accessors.add(match[1]);
|
|
2534
|
+
if (!memberIndices.has(match[1])) memberIndices.set(match[1], nameIndex);
|
|
2535
|
+
}
|
|
2328
2536
|
}
|
|
2329
2537
|
const decoratedPattern = /@(property|state)\b/g;
|
|
2330
2538
|
for (const match of code.matchAll(decoratedPattern)) {
|
|
@@ -2337,7 +2545,12 @@ function inspectElementMembers(body) {
|
|
|
2337
2545
|
cursor = close + 1;
|
|
2338
2546
|
}
|
|
2339
2547
|
const declaration = /^\s*(?:(?:public|private|protected|static|readonly|override|declare|abstract)\s+)*(?:accessor\s+)?([A-Za-z_$][\w$]*)/.exec(code.slice(cursor));
|
|
2340
|
-
if (declaration)
|
|
2548
|
+
if (declaration) {
|
|
2549
|
+
reactive.add(declaration[1]);
|
|
2550
|
+
if (!memberIndices.has(declaration[1])) {
|
|
2551
|
+
memberIndices.set(declaration[1], cursor + declaration.index + declaration[0].lastIndexOf(declaration[1]));
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2341
2554
|
}
|
|
2342
2555
|
|
|
2343
2556
|
// An undecorated native-style accessor can still be reactive when its
|
|
@@ -2366,7 +2579,7 @@ function inspectElementMembers(body) {
|
|
|
2366
2579
|
if (close >= 0 && invalidates(body.slice(open + 1, close))) invalidatingAccessors.add(match[1]);
|
|
2367
2580
|
}
|
|
2368
2581
|
|
|
2369
|
-
return { fields, accessors, readonly, functionFields, reactive, invalidatingAccessors };
|
|
2582
|
+
return { fields, accessors, readonly, functionFields, reactive, invalidatingAccessors, memberIndices };
|
|
2370
2583
|
}
|
|
2371
2584
|
|
|
2372
2585
|
function findDecoratedClassMethods(body, decoratorName, bodyStart = 0) {
|
package/bin/snice.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { execFile } from 'node:child_process';
|
|
15
15
|
import { createRequire } from 'node:module';
|
|
16
16
|
import { tmpdir } from 'node:os';
|
|
17
|
-
import { basename, dirname, join, resolve } from 'node:path';
|
|
17
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
18
18
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
19
19
|
import { promisify } from 'node:util';
|
|
20
20
|
import { analyzeProject, findImports, isTypeOnlyImport, maskComments } from './project-analyzer.js';
|
|
@@ -26,6 +26,7 @@ const packageRoot = join(__dirname, '..');
|
|
|
26
26
|
const packageJson = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
|
|
27
27
|
const argv = process.argv.slice(2);
|
|
28
28
|
const command = argv[0];
|
|
29
|
+
const DIAGNOSTIC_IGNORE_FILENAME = '.sniceignore';
|
|
29
30
|
|
|
30
31
|
function help() {
|
|
31
32
|
console.log(`Snice CLI ${packageJson.version}
|
|
@@ -52,6 +53,9 @@ Commands:
|
|
|
52
53
|
AI setup:
|
|
53
54
|
npx snice init-ai Install version-matched guidance before an agent writes Snice code.
|
|
54
55
|
|
|
56
|
+
Diagnostics:
|
|
57
|
+
.sniceignore Suppress diagnostic codes globally or at an exact source location.
|
|
58
|
+
|
|
55
59
|
Build options:
|
|
56
60
|
--output=<dir> Output directory (default: ./dist/cdn)
|
|
57
61
|
--format=iife,es Output formats (default: iife; table: iife,es)
|
|
@@ -207,21 +211,79 @@ function walkSource(root) {
|
|
|
207
211
|
}
|
|
208
212
|
|
|
209
213
|
function validateProject(targetDir, json = false) {
|
|
210
|
-
const
|
|
214
|
+
const ignored = readDiagnosticIgnores(targetDir);
|
|
215
|
+
const issues = collectValidationIssues(targetDir, ignored);
|
|
211
216
|
renderValidation(issues, json);
|
|
212
217
|
return issues;
|
|
213
218
|
}
|
|
214
219
|
|
|
215
|
-
function collectValidationIssues(targetDir) {
|
|
220
|
+
function collectValidationIssues(targetDir, ignored = readDiagnosticIgnores(targetDir)) {
|
|
216
221
|
const sourceRoot = existsSync(join(targetDir, 'src')) ? join(targetDir, 'src') : targetDir;
|
|
217
222
|
const projectFiles = ['package.json', 'tsconfig.json', 'index.html']
|
|
218
223
|
.map(filename => join(targetDir, filename))
|
|
219
224
|
.filter(existsSync);
|
|
220
225
|
const files = [...new Set([...projectFiles, ...walkSource(sourceRoot)])].sort();
|
|
221
|
-
|
|
226
|
+
const issues = analyzeProject(files.map(file => ({
|
|
222
227
|
filename: file,
|
|
223
228
|
source: readFileSync(file, 'utf8')
|
|
224
229
|
})));
|
|
230
|
+
return filterIgnoredDiagnostics(issues, ignored, targetDir);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function readDiagnosticIgnores(targetDir) {
|
|
234
|
+
const path = join(targetDir, DIAGNOSTIC_IGNORE_FILENAME);
|
|
235
|
+
if (!existsSync(path)) return [];
|
|
236
|
+
|
|
237
|
+
const entries = [];
|
|
238
|
+
for (const authoredLine of readFileSync(path, 'utf8').replace(/^\uFEFF/, '').split(/\r?\n/)) {
|
|
239
|
+
const line = authoredLine.replace(/\s+#.*$/, '').trim();
|
|
240
|
+
if (!line || line.startsWith('#')) continue;
|
|
241
|
+
const separator = line.search(/\s/);
|
|
242
|
+
const code = separator < 0 ? line : line.slice(0, separator);
|
|
243
|
+
const locator = separator < 0 ? '' : line.slice(separator).trim();
|
|
244
|
+
if (!locator) {
|
|
245
|
+
entries.push({ code });
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let file = locator;
|
|
250
|
+
let lineNumber;
|
|
251
|
+
let column;
|
|
252
|
+
const exact = /^(.*):(\d+):(\d+)$/.exec(locator);
|
|
253
|
+
const lineOnly = exact ? null : /^(.*):(\d+)$/.exec(locator);
|
|
254
|
+
if (exact) {
|
|
255
|
+
file = exact[1];
|
|
256
|
+
lineNumber = Number(exact[2]);
|
|
257
|
+
column = Number(exact[3]);
|
|
258
|
+
} else if (lineOnly) {
|
|
259
|
+
file = lineOnly[1];
|
|
260
|
+
lineNumber = Number(lineOnly[2]);
|
|
261
|
+
}
|
|
262
|
+
if (!file) continue;
|
|
263
|
+
entries.push({ code, file: normalizeDiagnosticPath(file), line: lineNumber, column });
|
|
264
|
+
}
|
|
265
|
+
return entries;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function filterIgnoredDiagnostics(diagnostics, ignored, targetDir) {
|
|
269
|
+
if (!ignored.length) return diagnostics;
|
|
270
|
+
return diagnostics.filter(diagnostic => !ignored.some(entry => {
|
|
271
|
+
const code = diagnostic.code ?? diagnostic.ruleId;
|
|
272
|
+
if (entry.code !== code) return false;
|
|
273
|
+
if (!entry.file) return true;
|
|
274
|
+
if (!diagnostic.file) return false;
|
|
275
|
+
const absolute = isAbsolute(diagnostic.file)
|
|
276
|
+
? diagnostic.file
|
|
277
|
+
: resolve(targetDir, diagnostic.file);
|
|
278
|
+
if (normalizeDiagnosticPath(relative(targetDir, absolute)) !== entry.file) return false;
|
|
279
|
+
if (entry.line !== undefined && diagnostic.line !== entry.line) return false;
|
|
280
|
+
if (entry.column !== undefined && diagnostic.column !== entry.column) return false;
|
|
281
|
+
return true;
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function normalizeDiagnosticPath(path) {
|
|
286
|
+
return path.replaceAll('\\', '/').replace(/^\.\//, '');
|
|
225
287
|
}
|
|
226
288
|
|
|
227
289
|
function renderValidation(issues, json = false) {
|
|
@@ -230,7 +292,7 @@ function renderValidation(issues, json = false) {
|
|
|
230
292
|
else {
|
|
231
293
|
for (const issue of issues) {
|
|
232
294
|
console.log(
|
|
233
|
-
`[${issue.severity.toUpperCase()}] ${issue.file ?? ''}:${issue.line}:${issue.column} ${issue.
|
|
295
|
+
`[${issue.severity.toUpperCase()}] ${issue.file ?? ''}:${issue.line}:${issue.column} ${issue.code}\n` +
|
|
234
296
|
` ${issue.message}\n ${issue.fix}`
|
|
235
297
|
);
|
|
236
298
|
}
|
|
@@ -280,7 +342,8 @@ function resolveTsconfigChain(dir, entry = 'tsconfig.json', seen = new Set()) {
|
|
|
280
342
|
}
|
|
281
343
|
|
|
282
344
|
async function doctor(targetDir, json = false) {
|
|
283
|
-
const
|
|
345
|
+
const ignored = readDiagnosticIgnores(targetDir);
|
|
346
|
+
const findings = await collectDoctorFindings(targetDir, ignored);
|
|
284
347
|
renderDoctor(findings, json);
|
|
285
348
|
return findings;
|
|
286
349
|
}
|
|
@@ -381,7 +444,7 @@ async function probeDecoratorTransform(targetDir, add, projectPackage) {
|
|
|
381
444
|
}
|
|
382
445
|
}
|
|
383
446
|
|
|
384
|
-
async function collectDoctorFindings(targetDir) {
|
|
447
|
+
async function collectDoctorFindings(targetDir, ignored = readDiagnosticIgnores(targetDir)) {
|
|
385
448
|
const findings = [];
|
|
386
449
|
const add = (severity, code, message) => findings.push({ severity, code, message });
|
|
387
450
|
const projectPackage = readJson(join(targetDir, 'package.json'));
|
|
@@ -443,7 +506,7 @@ async function collectDoctorFindings(targetDir) {
|
|
|
443
506
|
|
|
444
507
|
await probeDecoratorTransform(targetDir, add, projectPackage);
|
|
445
508
|
|
|
446
|
-
return findings;
|
|
509
|
+
return filterIgnoredDiagnostics(findings, ignored, targetDir);
|
|
447
510
|
}
|
|
448
511
|
|
|
449
512
|
function renderDoctor(findings, json = false) {
|
|
@@ -451,15 +514,16 @@ function renderDoctor(findings, json = false) {
|
|
|
451
514
|
const warnings = findings.filter(item => item.severity === 'warning').length;
|
|
452
515
|
if (json) console.log(JSON.stringify({ ok: errors === 0, findings }, null, 2));
|
|
453
516
|
else {
|
|
454
|
-
for (const finding of findings) console.log(`[${finding.severity.toUpperCase()}] ${finding.message}`);
|
|
517
|
+
for (const finding of findings) console.log(`[${finding.severity.toUpperCase()}] ${finding.code} ${finding.message}`);
|
|
455
518
|
console.log(`Doctor finished with ${errors} error(s) and ${warnings} warning(s).`);
|
|
456
519
|
}
|
|
457
520
|
if (errors) process.exitCode = 1;
|
|
458
521
|
}
|
|
459
522
|
|
|
460
523
|
async function checkProject(targetDir, json = false) {
|
|
461
|
-
const
|
|
462
|
-
const
|
|
524
|
+
const ignored = readDiagnosticIgnores(targetDir);
|
|
525
|
+
const findings = await collectDoctorFindings(targetDir, ignored);
|
|
526
|
+
const issues = collectValidationIssues(targetDir, ignored);
|
|
463
527
|
const ok = !findings.some(item => item.severity === 'error') &&
|
|
464
528
|
!issues.some(item => item.severity === 'error');
|
|
465
529
|
|
|
@@ -467,12 +531,12 @@ async function checkProject(targetDir, json = false) {
|
|
|
467
531
|
console.log(JSON.stringify({ ok, findings, issues }, null, 2));
|
|
468
532
|
} else {
|
|
469
533
|
console.log('Project configuration:');
|
|
470
|
-
for (const finding of findings) console.log(`[${finding.severity.toUpperCase()}] ${finding.message}`);
|
|
534
|
+
for (const finding of findings) console.log(`[${finding.severity.toUpperCase()}] ${finding.code} ${finding.message}`);
|
|
471
535
|
console.log('\nSnice source:');
|
|
472
536
|
if (!issues.length) console.log('No Snice authoring issues found.');
|
|
473
537
|
for (const issue of issues) {
|
|
474
538
|
console.log(
|
|
475
|
-
`[${issue.severity.toUpperCase()}] ${issue.file ?? ''}:${issue.line}:${issue.column} ${issue.
|
|
539
|
+
`[${issue.severity.toUpperCase()}] ${issue.file ?? ''}:${issue.line}:${issue.column} ${issue.code}\n` +
|
|
476
540
|
` ${issue.message}\n ${issue.fix}`
|
|
477
541
|
);
|
|
478
542
|
}
|
package/dist/app-context.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { getContext } from './context-provider';
|
|
1
|
+
import { getContext, getContextFetch } from './context-provider';
|
|
2
2
|
import type { AppContext } from './types/app-context';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*/
|
|
10
|
-
export declare function
|
|
11
|
-
export { getContext };
|
|
3
|
+
import type { Context } from './types/context';
|
|
4
|
+
export interface ContextProviderOptions {
|
|
5
|
+
/** Fetch function exposed to descendants through getContextFetch(). */
|
|
6
|
+
fetch?: typeof globalThis.fetch;
|
|
7
|
+
}
|
|
8
|
+
export declare function provideContext(root: EventTarget, context: AppContext, options?: ContextProviderOptions): () => void;
|
|
9
|
+
/** @internal Router-owned provider including live navigation state. */
|
|
10
|
+
export declare function provideRouterContext(root: EventTarget, navigation: Context): () => void;
|
|
11
|
+
export { getContext, getContextFetch };
|