snice 7.5.0 → 7.7.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/adapters/react/SniceProvider.js +1 -1
- package/adapters/react/SniceRouter.js +1 -1
- package/adapters/react/avatar.d.ts +1 -0
- package/adapters/react/avatar.d.ts.map +1 -1
- package/adapters/react/avatar.js +1 -1
- package/adapters/react/avatar.js.map +1 -1
- package/adapters/react/avatar.tsx +2 -1
- package/adapters/react/calendar.d.ts +4 -0
- package/adapters/react/calendar.d.ts.map +1 -1
- package/adapters/react/calendar.js +1 -1
- package/adapters/react/calendar.js.map +1 -1
- package/adapters/react/calendar.tsx +5 -1
- package/adapters/react/matchRoute.js +68 -2
- package/adapters/react/matchRoute.js.map +1 -1
- package/adapters/react/route-match.d.ts +7 -0
- package/adapters/react/useRequestHandler.js +1 -1
- package/bin/analyzer-contracts.js +60 -2
- package/bin/project-analyzer.js +1262 -118
- package/bin/snice.js +95 -5
- package/custom-elements.json +112 -0
- 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/README.md +1 -1
- package/dist/cdn/avatar/snice-avatar.js +9 -3
- package/dist/cdn/avatar/snice-avatar.js.map +1 -1
- package/dist/cdn/avatar/snice-avatar.min.js +2 -2
- package/dist/cdn/avatar/snice-avatar.min.js.map +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 +2 -2
- package/dist/cdn/button/snice-button.js.map +1 -1
- package/dist/cdn/button/snice-button.min.js +1 -1
- package/dist/cdn/button/snice-button.min.js.map +1 -1
- package/dist/cdn/calendar/README.md +2 -2
- package/dist/cdn/calendar/snice-calendar.js +562 -40
- package/dist/cdn/calendar/snice-calendar.js.map +1 -1
- package/dist/cdn/calendar/snice-calendar.min.js +28 -1
- package/dist/cdn/calendar/snice-calendar.min.js.map +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/README.md +1 -1
- package/dist/cdn/login/snice-login.js +2 -2
- package/dist/cdn/login/snice-login.js.map +1 -1
- package/dist/cdn/login/snice-login.min.js +1 -1
- package/dist/cdn/login/snice-login.min.js.map +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 +706 -267
- 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 +706 -267
- 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 +3 -1
- package/dist/cdn/select/snice-select.js.map +1 -1
- package/dist/cdn/select/snice-select.min.js +2 -2
- package/dist/cdn/select/snice-select.min.js.map +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/README.md +1 -1
- package/dist/cdn/switch/snice-switch.js +8 -4
- package/dist/cdn/switch/snice-switch.js.map +1 -1
- package/dist/cdn/switch/snice-switch.min.js +8 -4
- package/dist/cdn/switch/snice-switch.min.js.map +1 -1
- package/dist/cdn/table/README.md +2 -2
- package/dist/cdn/table/snice-table.esm.js +77 -5
- package/dist/cdn/table/snice-table.esm.js.map +1 -1
- package/dist/cdn/table/snice-table.esm.min.js +68 -47
- package/dist/cdn/table/snice-table.esm.min.js.map +1 -1
- package/dist/cdn/table/snice-table.js +76 -4
- package/dist/cdn/table/snice-table.js.map +1 -1
- package/dist/cdn/table/snice-table.min.js +53 -32
- package/dist/cdn/table/snice-table.min.js.map +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/avatar/snice-avatar.d.ts +1 -0
- package/dist/components/avatar/snice-avatar.js +8 -2
- package/dist/components/avatar/snice-avatar.js.map +1 -1
- package/dist/components/avatar/snice-avatar.types.d.ts +2 -0
- package/dist/components/button/snice-button.js +1 -1
- package/dist/components/button/snice-button.js.map +1 -1
- package/dist/components/calendar/snice-calendar.d.ts +38 -1
- package/dist/components/calendar/snice-calendar.js +390 -40
- package/dist/components/calendar/snice-calendar.js.map +1 -1
- package/dist/components/calendar/snice-calendar.types.d.ts +63 -0
- package/dist/components/select/snice-select.js +2 -0
- package/dist/components/select/snice-select.js.map +1 -1
- package/dist/components/switch/snice-switch.js +7 -3
- package/dist/components/switch/snice-switch.js.map +1 -1
- package/dist/components/table/snice-table.d.ts +9 -0
- package/dist/components/table/snice-table.js +72 -2
- package/dist/components/table/snice-table.js.map +1 -1
- package/dist/components/table/table-keyboard.js +1 -1
- package/dist/components/table/table-keyboard.js.map +1 -1
- package/dist/index.cjs +703 -264
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.esm.js +703 -264
- package/dist/index.esm.js.map +1 -1
- package/dist/index.iife.js +703 -264
- package/dist/index.iife.js.map +1 -1
- package/dist/parts.d.ts +14 -4
- 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 +68 -2
- package/dist/react/matchRoute.js.map +1 -1
- package/dist/react/route-match.d.ts +7 -0
- package/dist/react/useRequestHandler.js +1 -1
- package/dist/render-errors.d.ts +9 -0
- package/dist/route-match.d.ts +7 -0
- package/dist/symbols.cjs +1 -1
- package/dist/symbols.esm.js +1 -1
- package/dist/template-marker.d.ts +5 -0
- package/dist/testing-dom.cjs +1 -1
- package/dist/testing-dom.esm.js +1 -1
- package/dist/testing.d.ts +3 -0
- package/dist/transitions.cjs +1 -1
- package/dist/transitions.esm.js +1 -1
- package/dist/types/dispatch-options.d.ts +5 -4
- package/dist/types/event-timing.d.ts +7 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/on-options.d.ts +21 -6
- package/dist/utils.d.ts +3 -0
- package/docs/ai/DEVELOPMENT.md +1 -1
- package/docs/ai/api.md +7 -2
- package/docs/ai/cli.md +14 -0
- package/docs/ai/components/avatar.md +1 -0
- package/docs/ai/components/calendar.md +24 -1
- package/docs/ai/components/switch.md +3 -0
- package/docs/ai/components/table.md +1 -1
- package/docs/ai/decorators.md +3 -2
- package/docs/ai/events.md +45 -8
- package/docs/ai/rendering.md +7 -0
- package/docs/ai/routing.md +6 -1
- package/docs/ai/testing.md +17 -0
- package/docs/cli.md +19 -0
- package/docs/components/avatar.md +1 -0
- package/docs/components/calendar.md +112 -2
- package/docs/components/switch.md +7 -1
- package/docs/components/table.md +2 -1
- package/docs/events.md +90 -10
- package/docs/rendering.md +10 -0
- package/docs/routing.md +22 -1
- package/docs/testing.md +19 -0
- package/package.json +2 -2
- package/vscode.html-custom-data.json +28 -0
package/bin/project-analyzer.js
CHANGED
|
@@ -530,6 +530,38 @@ const RULE_DEFINITIONS = [
|
|
|
530
530
|
}
|
|
531
531
|
}
|
|
532
532
|
},
|
|
533
|
+
{
|
|
534
|
+
id: 'snice/unguarded-context-load',
|
|
535
|
+
severity: 'warning',
|
|
536
|
+
category: 'lifecycle',
|
|
537
|
+
description: 'Warn when a controller starts repeatable load work from an unguarded @context handler.',
|
|
538
|
+
check(context) {
|
|
539
|
+
if (isFrameworkImplementation(context.filename) || isTestFilename(context.filename)) return;
|
|
540
|
+
for (const declaration of findDecoratedClasses(context.source, 'controller')) {
|
|
541
|
+
for (const method of findDecoratedClassMethods(declaration.body, 'context', declaration.bodyStart)) {
|
|
542
|
+
if (/\bonce\s*:\s*true\b/.test(method.decoratorArguments)) continue;
|
|
543
|
+
|
|
544
|
+
const body = blankStringContents(maskComments(method.body));
|
|
545
|
+
const load = /\b(?:void\s+|await\s+)?this\.((?:re)?load|refresh|fetch)(?:[A-Z_$][\w$]*)?\s*\(/.exec(body);
|
|
546
|
+
if (!load) continue;
|
|
547
|
+
|
|
548
|
+
const beforeLoad = body.slice(0, load.index);
|
|
549
|
+
const statementPrefix = beforeLoad.slice(Math.max(
|
|
550
|
+
beforeLoad.lastIndexOf(';'),
|
|
551
|
+
beforeLoad.lastIndexOf('\n')
|
|
552
|
+
) + 1);
|
|
553
|
+
const hasGuard = /\b(?:if|switch|for|while)\s*\(|\breturn\b/.test(beforeLoad)
|
|
554
|
+
|| /(?:&&|\|\||\?)/.test(statementPrefix);
|
|
555
|
+
if (hasGuard) continue;
|
|
556
|
+
|
|
557
|
+
context.report(method.bodyStart + load.index, {
|
|
558
|
+
message: `${declaration.name}.${method.name} starts ${load[1]} work from @context(), which fires on every context update while attached.`,
|
|
559
|
+
fix: 'Add an explicit first-delivery guard reset in detach(), pass @context({ once: true }) for a true one-shot, or diff the context update before loading. Review docs/ai/controllers.md.'
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
},
|
|
533
565
|
{
|
|
534
566
|
id: 'snice/imperative-controller-attach',
|
|
535
567
|
severity: 'suggestion',
|
|
@@ -555,16 +587,8 @@ const RULE_DEFINITIONS = [
|
|
|
555
587
|
description: 'Warn when a Snice element redeclares an inherited HTMLElement IDL member.',
|
|
556
588
|
check(context) {
|
|
557
589
|
if (isFrameworkImplementation(context.filename)) return;
|
|
558
|
-
for (const
|
|
559
|
-
|
|
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
|
-
}
|
|
590
|
+
for (const finding of findNativeIdlShadowFindings(context.source, context.provenance)) {
|
|
591
|
+
context.report(finding.index, finding);
|
|
568
592
|
}
|
|
569
593
|
}
|
|
570
594
|
},
|
|
@@ -1512,6 +1536,14 @@ const RULE_DEFINITIONS = [
|
|
|
1512
1536
|
projectOnly: true,
|
|
1513
1537
|
check() {}
|
|
1514
1538
|
},
|
|
1539
|
+
{
|
|
1540
|
+
id: 'snice/route-param-has-no-binding-target',
|
|
1541
|
+
severity: 'warning',
|
|
1542
|
+
category: 'router',
|
|
1543
|
+
description: 'Warn when a page route parameter has no statically reachable binding target through the Router attribute channel.',
|
|
1544
|
+
projectOnly: true,
|
|
1545
|
+
check() {}
|
|
1546
|
+
},
|
|
1515
1547
|
{
|
|
1516
1548
|
id: 'snice/shadowed-query-route',
|
|
1517
1549
|
severity: 'warning',
|
|
@@ -1662,6 +1694,7 @@ export function analyzeProject(files) {
|
|
|
1662
1694
|
const routerConstructions = [];
|
|
1663
1695
|
const architectureDeclarations = [];
|
|
1664
1696
|
const pageClasses = [];
|
|
1697
|
+
const localClassDeclarations = [];
|
|
1665
1698
|
const reactiveRenderContracts = new Map();
|
|
1666
1699
|
const localElementContracts = new Map();
|
|
1667
1700
|
const staleGuards = [];
|
|
@@ -1686,8 +1719,37 @@ export function analyzeProject(files) {
|
|
|
1686
1719
|
staleGuards.push({ file, index: guard.index, field: guard[2], owner: owner?.kind });
|
|
1687
1720
|
}
|
|
1688
1721
|
const provenance = buildSourceProvenance(analyzable);
|
|
1722
|
+
enrichSniceProvenance(provenance, file, normalized);
|
|
1723
|
+
if (!isFrameworkImplementation(file.filename)) {
|
|
1724
|
+
for (const finding of findNativeIdlShadowFindings(analyzable, provenance)) {
|
|
1725
|
+
const location = sourceLocation(file.source, finding.index);
|
|
1726
|
+
const duplicate = diagnostics.some(diagnostic =>
|
|
1727
|
+
diagnostic.ruleId === 'snice/element-member-shadows-native-idl' &&
|
|
1728
|
+
diagnostic.file === file.filename &&
|
|
1729
|
+
diagnostic.line === location.line &&
|
|
1730
|
+
diagnostic.column === location.column
|
|
1731
|
+
);
|
|
1732
|
+
if (!duplicate) {
|
|
1733
|
+
diagnostics.push({
|
|
1734
|
+
severity: 'warning',
|
|
1735
|
+
ruleId: 'snice/element-member-shadows-native-idl',
|
|
1736
|
+
message: finding.message,
|
|
1737
|
+
fix: finding.fix,
|
|
1738
|
+
file: file.filename,
|
|
1739
|
+
line: location.line,
|
|
1740
|
+
column: location.column
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
localClassDeclarations.push(...findClassBodies(analyzable).map(declaration => ({
|
|
1746
|
+
...declaration,
|
|
1747
|
+
file,
|
|
1748
|
+
provenance,
|
|
1749
|
+
routeProperties: inspectRouteProperties(declaration.body, declaration.bodyStart, provenance)
|
|
1750
|
+
})));
|
|
1689
1751
|
routerConstructions.push(
|
|
1690
|
-
...findRouterConstructions(analyzable,
|
|
1752
|
+
...findRouterConstructions(analyzable, provenRootBindingNames(provenance, 'Router'))
|
|
1691
1753
|
.map(construction => ({ ...construction, file }))
|
|
1692
1754
|
);
|
|
1693
1755
|
for (const convention of [
|
|
@@ -1696,10 +1758,23 @@ export function analyzeProject(files) {
|
|
|
1696
1758
|
{ decorator: 'controller', folder: 'controllers', kind: 'controller' },
|
|
1697
1759
|
{ decorator: 'daemon', folder: 'daemons', kind: 'daemon' }
|
|
1698
1760
|
]) {
|
|
1699
|
-
|
|
1761
|
+
const bindingPageNames = convention.decorator === 'page'
|
|
1762
|
+
? routeDecoratorNames(provenance, file, normalized)
|
|
1763
|
+
: [];
|
|
1764
|
+
const decoratorNames = convention.decorator === 'page'
|
|
1765
|
+
? [...new Set(['page', ...bindingPageNames])]
|
|
1766
|
+
: [convention.decorator];
|
|
1767
|
+
const declarations = decoratorNames.flatMap(name =>
|
|
1768
|
+
findDecoratedClasses(analyzable, name).map(declaration => ({ ...declaration, routeDecorator: name }))
|
|
1769
|
+
);
|
|
1770
|
+
for (const declaration of declarations.filter((item, index, all) =>
|
|
1771
|
+
all.findIndex(candidate => candidate.bodyStart === item.bodyStart) === index
|
|
1772
|
+
)) {
|
|
1700
1773
|
const architectural = { ...declaration, ...convention, file };
|
|
1701
1774
|
architectureDeclarations.push(architectural);
|
|
1702
|
-
if (convention.decorator === 'page'
|
|
1775
|
+
if (convention.decorator === 'page' && bindingPageNames.includes(declaration.routeDecorator)) {
|
|
1776
|
+
pageClasses.push(architectural);
|
|
1777
|
+
}
|
|
1703
1778
|
if (convention.decorator === 'page' || convention.decorator === 'element') {
|
|
1704
1779
|
reactiveRenderContracts.set(`${file.filename}:${declaration.bodyStart}`, {
|
|
1705
1780
|
...architectural,
|
|
@@ -1968,6 +2043,86 @@ export function analyzeProject(files) {
|
|
|
1968
2043
|
});
|
|
1969
2044
|
}
|
|
1970
2045
|
|
|
2046
|
+
const bindingEnvironment = resolveRouteBindingEnvironment(page, localClassDeclarations, normalized);
|
|
2047
|
+
const reportedRouteAttributes = new Set();
|
|
2048
|
+
for (const route of findPageRoutes(page.file.source, page.index)) {
|
|
2049
|
+
for (const parameter of findRouteParameters(route)) {
|
|
2050
|
+
const routeAttribute = parameter.name.toLowerCase();
|
|
2051
|
+
if (reportedRouteAttributes.has(routeAttribute)) continue;
|
|
2052
|
+
|
|
2053
|
+
const reachable = bindingEnvironment.properties.find(property =>
|
|
2054
|
+
property.attributeKind !== 'false' &&
|
|
2055
|
+
property.attributeKind !== 'unknown' &&
|
|
2056
|
+
routePropertyAttribute(property, bindingEnvironment.naming) === routeAttribute
|
|
2057
|
+
);
|
|
2058
|
+
if (reachable) continue;
|
|
2059
|
+
|
|
2060
|
+
const sameName = bindingEnvironment.properties.find(property => property.name === parameter.name);
|
|
2061
|
+
if (sameName?.attributeKind === 'unknown') continue;
|
|
2062
|
+
if (sameName?.attributeKind === 'default' && bindingEnvironment.naming === 'unknown') continue;
|
|
2063
|
+
if (bindingEnvironment.customAttributes.has(routeAttribute)) continue;
|
|
2064
|
+
|
|
2065
|
+
reportedRouteAttributes.add(routeAttribute);
|
|
2066
|
+
const location = sourceLocation(page.file.source, parameter.index);
|
|
2067
|
+
const routeLabel = `${parameter.marker}${parameter.name}`;
|
|
2068
|
+
if (sameName?.attributeKind === 'false') {
|
|
2069
|
+
const disabledBy = sameName.kind === 'state'
|
|
2070
|
+
? '@state() is internal state and disables the attribute channel used by Router'
|
|
2071
|
+
: '@property({ attribute: false }) disables the attribute channel used by Router';
|
|
2072
|
+
const enableFix = sameName.kind === 'state'
|
|
2073
|
+
? `Replace @state() with a plain @property() on ${sameName.name}`
|
|
2074
|
+
: `Remove attribute: false so ${sameName.name} is a plain bindable @property()`;
|
|
2075
|
+
diagnostics.push({
|
|
2076
|
+
severity: 'warning',
|
|
2077
|
+
ruleId: 'snice/route-param-has-no-binding-target',
|
|
2078
|
+
message: `Route parameter "${routeLabel}" cannot bind ${page.name}.${sameName.name} because ${disabledBy}.`,
|
|
2079
|
+
fix: `${enableFix}, or remove/rename "${routeLabel}" if the URL value is not a page input. Review docs/ai/routing.md.`,
|
|
2080
|
+
file: page.file.filename,
|
|
2081
|
+
line: location.line,
|
|
2082
|
+
column: location.column
|
|
2083
|
+
});
|
|
2084
|
+
continue;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
if (sameName) {
|
|
2088
|
+
const observedAttribute = routePropertyAttribute(sameName, bindingEnvironment.naming);
|
|
2089
|
+
const defaultAttribute = defaultRoutePropertyAttribute(sameName.name, bindingEnvironment.naming);
|
|
2090
|
+
const removeAliasFix = sameName.attributeKind === 'alias' && defaultAttribute === routeAttribute
|
|
2091
|
+
? `remove the explicit attribute alias so @property() ${sameName.name} observes "${routeAttribute}"`
|
|
2092
|
+
: `declare @property({ attribute: '${parameter.name}' }) ${sameName.name} so it observes "${routeAttribute}"`;
|
|
2093
|
+
diagnostics.push({
|
|
2094
|
+
severity: 'warning',
|
|
2095
|
+
ruleId: 'snice/route-param-has-no-binding-target',
|
|
2096
|
+
message: `Route parameter "${routeLabel}" sets the "${routeAttribute}" attribute, but ${page.name}.${sameName.name} observes "${observedAttribute}", so Router cannot reach it.`,
|
|
2097
|
+
fix: `Change the route parameter spelling to "${parameter.marker}${observedAttribute}", or ${removeAliasFix}. Review docs/ai/routing.md.`,
|
|
2098
|
+
file: page.file.filename,
|
|
2099
|
+
line: location.line,
|
|
2100
|
+
column: location.column
|
|
2101
|
+
});
|
|
2102
|
+
continue;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
if (bindingEnvironment.nativeAttributes.has(routeAttribute)) continue;
|
|
2106
|
+
if (!bindingEnvironment.complete) continue;
|
|
2107
|
+
|
|
2108
|
+
const suggestedName = kebabToCamel(parameter.name);
|
|
2109
|
+
const propertySuggestion = /^[A-Za-z_$][\w$]*$/.test(suggestedName)
|
|
2110
|
+
? suggestedName === parameter.name
|
|
2111
|
+
? `Add @property() ${suggestedName} = ''; to ${page.name}`
|
|
2112
|
+
: `Add @property({ attribute: '${parameter.name}' }) ${suggestedName} = ''; to ${page.name}`
|
|
2113
|
+
: `Rename "${routeLabel}" to a valid property name and add a matching plain @property()`;
|
|
2114
|
+
diagnostics.push({
|
|
2115
|
+
severity: 'warning',
|
|
2116
|
+
ruleId: 'snice/route-param-has-no-binding-target',
|
|
2117
|
+
message: `Route parameter "${routeLabel}" has no bindable @property on ${page.name}; Router sets the "${routeAttribute}" attribute, which otherwise does not populate page state.`,
|
|
2118
|
+
fix: `${propertySuggestion}, or remove the parameter if the page does not consume it. Review docs/ai/routing.md.`,
|
|
2119
|
+
file: page.file.filename,
|
|
2120
|
+
line: location.line,
|
|
2121
|
+
column: location.column
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
|
|
1971
2126
|
const precedingBareRoutes = new Set();
|
|
1972
2127
|
for (const route of findPageStringRoutes(page.file.source, page.index)) {
|
|
1973
2128
|
const queryIndex = route.spec.indexOf('?');
|
|
@@ -2238,7 +2393,8 @@ function usesComponent(source, recommendation) {
|
|
|
2238
2393
|
|
|
2239
2394
|
export function findImports(source) {
|
|
2240
2395
|
const imports = [];
|
|
2241
|
-
|
|
2396
|
+
const code = maskNonExecutableCode(source);
|
|
2397
|
+
for (const start of code.matchAll(/^\s*import\b/gm)) {
|
|
2242
2398
|
const statement = source.slice(start.index, start.index + 4000);
|
|
2243
2399
|
const sideEffect = statement.match(/^\s*import\s*(['"])([^'"]+)\1\s*;?/);
|
|
2244
2400
|
if (sideEffect) {
|
|
@@ -2282,13 +2438,15 @@ function findObjectCalls(source, functionName) {
|
|
|
2282
2438
|
|
|
2283
2439
|
function findClassBodies(source) {
|
|
2284
2440
|
const classes = [];
|
|
2285
|
-
const pattern = /\bclass\s+[A-Za-z_$][\w$]
|
|
2441
|
+
const pattern = /\bclass\s+([A-Za-z_$][\w$]*)\s+extends\s+([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?)[^{]*\{/g;
|
|
2286
2442
|
for (const match of source.matchAll(pattern)) {
|
|
2287
2443
|
const open = source.indexOf('{', match.index);
|
|
2288
2444
|
const close = findMatchingDelimiter(source, open, '{', '}');
|
|
2289
2445
|
if (close < 0) continue;
|
|
2290
2446
|
classes.push({
|
|
2291
|
-
|
|
2447
|
+
index: match.index,
|
|
2448
|
+
name: match[1],
|
|
2449
|
+
base: match[2],
|
|
2292
2450
|
bodyStart: open + 1,
|
|
2293
2451
|
body: source.slice(open + 1, close)
|
|
2294
2452
|
});
|
|
@@ -2373,7 +2531,7 @@ function findDecoratedClasses(source, decoratorName) {
|
|
|
2373
2531
|
// intervening decorators so each Snice role resolves to that class.
|
|
2374
2532
|
while (true) {
|
|
2375
2533
|
while (/\s/.test(source[declarationStart] ?? '')) declarationStart++;
|
|
2376
|
-
const nextDecorator = /^@[A-Za-z_$][\w$]*/.exec(source.slice(declarationStart));
|
|
2534
|
+
const nextDecorator = /^@[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*/.exec(source.slice(declarationStart));
|
|
2377
2535
|
if (!nextDecorator) break;
|
|
2378
2536
|
declarationStart += nextDecorator[0].length;
|
|
2379
2537
|
while (/\s/.test(source[declarationStart] ?? '')) declarationStart++;
|
|
@@ -2392,6 +2550,7 @@ function findDecoratedClasses(source, decoratorName) {
|
|
|
2392
2550
|
declarations.push({
|
|
2393
2551
|
index: match.index,
|
|
2394
2552
|
name: declaration[1],
|
|
2553
|
+
base: /\bextends\s+([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?)/.exec(declaration[0])?.[1] ?? null,
|
|
2395
2554
|
bodyStart: open + 1,
|
|
2396
2555
|
body: source.slice(open + 1, close)
|
|
2397
2556
|
});
|
|
@@ -2412,6 +2571,91 @@ function findSniceElementClasses(source) {
|
|
|
2412
2571
|
return declarations;
|
|
2413
2572
|
}
|
|
2414
2573
|
|
|
2574
|
+
function parameterDeclaresBinding(parameters, name) {
|
|
2575
|
+
const escaped = escapeRegExp(name);
|
|
2576
|
+
return splitTopLevelArguments(parameters).some(parameter => {
|
|
2577
|
+
const value = parameter.trim().replace(/^\.\.\.\s*/, '');
|
|
2578
|
+
if (new RegExp(`^${escaped}(?=\\s*[?:=,]|\\s*$)`).test(value)) return true;
|
|
2579
|
+
if (!value.startsWith('{') && !value.startsWith('[')) return false;
|
|
2580
|
+
return new RegExp(
|
|
2581
|
+
`(?:^|[,\\[{:])\\s*${escaped}(?=\\s*[,}\\]=?:]|\\s*$)`
|
|
2582
|
+
).test(value);
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
function isImportedDecoratorBindingShadowed(source, bindingName, useIndex) {
|
|
2587
|
+
const controls = new Set(['if', 'for', 'while', 'switch', 'with']);
|
|
2588
|
+
for (let open = source.indexOf('('); open >= 0 && open < useIndex; open = source.indexOf('(', open + 1)) {
|
|
2589
|
+
const close = findMatchingDelimiter(source, open, '(', ')');
|
|
2590
|
+
if (close < 0 || close >= useIndex) continue;
|
|
2591
|
+
let cursor = close + 1;
|
|
2592
|
+
while (/\s/.test(source[cursor] ?? '')) cursor++;
|
|
2593
|
+
if (source[cursor] === ':') {
|
|
2594
|
+
cursor++;
|
|
2595
|
+
while (cursor < source.length && source[cursor] !== '{' && !source.startsWith('=>', cursor)) cursor++;
|
|
2596
|
+
}
|
|
2597
|
+
while (/\s/.test(source[cursor] ?? '')) cursor++;
|
|
2598
|
+
let bodyOpen = -1;
|
|
2599
|
+
if (source.startsWith('=>', cursor)) {
|
|
2600
|
+
cursor += 2;
|
|
2601
|
+
while (/\s/.test(source[cursor] ?? '')) cursor++;
|
|
2602
|
+
if (source[cursor] === '{') bodyOpen = cursor;
|
|
2603
|
+
} else if (source[cursor] === '{') {
|
|
2604
|
+
const prefix = source.slice(Math.max(0, open - 160), open);
|
|
2605
|
+
const preceding = /([A-Za-z_$][\w$]*)\s*$/.exec(prefix)?.[1];
|
|
2606
|
+
if (preceding && !controls.has(preceding)) bodyOpen = cursor;
|
|
2607
|
+
}
|
|
2608
|
+
if (bodyOpen < 0) continue;
|
|
2609
|
+
const bodyClose = findMatchingDelimiter(source, bodyOpen, '{', '}');
|
|
2610
|
+
if (bodyClose < useIndex) continue;
|
|
2611
|
+
if (parameterDeclaresBinding(source.slice(open + 1, close), bindingName)) return true;
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
const escaped = escapeRegExp(bindingName);
|
|
2615
|
+
for (const arrow of source.matchAll(new RegExp(`\\b${escaped}\\s*=>\\s*\\{`, 'g'))) {
|
|
2616
|
+
const bodyOpen = source.indexOf('{', arrow.index);
|
|
2617
|
+
const bodyClose = findMatchingDelimiter(source, bodyOpen, '{', '}');
|
|
2618
|
+
if (bodyOpen < useIndex && useIndex < bodyClose) return true;
|
|
2619
|
+
}
|
|
2620
|
+
return false;
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
function findProvenSniceElementClasses(source, provenance) {
|
|
2624
|
+
const code = maskNonExecutableCode(source, { preserveTemplateExpressions: true });
|
|
2625
|
+
const decoratorNames = new Set(provenRootBindingNames(provenance, 'element'));
|
|
2626
|
+
const declarations = [];
|
|
2627
|
+
const seen = new Set();
|
|
2628
|
+
for (const decoratorName of decoratorNames) {
|
|
2629
|
+
const bindingName = decoratorName.split('.')[0];
|
|
2630
|
+
for (const declaration of findDecoratedClasses(code, decoratorName)) {
|
|
2631
|
+
if (
|
|
2632
|
+
seen.has(declaration.bodyStart) ||
|
|
2633
|
+
isImportedDecoratorBindingShadowed(code, bindingName, declaration.index)
|
|
2634
|
+
) continue;
|
|
2635
|
+
seen.add(declaration.bodyStart);
|
|
2636
|
+
declarations.push(declaration);
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
return declarations;
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
function findNativeIdlShadowFindings(source, provenance) {
|
|
2643
|
+
const findings = [];
|
|
2644
|
+
for (const declaration of findProvenSniceElementClasses(source, provenance)) {
|
|
2645
|
+
const members = inspectElementMembers(declaration.body);
|
|
2646
|
+
for (const name of new Set([...members.fields, ...members.accessors, ...members.reactive])) {
|
|
2647
|
+
if (!NATIVE_ELEMENT_IDL_MEMBERS.has(name)) continue;
|
|
2648
|
+
const relativeIndex = members.memberIndices.get(name) ?? 0;
|
|
2649
|
+
findings.push({
|
|
2650
|
+
index: declaration.bodyStart + relativeIndex,
|
|
2651
|
+
message: `${declaration.name}.${name} shadows HTMLElement.${name}, so native reflection/accessibility behavior and Snice property binding can conflict.`,
|
|
2652
|
+
fix: `Rename the application-owned member. If native ${name} semantics are intended, use the inherited HTMLElement property/attribute without redeclaring it.`
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
return findings;
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2415
2659
|
/**
|
|
2416
2660
|
* Find standalone, static CustomEvent dispatches that @dispatch can express
|
|
2417
2661
|
* without changing control flow. Calls whose return value is consumed, whose
|
|
@@ -2472,29 +2716,777 @@ function findElementDecoratorTag(source, index) {
|
|
|
2472
2716
|
}
|
|
2473
2717
|
|
|
2474
2718
|
function findPageStringRoutes(source, decoratorIndex) {
|
|
2719
|
+
return findPageRoutes(source, decoratorIndex).filter(route => route.kind === 'string');
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
function findPageRoutes(source, decoratorIndex) {
|
|
2475
2723
|
const open = source.indexOf('(', decoratorIndex);
|
|
2476
2724
|
if (open < 0) return [];
|
|
2477
2725
|
const close = findMatchingDelimiter(source, open, '(', ')');
|
|
2478
2726
|
if (close < 0) return [];
|
|
2479
|
-
const
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2727
|
+
const options = unwrapStaticExpression(source, open + 1, close);
|
|
2728
|
+
if (!options || source[options.start] !== '{') return [];
|
|
2729
|
+
const routes = findStaticObjectProperty(source, options.start, options.end, 'routes');
|
|
2730
|
+
if (!routes) return [];
|
|
2731
|
+
const array = unwrapStaticExpression(source, routes.start, routes.end);
|
|
2732
|
+
if (!array || source[array.start] !== '[') return [];
|
|
2733
|
+
const arrayOpen = array.start;
|
|
2483
2734
|
const arrayClose = findMatchingDelimiter(source, arrayOpen, '[', ']');
|
|
2484
|
-
if (arrayClose < 0 || arrayClose >
|
|
2735
|
+
if (arrayClose < 0 || arrayClose > array.end) return [];
|
|
2485
2736
|
|
|
2486
2737
|
const body = source.slice(arrayOpen + 1, arrayClose);
|
|
2487
|
-
const
|
|
2738
|
+
const found = [];
|
|
2488
2739
|
let searchFrom = 0;
|
|
2489
2740
|
for (const item of splitTopLevelArguments(body)) {
|
|
2490
2741
|
const relative = body.indexOf(item, searchFrom);
|
|
2491
2742
|
if (relative < 0) continue;
|
|
2492
2743
|
searchFrom = relative + item.length;
|
|
2493
|
-
const
|
|
2494
|
-
if (!
|
|
2495
|
-
|
|
2744
|
+
const itemBounds = unwrapStaticExpression(source, arrayOpen + 1 + relative, arrayOpen + 1 + relative + item.length);
|
|
2745
|
+
if (!itemBounds) continue;
|
|
2746
|
+
const literal = parseStaticString(source, itemBounds.start, itemBounds.end);
|
|
2747
|
+
if (literal) {
|
|
2748
|
+
found.push({
|
|
2749
|
+
kind: 'string',
|
|
2750
|
+
spec: literal.value,
|
|
2751
|
+
index: arrayOpen + 1 + relative,
|
|
2752
|
+
specStart: literal.start,
|
|
2753
|
+
sourceIndices: literal.sourceIndices
|
|
2754
|
+
});
|
|
2755
|
+
continue;
|
|
2756
|
+
}
|
|
2757
|
+
if (source[itemBounds.start] !== '{') continue;
|
|
2758
|
+
const path = findStaticObjectProperty(source, itemBounds.start, itemBounds.end, 'path');
|
|
2759
|
+
if (!path) continue;
|
|
2760
|
+
const pathBounds = unwrapStaticExpression(source, path.start, path.end);
|
|
2761
|
+
if (!pathBounds) continue;
|
|
2762
|
+
const objectPath = parseStaticString(source, pathBounds.start, pathBounds.end);
|
|
2763
|
+
if (!objectPath) continue;
|
|
2764
|
+
found.push({
|
|
2765
|
+
kind: 'object',
|
|
2766
|
+
spec: objectPath.value,
|
|
2767
|
+
index: pathBounds.start,
|
|
2768
|
+
specStart: objectPath.start,
|
|
2769
|
+
sourceIndices: objectPath.sourceIndices
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
return found;
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
function findRouteParameters(route) {
|
|
2776
|
+
const parameters = [];
|
|
2777
|
+
for (const match of route.spec.matchAll(/(?:[:*])([\w-]+)/g)) {
|
|
2778
|
+
parameters.push({ name: match[1], marker: match[0][0], index: route.sourceIndices?.[match.index] ?? route.specStart + match.index });
|
|
2779
|
+
}
|
|
2780
|
+
return parameters;
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
function unwrapStaticExpression(source, start, end) {
|
|
2784
|
+
while (start < end && /\s/.test(source[start])) start++;
|
|
2785
|
+
while (end > start && /\s/.test(source[end - 1])) end--;
|
|
2786
|
+
while (source[start] === '(') {
|
|
2787
|
+
const close = findMatchingDelimiter(source, start, '(', ')');
|
|
2788
|
+
if (close < 0 || !/^\s*(?:as\s+const\s*)?$/.test(source.slice(close + 1, end))) break;
|
|
2789
|
+
start++;
|
|
2790
|
+
end = close;
|
|
2791
|
+
while (start < end && /\s/.test(source[start])) start++;
|
|
2792
|
+
while (end > start && /\s/.test(source[end - 1])) end--;
|
|
2793
|
+
}
|
|
2794
|
+
const suffix = /\s+as\s+const\s*$/.exec(source.slice(start, end));
|
|
2795
|
+
if (suffix) end = start + suffix.index;
|
|
2796
|
+
while (end > start && /\s/.test(source[end - 1])) end--;
|
|
2797
|
+
return start < end ? { start, end } : null;
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
function findStaticObjectProperty(source, open, end, wanted) {
|
|
2801
|
+
const close = findMatchingDelimiter(source, open, '{', '}');
|
|
2802
|
+
if (close < 0 || close >= end) return null;
|
|
2803
|
+
const body = source.slice(open + 1, close);
|
|
2804
|
+
let offset = 0;
|
|
2805
|
+
let result = null;
|
|
2806
|
+
for (const item of splitTopLevelArguments(body)) {
|
|
2807
|
+
const relative = body.indexOf(item, offset);
|
|
2808
|
+
offset = relative + item.length;
|
|
2809
|
+
const absolute = open + 1 + relative;
|
|
2810
|
+
const trimmed = item.trimStart();
|
|
2811
|
+
// A spread or computed/shorthand key can replace any earlier property.
|
|
2812
|
+
// A later explicit key is certain again because JavaScript uses last-key
|
|
2813
|
+
// semantics for object literals.
|
|
2814
|
+
if (trimmed.startsWith('...') || trimmed.startsWith('[')) {
|
|
2815
|
+
result = null;
|
|
2816
|
+
continue;
|
|
2817
|
+
}
|
|
2818
|
+
// splitTopLevelArguments already isolates the property; find the first
|
|
2819
|
+
// colon outside a quoted key.
|
|
2820
|
+
const keyMatch = /^\s*(?:([A-Za-z_$][\w$]*)|(['"])(.*?)\2)\s*:/.exec(item);
|
|
2821
|
+
if (!keyMatch) {
|
|
2822
|
+
const staticMethodOrShorthand = new RegExp(
|
|
2823
|
+
`^\\s*(?:(?:get|set|async)\\s+)?\\*?\\s*(?:${escapeRegExp(wanted)}|(['"])${escapeRegExp(wanted)}\\1)\\s*(?:\\(|,|$)`
|
|
2824
|
+
);
|
|
2825
|
+
if (staticMethodOrShorthand.test(item)) result = null;
|
|
2826
|
+
continue;
|
|
2827
|
+
}
|
|
2828
|
+
const key = keyMatch[1] ?? keyMatch[3];
|
|
2829
|
+
if (key !== wanted) continue;
|
|
2830
|
+
result = { start: absolute + keyMatch[0].length, end: absolute + item.length };
|
|
2831
|
+
}
|
|
2832
|
+
return result;
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
function parseStaticString(source, start, end) {
|
|
2836
|
+
const quote = source[start];
|
|
2837
|
+
if (!['\'', '"', '`'].includes(quote)) return null;
|
|
2838
|
+
let value = '';
|
|
2839
|
+
const sourceIndices = [];
|
|
2840
|
+
for (let index = start + 1; index < end; index++) {
|
|
2841
|
+
const char = source[index];
|
|
2842
|
+
if (char === quote) return /^\s*$/.test(source.slice(index + 1, end))
|
|
2843
|
+
? { value, start: start + 1, sourceIndices }
|
|
2844
|
+
: null;
|
|
2845
|
+
if (quote === '`' && char === '$' && source[index + 1] === '{') return null;
|
|
2846
|
+
if (char !== '\\') { value += char; sourceIndices.push(index); continue; }
|
|
2847
|
+
const escapeStart = index;
|
|
2848
|
+
const next = source[++index];
|
|
2849
|
+
if (next === '\n') continue;
|
|
2850
|
+
if (next === '\r') {
|
|
2851
|
+
if (source[index + 1] === '\n') index++;
|
|
2852
|
+
continue;
|
|
2853
|
+
}
|
|
2854
|
+
let emitted;
|
|
2855
|
+
if (next === 'x' && /^[0-9a-fA-F]{2}/.test(source.slice(index + 1, index + 3))) {
|
|
2856
|
+
emitted = String.fromCharCode(parseInt(source.slice(index + 1, index + 3), 16)); index += 2;
|
|
2857
|
+
} else if (next === 'u' && source[index + 1] === '{') {
|
|
2858
|
+
const braceClose = source.indexOf('}', index + 2);
|
|
2859
|
+
const digits = braceClose >= 0 ? source.slice(index + 2, braceClose) : '';
|
|
2860
|
+
const codePoint = /^[0-9a-fA-F]{1,6}$/.test(digits) ? parseInt(digits, 16) : -1;
|
|
2861
|
+
if (codePoint < 0 || codePoint > 0x10ffff) return null;
|
|
2862
|
+
emitted = String.fromCodePoint(codePoint); index = braceClose;
|
|
2863
|
+
} else if (next === 'u' && /^[0-9a-fA-F]{4}/.test(source.slice(index + 1, index + 5))) {
|
|
2864
|
+
emitted = String.fromCharCode(parseInt(source.slice(index + 1, index + 5), 16)); index += 4;
|
|
2865
|
+
} else emitted = ({ n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', v: '\v', 0: '\0' }[next] ?? next);
|
|
2866
|
+
value += emitted;
|
|
2867
|
+
for (let unit = 0; unit < emitted.length; unit++) sourceIndices.push(escapeStart);
|
|
2868
|
+
}
|
|
2869
|
+
return null;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
function inspectRouteProperties(body, bodyStart = 0, provenance = null) {
|
|
2873
|
+
const masked = maskComments(body);
|
|
2874
|
+
const code = blankStringContents(masked);
|
|
2875
|
+
const depths = new Uint16Array(code.length + 1);
|
|
2876
|
+
let depth = 0;
|
|
2877
|
+
for (let index = 0; index < code.length; index++) {
|
|
2878
|
+
depths[index] = depth;
|
|
2879
|
+
if (code[index] === '{') depth++;
|
|
2880
|
+
else if (code[index] === '}') depth = Math.max(0, depth - 1);
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
const properties = [];
|
|
2884
|
+
const propertyNames = decoratorBindingNames(provenance, 'property');
|
|
2885
|
+
if (!propertyNames.length) return properties;
|
|
2886
|
+
const pattern = new RegExp(`@(?:${propertyNames.map(escapeRegExp).join('|')})\\b`, 'g');
|
|
2887
|
+
for (const match of code.matchAll(pattern)) {
|
|
2888
|
+
if (depths[match.index] !== 0) continue;
|
|
2889
|
+
let cursor = match.index + match[0].length;
|
|
2890
|
+
while (/\s/.test(code[cursor] ?? '')) cursor++;
|
|
2891
|
+
let optionsSource = '';
|
|
2892
|
+
let optionsOpen = -1;
|
|
2893
|
+
let optionsClose = -1;
|
|
2894
|
+
let hasArguments = false;
|
|
2895
|
+
if (code[cursor] === '(') {
|
|
2896
|
+
const close = findMatchingDelimiter(masked, cursor, '(', ')');
|
|
2897
|
+
if (close < 0) continue;
|
|
2898
|
+
hasArguments = true;
|
|
2899
|
+
optionsSource = masked.slice(cursor + 1, close).trim();
|
|
2900
|
+
optionsOpen = cursor + 1;
|
|
2901
|
+
optionsClose = close;
|
|
2902
|
+
cursor = close + 1;
|
|
2903
|
+
}
|
|
2904
|
+
const declaration = /^\s*((?:(?:public|private|protected|static|readonly|override|declare|abstract)\s+)*)(?:accessor\s+)?([A-Za-z_$][\w$]*)/
|
|
2905
|
+
.exec(code.slice(cursor));
|
|
2906
|
+
if (!declaration || /\bstatic\b/.test(declaration[1])) continue;
|
|
2907
|
+
|
|
2908
|
+
let attributeKind = 'default';
|
|
2909
|
+
let attribute = null;
|
|
2910
|
+
if (hasArguments && optionsSource) {
|
|
2911
|
+
if (!optionsSource.startsWith('{')) {
|
|
2912
|
+
attributeKind = 'unknown';
|
|
2913
|
+
} else {
|
|
2914
|
+
const objectOpen = masked.indexOf('{', optionsOpen);
|
|
2915
|
+
const attributeProperty = findStaticObjectProperty(masked, objectOpen, optionsClose, 'attribute');
|
|
2916
|
+
const attributeBounds = attributeProperty
|
|
2917
|
+
? unwrapStaticExpression(masked, attributeProperty.start, attributeProperty.end)
|
|
2918
|
+
: null;
|
|
2919
|
+
const attributeOption = attributeBounds
|
|
2920
|
+
? masked.slice(attributeBounds.start, attributeBounds.end).trim()
|
|
2921
|
+
: null;
|
|
2922
|
+
if (!attributeProperty && /\.\.\./.test(optionsSource)) attributeKind = 'unknown';
|
|
2923
|
+
else if (attributeOption === 'false') attributeKind = 'false';
|
|
2924
|
+
else if (attributeOption === 'true') attributeKind = 'kebab';
|
|
2925
|
+
else if (attributeOption) {
|
|
2926
|
+
const alias = parseStaticString(masked, attributeBounds.start, attributeBounds.end);
|
|
2927
|
+
if (alias) {
|
|
2928
|
+
attributeKind = 'alias';
|
|
2929
|
+
attribute = alias.value.toLowerCase();
|
|
2930
|
+
} else {
|
|
2931
|
+
attributeKind = 'unknown';
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
const nameOffset = cursor + declaration.index + declaration[0].lastIndexOf(declaration[2]);
|
|
2937
|
+
properties.push({
|
|
2938
|
+
name: declaration[2],
|
|
2939
|
+
index: bodyStart + nameOffset,
|
|
2940
|
+
attributeKind,
|
|
2941
|
+
attribute
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
return properties;
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
function decoratorBindingNames(provenance, imported) {
|
|
2948
|
+
const known = provenRootBindingNames(provenance, imported);
|
|
2949
|
+
if (known.length) return known;
|
|
2950
|
+
const importedLocals = new Set((provenance?.imports ?? []).flatMap(entry =>
|
|
2951
|
+
parseNamedImportEntries(entry.clause).map(binding => binding.local)
|
|
2952
|
+
));
|
|
2953
|
+
const locallyDeclared = new RegExp(`\\b(?:const|let|var|function|class)\\s+${escapeRegExp(imported)}\\b`)
|
|
2954
|
+
.test(provenance?.source ?? '');
|
|
2955
|
+
return importedLocals.has(imported) || locallyDeclared ? [] : [imported];
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
function provenRootBindingNames(provenance, imported) {
|
|
2959
|
+
return [
|
|
2960
|
+
...localsFor(provenance?.rootBindings ?? new Map(), imported),
|
|
2961
|
+
...[...(provenance?.namespaceBindings ?? new Map())]
|
|
2962
|
+
.filter(([, path]) => path === 'snice')
|
|
2963
|
+
.map(([local]) => `${local}.${imported}`),
|
|
2964
|
+
...[...(provenance?.projectRootNamespaces ?? new Map())]
|
|
2965
|
+
.filter(([, exports]) => exports.has(imported))
|
|
2966
|
+
.map(([local]) => `${local}.${imported}`)
|
|
2967
|
+
];
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
function enrichSniceProvenance(provenance, file, projectFiles) {
|
|
2971
|
+
const projectRootNamespaces = new Map();
|
|
2972
|
+
for (const entry of provenance.imports) {
|
|
2973
|
+
if (entry.typeOnly) continue;
|
|
2974
|
+
if (!entry.path.startsWith('.')) continue;
|
|
2975
|
+
const target = resolveProjectSource(file.filename, entry.path, projectFiles);
|
|
2976
|
+
if (!target) continue;
|
|
2977
|
+
const namespace = /^\*\s+as\s+([A-Za-z_$][\w$]*)$/.exec(entry.clause ?? '');
|
|
2978
|
+
if (namespace) {
|
|
2979
|
+
const exports = new Set(['element', 'property', 'state', 'SniceElement', 'Router'].filter(name =>
|
|
2980
|
+
resolvesSniceRootExport(target, name, projectFiles)
|
|
2981
|
+
));
|
|
2982
|
+
if (exports.size) projectRootNamespaces.set(namespace[1], exports);
|
|
2983
|
+
for (const exported of sniceRootExportNames(target, 'element', projectFiles)) {
|
|
2984
|
+
const list = provenance.rootBindings.get('element') ?? [];
|
|
2985
|
+
list.push({
|
|
2986
|
+
imported: 'element',
|
|
2987
|
+
local: `${namespace[1]}.${exported}`,
|
|
2988
|
+
index: entry.index,
|
|
2989
|
+
path: entry.path
|
|
2990
|
+
});
|
|
2991
|
+
provenance.rootBindings.set('element', list);
|
|
2992
|
+
}
|
|
2993
|
+
continue;
|
|
2994
|
+
}
|
|
2995
|
+
for (const binding of parseNamedImportEntries(entry.clause)) {
|
|
2996
|
+
if (binding.typeOnly) continue;
|
|
2997
|
+
for (const imported of ['element', 'property', 'state', 'SniceElement', 'Router']) {
|
|
2998
|
+
if (!resolvesSniceRootExport(target, binding.imported, projectFiles, imported)) continue;
|
|
2999
|
+
const list = provenance.rootBindings.get(imported) ?? [];
|
|
3000
|
+
list.push({ imported, local: binding.local, index: entry.index, path: entry.path });
|
|
3001
|
+
provenance.rootBindings.set(imported, list);
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
provenance.projectRootNamespaces = projectRootNamespaces;
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
function readStaticQuotedString(source, quoteIndex) {
|
|
3009
|
+
const quote = source[quoteIndex];
|
|
3010
|
+
if (quote !== "'" && quote !== '"') return null;
|
|
3011
|
+
let escaped = false;
|
|
3012
|
+
for (let index = quoteIndex + 1; index < source.length; index++) {
|
|
3013
|
+
const character = source[index];
|
|
3014
|
+
if (character === '\n' || character === '\r') return null;
|
|
3015
|
+
if (escaped) escaped = false;
|
|
3016
|
+
else if (character === '\\') escaped = true;
|
|
3017
|
+
else if (character === quote) return parseStaticString(source, quoteIndex, index + 1);
|
|
3018
|
+
}
|
|
3019
|
+
return null;
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
function findNamedExportStatements(source) {
|
|
3023
|
+
const code = maskNonExecutableCode(source);
|
|
3024
|
+
const statements = [];
|
|
3025
|
+
for (const match of code.matchAll(/\bexport\s*\{([^}]*)\}/g)) {
|
|
3026
|
+
const after = match.index + match[0].length;
|
|
3027
|
+
const from = /^\s*from\s*(['"])/.exec(code.slice(after));
|
|
3028
|
+
let path = null;
|
|
3029
|
+
if (from) {
|
|
3030
|
+
const quoteIndex = after + from[0].lastIndexOf(from[1]);
|
|
3031
|
+
const literal = readStaticQuotedString(source, quoteIndex);
|
|
3032
|
+
if (!literal) continue;
|
|
3033
|
+
path = literal.value;
|
|
3034
|
+
}
|
|
3035
|
+
statements.push({ bindings: match[1], path, index: match.index });
|
|
3036
|
+
}
|
|
3037
|
+
return statements;
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
function findExportStarStatements(source) {
|
|
3041
|
+
const code = maskNonExecutableCode(source);
|
|
3042
|
+
const statements = [];
|
|
3043
|
+
for (const match of code.matchAll(/\bexport\s*\*\s*from\s*(['"])/g)) {
|
|
3044
|
+
const quoteIndex = match.index + match[0].lastIndexOf(match[1]);
|
|
3045
|
+
const literal = readStaticQuotedString(source, quoteIndex);
|
|
3046
|
+
if (literal) statements.push({ path: literal.value, index: match.index });
|
|
3047
|
+
}
|
|
3048
|
+
return statements;
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
function resolvesSniceRootExport(file, exportedName, projectFiles, wanted = exportedName, visited = new Set()) {
|
|
3052
|
+
const key = `${normalizeModuleFilename(file.filename)}:${exportedName}:${wanted}`;
|
|
3053
|
+
if (visited.has(key)) return false;
|
|
3054
|
+
visited.add(key);
|
|
3055
|
+
if (findExportStarStatements(file.source).some(statement => statement.path === 'snice') && exportedName === wanted) return true;
|
|
3056
|
+
const provenance = buildSourceProvenance(file.source);
|
|
3057
|
+
const localNames = new Set(localsFor(provenance.rootBindings, wanted));
|
|
3058
|
+
for (const statement of findNamedExportStatements(file.source)) {
|
|
3059
|
+
for (const binding of parseNamedImportEntries(`{${statement.bindings}}`)) {
|
|
3060
|
+
if (binding.typeOnly) continue;
|
|
3061
|
+
if (binding.local !== exportedName) continue;
|
|
3062
|
+
if (statement.path === 'snice') return binding.imported === wanted;
|
|
3063
|
+
if (!statement.path && localNames.has(binding.imported)) return true;
|
|
3064
|
+
if (statement.path?.startsWith('.')) {
|
|
3065
|
+
const target = resolveProjectSource(file.filename, statement.path, projectFiles);
|
|
3066
|
+
if (target && resolvesSniceRootExport(target, binding.imported, projectFiles, wanted, visited)) return true;
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
return false;
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
function sniceRootExportNames(file, wanted, projectFiles, visited = new Set()) {
|
|
3074
|
+
const filename = normalizeModuleFilename(file.filename);
|
|
3075
|
+
if (visited.has(filename)) return new Set();
|
|
3076
|
+
visited.add(filename);
|
|
3077
|
+
const exported = new Set();
|
|
3078
|
+
const provenance = buildSourceProvenance(file.source);
|
|
3079
|
+
const directLocals = new Set(localsFor(provenance.rootBindings, wanted));
|
|
3080
|
+
if (findExportStarStatements(file.source).some(statement => statement.path === 'snice')) {
|
|
3081
|
+
exported.add(wanted);
|
|
3082
|
+
}
|
|
3083
|
+
for (const statement of findNamedExportStatements(file.source)) {
|
|
3084
|
+
for (const binding of parseNamedImportEntries(`{${statement.bindings}}`)) {
|
|
3085
|
+
if (binding.typeOnly) continue;
|
|
3086
|
+
if (statement.path === 'snice' && binding.imported === wanted) {
|
|
3087
|
+
exported.add(binding.local);
|
|
3088
|
+
continue;
|
|
3089
|
+
}
|
|
3090
|
+
if (!statement.path) {
|
|
3091
|
+
if (directLocals.has(binding.imported)) {
|
|
3092
|
+
exported.add(binding.local);
|
|
3093
|
+
continue;
|
|
3094
|
+
}
|
|
3095
|
+
for (const entry of provenance.imports) {
|
|
3096
|
+
if (entry.typeOnly || !entry.path.startsWith('.')) continue;
|
|
3097
|
+
const imported = parseNamedImportEntries(entry.clause)
|
|
3098
|
+
.find(candidate => !candidate.typeOnly && candidate.local === binding.imported);
|
|
3099
|
+
if (!imported) continue;
|
|
3100
|
+
const target = resolveProjectSource(file.filename, entry.path, projectFiles);
|
|
3101
|
+
if (!target) continue;
|
|
3102
|
+
const targetExports = sniceRootExportNames(target, wanted, projectFiles, new Set(visited));
|
|
3103
|
+
if (targetExports.has(imported.imported)) exported.add(binding.local);
|
|
3104
|
+
}
|
|
3105
|
+
continue;
|
|
3106
|
+
}
|
|
3107
|
+
if (statement.path?.startsWith('.')) {
|
|
3108
|
+
const target = resolveProjectSource(file.filename, statement.path, projectFiles);
|
|
3109
|
+
if (!target) continue;
|
|
3110
|
+
const targetExports = sniceRootExportNames(target, wanted, projectFiles, new Set(visited));
|
|
3111
|
+
if (targetExports.has(binding.imported)) exported.add(binding.local);
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
for (const statement of findExportStarStatements(file.source)) {
|
|
3116
|
+
if (!statement.path.startsWith('.')) continue;
|
|
3117
|
+
const target = resolveProjectSource(file.filename, statement.path, projectFiles);
|
|
3118
|
+
if (!target) continue;
|
|
3119
|
+
for (const name of sniceRootExportNames(target, wanted, projectFiles, new Set(visited))) {
|
|
3120
|
+
exported.add(name);
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
return exported;
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
function routeDecoratorNames(provenance, file, projectFiles) {
|
|
3127
|
+
const names = [];
|
|
3128
|
+
let barePageImported = false;
|
|
3129
|
+
for (const entry of provenance.imports) {
|
|
3130
|
+
for (const binding of parseNamedImportEntries(entry.clause)) {
|
|
3131
|
+
if (binding.local === 'page') barePageImported = true;
|
|
3132
|
+
if (!entry.path.startsWith('.')) continue;
|
|
3133
|
+
const target = resolveProjectSource(file.filename, entry.path, projectFiles);
|
|
3134
|
+
if (target && routerPageExportNames(target, projectFiles).has(binding.imported)) names.push(binding.local);
|
|
3135
|
+
}
|
|
3136
|
+
const namespace = /^\*\s+as\s+([A-Za-z_$][\w$]*)$/.exec(entry.clause ?? '');
|
|
3137
|
+
if (namespace && entry.path.startsWith('.')) {
|
|
3138
|
+
const target = resolveProjectSource(file.filename, entry.path, projectFiles);
|
|
3139
|
+
if (target) {
|
|
3140
|
+
for (const exported of routerPageExportNames(target, projectFiles)) names.push(`${namespace[1]}.${exported}`);
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
for (const routerName of provenRootBindingNames(provenance, 'Router')) {
|
|
3145
|
+
const escaped = escapeRegExp(routerName);
|
|
3146
|
+
for (const match of provenance.source.matchAll(new RegExp(`(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*${escaped}\\s*\\(`, 'g'))) {
|
|
3147
|
+
names.push(`${match[1]}.page`);
|
|
3148
|
+
}
|
|
3149
|
+
for (const match of provenance.source.matchAll(new RegExp(`(?:const|let|var)\\s*\\{([^}]*)\\}\\s*=\\s*${escaped}\\s*\\(`, 'g'))) {
|
|
3150
|
+
const page = /(?:^|,)\s*page(?:\s*:\s*([A-Za-z_$][\w$]*))?(?=\s*,|\s*$)/.exec(match[1]);
|
|
3151
|
+
if (page) names.push(page[1] ?? 'page');
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
const localPageDeclaration = /\b(?:const|let|var|function|class)\s+page\b/.test(provenance.source ?? '');
|
|
3155
|
+
if (!barePageImported && !localPageDeclaration) names.push('page');
|
|
3156
|
+
return [...new Set(names)];
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
function resolveProjectSource(from, specifier, files) {
|
|
3160
|
+
const base = normalizeModuleFilename(`${from.replaceAll('\\', '/').replace(/\/[^/]*$/, '')}/${specifier}`);
|
|
3161
|
+
const candidates = new Set([base, ...['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts'].map(ext => base + ext),
|
|
3162
|
+
...['.ts', '.tsx', '.js', '.jsx'].map(ext => `${base}/index${ext}`)]);
|
|
3163
|
+
return files.find(candidate => candidates.has(normalizeModuleFilename(candidate.filename))) ?? null;
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
function routerPageExportNames(file, projectFiles, visited = new Set()) {
|
|
3167
|
+
const filename = normalizeModuleFilename(file.filename);
|
|
3168
|
+
if (visited.has(filename)) return new Set();
|
|
3169
|
+
visited.add(filename);
|
|
3170
|
+
const source = maskNonExecutableCode(file.source);
|
|
3171
|
+
const provenance = buildSourceProvenance(file.source);
|
|
3172
|
+
enrichSniceProvenance(provenance, file, projectFiles);
|
|
3173
|
+
const locals = new Set();
|
|
3174
|
+
for (const routerName of provenRootBindingNames(provenance, 'Router')) {
|
|
3175
|
+
const direct = new RegExp(`(?:export\\s+)?(?:const|let|var)\\s*\\{([^}]*)\\}\\s*=\\s*${escapeRegExp(routerName)}\\s*\\(`, 'g');
|
|
3176
|
+
for (const match of source.matchAll(direct)) {
|
|
3177
|
+
const page = /(?:^|,)\s*page(?:\s*:\s*([A-Za-z_$][\w$]*))?(?=\s*,|\s*$)/.exec(match[1]);
|
|
3178
|
+
if (page) locals.add(page[1] ?? 'page');
|
|
3179
|
+
}
|
|
3180
|
+
const instance = new RegExp(`(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*${escapeRegExp(routerName)}\\s*\\(`, 'g');
|
|
3181
|
+
for (const match of source.matchAll(instance)) {
|
|
3182
|
+
const destructure = new RegExp(`(?:export\\s+)?(?:const|let|var)\\s*\\{([^}]*)\\}\\s*=\\s*${escapeRegExp(match[1])}\\b`, 'g');
|
|
3183
|
+
for (const binding of source.matchAll(destructure)) {
|
|
3184
|
+
const page = /(?:^|,)\s*page(?:\s*:\s*([A-Za-z_$][\w$]*))?(?=\s*,|\s*$)/.exec(binding[1]);
|
|
3185
|
+
if (page) locals.add(page[1] ?? 'page');
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
const exported = new Set();
|
|
3190
|
+
for (const local of locals) {
|
|
3191
|
+
if (new RegExp(`export\\s+(?:const|let|var)\\s*\\{[^}]*\\b${escapeRegExp(local)}\\b`).test(source)) exported.add(local);
|
|
3192
|
+
for (const statement of source.matchAll(/export\s*\{([^}]*)\}/g)) {
|
|
3193
|
+
const after = source.slice(statement.index + statement[0].length);
|
|
3194
|
+
if (/^\s*from\b/.test(after)) continue;
|
|
3195
|
+
for (const binding of parseNamedImportEntries(`{${statement[1]}}`)) {
|
|
3196
|
+
if (binding.imported === local) exported.add(binding.local);
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
for (const statement of findNamedExportStatements(file.source)) {
|
|
3201
|
+
if (!statement.path?.startsWith('.')) continue;
|
|
3202
|
+
const target = resolveProjectSource(file.filename, statement.path, projectFiles);
|
|
3203
|
+
if (!target) continue;
|
|
3204
|
+
const targetExports = routerPageExportNames(target, projectFiles, visited);
|
|
3205
|
+
for (const binding of parseNamedImportEntries(`{${statement.bindings}}`)) {
|
|
3206
|
+
if (targetExports.has(binding.imported)) exported.add(binding.local);
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
return exported;
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
function inspectRouteStateNames(body, provenance) {
|
|
3213
|
+
const code = blankStringContents(maskComments(body));
|
|
3214
|
+
const names = new Set();
|
|
3215
|
+
let depth = 0;
|
|
3216
|
+
const depths = new Uint16Array(code.length + 1);
|
|
3217
|
+
for (let index = 0; index < code.length; index++) {
|
|
3218
|
+
depths[index] = depth;
|
|
3219
|
+
if (code[index] === '{') depth++;
|
|
3220
|
+
else if (code[index] === '}') depth = Math.max(0, depth - 1);
|
|
3221
|
+
}
|
|
3222
|
+
const stateNames = decoratorBindingNames(provenance, 'state');
|
|
3223
|
+
if (!stateNames.length) return names;
|
|
3224
|
+
const pattern = new RegExp(`@(?:${stateNames.map(escapeRegExp).join('|')})\\b`, 'g');
|
|
3225
|
+
for (const match of code.matchAll(pattern)) {
|
|
3226
|
+
if (depths[match.index] !== 0) continue;
|
|
3227
|
+
let cursor = match.index + match[0].length;
|
|
3228
|
+
while (/\s/.test(code[cursor] ?? '')) cursor++;
|
|
3229
|
+
if (code[cursor] === '(') {
|
|
3230
|
+
const close = findMatchingDelimiter(code, cursor, '(', ')');
|
|
3231
|
+
if (close < 0) continue;
|
|
3232
|
+
cursor = close + 1;
|
|
3233
|
+
}
|
|
3234
|
+
const declaration = /^\s*(?:(?:public|private|protected|readonly|override|declare|abstract)\s+)*(?:accessor\s+)?([A-Za-z_$][\w$]*)/.exec(code.slice(cursor));
|
|
3235
|
+
if (declaration) names.add(declaration[1]);
|
|
3236
|
+
}
|
|
3237
|
+
return names;
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
function resolveRouteBindingEnvironment(page, localClasses, projectFiles) {
|
|
3241
|
+
const propertiesByName = new Map();
|
|
3242
|
+
const shadowedNames = new Set();
|
|
3243
|
+
const visited = new Set();
|
|
3244
|
+
let current = localClasses.find(candidate =>
|
|
3245
|
+
candidate.file.filename === page.file.filename && candidate.bodyStart === page.bodyStart
|
|
3246
|
+
) ?? {
|
|
3247
|
+
name: page.name,
|
|
3248
|
+
base: page.base,
|
|
3249
|
+
body: page.body,
|
|
3250
|
+
bodyStart: page.bodyStart,
|
|
3251
|
+
file: page.file,
|
|
3252
|
+
provenance: buildSourceProvenance(maskComments(page.file.source)),
|
|
3253
|
+
routeProperties: inspectRouteProperties(page.body, page.bodyStart, buildSourceProvenance(maskComments(page.file.source)))
|
|
3254
|
+
};
|
|
3255
|
+
let naming = 'unknown';
|
|
3256
|
+
let complete = true;
|
|
3257
|
+
let customObserved = null;
|
|
3258
|
+
let observedAttributesDeclared = false;
|
|
3259
|
+
let hasAttributeCallback = false;
|
|
3260
|
+
let observedAttributesUnknown = false;
|
|
3261
|
+
|
|
3262
|
+
while (current && !visited.has(`${current.file.filename}:${current.bodyStart}`)) {
|
|
3263
|
+
visited.add(`${current.file.filename}:${current.bodyStart}`);
|
|
3264
|
+
if (hasUnresolvedRelativeRouteDecorator(current.body, current.provenance)) complete = false;
|
|
3265
|
+
const stateNames = inspectRouteStateNames(current.body, current.provenance);
|
|
3266
|
+
for (const name of stateNames) {
|
|
3267
|
+
if (!shadowedNames.has(name)) {
|
|
3268
|
+
propertiesByName.set(name, { name, attributeKind: 'false', attribute: null, kind: 'state' });
|
|
3269
|
+
}
|
|
3270
|
+
shadowedNames.add(name);
|
|
3271
|
+
}
|
|
3272
|
+
for (const property of current.routeProperties) {
|
|
3273
|
+
if (!shadowedNames.has(property.name)) propertiesByName.set(property.name, property);
|
|
3274
|
+
shadowedNames.add(property.name);
|
|
3275
|
+
}
|
|
3276
|
+
if (!observedAttributesDeclared) {
|
|
3277
|
+
const observed = inspectObservedAttributes(current.body);
|
|
3278
|
+
if (observed !== undefined) {
|
|
3279
|
+
observedAttributesDeclared = true;
|
|
3280
|
+
if (observed === null) observedAttributesUnknown = true;
|
|
3281
|
+
else customObserved = observed;
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
if (hasOwnAttributeChangedCallback(current.body)) hasAttributeCallback = true;
|
|
3285
|
+
|
|
3286
|
+
const sniceElementNames = new Set(provenRootBindingNames(current.provenance, 'SniceElement'));
|
|
3287
|
+
if (current.base === 'HTMLElement') {
|
|
3288
|
+
naming = 'lowercase';
|
|
3289
|
+
break;
|
|
3290
|
+
}
|
|
3291
|
+
if (sniceElementNames.has(current.base)) {
|
|
3292
|
+
naming = 'kebab';
|
|
3293
|
+
break;
|
|
3294
|
+
}
|
|
3295
|
+
if (!current.base) {
|
|
3296
|
+
complete = false;
|
|
3297
|
+
break;
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
const sameFile = localClasses.filter(candidate =>
|
|
3301
|
+
candidate.file.filename === current.file.filename && candidate.name === current.base
|
|
3302
|
+
);
|
|
3303
|
+
let candidates = sameFile;
|
|
3304
|
+
if (!candidates.length) {
|
|
3305
|
+
const imported = current.provenance.imports.flatMap(entry =>
|
|
3306
|
+
parseNamedImportEntries(entry.clause)
|
|
3307
|
+
.filter(binding => binding.local === current.base && entry.path.startsWith('.'))
|
|
3308
|
+
.map(binding => ({ entry, binding }))
|
|
3309
|
+
);
|
|
3310
|
+
candidates = imported.flatMap(({ entry, binding }) =>
|
|
3311
|
+
resolveImportedRouteClass(current.file.filename, entry.path, binding.imported, localClasses, projectFiles)
|
|
3312
|
+
);
|
|
3313
|
+
const namespaceBase = /^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/.exec(current.base);
|
|
3314
|
+
if (namespaceBase) {
|
|
3315
|
+
const path = current.provenance.namespaceBindings.get(namespaceBase[1]);
|
|
3316
|
+
if (path?.startsWith('.')) {
|
|
3317
|
+
candidates.push(...resolveImportedRouteClass(current.file.filename, path, namespaceBase[2], localClasses, projectFiles));
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
if (candidates.length !== 1) {
|
|
3322
|
+
complete = false;
|
|
3323
|
+
break;
|
|
3324
|
+
}
|
|
3325
|
+
current = candidates[0];
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
const nativeAttributes = new Set([...NATIVE_ELEMENT_IDL_MEMBERS].map(name => name.toLowerCase()));
|
|
3329
|
+
const customAttributes = hasAttributeCallback && Array.isArray(customObserved)
|
|
3330
|
+
? new Set(customObserved.map(name => name.toLowerCase()))
|
|
3331
|
+
: new Set();
|
|
3332
|
+
return {
|
|
3333
|
+
properties: [...propertiesByName.values()], naming,
|
|
3334
|
+
complete: complete && !(hasAttributeCallback && observedAttributesUnknown),
|
|
3335
|
+
nativeAttributes,
|
|
3336
|
+
customAttributes
|
|
3337
|
+
};
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
function hasUnresolvedRelativeRouteDecorator(body, provenance) {
|
|
3341
|
+
const proven = new Set([
|
|
3342
|
+
...provenRootBindingNames(provenance, 'property'),
|
|
3343
|
+
...provenRootBindingNames(provenance, 'state')
|
|
3344
|
+
]);
|
|
3345
|
+
const code = blankStringContents(maskComments(body));
|
|
3346
|
+
for (const entry of provenance.imports) {
|
|
3347
|
+
if (!entry.path.startsWith('.')) continue;
|
|
3348
|
+
const namespace = /^\*\s+as\s+([A-Za-z_$][\w$]*)$/.exec(entry.clause ?? '');
|
|
3349
|
+
if (namespace) {
|
|
3350
|
+
for (const member of ['property', 'state']) {
|
|
3351
|
+
const name = `${namespace[1]}.${member}`;
|
|
3352
|
+
if (!proven.has(name) && new RegExp(`@${escapeRegExp(name)}\\b`).test(code)) return true;
|
|
3353
|
+
}
|
|
3354
|
+
continue;
|
|
3355
|
+
}
|
|
3356
|
+
for (const binding of parseNamedImportEntries(entry.clause)) {
|
|
3357
|
+
if (!proven.has(binding.local) && new RegExp(`@${escapeRegExp(binding.local)}\\b`).test(code)) return true;
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
return false;
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
function normalizeModuleFilename(filename) {
|
|
3364
|
+
const parts = filename.replaceAll('\\', '/').split('/');
|
|
3365
|
+
const normalized = [];
|
|
3366
|
+
for (const part of parts) {
|
|
3367
|
+
if (part === '..') normalized.pop();
|
|
3368
|
+
else if (part !== '.') normalized.push(part);
|
|
2496
3369
|
}
|
|
2497
|
-
return
|
|
3370
|
+
return normalized.join('/');
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
function resolveLocalModule(from, specifier, localClasses) {
|
|
3374
|
+
const base = normalizeModuleFilename(`${from.replaceAll('\\', '/').replace(/\/[^/]*$/, '')}/${specifier}`);
|
|
3375
|
+
const candidates = new Set([base, ...['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts'].map(ext => base + ext),
|
|
3376
|
+
...['.ts', '.tsx', '.js', '.jsx'].map(ext => `${base}/index${ext}`)]);
|
|
3377
|
+
return new Set([...candidates].filter(candidate => localClasses.some(item => normalizeModuleFilename(item.file.filename) === candidate)));
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
function resolveImportedRouteClass(from, specifier, exportedName, localClasses, projectFiles, visited = new Set()) {
|
|
3381
|
+
const target = resolveProjectSource(from, specifier, projectFiles);
|
|
3382
|
+
const key = `${from}:${specifier}:${exportedName}`;
|
|
3383
|
+
if (visited.has(key) || !target) return [];
|
|
3384
|
+
visited.add(key);
|
|
3385
|
+
const results = [];
|
|
3386
|
+
for (const filename of [normalizeModuleFilename(target.filename)]) {
|
|
3387
|
+
const direct = localClasses.filter(candidate =>
|
|
3388
|
+
normalizeModuleFilename(candidate.file.filename) === filename && candidate.name === exportedName
|
|
3389
|
+
);
|
|
3390
|
+
results.push(...direct);
|
|
3391
|
+
const file = target;
|
|
3392
|
+
for (const statement of findNamedExportStatements(file.source)) {
|
|
3393
|
+
for (const binding of parseNamedImportEntries(`{${statement.bindings}}`)) {
|
|
3394
|
+
if (binding.local !== exportedName) continue;
|
|
3395
|
+
if (statement.path?.startsWith('.')) {
|
|
3396
|
+
results.push(...resolveImportedRouteClass(file.filename, statement.path, binding.imported, localClasses, projectFiles, visited));
|
|
3397
|
+
} else {
|
|
3398
|
+
results.push(...localClasses.filter(candidate =>
|
|
3399
|
+
normalizeModuleFilename(candidate.file.filename) === filename && candidate.name === binding.imported
|
|
3400
|
+
));
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
return results.filter((candidate, index, all) =>
|
|
3406
|
+
all.findIndex(item => item.file.filename === candidate.file.filename && item.bodyStart === candidate.bodyStart) === index
|
|
3407
|
+
);
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
function inspectObservedAttributes(body) {
|
|
3411
|
+
const source = maskComments(body);
|
|
3412
|
+
const code = blankStringContents(source);
|
|
3413
|
+
const depths = lexicalBraceDepths(code);
|
|
3414
|
+
const getterPattern = /(?:^|[;\n])\s*(?:(?:public|protected|private)\s+)?static\s+get\s+observedAttributes\s*\(\s*\)\s*\{/g;
|
|
3415
|
+
const fieldPattern = /(?:^|[;\n])\s*(?:(?:public|protected|private)\s+)?static\s+(?:readonly\s+)?observedAttributes\s*=/g;
|
|
3416
|
+
const getters = [...code.matchAll(getterPattern)].filter(match => depths[match.index] === 0);
|
|
3417
|
+
const fields = [...code.matchAll(fieldPattern)].filter(match => depths[match.index] === 0);
|
|
3418
|
+
if (getters.length + fields.length > 1) return null;
|
|
3419
|
+
for (const declaration of getters) {
|
|
3420
|
+
const open = code.indexOf('{', declaration.index);
|
|
3421
|
+
const close = findMatchingDelimiter(source, open, '{', '}');
|
|
3422
|
+
if (close < 0) return null;
|
|
3423
|
+
const getterBody = source.slice(open + 1, close);
|
|
3424
|
+
const returnStatement = /^\s*return\s+([\s\S]*?);?\s*$/.exec(blankStringContents(getterBody));
|
|
3425
|
+
if (!returnStatement) return null;
|
|
3426
|
+
const returnIndex = getterBody.search(/\breturn\b/) + 'return'.length;
|
|
3427
|
+
let expressionEnd = close;
|
|
3428
|
+
while (/\s/.test(source[expressionEnd - 1] ?? '')) expressionEnd--;
|
|
3429
|
+
if (source[expressionEnd - 1] === ';') expressionEnd--;
|
|
3430
|
+
const expression = unwrapStaticExpression(source, open + 1 + returnIndex, expressionEnd);
|
|
3431
|
+
return expression ? parseStaticStringArray(source, expression.start, expression.end) : null;
|
|
3432
|
+
}
|
|
3433
|
+
for (const declaration of fields) {
|
|
3434
|
+
const equals = code.indexOf('=', declaration.index);
|
|
3435
|
+
let end = code.indexOf(';', equals + 1);
|
|
3436
|
+
if (end < 0) end = code.indexOf('\n', equals + 1);
|
|
3437
|
+
if (end < 0) end = code.length;
|
|
3438
|
+
const expression = unwrapStaticExpression(source, equals + 1, end);
|
|
3439
|
+
return expression ? parseStaticStringArray(source, expression.start, expression.end) : null;
|
|
3440
|
+
}
|
|
3441
|
+
return undefined;
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
function parseStaticStringArray(source, start, end) {
|
|
3445
|
+
if (source[start] !== '[') return null;
|
|
3446
|
+
const arrayClose = findMatchingDelimiter(source, start, '[', ']');
|
|
3447
|
+
if (arrayClose < 0 || !/^\s*$/.test(source.slice(arrayClose + 1, end))) return null;
|
|
3448
|
+
const values = [];
|
|
3449
|
+
const body = source.slice(start + 1, arrayClose);
|
|
3450
|
+
let offset = 0;
|
|
3451
|
+
for (const item of splitTopLevelArguments(body)) {
|
|
3452
|
+
const relative = body.indexOf(item, offset);
|
|
3453
|
+
offset = relative + item.length;
|
|
3454
|
+
const itemStart = start + 1 + relative;
|
|
3455
|
+
const literal = parseStaticString(source, itemStart, itemStart + item.length);
|
|
3456
|
+
if (!literal) return null;
|
|
3457
|
+
values.push(literal.value);
|
|
3458
|
+
}
|
|
3459
|
+
return values;
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
function lexicalBraceDepths(code) {
|
|
3463
|
+
const depths = new Uint16Array(code.length + 1);
|
|
3464
|
+
let depth = 0;
|
|
3465
|
+
for (let index = 0; index < code.length; index++) {
|
|
3466
|
+
depths[index] = depth;
|
|
3467
|
+
if (code[index] === '{') depth++;
|
|
3468
|
+
else if (code[index] === '}') depth = Math.max(0, depth - 1);
|
|
3469
|
+
}
|
|
3470
|
+
return depths;
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
function hasOwnAttributeChangedCallback(body) {
|
|
3474
|
+
const code = blankStringContents(maskComments(body));
|
|
3475
|
+
const depths = lexicalBraceDepths(code);
|
|
3476
|
+
for (const match of code.matchAll(/(?:^|[;\n])\s*(?:(?:public|protected|private|override)\s+)*(?:async\s+)?attributeChangedCallback\s*\(/g)) {
|
|
3477
|
+
if (depths[match.index] === 0) return true;
|
|
3478
|
+
}
|
|
3479
|
+
return false;
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
function routePropertyAttribute(property, naming) {
|
|
3483
|
+
if (property.attributeKind === 'alias') return property.attribute;
|
|
3484
|
+
if (property.attributeKind === 'kebab') return camelToKebab(property.name).toLowerCase();
|
|
3485
|
+
return defaultRoutePropertyAttribute(property.name, naming);
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3488
|
+
function defaultRoutePropertyAttribute(name, naming) {
|
|
3489
|
+
return naming === 'kebab' ? camelToKebab(name).toLowerCase() : name.toLowerCase();
|
|
2498
3490
|
}
|
|
2499
3491
|
|
|
2500
3492
|
function inspectElementMembers(body) {
|
|
@@ -2911,6 +3903,223 @@ function blankStringContents(source) {
|
|
|
2911
3903
|
return output;
|
|
2912
3904
|
}
|
|
2913
3905
|
|
|
3906
|
+
/**
|
|
3907
|
+
* Preserve executable tokens and source positions while blanking comments and
|
|
3908
|
+
* literal contents. Provenance scans use this shared view so code examples in
|
|
3909
|
+
* documentation strings cannot manufacture imports, exports, or Router calls.
|
|
3910
|
+
*/
|
|
3911
|
+
function maskNonExecutableCode(source, { preserveTemplateExpressions = false } = {}) {
|
|
3912
|
+
const output = source.split('');
|
|
3913
|
+
const blank = (start, end) => {
|
|
3914
|
+
for (let index = start; index < end; index++) {
|
|
3915
|
+
if (source[index] !== '\n' && source[index] !== '\r') output[index] = ' ';
|
|
3916
|
+
}
|
|
3917
|
+
};
|
|
3918
|
+
const quotedEnd = (start, quote) => {
|
|
3919
|
+
for (let index = start + 1; index < source.length; index++) {
|
|
3920
|
+
if (source[index] === '\\') { index++; continue; }
|
|
3921
|
+
if (source[index] === quote) return index;
|
|
3922
|
+
if (quote !== '`' && (source[index] === '\n' || source[index] === '\r')) return source.length;
|
|
3923
|
+
}
|
|
3924
|
+
return source.length;
|
|
3925
|
+
};
|
|
3926
|
+
const controlBodies = new Set(['if', 'while', 'for', 'with']);
|
|
3927
|
+
const expressionKeywords = new Set([
|
|
3928
|
+
'await', 'case', 'delete', 'do', 'else', 'in', 'instanceof', 'new',
|
|
3929
|
+
'of', 'return', 'throw', 'typeof', 'void', 'yield'
|
|
3930
|
+
]);
|
|
3931
|
+
const controlParens = new Set(['if', 'while', 'for', 'with', 'switch', 'catch']);
|
|
3932
|
+
|
|
3933
|
+
const scanTemplate = start => {
|
|
3934
|
+
let rawStart = start + 1;
|
|
3935
|
+
for (let index = start + 1; index < source.length; index++) {
|
|
3936
|
+
if (source[index] === '\\') { index++; continue; }
|
|
3937
|
+
if (source[index] === '`') {
|
|
3938
|
+
if (preserveTemplateExpressions) blank(rawStart, index);
|
|
3939
|
+
return index;
|
|
3940
|
+
}
|
|
3941
|
+
if (source[index] !== '$' || source[index + 1] !== '{') continue;
|
|
3942
|
+
if (preserveTemplateExpressions) blank(rawStart, index);
|
|
3943
|
+
index = scanCode(index + 2, true) - 1;
|
|
3944
|
+
rawStart = index + 1;
|
|
3945
|
+
}
|
|
3946
|
+
if (preserveTemplateExpressions) blank(rawStart, source.length);
|
|
3947
|
+
return source.length;
|
|
3948
|
+
};
|
|
3949
|
+
|
|
3950
|
+
const scanCode = (start, templateExpression = false) => {
|
|
3951
|
+
let expectExpression = true;
|
|
3952
|
+
let expectStatement = !templateExpression;
|
|
3953
|
+
let pendingControl = null;
|
|
3954
|
+
let templateBraceDepth = templateExpression ? 1 : 0;
|
|
3955
|
+
const parens = [];
|
|
3956
|
+
const braces = [];
|
|
3957
|
+
|
|
3958
|
+
for (let index = start; index < source.length;) {
|
|
3959
|
+
const character = source[index];
|
|
3960
|
+
if (/\s/.test(character)) { index++; continue; }
|
|
3961
|
+
|
|
3962
|
+
if (character === '/' && source[index + 1] === '/') {
|
|
3963
|
+
const newline = source.indexOf('\n', index + 2);
|
|
3964
|
+
const end = newline < 0 ? source.length : newline;
|
|
3965
|
+
blank(index, end);
|
|
3966
|
+
index = end;
|
|
3967
|
+
continue;
|
|
3968
|
+
}
|
|
3969
|
+
if (character === '/' && source[index + 1] === '*') {
|
|
3970
|
+
const close = source.indexOf('*/', index + 2);
|
|
3971
|
+
const end = close < 0 ? source.length : close + 2;
|
|
3972
|
+
blank(index, end);
|
|
3973
|
+
index = end;
|
|
3974
|
+
continue;
|
|
3975
|
+
}
|
|
3976
|
+
if (character === "'" || character === '"') {
|
|
3977
|
+
const end = quotedEnd(index, character);
|
|
3978
|
+
blank(index + 1, end);
|
|
3979
|
+
index = end < source.length ? end + 1 : end;
|
|
3980
|
+
expectExpression = false;
|
|
3981
|
+
expectStatement = false;
|
|
3982
|
+
pendingControl = null;
|
|
3983
|
+
continue;
|
|
3984
|
+
}
|
|
3985
|
+
if (character === '`') {
|
|
3986
|
+
const end = scanTemplate(index);
|
|
3987
|
+
if (!preserveTemplateExpressions) blank(index + 1, end);
|
|
3988
|
+
index = end < source.length ? end + 1 : end;
|
|
3989
|
+
expectExpression = false;
|
|
3990
|
+
expectStatement = false;
|
|
3991
|
+
pendingControl = null;
|
|
3992
|
+
continue;
|
|
3993
|
+
}
|
|
3994
|
+
if (character === '/') {
|
|
3995
|
+
if (source[index + 1] === '=') {
|
|
3996
|
+
index += 2;
|
|
3997
|
+
expectExpression = true;
|
|
3998
|
+
expectStatement = false;
|
|
3999
|
+
pendingControl = null;
|
|
4000
|
+
continue;
|
|
4001
|
+
}
|
|
4002
|
+
if (expectExpression) {
|
|
4003
|
+
const end = regexLiteralEnd(source, index);
|
|
4004
|
+
if (end > index) {
|
|
4005
|
+
blank(index, end);
|
|
4006
|
+
index = end;
|
|
4007
|
+
expectExpression = false;
|
|
4008
|
+
expectStatement = false;
|
|
4009
|
+
pendingControl = null;
|
|
4010
|
+
continue;
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
index++;
|
|
4014
|
+
expectExpression = true;
|
|
4015
|
+
expectStatement = false;
|
|
4016
|
+
pendingControl = null;
|
|
4017
|
+
continue;
|
|
4018
|
+
}
|
|
4019
|
+
if (/[A-Za-z_$]/.test(character)) {
|
|
4020
|
+
let end = index + 1;
|
|
4021
|
+
while (/[\w$]/.test(source[end] ?? '')) end++;
|
|
4022
|
+
const word = source.slice(index, end);
|
|
4023
|
+
if (word === 'await' && pendingControl === 'for') {
|
|
4024
|
+
expectExpression = true;
|
|
4025
|
+
expectStatement = false;
|
|
4026
|
+
} else if (controlParens.has(word)) {
|
|
4027
|
+
pendingControl = word;
|
|
4028
|
+
expectExpression = true;
|
|
4029
|
+
expectStatement = false;
|
|
4030
|
+
} else {
|
|
4031
|
+
pendingControl = null;
|
|
4032
|
+
expectExpression = expressionKeywords.has(word);
|
|
4033
|
+
expectStatement = word === 'do' || word === 'else';
|
|
4034
|
+
}
|
|
4035
|
+
index = end;
|
|
4036
|
+
continue;
|
|
4037
|
+
}
|
|
4038
|
+
if (/\d/.test(character)) {
|
|
4039
|
+
index++;
|
|
4040
|
+
while (/[\w.]/.test(source[index] ?? '')) index++;
|
|
4041
|
+
expectExpression = false;
|
|
4042
|
+
expectStatement = false;
|
|
4043
|
+
pendingControl = null;
|
|
4044
|
+
continue;
|
|
4045
|
+
}
|
|
4046
|
+
if (character === '(') {
|
|
4047
|
+
parens.push(pendingControl);
|
|
4048
|
+
pendingControl = null;
|
|
4049
|
+
expectExpression = true;
|
|
4050
|
+
expectStatement = false;
|
|
4051
|
+
index++;
|
|
4052
|
+
continue;
|
|
4053
|
+
}
|
|
4054
|
+
if (character === ')') {
|
|
4055
|
+
const control = parens.pop();
|
|
4056
|
+
expectExpression = controlBodies.has(control);
|
|
4057
|
+
expectStatement = controlBodies.has(control);
|
|
4058
|
+
pendingControl = null;
|
|
4059
|
+
index++;
|
|
4060
|
+
continue;
|
|
4061
|
+
}
|
|
4062
|
+
if (character === '{') {
|
|
4063
|
+
braces.push(expectStatement);
|
|
4064
|
+
if (templateExpression) templateBraceDepth++;
|
|
4065
|
+
expectExpression = true;
|
|
4066
|
+
expectStatement = true;
|
|
4067
|
+
pendingControl = null;
|
|
4068
|
+
index++;
|
|
4069
|
+
continue;
|
|
4070
|
+
}
|
|
4071
|
+
if (character === '}') {
|
|
4072
|
+
if (templateExpression && --templateBraceDepth === 0) return index + 1;
|
|
4073
|
+
const block = braces.pop() ?? false;
|
|
4074
|
+
expectExpression = block;
|
|
4075
|
+
expectStatement = block;
|
|
4076
|
+
pendingControl = null;
|
|
4077
|
+
index++;
|
|
4078
|
+
continue;
|
|
4079
|
+
}
|
|
4080
|
+
if (character === '[') {
|
|
4081
|
+
expectExpression = true;
|
|
4082
|
+
expectStatement = false;
|
|
4083
|
+
pendingControl = null;
|
|
4084
|
+
index++;
|
|
4085
|
+
continue;
|
|
4086
|
+
}
|
|
4087
|
+
if (character === ']') {
|
|
4088
|
+
expectExpression = false;
|
|
4089
|
+
expectStatement = false;
|
|
4090
|
+
pendingControl = null;
|
|
4091
|
+
index++;
|
|
4092
|
+
continue;
|
|
4093
|
+
}
|
|
4094
|
+
if ((character === '+' || character === '-') && source[index + 1] === character) {
|
|
4095
|
+
index += 2;
|
|
4096
|
+
pendingControl = null;
|
|
4097
|
+
continue;
|
|
4098
|
+
}
|
|
4099
|
+
if ('=,:;!?&|+-*%^~<>'.includes(character)) {
|
|
4100
|
+
expectExpression = true;
|
|
4101
|
+
expectStatement = character === ';' && parens.length === 0;
|
|
4102
|
+
pendingControl = null;
|
|
4103
|
+
index++;
|
|
4104
|
+
continue;
|
|
4105
|
+
}
|
|
4106
|
+
if (character === '.') {
|
|
4107
|
+
expectExpression = false;
|
|
4108
|
+
expectStatement = false;
|
|
4109
|
+
pendingControl = null;
|
|
4110
|
+
index++;
|
|
4111
|
+
continue;
|
|
4112
|
+
}
|
|
4113
|
+
pendingControl = null;
|
|
4114
|
+
index++;
|
|
4115
|
+
}
|
|
4116
|
+
return source.length;
|
|
4117
|
+
};
|
|
4118
|
+
|
|
4119
|
+
scanCode(0);
|
|
4120
|
+
return output.join('');
|
|
4121
|
+
}
|
|
4122
|
+
|
|
2914
4123
|
function isClearlyMismatchedComponent(directory, moduleName) {
|
|
2915
4124
|
const target = moduleName.slice('snice-'.length);
|
|
2916
4125
|
const topLevel = new Set([
|
|
@@ -2950,10 +4159,11 @@ function parseNamedImportEntries(clause) {
|
|
|
2950
4159
|
return braces[1]
|
|
2951
4160
|
.split(',')
|
|
2952
4161
|
.map(value => {
|
|
4162
|
+
const typeOnly = /^type\s+/.test(value.trim());
|
|
2953
4163
|
const cleaned = value.trim().replace(/^type\s+/, '');
|
|
2954
4164
|
if (!cleaned) return null;
|
|
2955
4165
|
const [imported, local = imported] = cleaned.split(/\s+as\s+/);
|
|
2956
|
-
return { imported: imported.trim(), local: local.trim() };
|
|
4166
|
+
return { imported: imported.trim(), local: local.trim(), typeOnly };
|
|
2957
4167
|
})
|
|
2958
4168
|
.filter(Boolean);
|
|
2959
4169
|
}
|
|
@@ -2984,101 +4194,30 @@ function importHasProjectUse(source, entry) {
|
|
|
2984
4194
|
}
|
|
2985
4195
|
|
|
2986
4196
|
function hasCodeIdentifier(source, target) {
|
|
2987
|
-
const
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
const frame = stack.at(-1);
|
|
2991
|
-
const character = source[index];
|
|
2992
|
-
|
|
2993
|
-
if (frame.kind === 'quoted') {
|
|
2994
|
-
if (character === '\\') index += 2;
|
|
2995
|
-
else {
|
|
2996
|
-
index++;
|
|
2997
|
-
if (character === frame.quote) stack.pop();
|
|
2998
|
-
}
|
|
2999
|
-
continue;
|
|
3000
|
-
}
|
|
3001
|
-
if (frame.kind === 'regex') {
|
|
3002
|
-
if (character === '\\') {
|
|
3003
|
-
index += 2;
|
|
3004
|
-
} else if (character === '[') {
|
|
3005
|
-
frame.characterClass = true;
|
|
3006
|
-
index++;
|
|
3007
|
-
} else if (character === ']' && frame.characterClass) {
|
|
3008
|
-
frame.characterClass = false;
|
|
3009
|
-
index++;
|
|
3010
|
-
} else if (character === '/' && !frame.characterClass) {
|
|
3011
|
-
index++;
|
|
3012
|
-
while (/[A-Za-z]/.test(source[index] ?? '')) index++;
|
|
3013
|
-
stack.pop();
|
|
3014
|
-
} else {
|
|
3015
|
-
index++;
|
|
3016
|
-
}
|
|
3017
|
-
continue;
|
|
3018
|
-
}
|
|
3019
|
-
if (frame.kind === 'template') {
|
|
3020
|
-
if (character === '\\') {
|
|
3021
|
-
index += 2;
|
|
3022
|
-
} else if (character === '`') {
|
|
3023
|
-
stack.pop();
|
|
3024
|
-
index++;
|
|
3025
|
-
} else if (character === '$' && source[index + 1] === '{') {
|
|
3026
|
-
stack.push({ kind: 'code', templateExpression: true, braces: 1 });
|
|
3027
|
-
index += 2;
|
|
3028
|
-
} else {
|
|
3029
|
-
index++;
|
|
3030
|
-
}
|
|
3031
|
-
continue;
|
|
3032
|
-
}
|
|
4197
|
+
const code = maskNonExecutableCode(source, { preserveTemplateExpressions: true });
|
|
4198
|
+
return new RegExp(`(?:^|[^\\w$])${escapeRegExp(target)}(?![\\w$])`).test(code);
|
|
4199
|
+
}
|
|
3033
4200
|
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
if (character === '
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
continue;
|
|
3043
|
-
}
|
|
3044
|
-
if (character === '/' && startsRegexLiteral(source, index)) {
|
|
3045
|
-
stack.push({ kind: 'regex', characterClass: false });
|
|
3046
|
-
index++;
|
|
3047
|
-
continue;
|
|
3048
|
-
}
|
|
3049
|
-
if (frame.templateExpression && character === '{') {
|
|
3050
|
-
frame.braces++;
|
|
3051
|
-
index++;
|
|
3052
|
-
continue;
|
|
3053
|
-
}
|
|
3054
|
-
if (frame.templateExpression && character === '}') {
|
|
3055
|
-
frame.braces--;
|
|
3056
|
-
index++;
|
|
3057
|
-
if (frame.braces === 0) stack.pop();
|
|
4201
|
+
function regexLiteralEnd(source, slashIndex) {
|
|
4202
|
+
let characterClass = false;
|
|
4203
|
+
for (let index = slashIndex + 1; index < source.length; index++) {
|
|
4204
|
+
const character = source[index];
|
|
4205
|
+
if (character === '\n' || character === '\r') return -1;
|
|
4206
|
+
if (character === '\\') { index++; continue; }
|
|
4207
|
+
if (character === '[' && !characterClass) {
|
|
4208
|
+
characterClass = true;
|
|
3058
4209
|
continue;
|
|
3059
4210
|
}
|
|
3060
|
-
if (
|
|
3061
|
-
|
|
3062
|
-
while (/[\w$]/.test(source[end] ?? '')) end++;
|
|
3063
|
-
if (source.slice(index, end) === target) return true;
|
|
3064
|
-
index = end;
|
|
4211
|
+
if (character === ']' && characterClass) {
|
|
4212
|
+
characterClass = false;
|
|
3065
4213
|
continue;
|
|
3066
4214
|
}
|
|
4215
|
+
if (character !== '/' || characterClass) continue;
|
|
3067
4216
|
index++;
|
|
4217
|
+
while (/[A-Za-z]/.test(source[index] ?? '')) index++;
|
|
4218
|
+
return index;
|
|
3068
4219
|
}
|
|
3069
|
-
return
|
|
3070
|
-
}
|
|
3071
|
-
|
|
3072
|
-
function startsRegexLiteral(source, slashIndex) {
|
|
3073
|
-
let previous = slashIndex - 1;
|
|
3074
|
-
while (previous >= 0 && /\s/.test(source[previous])) previous--;
|
|
3075
|
-
if (previous < 0 || /[([{=:;,!?&|+*%^~<>-]/.test(source[previous])) return true;
|
|
3076
|
-
if (!/[\w$]/.test(source[previous])) return false;
|
|
3077
|
-
let start = previous;
|
|
3078
|
-
while (start > 0 && /[\w$]/.test(source[start - 1])) start--;
|
|
3079
|
-
return /^(?:return|case|throw|else|do|typeof|instanceof|in|of|yield|await|void|delete)$/.test(
|
|
3080
|
-
source.slice(start, previous + 1)
|
|
3081
|
-
);
|
|
4220
|
+
return -1;
|
|
3082
4221
|
}
|
|
3083
4222
|
|
|
3084
4223
|
function reactExportReplacement(name) {
|
|
@@ -3093,13 +4232,18 @@ function reactExportReplacement(name) {
|
|
|
3093
4232
|
}
|
|
3094
4233
|
|
|
3095
4234
|
function buildSourceProvenance(source) {
|
|
4235
|
+
const code = maskNonExecutableCode(source);
|
|
3096
4236
|
const imports = findImports(source);
|
|
3097
4237
|
const rootBindings = new Map();
|
|
4238
|
+
const namespaceBindings = new Map();
|
|
3098
4239
|
const reactBindings = [];
|
|
3099
4240
|
const componentVariables = new Map();
|
|
3100
4241
|
for (const entry of imports) {
|
|
3101
|
-
|
|
4242
|
+
const namespace = /^\*\s+as\s+([A-Za-z_$][\w$]*)$/.exec(entry.clause ?? '');
|
|
4243
|
+
if (namespace && !entry.typeOnly) namespaceBindings.set(namespace[1], entry.path);
|
|
4244
|
+
if (entry.path === 'snice' && !entry.typeOnly) {
|
|
3102
4245
|
for (const binding of parseNamedImportEntries(entry.clause)) {
|
|
4246
|
+
if (binding.typeOnly) continue;
|
|
3103
4247
|
const list = rootBindings.get(binding.imported) ?? [];
|
|
3104
4248
|
list.push({ ...binding, index: entry.index, path: entry.path });
|
|
3105
4249
|
rootBindings.set(binding.imported, list);
|
|
@@ -3121,7 +4265,7 @@ function buildSourceProvenance(source) {
|
|
|
3121
4265
|
for (const match of source.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:document\.)?querySelector(?:<[^>]+>)?\(\s*['"](snice-[a-z0-9-]+)['"]\s*\)/g)) {
|
|
3122
4266
|
if (COMPONENT_CONTRACTS[match[2]]) componentVariables.set(match[1], match[2]);
|
|
3123
4267
|
}
|
|
3124
|
-
return { imports, rootBindings, reactBindings, componentVariables };
|
|
4268
|
+
return { source: code, imports, rootBindings, namespaceBindings, reactBindings, componentVariables };
|
|
3125
4269
|
}
|
|
3126
4270
|
|
|
3127
4271
|
function normalizeReactExport(name) {
|