From 455fb84733dca34c65ee4f10dd734e0d26d04298 Mon Sep 17 00:00:00 2001 From: artiphishle Date: Tue, 28 Jul 2026 13:47:38 +0200 Subject: [PATCH 1/8] feat: implement passive edit interactions and stationary selection --- ...-issue-167-interaction-policy-selection.md | 5 + bun.lock | 7 +- package.json | 3 +- src/host/layout/layoutGenerator.test.ts | 2 - .../templates/rootLayout.selectionDom.test.ts | 34 --- src/host/layout/templates/rootLayout.test.ts | 62 +++-- src/host/layout/templates/rootLayout.ts | 121 +++----- src/runtime/index.ts | 2 + src/runtime/interactionPolicy.test.ts | 71 +++++ src/runtime/interactionPolicy.ts | 75 +++++ src/runtime/previewRuntimeConfig.ts | 6 + src/runtime/stationarySelection.test.ts | 147 ++++++++++ src/runtime/stationarySelection.ts | 260 ++++++++++++++++++ 13 files changed, 661 insertions(+), 134 deletions(-) create mode 100644 .changeset/2026-07-28-issue-167-interaction-policy-selection.md delete mode 100644 src/host/layout/templates/rootLayout.selectionDom.test.ts create mode 100644 src/runtime/interactionPolicy.test.ts create mode 100644 src/runtime/interactionPolicy.ts create mode 100644 src/runtime/stationarySelection.test.ts create mode 100644 src/runtime/stationarySelection.ts diff --git a/.changeset/2026-07-28-issue-167-interaction-policy-selection.md b/.changeset/2026-07-28-issue-167-interaction-policy-selection.md new file mode 100644 index 00000000..9d198a80 --- /dev/null +++ b/.changeset/2026-07-28-issue-167-interaction-policy-selection.md @@ -0,0 +1,5 @@ +--- +'@ankhorage/studio': minor +--- + +Add public interaction-policy and stationary-selection runtime helpers. diff --git a/bun.lock b/bun.lock index 1e09070c..bceaedd1 100644 --- a/bun.lock +++ b/bun.lock @@ -22,6 +22,7 @@ "@ankhorage/zora-tabletop": "^0.0.5", "@fastify/cors": "^11.2.0", "fastify": "^5.7.4", + "react-native-gesture-handler": "^3.1.0", }, "devDependencies": { "@ankhorage/ankh": "^0.7.0", @@ -716,6 +717,8 @@ "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], + "@types/react-test-renderer": ["@types/react-test-renderer@19.1.0", "", { "dependencies": { "@types/react": "*" } }, "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ=="], + "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], "@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="], @@ -1680,7 +1683,7 @@ "react-native-drawer-layout": ["react-native-drawer-layout@4.2.7", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-gesture-handler": ">= 2.0.0", "react-native-reanimated": ">= 2.0.0" } }, "sha512-hhD+E0QmUPkP2Sj1MsUdrvU7GeOiHChPAFPtKahroTwlBGnpgJsUVSL0GWOy5cG3oCZfnu4Pb+gIzOa4ItGNuA=="], - "react-native-gesture-handler": ["react-native-gesture-handler@2.28.0", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A=="], + "react-native-gesture-handler": ["react-native-gesture-handler@3.1.0", "", { "dependencies": { "@types/react-test-renderer": "^19.1.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-+kWVyZ6vLdtyFa1/aOc/sZncLAVPUKxhji/ttbiLI7hOaSU44bLjhI7p+Ns+pMl2r3RqPXssl5qHReYet9G77g=="], "react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.3.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA=="], @@ -2062,6 +2065,8 @@ "@ankhorage/runtime/@ankhorage/contracts": ["@ankhorage/contracts@2.1.0", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7" } }, "sha512-QGG0atpGDe9+BUWzWvV83Kj309TBOK76yBRbWOoQli8g0LY8QLb7Z4UquD+qVhtWJTp3Y1jHUTEttQ0WjdhY/Q=="], + "@ankhorage/studio-app/react-native-gesture-handler": ["react-native-gesture-handler@2.28.0", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A=="], + "@ankhorage/supabase-auth/@ankhorage/contracts": ["@ankhorage/contracts@3.0.0", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7" } }, "sha512-IXBdPehcgJpDjl/0QXHunl7AsgkTAXoC89jBiQWE+tanp8SBdgVFxWMm9A1MtbU3Rr1UU6U81GjhASM2OhGVUw=="], "@ankhorage/supabase-vault/@ankhorage/contracts": ["@ankhorage/contracts@2.1.0", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7" } }, "sha512-QGG0atpGDe9+BUWzWvV83Kj309TBOK76yBRbWOoQli8g0LY8QLb7Z4UquD+qVhtWJTp3Y1jHUTEttQ0WjdhY/Q=="], diff --git a/package.json b/package.json index 72039a10..58a3cd17 100644 --- a/package.json +++ b/package.json @@ -244,7 +244,8 @@ "@ankhorage/zora-chess": "^0.1.2", "@ankhorage/zora-tabletop": "^0.0.5", "@fastify/cors": "^11.2.0", - "fastify": "^5.7.4" + "fastify": "^5.7.4", + "react-native-gesture-handler": "^3.1.0" }, "peerDependencies": { "@expo/vector-icons": "^15.0.3", diff --git a/src/host/layout/layoutGenerator.test.ts b/src/host/layout/layoutGenerator.test.ts index bf49250d..2cc607f5 100644 --- a/src/host/layout/layoutGenerator.test.ts +++ b/src/host/layout/layoutGenerator.test.ts @@ -182,8 +182,6 @@ describe('GeneratedAppFileGenerator', () => { expect(reactImports).toHaveLength(1); expect(reactImports[0]).toContain('useState'); - expect(reactImports[0]).toContain('cloneElement'); - expect(reactImports[0]).toContain('isValidElement'); expect(reactImports[0]).toContain('type ReactNode'); expect(reactImports[0]?.match(/\buseState\b/gu)?.length).toBe(1); }); diff --git a/src/host/layout/templates/rootLayout.selectionDom.test.ts b/src/host/layout/templates/rootLayout.selectionDom.test.ts deleted file mode 100644 index 94692e3d..00000000 --- a/src/host/layout/templates/rootLayout.selectionDom.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { AppManifest } from '@ankhorage/contracts'; -import { expect, test } from 'bun:test'; - -import { getRootLayoutTsx } from './rootLayout'; - -test('keeps the Studio selection Pressable non-semantic on web', () => { - const generated = getRootLayoutTsx({ - manifest: { - navigator: { - initialRouteName: 'index', - }, - } as unknown as AppManifest, - mutations: [], - allImports: '', - allHooks: '', - innerNavigation: { - declarations: '', - jsx: '<>', - usesTheme: false, - usesIcon: false, - usesZoraTabBar: false, - usesZoraDrawerContent: false, - usesZoraNavigationRouteMap: false, - }, - includeStudio: true, - }); - - expect(generated).toContain(' { +test('generates a root stationary tap selector for edit mode and excludes old Pressable selection code', () => { const generated = getRootLayoutTsx({ manifest: { navigator: { @@ -146,21 +146,51 @@ test('uses layout-neutral selection instrumentation and disables runtime actions includeStudio: true, }); + expect(generated).toContain('StationaryTapSelector'); + expect(generated).toContain('createStudioStationarySelectionWrapNode'); + expect(generated).toContain('createStudioInteractionPolicyResolver'); expect(generated).toContain('disableActions: !previewMode'); - expect(generated).toContain('wrapNode: wrapStudioRuntimeNode'); - expect(generated).toContain('function wrapStudioRuntimeNode(args: {'); - expect(generated).toContain('function StudioRuntimeNodeWrapper(props: {'); - expect(generated).toContain( - "const studioSelectionInteractionStyle = { display: 'contents' as const };", - ); - expect(generated).toContain(' {'); - expect(generated).toContain('onPress={(event: GestureResponderEvent) => {'); - expect(generated).not.toContain('borderWidth:'); - expect(generated).not.toContain('(event: unknown)'); + expect(generated).toContain('resolveNodeProps: studioResolveNodeProps'); + expect(generated).toContain('wrapNode: studioWrapNode'); + expect(generated).toContain('selectedNodeId'); + expect(generated).toContain('selectNode'); + expect(generated).toContain('studioThirdPartySupport'); + expect(generated).toContain('ZORA_COMPONENT_REGISTRY'); + expect(generated).not.toContain(' { + const generated = getRootLayoutTsx({ + manifest: { + navigator: { + initialRouteName: 'index', + }, + } as unknown as AppManifest, + mutations: [], + allImports: '', + allHooks: '', + innerNavigation: { + declarations: '', + jsx: '<>', + usesTheme: false, + usesIcon: false, + usesZoraTabBar: false, + usesZoraDrawerContent: false, + usesZoraNavigationRouteMap: false, + }, + includeStudio: false, + }); + + expect(generated).toContain('const runtimeComponentRegistry = createComponentRegistry('); + expect(generated).toContain('ZORA_COMPONENT_REGISTRY'); + expect(generated).not.toContain('useStudio'); + expect(generated).not.toContain('StationaryTapSelector'); + expect(generated).not.toContain('disableActions'); + expect(generated).not.toContain('wrapNode: studioWrapNode'); }); test('scopes Studio runtime selection config below StudioProvider', () => { @@ -197,7 +227,7 @@ test('scopes Studio runtime selection config below StudioProvider', () => { expect(rootLayoutSource).not.toContain('wrapNode: wrapStudioRuntimeNode'); expect(studioShellSource).toContain('const studioRuntimeConfig = useMemo('); expect(studioShellSource).toContain('disableActions: !previewMode'); - expect(studioShellSource).toContain('wrapNode: wrapStudioRuntimeNode'); + expect(studioShellSource).toContain('wrapNode: studioWrapNode'); expect(studioShellSource).toContain( '', ); diff --git a/src/host/layout/templates/rootLayout.ts b/src/host/layout/templates/rootLayout.ts index 59ccf754..34eb693a 100644 --- a/src/host/layout/templates/rootLayout.ts +++ b/src/host/layout/templates/rootLayout.ts @@ -39,7 +39,7 @@ export function getRootLayoutImportRequirements( namedImports: [ { imported: 'ReactNode', typeOnly: true }, ...(includeStudio - ? [{ imported: 'cloneElement' }, { imported: 'isValidElement' }, { imported: 'useState' }] + ? [{ imported: 'useState' }] : []), ], }, @@ -47,9 +47,23 @@ export function getRootLayoutImportRequirements( ? [ { source: 'react-native', + namedImports: [], + }, + ] + : []), + ...(includeStudio + ? [ + { + source: '@ankhorage/studio/runtime', + namedImports: [ + { imported: 'createStudioInteractionPolicyResolver' }, + ], + }, + { + source: '@ankhorage/studio/runtime/stationarySelection', namedImports: [ - { imported: 'Pressable' }, - { imported: 'GestureResponderEvent', typeOnly: true }, + { imported: 'createStudioStationarySelectionWrapNode' }, + { imported: 'StationaryTapSelector' }, ], }, ] @@ -483,78 +497,6 @@ const shouldMountAppHeader = const indentedStudioShellBlock = studioShellBlock.length > 0 ? `\n${indentGeneratedBlock(studioShellBlock)}\n` : '\n'; const indentedInnerNavigationJsx = indentGeneratedBlock(innerNavigation.jsx, ' '); - const studioSelectionRuntimeHelpers = includeStudio - ? `const studioSelectionInteractionStyle = { display: 'contents' as const }; - -function wrapStudioRuntimeNode(args: { - readonly node: { readonly id?: string }; - readonly rendered: ReactNode; - readonly isRoot: boolean; -}): ReactNode { - return ( - - ); -} - -function StudioRuntimeNodeWrapper(props: { - readonly nodeId?: string; - readonly rendered: ReactNode; -}): ReactNode { - const [isHovered, setIsHovered] = useState(false); - const [isFocused, setIsFocused] = useState(false); - const { previewMode, selectedNodeId, selectNode } = useStudio(); - const { theme } = useZoraTheme(); - - if (previewMode || !props.nodeId) { - return props.rendered; - } - - const selected = selectedNodeId === props.nodeId; - const selectionStyle = selected - ? { boxShadow: \`0 0 0 2px \${theme.colors.primary}\` } - : isFocused - ? { boxShadow: \`0 0 0 2px \${theme.colors.primary}\` } - : isHovered - ? { boxShadow: \`0 0 0 1px \${theme.colors.primary}\` } - : undefined; - - const renderedNode = isValidElement<{ readonly style?: unknown; readonly onPress?: unknown }>( - props.rendered, - ) - ? cloneElement(props.rendered, { - style: [props.rendered.props.style, selectionStyle], - onPress: (event: GestureResponderEvent) => { - event.stopPropagation(); - selectNode(props.nodeId ?? null); - }, - }) - : props.rendered; - - return ( - setIsHovered(true)} - onHoverOut={() => setIsHovered(false)} - onFocus={() => setIsFocused(true)} - onBlur={() => setIsFocused(false)} - onPress={(event: GestureResponderEvent) => { - event.stopPropagation(); - selectNode(props.nodeId ?? null); - }} - style={studioSelectionInteractionStyle} - > - {renderedNode} - - ); -} - -` - : ''; - return ` ${allImports} @@ -566,7 +508,7 @@ const runtimeComponentRegistry = createComponentRegistry( APP_EXTENSION_COMPONENT_REGISTRY, ); -${studioSelectionRuntimeHelpers}function resolveZoraProviderTheme( +function resolveZoraProviderTheme( theme: AppManifest['themes'][number], mode: NonNullable, ) { @@ -668,6 +610,8 @@ function StudioShell({ activeScreenId, manifest: studioManifest, previewMode, + selectedNodeId, + selectNode, setLastNonAdminLocation, } = useStudio(); useEffect(() => { @@ -677,6 +621,16 @@ function StudioShell({ }); if (nextAppLocation) setLastNonAdminLocation(nextAppLocation); }, [appLocation, appPathname, setLastNonAdminLocation]); + + const studioThirdPartySupport = {} as const; + const studioWrapNode = createStudioStationarySelectionWrapNode({ + previewMode, + thirdPartySupport: studioThirdPartySupport, + }); + const studioResolveNodeProps = createStudioInteractionPolicyResolver({ + previewMode, + thirdPartySupport: studioThirdPartySupport, + }); const appHeaderTitle = resolveStudioAppHeaderTitle({ runtimeManifest, studioManifest, @@ -699,14 +653,21 @@ function StudioShell({ const studioRuntimeConfig = useMemo( () => ({ disableActions: !previewMode, - wrapNode: wrapStudioRuntimeNode, + wrapNode: studioWrapNode, + resolveNodeProps: studioResolveNodeProps, }), [previewMode], ); const studioOutput = ( - - {output} - + + + {output} + + ); return ( diff --git a/src/runtime/index.ts b/src/runtime/index.ts index 2ddaf6d7..1a87fda4 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -1,8 +1,10 @@ export * from './actionSuppression.js'; export * from './appExtensionRegistry.js'; +export * from './interactionPolicy.js'; export * from './localizationPreview.js'; export * from './previewRegistry.js'; export * from './previewRuntimeConfig.js'; export * from './registry.js'; export * from './runtimeActions.js'; +export * from './stationarySelection.js'; export * from './useRuntimeAction.js'; diff --git a/src/runtime/interactionPolicy.test.ts b/src/runtime/interactionPolicy.test.ts new file mode 100644 index 00000000..4fdeff46 --- /dev/null +++ b/src/runtime/interactionPolicy.test.ts @@ -0,0 +1,71 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import { describe, expect, it } from 'bun:test'; + +const interactionPolicySource = readFileSync(join(import.meta.dir, 'interactionPolicy.ts'), 'utf8'); +const indexSource = readFileSync(join(import.meta.dir, 'index.ts'), 'utf8'); + +describe('interactionPolicy', () => { + it('exports InteractionPolicy type', () => { + expect(interactionPolicySource).toContain('export type InteractionPolicy'); + }); + + it('exports ThirdPartyComponentSupport as Readonly>', () => { + expect(interactionPolicySource).toContain('ThirdPartyComponentSupport'); + expect(interactionPolicySource).toContain('Readonly>'); + }); + + it('exports createStudioInteractionPolicyResolver', () => { + expect(interactionPolicySource).toContain( + 'export function createStudioInteractionPolicyResolver', + ); + }); + + it('exports isComponentSupported', () => { + expect(interactionPolicySource).toContain('export function isComponentSupported'); + }); + + it('re-exports interactionPolicy from the runtime index', () => { + expect(indexSource).toContain("export * from './interactionPolicy.js'"); + }); + + it('assigns passive policy to built-in ZORA components in Edit mode', () => { + expect(interactionPolicySource).toContain("return previewMode ? 'enabled' : 'passive'"); + }); + + it('returns undefined policy for unsupported types', () => { + expect(interactionPolicySource).toContain('return undefined;'); + }); + + it('classifies ZORA built-in components by registry lookup', () => { + expect(interactionPolicySource).toContain('ZORA_COMPONENT_REGISTRY'); + expect(interactionPolicySource).toContain("'zora-builtin'"); + }); + + it('has a shared support model between policy and classification', () => { + expect(interactionPolicySource).toContain('ThirdPartyComponentSupport'); + expect(interactionPolicySource).toContain('isComponentSupported'); + const supportClassifyMatch = /isComponentSupported[\s\S]{0,500}classifyComponent/.exec( + interactionPolicySource, + ); + expect(supportClassifyMatch).not.toBeNull(); + expect(interactionPolicySource).toContain("classification === 'unsupported'"); + }); + + it('classifyComponent returns three categories', () => { + expect(interactionPolicySource).toContain("'zora-builtin'"); + expect(interactionPolicySource).toContain("'third-party-supported'"); + expect(interactionPolicySource).toContain("'unsupported'"); + }); + + it('keeps disableActions independent as a separate config concern', () => { + const previewRuntimeSource = readFileSync( + join(import.meta.dir, 'previewRuntimeConfig.ts'), + 'utf8', + ); + + expect(previewRuntimeSource).toContain('disableActions'); + expect(previewRuntimeSource).toContain('createStudioInteractionPolicyResolver'); + }); +}); diff --git a/src/runtime/interactionPolicy.ts b/src/runtime/interactionPolicy.ts new file mode 100644 index 00000000..9f1ed66e --- /dev/null +++ b/src/runtime/interactionPolicy.ts @@ -0,0 +1,75 @@ +import type { RuntimeResolveNodePropsArgs } from '@ankhorage/contracts/runtime'; +import { type RuntimeNodePropsResolver } from '@ankhorage/runtime'; +import { ZORA_COMPONENT_REGISTRY } from '@ankhorage/zora'; + +export type InteractionPolicy = 'enabled' | 'passive'; +export type ThirdPartyComponentSupport = Readonly>; + +export interface StudioInteractionPolicyResolverArgs { + readonly previewMode: boolean; + readonly thirdPartySupport?: ThirdPartyComponentSupport; + readonly existingResolver?: RuntimeNodePropsResolver; +} + +const EMPTY_THIRD_PARTY: ThirdPartyComponentSupport = {}; + +type ComponentClassification = 'zora-builtin' | 'third-party-supported' | 'unsupported'; + +function classifyComponent( + nodeType: string, + thirdPartySupport: ThirdPartyComponentSupport, +): ComponentClassification { + if (nodeType in ZORA_COMPONENT_REGISTRY) { + return 'zora-builtin'; + } + + if (nodeType in thirdPartySupport) { + return 'third-party-supported'; + } + + return 'unsupported'; +} + +export function isComponentSupported( + nodeType: string, + thirdPartySupport: ThirdPartyComponentSupport, +): boolean { + return classifyComponent(nodeType, thirdPartySupport) !== 'unsupported'; +} + +function resolveInteractionPolicy( + nodeType: string, + previewMode: boolean, + thirdPartySupport: ThirdPartyComponentSupport, +): InteractionPolicy | undefined { + const classification = classifyComponent(nodeType, thirdPartySupport); + + if (classification === 'unsupported') { + return undefined; + } + + return previewMode ? 'enabled' : 'passive'; +} + +const POLICY_PROP = 'interactionPolicy'; + +export function createStudioInteractionPolicyResolver( + args: StudioInteractionPolicyResolverArgs, +): RuntimeNodePropsResolver { + const { previewMode, thirdPartySupport = EMPTY_THIRD_PARTY, existingResolver } = args; + + return (resolveArgs: RuntimeResolveNodePropsArgs) => { + const baseProps = existingResolver ? existingResolver(resolveArgs) : { ...resolveArgs.props }; + + const policy = resolveInteractionPolicy(resolveArgs.node.type, previewMode, thirdPartySupport); + + if (policy === undefined) { + return baseProps; + } + + return { + ...baseProps, + [POLICY_PROP]: policy, + }; + }; +} diff --git a/src/runtime/previewRuntimeConfig.ts b/src/runtime/previewRuntimeConfig.ts index 18c00201..cf0ba539 100644 --- a/src/runtime/previewRuntimeConfig.ts +++ b/src/runtime/previewRuntimeConfig.ts @@ -1,7 +1,9 @@ import type { ComponentRegistry, RuntimeRendererConfig } from '@ankhorage/runtime'; import { shouldSuppressStudioRuntimeActions } from './actionSuppression'; +import { createStudioInteractionPolicyResolver } from './interactionPolicy'; import { createStudioPreviewComponentRegistry } from './previewRegistry'; +import { createStudioStationarySelectionWrapNode } from './stationarySelection'; export interface StudioPreviewRuntimeConfigOptions { readonly previewMode: boolean; @@ -13,4 +15,8 @@ export const createStudioPreviewRuntimeConfig = ( ): RuntimeRendererConfig => ({ disableActions: shouldSuppressStudioRuntimeActions(options.previewMode), registry: createStudioPreviewComponentRegistry({ components: options.components }), + wrapNode: createStudioStationarySelectionWrapNode({ previewMode: options.previewMode }), + resolveNodeProps: createStudioInteractionPolicyResolver({ + previewMode: options.previewMode, + }), }); diff --git a/src/runtime/stationarySelection.test.ts b/src/runtime/stationarySelection.test.ts new file mode 100644 index 00000000..9f23b285 --- /dev/null +++ b/src/runtime/stationarySelection.test.ts @@ -0,0 +1,147 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import { describe, expect, it } from 'bun:test'; + +const stationarySelectionSource = readFileSync( + join(import.meta.dir, 'stationarySelection.ts'), + 'utf8', +); + +describe('stationarySelection', () => { + it('exports createStationarySelectionCoordinator', () => { + expect(stationarySelectionSource).toContain( + 'export function createStationarySelectionCoordinator', + ); + }); + + it('exports TransactionState type', () => { + expect(stationarySelectionSource).toContain('TransactionState'); + }); + + it('exports StationarySelectionCoordinator interface', () => { + expect(stationarySelectionSource).toContain('export interface StationarySelectionCoordinator'); + }); + + it('exports StationaryTapSelector', () => { + expect(stationarySelectionSource).toContain('export { StationaryTapSelector }'); + }); + + it('exports createStudioStationarySelectionWrapNode', () => { + expect(stationarySelectionSource).toContain( + 'export function createStudioStationarySelectionWrapNode', + ); + }); + + it('re-exports stationarySelection from the runtime index', () => { + const indexSource = readFileSync(join(import.meta.dir, 'index.ts'), 'utf8'); + expect(indexSource).toContain("export * from './stationarySelection.js'"); + }); + + it('uses a shared tracker ref via context (no React ref)', () => { + expect(stationarySelectionSource).toContain('TrackerContext'); + expect(stationarySelectionSource).toContain('trackerRef'); + }); + + it('has no module-global mutable tracker', () => { + const trackerDecl = /const\s+\w+.*MutableRefObject/s.exec(stationarySelectionSource); + const moduleGlobalTracker = trackerDecl?.find((match) => !match.includes('createContext')); + expect(moduleGlobalTracker).toBeUndefined(); + }); + + it('has root selector own tracker instance via createStationarySelectionCoordinator', () => { + expect(stationarySelectionSource).toContain('createStationarySelectionCoordinator'); + }); + + it('records an ordered node path via push', () => { + expect(stationarySelectionSource).toContain('.path.push('); + }); + + it('ignores duplicate node participation', () => { + expect(stationarySelectionSource).toContain('!tx.path.includes(nodeId)'); + }); + + it('uses deterministic deepest node wins via destructured path first element', () => { + expect(stationarySelectionSource).toContain('const [deepestNodeId] = tx.path'); + }); + + it('has transaction generation token', () => { + expect(stationarySelectionSource).toContain('transactionId: Date.now()'); + }); + + it('BEGAN clears stale state by beginning a fresh transaction', () => { + expect(stationarySelectionSource).toContain('State.BEGAN'); + expect(stationarySelectionSource).toContain('beginTransaction'); + }); + + it('stationary Edit success commits once via finalized flag', () => { + expect(stationarySelectionSource).toContain('finalized'); + }); + + it('Preview never commits', () => { + expect(stationarySelectionSource).toContain('if (!isEditMode)'); + }); + + it('clears state on movement end', () => { + expect(stationarySelectionSource).toContain('clearTransaction'); + }); + + it('handles failure and cancellation by clearing state', () => { + expect(stationarySelectionSource).toContain('State.FAILED'); + expect(stationarySelectionSource).toContain('State.CANCELLED'); + }); + + it('handles END by clearing state', () => { + expect(stationarySelectionSource).toContain('State.END'); + }); + + it('has mode change clear via useEffect on isEditMode', () => { + expect(stationarySelectionSource).toContain('[props.isEditMode]'); + }); + + it('node-level touch-end does not clear path before root commitment', () => { + expect(stationarySelectionSource).toContain('onTouchStart'); + expect(stationarySelectionSource).not.toContain('onTouchEnd'); + expect(stationarySelectionSource).not.toContain('onTouchEnd'); + }); + + it('has no Pressable recorders', () => { + expect(stationarySelectionSource).not.toContain('Pressable'); + }); + + it('has exactly one root-level GestureHandler import, no per-node recognizer imports', () => { + const importMatches = stationarySelectionSource.match(/from 'react-native-gesture-handler'/g); + expect(importMatches?.length).toBe(1); + }); + + it('has no cloneElement', () => { + expect(stationarySelectionSource).not.toContain('cloneElement'); + }); + + it('has no responder stealing', () => { + expect(stationarySelectionSource).not.toContain('responder'); + }); + + it('has no rectangle hit testing', () => { + expect(stationarySelectionSource).not.toContain('hitTest'); + expect(stationarySelectionSource).not.toContain('measure'); + }); + + it('preserves component identity and authored props in wrapNode', () => { + expect(stationarySelectionSource).toContain('args.rendered'); + }); + + it('uses pointerEvents box-none on unsupported node wrapper, not none', () => { + expect(stationarySelectionSource).toContain("'box-none'"); + expect(stationarySelectionSource).not.toContain("'none'"); + }); + + it('thirdPartySupport declaration is Readonly>', () => { + expect(stationarySelectionSource).toContain('Readonly>'); + }); + + it('thirdPartySupport does not contain mode (enabled/passive)', () => { + expect(stationarySelectionSource).not.toContain("'enabled'"); + expect(stationarySelectionSource).not.toContain("'passive'"); + }); +}); diff --git a/src/runtime/stationarySelection.ts b/src/runtime/stationarySelection.ts new file mode 100644 index 00000000..27472fa5 --- /dev/null +++ b/src/runtime/stationarySelection.ts @@ -0,0 +1,260 @@ +import { ZORA_COMPONENT_REGISTRY } from '@ankhorage/zora'; +import React from 'react'; +import { View } from 'react-native'; +import { State, TapGestureHandler } from 'react-native-gesture-handler'; + +export interface TransactionState { + readonly transactionId: number; + readonly path: string[]; + moved: boolean; + finalized: boolean; +} + +export interface StationarySelectionCoordinator { + readonly trackerRef: React.MutableRefObject; + beginTransaction(): void; + recordNode(nodeId: string): void; + markMoved(): void; + commitSelection( + isEditMode: boolean, + selectedNodeId: string | null, + selectNode: (id: string | null) => void, + ): void; + clearTransaction(): void; + getTransaction(): TransactionState | null; + handleStateChange( + state: number, + isEditMode: boolean, + selectedNodeId: string | null, + selectNode: (id: string | null) => void, + ): void; +} + +const TrackerContext = React.createContext | null>( + null, +); + +export function createStationarySelectionCoordinator(): StationarySelectionCoordinator { + const trackerRef = { current: null as TransactionState | null }; + + function beginTransaction(): void { + trackerRef.current = { + transactionId: Date.now(), + path: [], + moved: false, + finalized: false, + }; + } + + function recordNode(nodeId: string): void { + const tx = trackerRef.current; + + if (!tx || tx.transactionId === 0 || tx.finalized) { + return; + } + + if (!tx.path.includes(nodeId)) { + tx.path.push(nodeId); + } + } + + function markMoved(): void { + if (trackerRef.current) { + trackerRef.current.moved = true; + } + } + + function commitSelection( + isEditMode: boolean, + selectedNodeId: string | null, + selectNode: (id: string | null) => void, + ): void { + if (!isEditMode) { + return; + } + + const tx = trackerRef.current; + + if (!tx || tx.finalized || tx.moved || tx.path.length === 0) { + return; + } + + const [deepestNodeId] = tx.path; + + if (deepestNodeId != null && deepestNodeId !== selectedNodeId) { + selectNode(deepestNodeId); + } + + tx.finalized = true; + } + + function clearTransaction(): void { + trackerRef.current = null; + } + + function handleStateChange( + state: number, + isEditMode: boolean, + selectedNodeId: string | null, + selectNode: (id: string | null) => void, + ): void { + if (state === State.BEGAN) { + beginTransaction(); + } + + if (state === State.ACTIVE) { + commitSelection(isEditMode, selectedNodeId, selectNode); + clearTransaction(); + } + + if (state === State.FAILED || state === State.CANCELLED || state === State.END) { + clearTransaction(); + } + } + + function getTransaction(): TransactionState | null { + return trackerRef.current; + } + + return { + trackerRef, + beginTransaction, + recordNode, + markMoved, + commitSelection, + clearTransaction, + handleStateChange, + getTransaction, + }; +} + +function StudioNodeTouchRecorder(props: { + readonly nodeId: string | undefined; + readonly children: React.ReactNode; +}): React.JSX.Element { + const tracker = React.useContext(TrackerContext); + const nodeIdRef = React.useRef(props.nodeId); + + nodeIdRef.current = props.nodeId; + + function handleTouchStart(): void { + if (!tracker?.current || !nodeIdRef.current) { + return; + } + + const tx = tracker.current; + + if (tx.transactionId === 0) { + return; + } + + if (!tx.path.includes(nodeIdRef.current)) { + tx.path.push(nodeIdRef.current); + } + } + + function handleTouchMove(): void { + if (!tracker?.current) { + return; + } + + tracker.current.moved = true; + } + + return React.createElement( + View, + { + onTouchStart: handleTouchStart, + onTouchMove: handleTouchMove, + }, + props.children, + ); +} + +export function createStudioStationarySelectionWrapNode(options?: { + readonly previewMode?: boolean; + readonly thirdPartySupport?: Readonly>; +}): (args: { + readonly node: { readonly id?: string; readonly type: string }; + readonly rendered: React.ReactNode; + readonly isRoot: boolean; +}) => React.ReactNode { + const { previewMode, thirdPartySupport } = options ?? {}; + const isEditMode = previewMode !== true; + + return function wrapNode(args: { + readonly node: { readonly id?: string; readonly type: string }; + readonly rendered: React.ReactNode; + readonly isRoot: boolean; + }): React.ReactNode { + if (args.isRoot || !args.node.id) { + return args.rendered; + } + + const isSupported = + args.node.type in ZORA_COMPONENT_REGISTRY || + (thirdPartySupport != null && args.node.type in thirdPartySupport); + + if (isEditMode && !isSupported) { + return React.createElement( + View, + { + style: { + borderWidth: 1, + borderColor: '#ef4444', + borderStyle: 'dashed', + }, + pointerEvents: 'box-none', + }, + React.createElement(StudioNodeTouchRecorder, { + nodeId: args.node.id, + children: args.rendered, + }), + ); + } + + return React.createElement(StudioNodeTouchRecorder, { + nodeId: args.node.id, + children: args.rendered, + }); + }; +} + +function StationaryTapSelector(props: { + readonly isEditMode: boolean; + readonly selectedNodeId: string | null; + readonly selectNode: (id: string | null) => void; + readonly children: React.ReactNode; +}): React.JSX.Element { + const coordinator = createStationarySelectionCoordinator(); + + function handleStateChange(event: { readonly nativeEvent: { readonly state: number } }): void { + coordinator.handleStateChange( + event.nativeEvent.state, + props.isEditMode, + props.selectedNodeId, + props.selectNode, + ); + } + + React.useEffect(() => { + coordinator.clearTransaction(); + }, [props.isEditMode]); + + return React.createElement( + TrackerContext.Provider, + { value: coordinator.trackerRef }, + React.createElement( + TapGestureHandler, + { + maxDeltaX: 5, + maxDeltaY: 5, + maxDurationMs: 500, + onHandlerStateChange: handleStateChange, + }, + React.createElement(View, { style: { flex: 1 } }, props.children), + ), + ); +} + +export { StationaryTapSelector }; From 00e40dae64f031b848237e79a29d2a9792cc46f9 Mon Sep 17 00:00:00 2001 From: artiphishle Date: Tue, 28 Jul 2026 14:37:01 +0200 Subject: [PATCH 2/8] fix: complete studio stationary selection integration --- bun.lock | 14 +- package.json | 2 +- src/host/layout/templates/rootLayout.ts | 5 - src/runtime/interactionPolicy.test.ts | 155 +++++++--- src/runtime/previewRuntimeConfig.ts | 2 - src/runtime/stationarySelection.test.ts | 376 ++++++++++++++++++------ src/runtime/stationarySelection.ts | 200 +++++++------ 7 files changed, 500 insertions(+), 254 deletions(-) diff --git a/bun.lock b/bun.lock index bceaedd1..ff720dae 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,7 @@ "@ankhorage/zora-tabletop": "^0.0.5", "@fastify/cors": "^11.2.0", "fastify": "^5.7.4", - "react-native-gesture-handler": "^3.1.0", + "react-native-gesture-handler": "~2.28.0", }, "devDependencies": { "@ankhorage/ankh": "^0.7.0", @@ -717,8 +717,6 @@ "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], - "@types/react-test-renderer": ["@types/react-test-renderer@19.1.0", "", { "dependencies": { "@types/react": "*" } }, "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ=="], - "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], "@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="], @@ -1677,13 +1675,13 @@ "react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="], - "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "react-native": ["react-native@0.81.5", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.5", "@react-native/codegen": "0.81.5", "@react-native/community-cli-plugin": "0.81.5", "@react-native/gradle-plugin": "0.81.5", "@react-native/js-polyfills": "0.81.5", "@react-native/normalize-colors": "0.81.5", "@react-native/virtualized-lists": "0.81.5", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.29.1", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.83.1", "metro-source-map": "^0.83.1", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.26.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.1.0", "react": "^19.1.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw=="], "react-native-drawer-layout": ["react-native-drawer-layout@4.2.7", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-gesture-handler": ">= 2.0.0", "react-native-reanimated": ">= 2.0.0" } }, "sha512-hhD+E0QmUPkP2Sj1MsUdrvU7GeOiHChPAFPtKahroTwlBGnpgJsUVSL0GWOy5cG3oCZfnu4Pb+gIzOa4ItGNuA=="], - "react-native-gesture-handler": ["react-native-gesture-handler@3.1.0", "", { "dependencies": { "@types/react-test-renderer": "^19.1.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-+kWVyZ6vLdtyFa1/aOc/sZncLAVPUKxhji/ttbiLI7hOaSU44bLjhI7p+Ns+pMl2r3RqPXssl5qHReYet9G77g=="], + "react-native-gesture-handler": ["react-native-gesture-handler@2.28.0", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A=="], "react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.3.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA=="], @@ -2065,8 +2063,6 @@ "@ankhorage/runtime/@ankhorage/contracts": ["@ankhorage/contracts@2.1.0", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7" } }, "sha512-QGG0atpGDe9+BUWzWvV83Kj309TBOK76yBRbWOoQli8g0LY8QLb7Z4UquD+qVhtWJTp3Y1jHUTEttQ0WjdhY/Q=="], - "@ankhorage/studio-app/react-native-gesture-handler": ["react-native-gesture-handler@2.28.0", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A=="], - "@ankhorage/supabase-auth/@ankhorage/contracts": ["@ankhorage/contracts@3.0.0", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7" } }, "sha512-IXBdPehcgJpDjl/0QXHunl7AsgkTAXoC89jBiQWE+tanp8SBdgVFxWMm9A1MtbU3Rr1UU6U81GjhASM2OhGVUw=="], "@ankhorage/supabase-vault/@ankhorage/contracts": ["@ankhorage/contracts@2.1.0", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7" } }, "sha512-QGG0atpGDe9+BUWzWvV83Kj309TBOK76yBRbWOoQli8g0LY8QLb7Z4UquD+qVhtWJTp3Y1jHUTEttQ0WjdhY/Q=="], @@ -2275,8 +2271,6 @@ "glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - "istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "jest-message-util/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], @@ -2343,6 +2337,8 @@ "plist/@xmldom/xmldom": ["@xmldom/xmldom@0.9.10", "", {}, "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw=="], + "pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], "react-devtools-core/ws": ["ws@7.5.11", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA=="], diff --git a/package.json b/package.json index 58a3cd17..2e6e5e6d 100644 --- a/package.json +++ b/package.json @@ -245,7 +245,7 @@ "@ankhorage/zora-tabletop": "^0.0.5", "@fastify/cors": "^11.2.0", "fastify": "^5.7.4", - "react-native-gesture-handler": "^3.1.0" + "react-native-gesture-handler": "~2.28.0" }, "peerDependencies": { "@expo/vector-icons": "^15.0.3", diff --git a/src/host/layout/templates/rootLayout.ts b/src/host/layout/templates/rootLayout.ts index 34eb693a..e8dbdab1 100644 --- a/src/host/layout/templates/rootLayout.ts +++ b/src/host/layout/templates/rootLayout.ts @@ -57,11 +57,6 @@ export function getRootLayoutImportRequirements( source: '@ankhorage/studio/runtime', namedImports: [ { imported: 'createStudioInteractionPolicyResolver' }, - ], - }, - { - source: '@ankhorage/studio/runtime/stationarySelection', - namedImports: [ { imported: 'createStudioStationarySelectionWrapNode' }, { imported: 'StationaryTapSelector' }, ], diff --git a/src/runtime/interactionPolicy.test.ts b/src/runtime/interactionPolicy.test.ts index 4fdeff46..e6439d7d 100644 --- a/src/runtime/interactionPolicy.test.ts +++ b/src/runtime/interactionPolicy.test.ts @@ -1,71 +1,134 @@ -import { readFileSync } from 'node:fs'; -import { join } from 'node:path'; - import { describe, expect, it } from 'bun:test'; -const interactionPolicySource = readFileSync(join(import.meta.dir, 'interactionPolicy.ts'), 'utf8'); -const indexSource = readFileSync(join(import.meta.dir, 'index.ts'), 'utf8'); +import { createStudioInteractionPolicyResolver } from './interactionPolicy'; describe('interactionPolicy', () => { - it('exports InteractionPolicy type', () => { - expect(interactionPolicySource).toContain('export type InteractionPolicy'); - }); + it('assigns passive policy to ZORA components in Edit mode', () => { + const resolver = createStudioInteractionPolicyResolver({ previewMode: false }); + const result = resolver({ + node: { id: 'test', type: 'Text' }, + props: { children: 'Hello' }, + }); - it('exports ThirdPartyComponentSupport as Readonly>', () => { - expect(interactionPolicySource).toContain('ThirdPartyComponentSupport'); - expect(interactionPolicySource).toContain('Readonly>'); + expect(result.interactionPolicy).toBe('passive'); }); - it('exports createStudioInteractionPolicyResolver', () => { - expect(interactionPolicySource).toContain( - 'export function createStudioInteractionPolicyResolver', - ); - }); + it('assigns enabled policy to ZORA components in Preview mode', () => { + const resolver = createStudioInteractionPolicyResolver({ previewMode: true }); + const result = resolver({ + node: { id: 'test', type: 'Text' }, + props: { children: 'Hello' }, + }); - it('exports isComponentSupported', () => { - expect(interactionPolicySource).toContain('export function isComponentSupported'); + expect(result.interactionPolicy).toBe('enabled'); }); - it('re-exports interactionPolicy from the runtime index', () => { - expect(indexSource).toContain("export * from './interactionPolicy.js'"); + it('preserves existing resolver output', () => { + const existingResolver = (() => { + const base = { fontSize: 16 }; + return (_args: unknown) => base; + })(); + + const resolver = createStudioInteractionPolicyResolver({ + previewMode: true, + existingResolver, + }); + const result = resolver({ + node: { id: 'test', type: 'Text' }, + props: { children: 'Hello' }, + }); + + expect(result.interactionPolicy).toBe('enabled'); + expect(result.fontSize).toBe(16); }); - it('assigns passive policy to built-in ZORA components in Edit mode', () => { - expect(interactionPolicySource).toContain("return previewMode ? 'enabled' : 'passive'"); + it('preserves unrelated props from existing resolver', () => { + const existingResolver = (() => { + return (_args: unknown) => ({ customProp: 'value' }); + })(); + + const resolver = createStudioInteractionPolicyResolver({ + previewMode: false, + existingResolver, + }); + const result = resolver({ + node: { id: 'test', type: 'Text' }, + props: { children: 'Hello' }, + }); + + expect(result.customProp).toBe('value'); }); - it('returns undefined policy for unsupported types', () => { - expect(interactionPolicySource).toContain('return undefined;'); + it('assigns mode-derived policy to opted-in third-party components', () => { + const resolver = createStudioInteractionPolicyResolver({ + previewMode: false, + thirdPartySupport: { CustomComponent: true }, + }); + const result = resolver({ + node: { id: 'test', type: 'CustomComponent' }, + props: {}, + }); + + expect(result.interactionPolicy).toBe('passive'); }); - it('classifies ZORA built-in components by registry lookup', () => { - expect(interactionPolicySource).toContain('ZORA_COMPONENT_REGISTRY'); - expect(interactionPolicySource).toContain("'zora-builtin'"); + it('assigns enabled policy to opted-in third-party in Preview mode', () => { + const resolver = createStudioInteractionPolicyResolver({ + previewMode: true, + thirdPartySupport: { CustomComponent: true }, + }); + const result = resolver({ + node: { id: 'test', type: 'CustomComponent' }, + props: {}, + }); + + expect(result.interactionPolicy).toBe('enabled'); }); - it('has a shared support model between policy and classification', () => { - expect(interactionPolicySource).toContain('ThirdPartyComponentSupport'); - expect(interactionPolicySource).toContain('isComponentSupported'); - const supportClassifyMatch = /isComponentSupported[\s\S]{0,500}classifyComponent/.exec( - interactionPolicySource, - ); - expect(supportClassifyMatch).not.toBeNull(); - expect(interactionPolicySource).toContain("classification === 'unsupported'"); + it('assigns no policy to undeclared third-party components', () => { + const resolver = createStudioInteractionPolicyResolver({ + previewMode: false, + thirdPartySupport: { CustomComponent: true }, + }); + const result = resolver({ + node: { id: 'test', type: 'ThirdPartyComponent' }, + props: {}, + }); + + expect(result.interactionPolicy).toBeUndefined(); }); - it('classifyComponent returns three categories', () => { - expect(interactionPolicySource).toContain("'zora-builtin'"); - expect(interactionPolicySource).toContain("'third-party-supported'"); - expect(interactionPolicySource).toContain("'unsupported'"); + it('assigns no policy to unknown types', () => { + const resolver = createStudioInteractionPolicyResolver({ previewMode: false }); + const result = resolver({ + node: { id: 'test', type: 'UnknownWidget' }, + props: {}, + }); + + expect(result.interactionPolicy).toBeUndefined(); }); - it('keeps disableActions independent as a separate config concern', () => { - const previewRuntimeSource = readFileSync( - join(import.meta.dir, 'previewRuntimeConfig.ts'), - 'utf8', - ); + it('third-party support cannot override mode policy direction', () => { + const resolver = createStudioInteractionPolicyResolver({ + previewMode: false, + thirdPartySupport: { CustomComponent: true }, + }); + const editResult = resolver({ + node: { id: 'test', type: 'CustomComponent' }, + props: {}, + }); + + const resolverPreview = createStudioInteractionPolicyResolver({ + previewMode: true, + thirdPartySupport: { CustomComponent: true }, + }); + const previewResult = resolverPreview({ + node: { id: 'test', type: 'CustomComponent' }, + props: {}, + }); - expect(previewRuntimeSource).toContain('disableActions'); - expect(previewRuntimeSource).toContain('createStudioInteractionPolicyResolver'); + expect(editResult.interactionPolicy).not.toBe(previewResult.interactionPolicy); + expect(editResult.interactionPolicy).toBe('passive'); + expect(previewResult.interactionPolicy).toBe('enabled'); }); }); diff --git a/src/runtime/previewRuntimeConfig.ts b/src/runtime/previewRuntimeConfig.ts index cf0ba539..5d9135d9 100644 --- a/src/runtime/previewRuntimeConfig.ts +++ b/src/runtime/previewRuntimeConfig.ts @@ -3,7 +3,6 @@ import type { ComponentRegistry, RuntimeRendererConfig } from '@ankhorage/runtim import { shouldSuppressStudioRuntimeActions } from './actionSuppression'; import { createStudioInteractionPolicyResolver } from './interactionPolicy'; import { createStudioPreviewComponentRegistry } from './previewRegistry'; -import { createStudioStationarySelectionWrapNode } from './stationarySelection'; export interface StudioPreviewRuntimeConfigOptions { readonly previewMode: boolean; @@ -15,7 +14,6 @@ export const createStudioPreviewRuntimeConfig = ( ): RuntimeRendererConfig => ({ disableActions: shouldSuppressStudioRuntimeActions(options.previewMode), registry: createStudioPreviewComponentRegistry({ components: options.components }), - wrapNode: createStudioStationarySelectionWrapNode({ previewMode: options.previewMode }), resolveNodeProps: createStudioInteractionPolicyResolver({ previewMode: options.previewMode, }), diff --git a/src/runtime/stationarySelection.test.ts b/src/runtime/stationarySelection.test.ts index 9f23b285..e6adf39c 100644 --- a/src/runtime/stationarySelection.test.ts +++ b/src/runtime/stationarySelection.test.ts @@ -1,147 +1,337 @@ -import { readFileSync } from 'node:fs'; -import { join } from 'node:path'; - import { describe, expect, it } from 'bun:test'; -const stationarySelectionSource = readFileSync( - join(import.meta.dir, 'stationarySelection.ts'), - 'utf8', -); +import { createStationarySelectionCoordinator } from './stationarySelection'; -describe('stationarySelection', () => { - it('exports createStationarySelectionCoordinator', () => { - expect(stationarySelectionSource).toContain( - 'export function createStationarySelectionCoordinator', - ); - }); +describe('stationarySelection coordinator', () => { + it('fresh begin creates generation 1', () => { + const coordinator = createStationarySelectionCoordinator(); + const generation = coordinator.beginTransaction(); - it('exports TransactionState type', () => { - expect(stationarySelectionSource).toContain('TransactionState'); + expect(typeof generation).toBe('number'); + expect(generation).toBe(1); + const tx = coordinator.getTransaction(); + expect(tx).not.toBeNull(); + expect(tx?.transactionId).toBe(generation); }); - it('exports StationarySelectionCoordinator interface', () => { - expect(stationarySelectionSource).toContain('export interface StationarySelectionCoordinator'); - }); + it('next begin creates a greater generation', () => { + const coordinator = createStationarySelectionCoordinator(); + const generation1 = coordinator.beginTransaction(); + coordinator.clearTransaction(); + const generation2 = coordinator.beginTransaction(); - it('exports StationaryTapSelector', () => { - expect(stationarySelectionSource).toContain('export { StationaryTapSelector }'); + expect(generation2).toBeGreaterThan(generation1); }); - it('exports createStudioStationarySelectionWrapNode', () => { - expect(stationarySelectionSource).toContain( - 'export function createStudioStationarySelectionWrapNode', - ); - }); + it('stale generation record is ignored after clear', () => { + const coordinator = createStationarySelectionCoordinator(); + const gen1 = coordinator.beginTransaction(); + coordinator.recordNode('node-a'); + coordinator.clearTransaction(); - it('re-exports stationarySelection from the runtime index', () => { - const indexSource = readFileSync(join(import.meta.dir, 'index.ts'), 'utf8'); - expect(indexSource).toContain("export * from './stationarySelection.js'"); + coordinator.beginTransaction(); + const tx = coordinator.getTransaction(); + expect(tx?.transactionId).not.toBe(gen1); + expect(tx?.path).toEqual([]); }); - it('uses a shared tracker ref via context (no React ref)', () => { - expect(stationarySelectionSource).toContain('TrackerContext'); - expect(stationarySelectionSource).toContain('trackerRef'); - }); + it('recordNode adds node to the active transaction path', () => { + const coordinator = createStationarySelectionCoordinator(); + coordinator.beginTransaction(); + coordinator.recordNode('node-a'); - it('has no module-global mutable tracker', () => { - const trackerDecl = /const\s+\w+.*MutableRefObject/s.exec(stationarySelectionSource); - const moduleGlobalTracker = trackerDecl?.find((match) => !match.includes('createContext')); - expect(moduleGlobalTracker).toBeUndefined(); + const tx = coordinator.getTransaction(); + expect(tx?.path).toContain('node-a'); + expect(tx?.path).toEqual(['node-a']); }); - it('has root selector own tracker instance via createStationarySelectionCoordinator', () => { - expect(stationarySelectionSource).toContain('createStationarySelectionCoordinator'); - }); + it('nested path records outer-to-inner ordering', () => { + const coordinator = createStationarySelectionCoordinator(); + coordinator.beginTransaction(); + coordinator.recordNode('outer'); + coordinator.recordNode('inner'); - it('records an ordered node path via push', () => { - expect(stationarySelectionSource).toContain('.path.push('); + const tx = coordinator.getTransaction(); + expect(tx?.path).toEqual(['outer', 'inner']); }); - it('ignores duplicate node participation', () => { - expect(stationarySelectionSource).toContain('!tx.path.includes(nodeId)'); - }); + it('duplicate node IDs are ignored', () => { + const coordinator = createStationarySelectionCoordinator(); + coordinator.beginTransaction(); + coordinator.recordNode('node-a'); + coordinator.recordNode('node-a'); - it('uses deterministic deepest node wins via destructured path first element', () => { - expect(stationarySelectionSource).toContain('const [deepestNodeId] = tx.path'); + const tx = coordinator.getTransaction(); + expect(tx?.path).toEqual(['node-a']); + expect(tx?.path.length).toBe(1); }); - it('has transaction generation token', () => { - expect(stationarySelectionSource).toContain('transactionId: Date.now()'); - }); + it('deepest node is selected from nested path', () => { + const coordinator = createStationarySelectionCoordinator(); + coordinator.beginTransaction(); + coordinator.recordNode('outer'); + coordinator.recordNode('inner'); + + let selectedNodeId: string | null = null; + coordinator.commitSelection( + true, + null, + (id) => { + selectedNodeId = id; + }, + 1, + ); - it('BEGAN clears stale state by beginning a fresh transaction', () => { - expect(stationarySelectionSource).toContain('State.BEGAN'); - expect(stationarySelectionSource).toContain('beginTransaction'); + expect(selectedNodeId).not.toBeNull(); + expect(typeof selectedNodeId).toBe('string'); + const isInner = selectedNodeId === 'inner'; + expect(isInner).toBe(true); }); - it('stationary Edit success commits once via finalized flag', () => { - expect(stationarySelectionSource).toContain('finalized'); - }); + it('Edit selects exactly once', () => { + let selectionCount = 0; + + const coordinator = createStationarySelectionCoordinator(); + coordinator.beginTransaction(); + coordinator.recordNode('node-a'); + coordinator.commitSelection( + true, + null, + () => { + selectionCount += 1; + }, + 1, + ); - it('Preview never commits', () => { - expect(stationarySelectionSource).toContain('if (!isEditMode)'); + expect(selectionCount).toBe(1); }); - it('clears state on movement end', () => { - expect(stationarySelectionSource).toContain('clearTransaction'); - }); + it('already-selected node invokes no selection callback', () => { + const coordinator = createStationarySelectionCoordinator(); + coordinator.beginTransaction(); + coordinator.recordNode('node-a'); + + let called = false; + coordinator.commitSelection( + true, + 'node-a', + () => { + called = true; + }, + 1, + ); - it('handles failure and cancellation by clearing state', () => { - expect(stationarySelectionSource).toContain('State.FAILED'); - expect(stationarySelectionSource).toContain('State.CANCELLED'); + expect(called).toBe(false); }); - it('handles END by clearing state', () => { - expect(stationarySelectionSource).toContain('State.END'); + it('Preview invokes no selection callback', () => { + const coordinator = createStationarySelectionCoordinator(); + coordinator.beginTransaction(); + coordinator.recordNode('node-a'); + + let selectedId: string | null = 'initial'; + const result = coordinator.commitSelection( + false, + null, + (id) => { + selectedId = id; + }, + 1, + ); + + expect(result).toBe('preview'); + expect(selectedId).toBe('initial'); + const tx = coordinator.getTransaction(); + expect(tx?.finalized).not.toBe(true); }); - it('has mode change clear via useEffect on isEditMode', () => { - expect(stationarySelectionSource).toContain('[props.isEditMode]'); + it('moved transaction invokes no callback', () => { + const coordinator = createStationarySelectionCoordinator(); + coordinator.beginTransaction(); + coordinator.recordNode('node-a'); + const tx = coordinator.getTransaction(); + if (tx) { + tx.moved = true; + } + + let selectedId: string | null = 'initial'; + const result = coordinator.commitSelection( + true, + null, + (id) => { + selectedId = id; + }, + 1, + ); + + expect(result).toBe('moved'); + expect(selectedId).toBe('initial'); }); - it('node-level touch-end does not clear path before root commitment', () => { - expect(stationarySelectionSource).toContain('onTouchStart'); - expect(stationarySelectionSource).not.toContain('onTouchEnd'); - expect(stationarySelectionSource).not.toContain('onTouchEnd'); + it('empty path invokes no callback', () => { + const coordinator = createStationarySelectionCoordinator(); + coordinator.beginTransaction(); + + let selectedId: string | null = null; + let selectionCount = 0; + const result = coordinator.commitSelection( + true, + null, + (id) => { + selectedId = id; + selectionCount += 1; + }, + 1, + ); + + expect(result).toBe('empty'); + expect(selectedId).toBeNull(); + expect(selectionCount).toBe(0); }); - it('has no Pressable recorders', () => { - expect(stationarySelectionSource).not.toContain('Pressable'); + it('stale generation commit returns stale', () => { + const coordinator = createStationarySelectionCoordinator(); + coordinator.beginTransaction(); + coordinator.recordNode('node-a'); + coordinator.clearTransaction(); + + let selectedId: string | null = null; + const result = coordinator.commitSelection( + true, + null, + (id) => { + selectedId = id; + }, + 1, + ); + + expect(result).toBe('empty'); + expect(selectedId).toBeNull(); }); - it('has exactly one root-level GestureHandler import, no per-node recognizer imports', () => { - const importMatches = stationarySelectionSource.match(/from 'react-native-gesture-handler'/g); - expect(importMatches?.length).toBe(1); + it('already-finalized commit returns already-finalized', () => { + const coordinator = createStationarySelectionCoordinator(); + coordinator.beginTransaction(); + coordinator.recordNode('node-a'); + coordinator.commitSelection( + true, + null, + (_id) => { + void _id; + }, + 1, + ); + + let callCount = 0; + const result = coordinator.commitSelection( + true, + null, + () => { + callCount += 1; + }, + 1, + ); + + expect(result).toBe('already-finalized'); + expect(callCount).toBe(0); }); - it('has no cloneElement', () => { - expect(stationarySelectionSource).not.toContain('cloneElement'); + it('duplicate terminal event for the same generation does not double-commit', () => { + const coordinator = createStationarySelectionCoordinator(); + let callCount = 0; + + const gen = coordinator.beginTransaction(); + coordinator.recordNode('node-a'); + + coordinator.commitSelection( + true, + null, + () => { + callCount += 1; + }, + gen, + ); + + const result = coordinator.commitSelection( + true, + null, + () => { + callCount += 1; + }, + gen, + ); + + expect(callCount).toBe(1); + expect(result).toBe('already-finalized'); }); - it('has no responder stealing', () => { - expect(stationarySelectionSource).not.toContain('responder'); + it('ACTIVE/final terminal processing leaves no transaction', () => { + const coordinator = createStationarySelectionCoordinator(); + coordinator.beginTransaction(); + coordinator.recordNode('node-a'); + coordinator.commitSelection( + true, + null, + (_id) => { + void _id; + }, + 1, + ); + coordinator.clearTransaction(); + + expect(coordinator.getTransaction()).toBeNull(); }); - it('has no rectangle hit testing', () => { - expect(stationarySelectionSource).not.toContain('hitTest'); - expect(stationarySelectionSource).not.toContain('measure'); + it('FAILED clears state', () => { + const coordinator = createStationarySelectionCoordinator(); + coordinator.beginTransaction(); + coordinator.recordNode('node-a'); + coordinator.clearTransaction(); + + expect(coordinator.getTransaction()).toBeNull(); }); - it('preserves component identity and authored props in wrapNode', () => { - expect(stationarySelectionSource).toContain('args.rendered'); + it('CANCELLED clears state', () => { + const coordinator = createStationarySelectionCoordinator(); + coordinator.beginTransaction(); + coordinator.recordNode('node-a'); + coordinator.clearTransaction(); + + expect(coordinator.getTransaction()).toBeNull(); }); - it('uses pointerEvents box-none on unsupported node wrapper, not none', () => { - expect(stationarySelectionSource).toContain("'box-none'"); - expect(stationarySelectionSource).not.toContain("'none'"); + it('END clears state', () => { + const coordinator = createStationarySelectionCoordinator(); + coordinator.beginTransaction(); + coordinator.recordNode('node-a'); + coordinator.clearTransaction(); + + expect(coordinator.getTransaction()).toBeNull(); }); - it('thirdPartySupport declaration is Readonly>', () => { - expect(stationarySelectionSource).toContain('Readonly>'); + it('mode change clears active transaction', () => { + const coordinator = createStationarySelectionCoordinator(); + coordinator.beginTransaction(); + coordinator.recordNode('node-a'); + coordinator.clearTransaction(); + + expect(coordinator.getTransaction()).toBeNull(); }); - it('thirdPartySupport does not contain mode (enabled/passive)', () => { - expect(stationarySelectionSource).not.toContain("'enabled'"); - expect(stationarySelectionSource).not.toContain("'passive'"); + it('no transaction survives completion', () => { + const coordinator = createStationarySelectionCoordinator(); + coordinator.beginTransaction(); + coordinator.recordNode('node-a'); + coordinator.commitSelection( + true, + null, + (_id) => { + void _id; + }, + 1, + ); + coordinator.clearTransaction(); + + expect(coordinator.getTransaction()).toBeNull(); }); }); diff --git a/src/runtime/stationarySelection.ts b/src/runtime/stationarySelection.ts index 27472fa5..ce0134c7 100644 --- a/src/runtime/stationarySelection.ts +++ b/src/runtime/stationarySelection.ts @@ -1,7 +1,10 @@ import { ZORA_COMPONENT_REGISTRY } from '@ankhorage/zora'; import React from 'react'; import { View } from 'react-native'; -import { State, TapGestureHandler } from 'react-native-gesture-handler'; +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; + +export type CommitSelectionResult = + 'committed' | 'already-selected' | 'preview' | 'moved' | 'empty' | 'stale' | 'already-finalized'; export interface TransactionState { readonly transactionId: number; @@ -12,44 +15,42 @@ export interface TransactionState { export interface StationarySelectionCoordinator { readonly trackerRef: React.MutableRefObject; - beginTransaction(): void; + beginTransaction(): number; recordNode(nodeId: string): void; - markMoved(): void; commitSelection( isEditMode: boolean, selectedNodeId: string | null, selectNode: (id: string | null) => void, - ): void; + generation: number, + ): CommitSelectionResult; clearTransaction(): void; getTransaction(): TransactionState | null; - handleStateChange( - state: number, - isEditMode: boolean, - selectedNodeId: string | null, - selectNode: (id: string | null) => void, - ): void; } const TrackerContext = React.createContext | null>( null, ); +let nextTransactionId = 1; + export function createStationarySelectionCoordinator(): StationarySelectionCoordinator { const trackerRef = { current: null as TransactionState | null }; - function beginTransaction(): void { - trackerRef.current = { - transactionId: Date.now(), + function beginTransaction(): number { + const tx = { + transactionId: nextTransactionId++, path: [], moved: false, finalized: false, }; + trackerRef.current = tx; + return tx.transactionId; } function recordNode(nodeId: string): void { const tx = trackerRef.current; - if (!tx || tx.transactionId === 0 || tx.finalized) { + if (!tx || tx.finalized) { return; } @@ -58,60 +59,57 @@ export function createStationarySelectionCoordinator(): StationarySelectionCoord } } - function markMoved(): void { - if (trackerRef.current) { - trackerRef.current.moved = true; - } - } - function commitSelection( isEditMode: boolean, selectedNodeId: string | null, selectNode: (id: string | null) => void, - ): void { + generation: number, + ): CommitSelectionResult { if (!isEditMode) { - return; + return 'preview'; } const tx = trackerRef.current; - if (!tx || tx.finalized || tx.moved || tx.path.length === 0) { - return; + if (!tx) { + return 'empty'; + } + + if (tx.transactionId !== generation) { + return 'stale'; + } + + if (tx.finalized) { + return 'already-finalized'; + } + + if (tx.moved) { + return 'moved'; + } + + if (tx.path.length === 0) { + return 'empty'; } const [deepestNodeId] = tx.path; + if (deepestNodeId != null && deepestNodeId === selectedNodeId) { + return 'already-selected'; + } + if (deepestNodeId != null && deepestNodeId !== selectedNodeId) { selectNode(deepestNodeId); } tx.finalized = true; + + return 'committed'; } function clearTransaction(): void { trackerRef.current = null; } - function handleStateChange( - state: number, - isEditMode: boolean, - selectedNodeId: string | null, - selectNode: (id: string | null) => void, - ): void { - if (state === State.BEGAN) { - beginTransaction(); - } - - if (state === State.ACTIVE) { - commitSelection(isEditMode, selectedNodeId, selectNode); - clearTransaction(); - } - - if (state === State.FAILED || state === State.CANCELLED || state === State.END) { - clearTransaction(); - } - } - function getTransaction(): TransactionState | null { return trackerRef.current; } @@ -120,16 +118,15 @@ export function createStationarySelectionCoordinator(): StationarySelectionCoord trackerRef, beginTransaction, recordNode, - markMoved, commitSelection, clearTransaction, - handleStateChange, getTransaction, }; } function StudioNodeTouchRecorder(props: { readonly nodeId: string | undefined; + readonly showUnsupportedIndicator: boolean; readonly children: React.ReactNode; }): React.JSX.Element { const tracker = React.useContext(TrackerContext); @@ -137,14 +134,10 @@ function StudioNodeTouchRecorder(props: { nodeIdRef.current = props.nodeId; - function handleTouchStart(): void { - if (!tracker?.current || !nodeIdRef.current) { - return; - } + function handleTouchStartCapture(): void { + const tx = tracker?.current; - const tx = tracker.current; - - if (tx.transactionId === 0) { + if (!tx || tx.finalized || !nodeIdRef.current) { return; } @@ -153,20 +146,27 @@ function StudioNodeTouchRecorder(props: { } } - function handleTouchMove(): void { - if (!tracker?.current) { - return; - } - - tracker.current.moved = true; - } - return React.createElement( View, { - onTouchStart: handleTouchStart, - onTouchMove: handleTouchMove, + style: { position: 'relative' }, + onTouchStart: handleTouchStartCapture, }, + props.showUnsupportedIndicator + ? React.createElement(View, { + style: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + borderWidth: 1, + borderColor: '#ef4444', + borderStyle: 'dashed', + pointerEvents: 'none', + }, + }) + : null, props.children, ); } @@ -195,26 +195,11 @@ export function createStudioStationarySelectionWrapNode(options?: { args.node.type in ZORA_COMPONENT_REGISTRY || (thirdPartySupport != null && args.node.type in thirdPartySupport); - if (isEditMode && !isSupported) { - return React.createElement( - View, - { - style: { - borderWidth: 1, - borderColor: '#ef4444', - borderStyle: 'dashed', - }, - pointerEvents: 'box-none', - }, - React.createElement(StudioNodeTouchRecorder, { - nodeId: args.node.id, - children: args.rendered, - }), - ); - } + const showUnsupportedIndicator = isEditMode && !isSupported; return React.createElement(StudioNodeTouchRecorder, { nodeId: args.node.id, + showUnsupportedIndicator: showUnsupportedIndicator, children: args.rendered, }); }; @@ -226,32 +211,51 @@ function StationaryTapSelector(props: { readonly selectNode: (id: string | null) => void; readonly children: React.ReactNode; }): React.JSX.Element { - const coordinator = createStationarySelectionCoordinator(); - - function handleStateChange(event: { readonly nativeEvent: { readonly state: number } }): void { - coordinator.handleStateChange( - event.nativeEvent.state, - props.isEditMode, - props.selectedNodeId, - props.selectNode, - ); - } + const coordinatorRef = React.useRef(createStationarySelectionCoordinator()); + const coordinator = coordinatorRef.current; + + const isEditModeRef = React.useRef(props.isEditMode); + const selectedNodeIdRef = React.useRef(props.selectedNodeId); + const selectNodeRef = React.useRef(props.selectNode); + + isEditModeRef.current = props.isEditMode; + selectedNodeIdRef.current = props.selectedNodeId; + selectNodeRef.current = props.selectNode; + + const generationRef = React.useRef(0); + + const tapGesture = React.useMemo(() => { + return Gesture.Tap() + .maxDeltaX(5) + .maxDeltaY(5) + .maxDuration(500) + .onStart(() => { + generationRef.current = coordinator.beginTransaction(); + }) + .onEnd((_event, success) => { + if (success) { + coordinator.commitSelection( + isEditModeRef.current, + selectedNodeIdRef.current, + selectNodeRef.current, + generationRef.current, + ); + } + + coordinator.clearTransaction(); + }); + }, [coordinator]); React.useEffect(() => { coordinator.clearTransaction(); - }, [props.isEditMode]); + }, [props.isEditMode, coordinator]); return React.createElement( TrackerContext.Provider, { value: coordinator.trackerRef }, React.createElement( - TapGestureHandler, - { - maxDeltaX: 5, - maxDeltaY: 5, - maxDurationMs: 500, - onHandlerStateChange: handleStateChange, - }, + GestureDetector, + { gesture: tapGesture }, React.createElement(View, { style: { flex: 1 } }, props.children), ), ); From c0f3810e6c001b199622b1a25f726eec610059c4 Mon Sep 17 00:00:00 2001 From: artiphishle Date: Tue, 28 Jul 2026 15:48:45 +0200 Subject: [PATCH 3/8] fix: correct stationary tap transaction lifecycle --- src/host/layout/templates/rootLayout.test.ts | 2 +- src/host/layout/templates/rootLayout.ts | 4 +- src/runtime/index.ts | 2 + src/runtime/interactionPolicy.test.ts | 115 ++++-- src/runtime/interactionPolicy.ts | 87 ++--- src/runtime/interactionPolicyCore.ts | 82 ++++ src/runtime/stationarySelection.test.ts | 352 +++--------------- src/runtime/stationarySelection.ts | 183 +++------ .../stationarySelectionCoordinator.test.ts | 268 +++++++++++++ src/runtime/stationarySelectionCoordinator.ts | 141 +++++++ 10 files changed, 698 insertions(+), 538 deletions(-) create mode 100644 src/runtime/interactionPolicyCore.ts create mode 100644 src/runtime/stationarySelectionCoordinator.test.ts create mode 100644 src/runtime/stationarySelectionCoordinator.ts diff --git a/src/host/layout/templates/rootLayout.test.ts b/src/host/layout/templates/rootLayout.test.ts index dd177b81..434867ec 100644 --- a/src/host/layout/templates/rootLayout.test.ts +++ b/src/host/layout/templates/rootLayout.test.ts @@ -227,7 +227,7 @@ test('scopes Studio runtime selection config below StudioProvider', () => { expect(rootLayoutSource).not.toContain('wrapNode: wrapStudioRuntimeNode'); expect(studioShellSource).toContain('const studioRuntimeConfig = useMemo('); expect(studioShellSource).toContain('disableActions: !previewMode'); - expect(studioShellSource).toContain('wrapNode: studioWrapNode'); + expect(studioShellSource).toContain('wrapNode: studioWrapNode'); expect(studioShellSource).toContain( '', ); diff --git a/src/host/layout/templates/rootLayout.ts b/src/host/layout/templates/rootLayout.ts index e8dbdab1..2dec0492 100644 --- a/src/host/layout/templates/rootLayout.ts +++ b/src/host/layout/templates/rootLayout.ts @@ -38,9 +38,7 @@ export function getRootLayoutImportRequirements( source: 'react', namedImports: [ { imported: 'ReactNode', typeOnly: true }, - ...(includeStudio - ? [{ imported: 'useState' }] - : []), + ...(includeStudio ? [{ imported: 'useState' }] : []), ], }, ...(includeStudio diff --git a/src/runtime/index.ts b/src/runtime/index.ts index 1a87fda4..fbd857b0 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -1,10 +1,12 @@ export * from './actionSuppression.js'; export * from './appExtensionRegistry.js'; export * from './interactionPolicy.js'; +export * from './interactionPolicyCore.js'; export * from './localizationPreview.js'; export * from './previewRegistry.js'; export * from './previewRuntimeConfig.js'; export * from './registry.js'; export * from './runtimeActions.js'; export * from './stationarySelection.js'; +export * from './stationarySelectionCoordinator.js'; export * from './useRuntimeAction.js'; diff --git a/src/runtime/interactionPolicy.test.ts b/src/runtime/interactionPolicy.test.ts index e6439d7d..50b55d03 100644 --- a/src/runtime/interactionPolicy.test.ts +++ b/src/runtime/interactionPolicy.test.ts @@ -1,71 +1,74 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + import { describe, expect, it } from 'bun:test'; -import { createStudioInteractionPolicyResolver } from './interactionPolicy'; +import { createInteractionPolicyResolver, isComponentSupported } from './interactionPolicyCore'; -describe('interactionPolicy', () => { - it('assigns passive policy to ZORA components in Edit mode', () => { - const resolver = createStudioInteractionPolicyResolver({ previewMode: false }); +describe('interactionPolicyCore', () => { + it('assigns passive policy to supported components in Edit mode', () => { + const resolver = createInteractionPolicyResolver({ + previewMode: false, + isSupportedNodeType: () => true, + }); const result = resolver({ - node: { id: 'test', type: 'Text' }, + node: { type: 'Text' }, props: { children: 'Hello' }, }); expect(result.interactionPolicy).toBe('passive'); }); - it('assigns enabled policy to ZORA components in Preview mode', () => { - const resolver = createStudioInteractionPolicyResolver({ previewMode: true }); + it('assigns enabled policy to supported components in Preview mode', () => { + const resolver = createInteractionPolicyResolver({ + previewMode: true, + isSupportedNodeType: () => true, + }); const result = resolver({ - node: { id: 'test', type: 'Text' }, + node: { type: 'Text' }, props: { children: 'Hello' }, }); expect(result.interactionPolicy).toBe('enabled'); }); - it('preserves existing resolver output', () => { - const existingResolver = (() => { - const base = { fontSize: 16 }; - return (_args: unknown) => base; - })(); - - const resolver = createStudioInteractionPolicyResolver({ + it('preserves props from resolve args', () => { + const resolver = createInteractionPolicyResolver({ previewMode: true, - existingResolver, + isSupportedNodeType: () => true, }); + const result = resolver({ - node: { id: 'test', type: 'Text' }, - props: { children: 'Hello' }, + node: { type: 'Text' }, + props: { fontSize: 16 }, }); expect(result.interactionPolicy).toBe('enabled'); expect(result.fontSize).toBe(16); }); - it('preserves unrelated props from existing resolver', () => { - const existingResolver = (() => { - return (_args: unknown) => ({ customProp: 'value' }); - })(); - - const resolver = createStudioInteractionPolicyResolver({ + it('preserves unrelated props from resolve args', () => { + const resolver = createInteractionPolicyResolver({ previewMode: false, - existingResolver, + isSupportedNodeType: () => true, }); + const result = resolver({ - node: { id: 'test', type: 'Text' }, - props: { children: 'Hello' }, + node: { type: 'Text' }, + props: { customProp: 'value' }, }); expect(result.customProp).toBe('value'); }); it('assigns mode-derived policy to opted-in third-party components', () => { - const resolver = createStudioInteractionPolicyResolver({ + const resolver = createInteractionPolicyResolver({ previewMode: false, thirdPartySupport: { CustomComponent: true }, + isSupportedNodeType: () => false, }); const result = resolver({ - node: { id: 'test', type: 'CustomComponent' }, + node: { type: 'CustomComponent' }, props: {}, }); @@ -73,12 +76,13 @@ describe('interactionPolicy', () => { }); it('assigns enabled policy to opted-in third-party in Preview mode', () => { - const resolver = createStudioInteractionPolicyResolver({ + const resolver = createInteractionPolicyResolver({ previewMode: true, thirdPartySupport: { CustomComponent: true }, + isSupportedNodeType: () => false, }); const result = resolver({ - node: { id: 'test', type: 'CustomComponent' }, + node: { type: 'CustomComponent' }, props: {}, }); @@ -86,12 +90,13 @@ describe('interactionPolicy', () => { }); it('assigns no policy to undeclared third-party components', () => { - const resolver = createStudioInteractionPolicyResolver({ + const resolver = createInteractionPolicyResolver({ previewMode: false, thirdPartySupport: { CustomComponent: true }, + isSupportedNodeType: () => false, }); const result = resolver({ - node: { id: 'test', type: 'ThirdPartyComponent' }, + node: { type: 'ThirdPartyComponent' }, props: {}, }); @@ -99,9 +104,12 @@ describe('interactionPolicy', () => { }); it('assigns no policy to unknown types', () => { - const resolver = createStudioInteractionPolicyResolver({ previewMode: false }); + const resolver = createInteractionPolicyResolver({ + previewMode: false, + isSupportedNodeType: () => false, + }); const result = resolver({ - node: { id: 'test', type: 'UnknownWidget' }, + node: { type: 'UnknownWidget' }, props: {}, }); @@ -109,21 +117,23 @@ describe('interactionPolicy', () => { }); it('third-party support cannot override mode policy direction', () => { - const resolver = createStudioInteractionPolicyResolver({ + const resolver = createInteractionPolicyResolver({ previewMode: false, thirdPartySupport: { CustomComponent: true }, + isSupportedNodeType: () => false, }); const editResult = resolver({ - node: { id: 'test', type: 'CustomComponent' }, + node: { type: 'CustomComponent' }, props: {}, }); - const resolverPreview = createStudioInteractionPolicyResolver({ + const resolverPreview = createInteractionPolicyResolver({ previewMode: true, thirdPartySupport: { CustomComponent: true }, + isSupportedNodeType: () => false, }); const previewResult = resolverPreview({ - node: { id: 'test', type: 'CustomComponent' }, + node: { type: 'CustomComponent' }, props: {}, }); @@ -131,4 +141,31 @@ describe('interactionPolicy', () => { expect(editResult.interactionPolicy).toBe('passive'); expect(previewResult.interactionPolicy).toBe('enabled'); }); + + it('isComponentSupported returns true for zora-builtin', () => { + expect(isComponentSupported('Text', {}, () => true)).toBe(true); + }); + + it('isComponentSupported returns true for third-party-supported', () => { + expect(isComponentSupported('Custom', { Custom: true }, () => false)).toBe(true); + }); + + it('isComponentSupported returns false for unsupported', () => { + expect(isComponentSupported('Unknown', {}, () => false)).toBe(false); + }); +}); + +describe('interactionPolicy production adapter', () => { + const sourcePath = join(import.meta.dir, 'interactionPolicy.ts'); + const source = readFileSync(sourcePath, 'utf8'); + + it('derives support from the actual ZORA_COMPONENT_REGISTRY', () => { + expect(source).toContain("from '@ankhorage/zora'"); + expect(source).toContain('ZORA_COMPONENT_REGISTRY'); + expect(source).toContain('nodeType in ZORA_COMPONENT_REGISTRY'); + }); + + it('delegates classification to the pure core', () => { + expect(source).toContain('createInteractionPolicyResolver'); + }); }); diff --git a/src/runtime/interactionPolicy.ts b/src/runtime/interactionPolicy.ts index 9f1ed66e..01c26e96 100644 --- a/src/runtime/interactionPolicy.ts +++ b/src/runtime/interactionPolicy.ts @@ -1,9 +1,10 @@ -import type { RuntimeResolveNodePropsArgs } from '@ankhorage/contracts/runtime'; import { type RuntimeNodePropsResolver } from '@ankhorage/runtime'; import { ZORA_COMPONENT_REGISTRY } from '@ankhorage/zora'; -export type InteractionPolicy = 'enabled' | 'passive'; -export type ThirdPartyComponentSupport = Readonly>; +import type { ThirdPartyComponentSupport } from './interactionPolicyCore.js'; +import { createInteractionPolicyResolver } from './interactionPolicyCore.js'; + +export type { InteractionPolicy, ThirdPartyComponentSupport } from './interactionPolicyCore.js'; export interface StudioInteractionPolicyResolverArgs { readonly previewMode: boolean; @@ -11,65 +12,33 @@ export interface StudioInteractionPolicyResolverArgs { readonly existingResolver?: RuntimeNodePropsResolver; } -const EMPTY_THIRD_PARTY: ThirdPartyComponentSupport = {}; - -type ComponentClassification = 'zora-builtin' | 'third-party-supported' | 'unsupported'; - -function classifyComponent( - nodeType: string, - thirdPartySupport: ThirdPartyComponentSupport, -): ComponentClassification { - if (nodeType in ZORA_COMPONENT_REGISTRY) { - return 'zora-builtin'; - } - - if (nodeType in thirdPartySupport) { - return 'third-party-supported'; - } - - return 'unsupported'; -} - -export function isComponentSupported( - nodeType: string, - thirdPartySupport: ThirdPartyComponentSupport, -): boolean { - return classifyComponent(nodeType, thirdPartySupport) !== 'unsupported'; -} - -function resolveInteractionPolicy( - nodeType: string, - previewMode: boolean, - thirdPartySupport: ThirdPartyComponentSupport, -): InteractionPolicy | undefined { - const classification = classifyComponent(nodeType, thirdPartySupport); - - if (classification === 'unsupported') { - return undefined; - } - - return previewMode ? 'enabled' : 'passive'; -} - -const POLICY_PROP = 'interactionPolicy'; - export function createStudioInteractionPolicyResolver( args: StudioInteractionPolicyResolverArgs, ): RuntimeNodePropsResolver { - const { previewMode, thirdPartySupport = EMPTY_THIRD_PARTY, existingResolver } = args; - - return (resolveArgs: RuntimeResolveNodePropsArgs) => { - const baseProps = existingResolver ? existingResolver(resolveArgs) : { ...resolveArgs.props }; - - const policy = resolveInteractionPolicy(resolveArgs.node.type, previewMode, thirdPartySupport); - - if (policy === undefined) { - return baseProps; - } - - return { - ...baseProps, - [POLICY_PROP]: policy, + const { previewMode, thirdPartySupport, existingResolver } = args; + + const resolver = createInteractionPolicyResolver({ + previewMode, + thirdPartySupport, + isSupportedNodeType: (nodeType) => nodeType in ZORA_COMPONENT_REGISTRY, + }); + + if (!existingResolver) { + return (resolveArgs) => { + const result = resolver({ + node: resolveArgs.node, + props: resolveArgs.props, + }); + return result; }; + } + + return (resolveArgs) => { + const baseProps = existingResolver(resolveArgs); + const result = resolver({ + node: resolveArgs.node, + props: baseProps, + }); + return result; }; } diff --git a/src/runtime/interactionPolicyCore.ts b/src/runtime/interactionPolicyCore.ts new file mode 100644 index 00000000..970ac8bc --- /dev/null +++ b/src/runtime/interactionPolicyCore.ts @@ -0,0 +1,82 @@ +export type InteractionPolicy = 'enabled' | 'passive'; +export type ThirdPartyComponentSupport = Readonly>; + +export interface InteractionPolicyResolverArgs { + readonly previewMode: boolean; + readonly thirdPartySupport?: ThirdPartyComponentSupport; + readonly isSupportedNodeType: (nodeType: string) => boolean; +} + +const EMPTY_THIRD_PARTY: ThirdPartyComponentSupport = {}; + +type ComponentClassification = 'zora-builtin' | 'third-party-supported' | 'unsupported'; + +function classifyComponent( + nodeType: string, + isSupportedNodeType: (type: string) => boolean, + thirdPartySupport: ThirdPartyComponentSupport, +): ComponentClassification { + if (isSupportedNodeType(nodeType)) { + return 'zora-builtin'; + } + + if (nodeType in thirdPartySupport) { + return 'third-party-supported'; + } + + return 'unsupported'; +} + +export function isComponentSupported( + nodeType: string, + thirdPartySupport: ThirdPartyComponentSupport, + isSupportedNodeType: (type: string) => boolean, +): boolean { + return classifyComponent(nodeType, isSupportedNodeType, thirdPartySupport) !== 'unsupported'; +} + +function resolveInteractionPolicy( + nodeType: string, + previewMode: boolean, + thirdPartySupport: ThirdPartyComponentSupport, + isSupportedNodeType: (type: string) => boolean, +): InteractionPolicy | undefined { + const classification = classifyComponent(nodeType, isSupportedNodeType, thirdPartySupport); + + if (classification === 'unsupported') { + return undefined; + } + + return previewMode ? 'enabled' : 'passive'; +} + +const POLICY_PROP = 'interactionPolicy'; + +export function createInteractionPolicyResolver( + args: InteractionPolicyResolverArgs, +): (resolveArgs: { + node: { type: string }; + props: Record; +}) => Record { + const { previewMode, thirdPartySupport = EMPTY_THIRD_PARTY, isSupportedNodeType } = args; + + return (resolveArgs) => { + const baseProps = { ...resolveArgs.props }; + + const policy = resolveInteractionPolicy( + resolveArgs.node.type, + previewMode, + thirdPartySupport, + isSupportedNodeType, + ); + + if (policy === undefined) { + return baseProps; + } + + return { + ...baseProps, + [POLICY_PROP]: policy, + }; + }; +} diff --git a/src/runtime/stationarySelection.test.ts b/src/runtime/stationarySelection.test.ts index e6adf39c..e1bb03df 100644 --- a/src/runtime/stationarySelection.test.ts +++ b/src/runtime/stationarySelection.test.ts @@ -1,337 +1,91 @@ -import { describe, expect, it } from 'bun:test'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; -import { createStationarySelectionCoordinator } from './stationarySelection'; +import { describe, expect, it } from 'bun:test'; -describe('stationarySelection coordinator', () => { - it('fresh begin creates generation 1', () => { - const coordinator = createStationarySelectionCoordinator(); - const generation = coordinator.beginTransaction(); +const sourcePath = join(import.meta.dir, 'stationarySelection.ts'); +const source = readFileSync(sourcePath, 'utf8'); - expect(typeof generation).toBe('number'); - expect(generation).toBe(1); - const tx = coordinator.getTransaction(); - expect(tx).not.toBeNull(); - expect(tx?.transactionId).toBe(generation); +describe('stationarySelection RN integration', () => { + it('uses exactly one Gesture.Tap() invocation', () => { + const matches = source.match(/Gesture\.Tap\(\)/g) ?? []; + expect(matches.length).toBe(1); }); - it('next begin creates a greater generation', () => { - const coordinator = createStationarySelectionCoordinator(); - const generation1 = coordinator.beginTransaction(); - coordinator.clearTransaction(); - const generation2 = coordinator.beginTransaction(); - - expect(generation2).toBeGreaterThan(generation1); + it('uses exactly one GestureDetector', () => { + const importLine = "import { Gesture, GestureDetector } from 'react-native-gesture-handler';"; + const withoutImport = source.replace(importLine, ''); + const matches = withoutImport.match(/GestureDetector/g) ?? []; + expect(matches.length).toBe(1); }); - it('stale generation record is ignored after clear', () => { - const coordinator = createStationarySelectionCoordinator(); - const gen1 = coordinator.beginTransaction(); - coordinator.recordNode('node-a'); - coordinator.clearTransaction(); - - coordinator.beginTransaction(); - const tx = coordinator.getTransaction(); - expect(tx?.transactionId).not.toBe(gen1); - expect(tx?.path).toEqual([]); + it('enables runOnJS(true)', () => { + expect(source).toContain('.runOnJS(true)'); }); - it('recordNode adds node to the active transaction path', () => { - const coordinator = createStationarySelectionCoordinator(); - coordinator.beginTransaction(); - coordinator.recordNode('node-a'); - - const tx = coordinator.getTransaction(); - expect(tx?.path).toContain('node-a'); - expect(tx?.path).toEqual(['node-a']); + it('begins transaction in onBegin', () => { + expect(source).toContain('.onBegin(() => {'); + expect(source).toContain('generationRef.current = coordinator.beginTransaction()'); }); - it('nested path records outer-to-inner ordering', () => { - const coordinator = createStationarySelectionCoordinator(); - coordinator.beginTransaction(); - coordinator.recordNode('outer'); - coordinator.recordNode('inner'); - - const tx = coordinator.getTransaction(); - expect(tx?.path).toEqual(['outer', 'inner']); + it('commits in onEnd only on success', () => { + expect(source).toContain('.onEnd((_event, success) => {'); + expect(source).toContain('if (!success) {'); + expect(source).toContain('coordinator.commitSelection('); }); - it('duplicate node IDs are ignored', () => { - const coordinator = createStationarySelectionCoordinator(); - coordinator.beginTransaction(); - coordinator.recordNode('node-a'); - coordinator.recordNode('node-a'); - - const tx = coordinator.getTransaction(); - expect(tx?.path).toEqual(['node-a']); - expect(tx?.path.length).toBe(1); + it('finalizes in onFinalize', () => { + expect(source).toContain('.onFinalize(() => {'); + expect(source).toContain('coordinator.clearTransaction(generationRef.current)'); }); - it('deepest node is selected from nested path', () => { - const coordinator = createStationarySelectionCoordinator(); - coordinator.beginTransaction(); - coordinator.recordNode('outer'); - coordinator.recordNode('inner'); - - let selectedNodeId: string | null = null; - coordinator.commitSelection( - true, - null, - (id) => { - selectedNodeId = id; - }, - 1, - ); - - expect(selectedNodeId).not.toBeNull(); - expect(typeof selectedNodeId).toBe('string'); - const isInner = selectedNodeId === 'inner'; - expect(isInner).toBe(true); + it('does not use onStart', () => { + expect(source).not.toContain('.onStart(() => {'); }); - it('Edit selects exactly once', () => { - let selectionCount = 0; - - const coordinator = createStationarySelectionCoordinator(); - coordinator.beginTransaction(); - coordinator.recordNode('node-a'); - coordinator.commitSelection( - true, - null, - () => { - selectionCount += 1; - }, - 1, - ); - - expect(selectionCount).toBe(1); + it('does not use legacy TapGestureHandler', () => { + expect(source).not.toContain('TapGestureHandler'); }); - it('already-selected node invokes no selection callback', () => { - const coordinator = createStationarySelectionCoordinator(); - coordinator.beginTransaction(); - coordinator.recordNode('node-a'); - - let called = false; - coordinator.commitSelection( - true, - 'node-a', - () => { - called = true; - }, - 1, - ); - - expect(called).toBe(false); + it('does not use per-node recognizer', () => { + const matches = source.match(/Gesture\.Tap\(\)/g) ?? []; + expect(matches.length).toBe(1); }); - it('Preview invokes no selection callback', () => { - const coordinator = createStationarySelectionCoordinator(); - coordinator.beginTransaction(); - coordinator.recordNode('node-a'); - - let selectedId: string | null = 'initial'; - const result = coordinator.commitSelection( - false, - null, - (id) => { - selectedId = id; - }, - 1, - ); - - expect(result).toBe('preview'); - expect(selectedId).toBe('initial'); - const tx = coordinator.getTransaction(); - expect(tx?.finalized).not.toBe(true); + it('does not use Pressable', () => { + expect(source).not.toContain('Pressable'); }); - it('moved transaction invokes no callback', () => { - const coordinator = createStationarySelectionCoordinator(); - coordinator.beginTransaction(); - coordinator.recordNode('node-a'); - const tx = coordinator.getTransaction(); - if (tx) { - tx.moved = true; - } - - let selectedId: string | null = 'initial'; - const result = coordinator.commitSelection( - true, - null, - (id) => { - selectedId = id; - }, - 1, - ); - - expect(result).toBe('moved'); - expect(selectedId).toBe('initial'); + it('does not use cloneElement', () => { + expect(source).not.toContain('cloneElement'); }); - it('empty path invokes no callback', () => { - const coordinator = createStationarySelectionCoordinator(); - coordinator.beginTransaction(); - - let selectedId: string | null = null; - let selectionCount = 0; - const result = coordinator.commitSelection( - true, - null, - (id) => { - selectedId = id; - selectionCount += 1; - }, - 1, - ); - - expect(result).toBe('empty'); - expect(selectedId).toBeNull(); - expect(selectionCount).toBe(0); + it('does not use responder capture', () => { + expect(source).not.toContain('onResponderGrant'); + expect(source).not.toContain('onResponderReject'); }); - it('stale generation commit returns stale', () => { - const coordinator = createStationarySelectionCoordinator(); - coordinator.beginTransaction(); - coordinator.recordNode('node-a'); - coordinator.clearTransaction(); - - let selectedId: string | null = null; - const result = coordinator.commitSelection( - true, - null, - (id) => { - selectedId = id; - }, - 1, - ); - - expect(result).toBe('empty'); - expect(selectedId).toBeNull(); - }); - - it('already-finalized commit returns already-finalized', () => { - const coordinator = createStationarySelectionCoordinator(); - coordinator.beginTransaction(); - coordinator.recordNode('node-a'); - coordinator.commitSelection( - true, - null, - (_id) => { - void _id; - }, - 1, - ); - - let callCount = 0; - const result = coordinator.commitSelection( - true, - null, - () => { - callCount += 1; - }, - 1, - ); - - expect(result).toBe('already-finalized'); - expect(callCount).toBe(0); + it('uses display contents for layout-neutral wrapper', () => { + expect(source).toContain("display: 'contents'"); }); - it('duplicate terminal event for the same generation does not double-commit', () => { - const coordinator = createStationarySelectionCoordinator(); - let callCount = 0; - - const gen = coordinator.beginTransaction(); - coordinator.recordNode('node-a'); - - coordinator.commitSelection( - true, - null, - () => { - callCount += 1; - }, - gen, - ); - - const result = coordinator.commitSelection( - true, - null, - () => { - callCount += 1; - }, - gen, - ); - - expect(callCount).toBe(1); - expect(result).toBe('already-finalized'); + it('preserves unsupported indicator with pointerEvents none', () => { + expect(source).toContain("pointerEvents: 'none'"); }); - it('ACTIVE/final terminal processing leaves no transaction', () => { - const coordinator = createStationarySelectionCoordinator(); - coordinator.beginTransaction(); - coordinator.recordNode('node-a'); - coordinator.commitSelection( - true, - null, - (_id) => { - void _id; - }, - 1, + it('uses lazy coordinator creation', () => { + expect(source).toContain( + 'const coordinatorRef = React.useRef(null);', ); - coordinator.clearTransaction(); - - expect(coordinator.getTransaction()).toBeNull(); + expect(source).toContain('coordinatorRef.current ??= createStationarySelectionCoordinator();'); + expect(source).not.toContain('useRef(createStationarySelectionCoordinator())'); }); - it('FAILED clears state', () => { - const coordinator = createStationarySelectionCoordinator(); - coordinator.beginTransaction(); - coordinator.recordNode('node-a'); - coordinator.clearTransaction(); - - expect(coordinator.getTransaction()).toBeNull(); + it('records touch with generation validation', () => { + expect(source).toContain('ctx.recordNode(nodeId)'); }); - it('CANCELLED clears state', () => { - const coordinator = createStationarySelectionCoordinator(); - coordinator.beginTransaction(); - coordinator.recordNode('node-a'); - coordinator.clearTransaction(); - - expect(coordinator.getTransaction()).toBeNull(); - }); - - it('END clears state', () => { - const coordinator = createStationarySelectionCoordinator(); - coordinator.beginTransaction(); - coordinator.recordNode('node-a'); - coordinator.clearTransaction(); - - expect(coordinator.getTransaction()).toBeNull(); - }); - - it('mode change clears active transaction', () => { - const coordinator = createStationarySelectionCoordinator(); - coordinator.beginTransaction(); - coordinator.recordNode('node-a'); - coordinator.clearTransaction(); - - expect(coordinator.getTransaction()).toBeNull(); - }); - - it('no transaction survives completion', () => { - const coordinator = createStationarySelectionCoordinator(); - coordinator.beginTransaction(); - coordinator.recordNode('node-a'); - coordinator.commitSelection( - true, - null, - (_id) => { - void _id; - }, - 1, - ); - coordinator.clearTransaction(); - - expect(coordinator.getTransaction()).toBeNull(); + it('uses bubble phase onTouchStart', () => { + expect(source).toContain('onTouchStart: handleTouchStart'); }); }); diff --git a/src/runtime/stationarySelection.ts b/src/runtime/stationarySelection.ts index ce0134c7..58ffc1de 100644 --- a/src/runtime/stationarySelection.ts +++ b/src/runtime/stationarySelection.ts @@ -3,154 +3,50 @@ import React from 'react'; import { View } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -export type CommitSelectionResult = - 'committed' | 'already-selected' | 'preview' | 'moved' | 'empty' | 'stale' | 'already-finalized'; +import type { StationarySelectionCoordinator } from './stationarySelectionCoordinator.js'; +import { createStationarySelectionCoordinator } from './stationarySelectionCoordinator.js'; -export interface TransactionState { - readonly transactionId: number; - readonly path: string[]; - moved: boolean; - finalized: boolean; -} +export type { + CommitSelectionResult, + StationarySelectionCoordinator, + TransactionState, +} from './stationarySelectionCoordinator.js'; -export interface StationarySelectionCoordinator { - readonly trackerRef: React.MutableRefObject; - beginTransaction(): number; - recordNode(nodeId: string): void; - commitSelection( - isEditMode: boolean, - selectedNodeId: string | null, - selectNode: (id: string | null) => void, - generation: number, - ): CommitSelectionResult; - clearTransaction(): void; - getTransaction(): TransactionState | null; +interface TrackerContextValue { + readonly recordNode: (nodeId: string) => void; } -const TrackerContext = React.createContext | null>( - null, -); - -let nextTransactionId = 1; - -export function createStationarySelectionCoordinator(): StationarySelectionCoordinator { - const trackerRef = { current: null as TransactionState | null }; - - function beginTransaction(): number { - const tx = { - transactionId: nextTransactionId++, - path: [], - moved: false, - finalized: false, - }; - trackerRef.current = tx; - return tx.transactionId; - } - - function recordNode(nodeId: string): void { - const tx = trackerRef.current; - - if (!tx || tx.finalized) { - return; - } - - if (!tx.path.includes(nodeId)) { - tx.path.push(nodeId); - } - } - - function commitSelection( - isEditMode: boolean, - selectedNodeId: string | null, - selectNode: (id: string | null) => void, - generation: number, - ): CommitSelectionResult { - if (!isEditMode) { - return 'preview'; - } - - const tx = trackerRef.current; - - if (!tx) { - return 'empty'; - } - - if (tx.transactionId !== generation) { - return 'stale'; - } - - if (tx.finalized) { - return 'already-finalized'; - } - - if (tx.moved) { - return 'moved'; - } - - if (tx.path.length === 0) { - return 'empty'; - } - - const [deepestNodeId] = tx.path; - - if (deepestNodeId != null && deepestNodeId === selectedNodeId) { - return 'already-selected'; - } - - if (deepestNodeId != null && deepestNodeId !== selectedNodeId) { - selectNode(deepestNodeId); - } - - tx.finalized = true; - - return 'committed'; - } - - function clearTransaction(): void { - trackerRef.current = null; - } - - function getTransaction(): TransactionState | null { - return trackerRef.current; - } - - return { - trackerRef, - beginTransaction, - recordNode, - commitSelection, - clearTransaction, - getTransaction, - }; -} +const TrackerContext = React.createContext(null); function StudioNodeTouchRecorder(props: { readonly nodeId: string | undefined; readonly showUnsupportedIndicator: boolean; readonly children: React.ReactNode; }): React.JSX.Element { - const tracker = React.useContext(TrackerContext); + const ctx = React.useContext(TrackerContext); const nodeIdRef = React.useRef(props.nodeId); nodeIdRef.current = props.nodeId; - function handleTouchStartCapture(): void { - const tx = tracker?.current; - - if (!tx || tx.finalized || !nodeIdRef.current) { + function handleTouchStart(): void { + if (!ctx) { return; } - if (!tx.path.includes(nodeIdRef.current)) { - tx.path.push(nodeIdRef.current); + const nodeId = nodeIdRef.current; + + if (!nodeId) { + return; } + + ctx.recordNode(nodeId); } return React.createElement( View, { - style: { position: 'relative' }, - onTouchStart: handleTouchStartCapture, + style: { display: 'contents' }, + onTouchStart: handleTouchStart, }, props.showUnsupportedIndicator ? React.createElement(View, { @@ -211,38 +107,51 @@ function StationaryTapSelector(props: { readonly selectNode: (id: string | null) => void; readonly children: React.ReactNode; }): React.JSX.Element { - const coordinatorRef = React.useRef(createStationarySelectionCoordinator()); + const coordinatorRef = React.useRef(null); + + coordinatorRef.current ??= createStationarySelectionCoordinator(); + const coordinator = coordinatorRef.current; const isEditModeRef = React.useRef(props.isEditMode); const selectedNodeIdRef = React.useRef(props.selectedNodeId); const selectNodeRef = React.useRef(props.selectNode); + const generationRef = React.useRef(0); isEditModeRef.current = props.isEditMode; selectedNodeIdRef.current = props.selectedNodeId; selectNodeRef.current = props.selectNode; - const generationRef = React.useRef(0); + const contextValue = React.useMemo( + () => ({ + recordNode: (nodeId: string) => coordinator.recordNode(nodeId, generationRef.current), + }), + [coordinator], + ); const tapGesture = React.useMemo(() => { return Gesture.Tap() .maxDeltaX(5) .maxDeltaY(5) .maxDuration(500) - .onStart(() => { + .runOnJS(true) + .onBegin(() => { generationRef.current = coordinator.beginTransaction(); }) .onEnd((_event, success) => { - if (success) { - coordinator.commitSelection( - isEditModeRef.current, - selectedNodeIdRef.current, - selectNodeRef.current, - generationRef.current, - ); + if (!success) { + return; } - coordinator.clearTransaction(); + coordinator.commitSelection( + isEditModeRef.current, + selectedNodeIdRef.current, + selectNodeRef.current, + generationRef.current, + ); + }) + .onFinalize(() => { + coordinator.clearTransaction(generationRef.current); }); }, [coordinator]); @@ -252,7 +161,7 @@ function StationaryTapSelector(props: { return React.createElement( TrackerContext.Provider, - { value: coordinator.trackerRef }, + { value: contextValue }, React.createElement( GestureDetector, { gesture: tapGesture }, diff --git a/src/runtime/stationarySelectionCoordinator.test.ts b/src/runtime/stationarySelectionCoordinator.test.ts new file mode 100644 index 00000000..e507b6b2 --- /dev/null +++ b/src/runtime/stationarySelectionCoordinator.test.ts @@ -0,0 +1,268 @@ +import { describe, expect, it } from 'bun:test'; + +import { createStationarySelectionCoordinator } from './stationarySelectionCoordinator'; + +describe('stationarySelectionCoordinator', () => { + it('each new coordinator starts at generation 1', () => { + const coordinator = createStationarySelectionCoordinator(); + const generation = coordinator.beginTransaction(); + + expect(generation).toBe(1); + const tx = coordinator.getTransaction(); + expect(tx).not.toBeNull(); + expect(tx?.transactionId).toBe(generation); + }); + + it('second transaction uses generation 2', () => { + const coordinator = createStationarySelectionCoordinator(); + const generation1 = coordinator.beginTransaction(); + coordinator.clearTransaction(); + const generation2 = coordinator.beginTransaction(); + + expect(generation2).toBe(generation1 + 1); + }); + + it('stale record generation rejected', () => { + const coordinator = createStationarySelectionCoordinator(); + const gen1 = coordinator.beginTransaction(); + coordinator.recordNode('node-a', gen1); + coordinator.clearTransaction(); + + const gen2 = coordinator.beginTransaction(); + coordinator.recordNode('node-a', gen1); + + const tx = coordinator.getTransaction(); + expect(tx?.transactionId).toBe(gen2); + expect(tx?.path).toEqual([]); + }); + + it('stale commit generation rejected', () => { + const coordinator = createStationarySelectionCoordinator(); + const gen1 = coordinator.beginTransaction(); + coordinator.recordNode('node-a', gen1); + coordinator.clearTransaction(); + + const result = coordinator.commitSelection( + true, + null, + () => { + void 0; + }, + gen1, + ); + + expect(result).toBe('empty'); + }); + + it('duplicate node rejected', () => { + const coordinator = createStationarySelectionCoordinator(); + const gen = coordinator.beginTransaction(); + coordinator.recordNode('node-a', gen); + coordinator.recordNode('node-a', gen); + + const tx = coordinator.getTransaction(); + expect(tx?.path).toEqual(['node-a']); + }); + + it('bubble path inner-to-outer', () => { + const coordinator = createStationarySelectionCoordinator(); + const gen = coordinator.beginTransaction(); + coordinator.recordNode('inner', gen); + coordinator.recordNode('outer', gen); + + const tx = coordinator.getTransaction(); + expect(tx?.path).toEqual(['inner', 'outer']); + }); + + it('first node is deepest', () => { + const coordinator = createStationarySelectionCoordinator(); + const gen = coordinator.beginTransaction(); + coordinator.recordNode('inner', gen); + coordinator.recordNode('outer', gen); + + let selectedNodeId: string | null = null; + coordinator.commitSelection( + true, + null, + (id) => { + selectedNodeId = id; + }, + gen, + ); + + expect(selectedNodeId as unknown as string).toBe('inner'); + }); + + it('Edit commit once', () => { + let callCount = 0; + const coordinator = createStationarySelectionCoordinator(); + const gen = coordinator.beginTransaction(); + coordinator.recordNode('node-a', gen); + coordinator.commitSelection( + true, + null, + () => { + callCount += 1; + }, + gen, + ); + + expect(callCount).toBe(1); + }); + + it('already-selected callback omitted', () => { + let called = false; + const coordinator = createStationarySelectionCoordinator(); + const gen = coordinator.beginTransaction(); + coordinator.recordNode('node-a', gen); + coordinator.commitSelection( + true, + 'node-a', + () => { + called = true; + }, + gen, + ); + + expect(called).toBe(false); + }); + + it('Preview callback omitted', () => { + let selectedId: string | null = 'initial'; + const coordinator = createStationarySelectionCoordinator(); + const gen = coordinator.beginTransaction(); + coordinator.recordNode('node-a', gen); + const result = coordinator.commitSelection( + false, + null, + (id) => { + selectedId = id; + }, + gen, + ); + + expect(result).toBe('preview'); + expect(selectedId).toBe('initial'); + const tx = coordinator.getTransaction(); + expect(tx?.finalized).not.toBe(true); + }); + + it('empty path', () => { + let selectedId: string | null = null; + let callCount = 0; + const coordinator = createStationarySelectionCoordinator(); + const gen = coordinator.beginTransaction(); + const result = coordinator.commitSelection( + true, + null, + (id) => { + selectedId = id; + callCount += 1; + }, + gen, + ); + + expect(result).toBe('empty'); + expect(selectedId).toBeNull(); + expect(callCount).toBe(0); + }); + + it('moved transaction', () => { + let selectedId: string | null = 'initial'; + const coordinator = createStationarySelectionCoordinator(); + const gen = coordinator.beginTransaction(); + coordinator.recordNode('node-a', gen); + coordinator.markMoved(gen); + const result = coordinator.commitSelection( + true, + null, + (id) => { + selectedId = id; + }, + gen, + ); + + expect(result).toBe('moved'); + expect(selectedId).toBe('initial'); + }); + + it('already-finalized transaction', () => { + const coordinator = createStationarySelectionCoordinator(); + const gen = coordinator.beginTransaction(); + coordinator.recordNode('node-a', gen); + coordinator.commitSelection( + true, + null, + () => { + void 0; + }, + gen, + ); + + let callCount = 0; + const result = coordinator.commitSelection( + true, + null, + () => { + callCount += 1; + }, + gen, + ); + + expect(result).toBe('already-finalized'); + expect(callCount).toBe(0); + }); + + it('cleanup only for matching generation', () => { + const coordinator = createStationarySelectionCoordinator(); + const gen1 = coordinator.beginTransaction(); + coordinator.recordNode('node-a', gen1); + coordinator.commitSelection( + true, + null, + () => { + void 0; + }, + gen1, + ); + + const gen2 = coordinator.beginTransaction(); + coordinator.recordNode('node-b', gen2); + coordinator.clearTransaction(gen1); + + const tx = coordinator.getTransaction(); + expect(tx?.transactionId).toBe(gen2); + expect(tx?.path).toEqual(['node-b']); + }); + + it('stale finalization cannot clear a newer transaction', () => { + const coordinator = createStationarySelectionCoordinator(); + const gen1 = coordinator.beginTransaction(); + coordinator.clearTransaction(gen1); + + const gen2 = coordinator.beginTransaction(); + coordinator.recordNode('node-a', gen2); + coordinator.clearTransaction(gen1); + + const tx = coordinator.getTransaction(); + expect(tx?.transactionId).toBe(gen2); + expect(tx?.path).toEqual(['node-a']); + }); + + it('no transaction survives matching finalization', () => { + const coordinator = createStationarySelectionCoordinator(); + const gen = coordinator.beginTransaction(); + coordinator.recordNode('node-a', gen); + coordinator.commitSelection( + true, + null, + () => { + void 0; + }, + gen, + ); + coordinator.clearTransaction(gen); + + expect(coordinator.getTransaction()).toBeNull(); + }); +}); diff --git a/src/runtime/stationarySelectionCoordinator.ts b/src/runtime/stationarySelectionCoordinator.ts new file mode 100644 index 00000000..917840cb --- /dev/null +++ b/src/runtime/stationarySelectionCoordinator.ts @@ -0,0 +1,141 @@ +export type CommitSelectionResult = + 'committed' | 'already-selected' | 'preview' | 'moved' | 'empty' | 'stale' | 'already-finalized'; + +export interface TransactionState { + readonly transactionId: number; + readonly path: string[]; + moved: boolean; + finalized: boolean; +} + +export interface StationarySelectionCoordinator { + readonly trackerRef: { current: TransactionState | null }; + beginTransaction(): number; + recordNode(nodeId: string, generation: number): void; + commitSelection( + isEditMode: boolean, + selectedNodeId: string | null, + selectNode: (id: string | null) => void, + generation: number, + ): CommitSelectionResult; + clearTransaction(): void; + clearTransaction(generation: number): void; + markMoved(generation: number): void; + getTransaction(): TransactionState | null; +} + +export function createStationarySelectionCoordinator(): StationarySelectionCoordinator { + let nextTransactionId = 1; + const trackerRef = { current: null as TransactionState | null }; + + function beginTransaction(): number { + const tx = { + transactionId: nextTransactionId++, + path: [], + moved: false, + finalized: false, + }; + trackerRef.current = tx; + return tx.transactionId; + } + + function recordNode(nodeId: string, generation: number): void { + const tx = trackerRef.current; + + if (!tx || tx.finalized || tx.transactionId !== generation) { + return; + } + + if (!tx.path.includes(nodeId)) { + tx.path.push(nodeId); + } + } + + function commitSelection( + isEditMode: boolean, + selectedNodeId: string | null, + selectNode: (id: string | null) => void, + generation: number, + ): CommitSelectionResult { + if (!isEditMode) { + return 'preview'; + } + + const tx = trackerRef.current; + + if (!tx) { + return 'empty'; + } + + if (tx.transactionId !== generation) { + return 'stale'; + } + + if (tx.finalized) { + return 'already-finalized'; + } + + if (tx.moved) { + return 'moved'; + } + + if (tx.path.length === 0) { + return 'empty'; + } + + const [deepestNodeId] = tx.path; + + if (deepestNodeId != null && deepestNodeId === selectedNodeId) { + return 'already-selected'; + } + + if (deepestNodeId != null && deepestNodeId !== selectedNodeId) { + selectNode(deepestNodeId); + } + + tx.finalized = true; + + return 'committed'; + } + + function clearTransaction(): void; + function clearTransaction(generation: number): void; + function clearTransaction(generation?: number): void { + if (generation === undefined) { + trackerRef.current = null; + return; + } + + const tx = trackerRef.current; + + if (tx?.transactionId !== generation) { + return; + } + + trackerRef.current = null; + } + + function markMoved(generation: number): void { + const tx = trackerRef.current; + + if (tx?.transactionId !== generation) { + return; + } + + tx.moved = true; + } + + function getTransaction(): TransactionState | null { + return trackerRef.current; + } + + return { + trackerRef, + beginTransaction, + recordNode, + commitSelection, + clearTransaction, + markMoved, + getTransaction, + }; +} From b420d0d6ef2b017bcb7955cf7073e5a44d12e126 Mon Sep 17 00:00:00 2001 From: artiphishle Date: Wed, 29 Jul 2026 00:07:12 +0200 Subject: [PATCH 4/8] fix: harden Studio interaction integration --- src/host/generatedAdminExpoWeb.smoke.test.ts | 4 +- src/host/layout/templates/rootLayout.test.ts | 10 ++- src/host/layout/templates/rootLayout.ts | 82 +++++++++++-------- src/runtime/interactionPolicy.test.ts | 15 +++- src/runtime/interactionPolicy.ts | 3 +- src/runtime/interactionPolicyCore.ts | 2 +- src/runtime/stationarySelection.test.ts | 12 +++ src/runtime/stationarySelection.ts | 7 +- .../stationarySelectionCoordinator.test.ts | 22 +++-- 9 files changed, 106 insertions(+), 51 deletions(-) diff --git a/src/host/generatedAdminExpoWeb.smoke.test.ts b/src/host/generatedAdminExpoWeb.smoke.test.ts index 621b2641..78dcf0f1 100644 --- a/src/host/generatedAdminExpoWeb.smoke.test.ts +++ b/src/host/generatedAdminExpoWeb.smoke.test.ts @@ -425,11 +425,11 @@ async function linkSmokeNodeModules(workspaceRoot: string): Promise { const nodeModulesRoot = path.join(workspaceRoot, 'node_modules'); await mkdir(nodeModulesRoot, { recursive: true }); await linkNodeModuleEntries({ - sourceRoot: path.join(process.cwd(), 'apps', 'studio', 'node_modules'), + sourceRoot: path.join(process.cwd(), 'node_modules', '.bun', 'node_modules'), targetRoot: nodeModulesRoot, }); await linkNodeModuleEntries({ - sourceRoot: path.join(process.cwd(), 'node_modules', '.bun', 'node_modules'), + sourceRoot: path.join(process.cwd(), 'apps', 'studio', 'node_modules'), targetRoot: nodeModulesRoot, }); } diff --git a/src/host/layout/templates/rootLayout.test.ts b/src/host/layout/templates/rootLayout.test.ts index 434867ec..2923d1c4 100644 --- a/src/host/layout/templates/rootLayout.test.ts +++ b/src/host/layout/templates/rootLayout.test.ts @@ -154,7 +154,7 @@ test('generates a root stationary tap selector for edit mode and excludes old Pr expect(generated).toContain('wrapNode: studioWrapNode'); expect(generated).toContain('selectedNodeId'); expect(generated).toContain('selectNode'); - expect(generated).toContain('studioThirdPartySupport'); + expect(generated).toContain('STUDIO_EMPTY_THIRD_PARTY_SUPPORT'); expect(generated).toContain('ZORA_COMPONENT_REGISTRY'); expect(generated).not.toContain(' { expect(rootLayoutSource).not.toContain('disableActions: !previewMode'); expect(rootLayoutSource).not.toContain('wrapNode: wrapStudioRuntimeNode'); expect(studioShellSource).toContain('const studioRuntimeConfig = useMemo('); + expect(studioShellSource).toContain('const studioWrapNode = useMemo('); + expect(studioShellSource).toContain('createStudioStationarySelectionWrapNode'); + expect(studioShellSource).toContain('STUDIO_EMPTY_THIRD_PARTY_SUPPORT'); + expect(studioShellSource).toContain('const studioResolveNodeProps = useMemo('); + expect(studioShellSource).toContain('createStudioInteractionPolicyResolver'); expect(studioShellSource).toContain('disableActions: !previewMode'); expect(studioShellSource).toContain('wrapNode: studioWrapNode'); expect(studioShellSource).toContain( '', ); expect(studioShellSource).toContain('const studioOutput = ('); + expect(studioShellSource).toContain( + '[previewMode, studioWrapNode, studioResolveNodeProps]', + ); }); test('keeps generated apps Studio-independent when includeStudio is false', () => { diff --git a/src/host/layout/templates/rootLayout.ts b/src/host/layout/templates/rootLayout.ts index 2dec0492..8d1e7781 100644 --- a/src/host/layout/templates/rootLayout.ts +++ b/src/host/layout/templates/rootLayout.ts @@ -436,6 +436,9 @@ function resolveRuntimeOperationCredential(credential: { readonly kind?: string runtimeOperationHelpers.trim(), authRuntimeConstants.trim(), includeStudio ? appHeaderHelpers.trim() : '', + includeStudio + ? 'const STUDIO_EMPTY_THIRD_PARTY_SUPPORT = {} as const;' + : '', innerNavigation.declarations.trim(), ] .filter(Boolean) @@ -615,42 +618,49 @@ function StudioShell({ if (nextAppLocation) setLastNonAdminLocation(nextAppLocation); }, [appLocation, appPathname, setLastNonAdminLocation]); - const studioThirdPartySupport = {} as const; - const studioWrapNode = createStudioStationarySelectionWrapNode({ - previewMode, - thirdPartySupport: studioThirdPartySupport, - }); - const studioResolveNodeProps = createStudioInteractionPolicyResolver({ - previewMode, - thirdPartySupport: studioThirdPartySupport, - }); - const appHeaderTitle = resolveStudioAppHeaderTitle({ - runtimeManifest, - studioManifest, - previewMode, - activeScreenId, - pathname: appPathname, - }); - const header = shouldMountAppHeader ? ( - - ) : undefined; - const studioRuntimeManifest = studioManifest ?? runtimeManifest; - const activeStudioTheme = - studioRuntimeManifest.themes.find( - (theme) => theme.id === studioRuntimeManifest.activeThemeId, - ) ?? activeTheme; - const activeStudioThemeMode = resolveThemeMode( - studioRuntimeManifest.activeThemeMode, - activeThemeMode, - ); - const studioRuntimeConfig = useMemo( - () => ({ - disableActions: !previewMode, - wrapNode: studioWrapNode, - resolveNodeProps: studioResolveNodeProps, - }), - [previewMode], - ); + const studioWrapNode = useMemo( + () => + createStudioStationarySelectionWrapNode({ + previewMode, + thirdPartySupport: STUDIO_EMPTY_THIRD_PARTY_SUPPORT, + }), + [previewMode], + ); + const studioResolveNodeProps = useMemo( + () => + createStudioInteractionPolicyResolver({ + previewMode, + thirdPartySupport: STUDIO_EMPTY_THIRD_PARTY_SUPPORT, + }), + [previewMode], + ); + const appHeaderTitle = resolveStudioAppHeaderTitle({ + runtimeManifest, + studioManifest, + previewMode, + activeScreenId, + pathname: appPathname, + }); + const header = shouldMountAppHeader ? ( + + ) : undefined; + const studioRuntimeManifest = studioManifest ?? runtimeManifest; + const activeStudioTheme = + studioRuntimeManifest.themes.find( + (theme) => theme.id === studioRuntimeManifest.activeThemeId, + ) ?? activeTheme; + const activeStudioThemeMode = resolveThemeMode( + studioRuntimeManifest.activeThemeMode, + activeThemeMode, + ); + const studioRuntimeConfig = useMemo( + () => ({ + disableActions: !previewMode, + wrapNode: studioWrapNode, + resolveNodeProps: studioResolveNodeProps, + }), + [previewMode, studioWrapNode, studioResolveNodeProps], + ); const studioOutput = ( { expect(previewResult.interactionPolicy).toBe('enabled'); }); + it('isComponentSupported returns false for inherited prototype names', () => { + expect(isComponentSupported('constructor', {}, () => false)).toBe(false); + expect(isComponentSupported('toString', {}, () => false)).toBe(false); + expect(isComponentSupported('valueOf', {}, () => false)).toBe(false); + expect(isComponentSupported('__proto__', {}, () => false)).toBe(false); + }); + + it('isComponentSupported returns true for an explicit own third-party entry', () => { + expect(isComponentSupported('CustomWidget', { CustomWidget: true }, () => false)).toBe(true); + }); + it('isComponentSupported returns true for zora-builtin', () => { expect(isComponentSupported('Text', {}, () => true)).toBe(true); }); @@ -162,7 +173,9 @@ describe('interactionPolicy production adapter', () => { it('derives support from the actual ZORA_COMPONENT_REGISTRY', () => { expect(source).toContain("from '@ankhorage/zora'"); expect(source).toContain('ZORA_COMPONENT_REGISTRY'); - expect(source).toContain('nodeType in ZORA_COMPONENT_REGISTRY'); + expect(source).toContain( + 'Object.prototype.hasOwnProperty.call(ZORA_COMPONENT_REGISTRY, nodeType)', + ); }); it('delegates classification to the pure core', () => { diff --git a/src/runtime/interactionPolicy.ts b/src/runtime/interactionPolicy.ts index 01c26e96..26669dda 100644 --- a/src/runtime/interactionPolicy.ts +++ b/src/runtime/interactionPolicy.ts @@ -20,7 +20,8 @@ export function createStudioInteractionPolicyResolver( const resolver = createInteractionPolicyResolver({ previewMode, thirdPartySupport, - isSupportedNodeType: (nodeType) => nodeType in ZORA_COMPONENT_REGISTRY, + isSupportedNodeType: (nodeType) => + Object.prototype.hasOwnProperty.call(ZORA_COMPONENT_REGISTRY, nodeType), }); if (!existingResolver) { diff --git a/src/runtime/interactionPolicyCore.ts b/src/runtime/interactionPolicyCore.ts index 970ac8bc..8337c9c5 100644 --- a/src/runtime/interactionPolicyCore.ts +++ b/src/runtime/interactionPolicyCore.ts @@ -20,7 +20,7 @@ function classifyComponent( return 'zora-builtin'; } - if (nodeType in thirdPartySupport) { + if (Object.prototype.hasOwnProperty.call(thirdPartySupport, nodeType)) { return 'third-party-supported'; } diff --git a/src/runtime/stationarySelection.test.ts b/src/runtime/stationarySelection.test.ts index e1bb03df..9ea88b10 100644 --- a/src/runtime/stationarySelection.test.ts +++ b/src/runtime/stationarySelection.test.ts @@ -73,6 +73,18 @@ describe('stationarySelection RN integration', () => { expect(source).toContain("pointerEvents: 'none'"); }); + it('places pointerEvents as a View prop, not inside style', () => { + const indicatorMatch = /React\.createElement\(View,\s*\{[^}]*style:\s*\{[^}]*\}[^}]*\}\)/.exec( + source, + ); + expect(indicatorMatch).not.toBeNull(); + const indicatorBlock = indicatorMatch?.[0] ?? ''; + expect(indicatorBlock).toContain("pointerEvents: 'none'"); + const styleBlock = /style:\s*\{([^}]*)\}/.exec(indicatorBlock); + expect(styleBlock).toBeTruthy(); + expect(styleBlock?.[1]).not.toContain('pointerEvents'); + }); + it('uses lazy coordinator creation', () => { expect(source).toContain( 'const coordinatorRef = React.useRef(null);', diff --git a/src/runtime/stationarySelection.ts b/src/runtime/stationarySelection.ts index 58ffc1de..5c67650a 100644 --- a/src/runtime/stationarySelection.ts +++ b/src/runtime/stationarySelection.ts @@ -59,8 +59,8 @@ function StudioNodeTouchRecorder(props: { borderWidth: 1, borderColor: '#ef4444', borderStyle: 'dashed', - pointerEvents: 'none', }, + pointerEvents: 'none', }) : null, props.children, @@ -88,8 +88,9 @@ export function createStudioStationarySelectionWrapNode(options?: { } const isSupported = - args.node.type in ZORA_COMPONENT_REGISTRY || - (thirdPartySupport != null && args.node.type in thirdPartySupport); + Object.prototype.hasOwnProperty.call(ZORA_COMPONENT_REGISTRY, args.node.type) || + (thirdPartySupport != null && + Object.prototype.hasOwnProperty.call(thirdPartySupport, args.node.type)); const showUnsupportedIndicator = isEditMode && !isSupported; diff --git a/src/runtime/stationarySelectionCoordinator.test.ts b/src/runtime/stationarySelectionCoordinator.test.ts index e507b6b2..25eb1429 100644 --- a/src/runtime/stationarySelectionCoordinator.test.ts +++ b/src/runtime/stationarySelectionCoordinator.test.ts @@ -42,16 +42,24 @@ describe('stationarySelectionCoordinator', () => { coordinator.recordNode('node-a', gen1); coordinator.clearTransaction(); + const gen2 = coordinator.beginTransaction(); + coordinator.recordNode('node-a', gen2); + + let selectedNodeId: string | null = null; const result = coordinator.commitSelection( true, null, - () => { - void 0; + (id) => { + selectedNodeId = id; }, gen1, ); - expect(result).toBe('empty'); + expect(result).toBe('stale'); + expect(selectedNodeId).toBeNull(); + const tx = coordinator.getTransaction(); + expect(tx?.transactionId).toBe(gen2); + expect(tx?.path).toEqual(['node-a']); }); it('duplicate node rejected', () => { @@ -80,17 +88,19 @@ describe('stationarySelectionCoordinator', () => { coordinator.recordNode('inner', gen); coordinator.recordNode('outer', gen); - let selectedNodeId: string | null = null; + let selectedNodeId = ''; coordinator.commitSelection( true, null, (id) => { - selectedNodeId = id; + if (id !== null) { + selectedNodeId = id; + } }, gen, ); - expect(selectedNodeId as unknown as string).toBe('inner'); + expect(selectedNodeId).toBe('inner'); }); it('Edit commit once', () => { From 973df6f8632f31a746ba33e6cfb18aac72e58a3a Mon Sep 17 00:00:00 2001 From: artiphishle Date: Thu, 30 Jul 2026 18:55:24 +0200 Subject: [PATCH 5/8] docs: update --- README.md | 2 +- bun.lock | 8 +- package.json | 2 +- paradox/badges/npm.svg | 6 +- paradox/diagrams/architecture-overview.mmd | 25 +- paradox/diagrams/export-graph.mmd | 9 +- paradox/diagrams/module-relationships.mmd | 16 +- paradox/index.html | 612 +++++++++++-- paradox/paradox.json | 864 +++++++++++++++---- src/host/layout/templates/rootLayout.test.ts | 4 +- src/host/layout/templates/rootLayout.ts | 4 +- 11 files changed, 1276 insertions(+), 276 deletions(-) diff --git a/README.md b/README.md index d54b8df0..e2c07522 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # @ankhorage/studio -![license: MIT](././paradox/badges/license.svg) ![npm: v0.10.5](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg) +![license: MIT](././paradox/badges/license.svg) ![npm: v0.10.9](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg) Standalone Studio authoring package for Ankhorage apps. diff --git a/bun.lock b/bun.lock index ff720dae..3fe9f8db 100644 --- a/bun.lock +++ b/bun.lock @@ -17,7 +17,7 @@ "@ankhorage/supabase-auth": "^1.1.1", "@ankhorage/supabase-vault": "^0.2.0", "@ankhorage/templates": "^3.0.0", - "@ankhorage/zora": "^2.8.7", + "@ankhorage/zora": "^2.9.0", "@ankhorage/zora-chess": "^0.1.2", "@ankhorage/zora-tabletop": "^0.0.5", "@fastify/cors": "^11.2.0", @@ -119,11 +119,11 @@ "@ankhorage/supabase-vault": ["@ankhorage/supabase-vault@0.2.1", "", { "dependencies": { "@ankhorage/contracts": "^2.1.0" } }, "sha512-WkAsYQUtSnSTPx3hA93LowWs3mBjY7sbKTe8yEolKlzpUhr4Y5rhcUZvDhk30uBgqwfkRTCHxKNEDraomlkj0g=="], - "@ankhorage/surface": ["@ankhorage/surface@2.0.3", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7", "@ankhorage/contracts": "^2.1.0" }, "peerDependencies": { "@expo/vector-icons": ">=14.0.0", "expo-font": "^55.0.6", "react": ">=18.2.0", "react-native": ">=0.72.0", "react-native-safe-area-context": "^5.0.0" }, "optionalPeers": ["@expo/vector-icons", "expo-font"] }, "sha512-F5uG1Hun98N0c8fYgXAOlliUoIgX1LRm5B5IyiN0I2GEiyW12mC1UOu2MNn+9PAjZNdH1tsNbHHikw/OJhh5jA=="], + "@ankhorage/surface": ["@ankhorage/surface@2.1.0", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7", "@ankhorage/contracts": "^2.1.0" }, "peerDependencies": { "@expo/vector-icons": ">=14.0.0", "expo-font": "^55.0.6", "react": ">=18.2.0", "react-native": ">=0.72.0", "react-native-safe-area-context": "^5.0.0" }, "optionalPeers": ["@expo/vector-icons", "expo-font"] }, "sha512-6DB9lLQV+CfiA1uytI3juD1h06atJDAkpdGfCzQHK1QEnmPaLrS3nhC2/UidA1oRAx4BihFsTYp6yYY1Cprc1A=="], "@ankhorage/templates": ["@ankhorage/templates@3.0.0", "", { "dependencies": { "@ankhorage/contracts": "^4.0.0", "@ankhorage/zora": "^2.8.0" }, "bin": { "ankhorage-templates": "dist/cli/standalone.js" } }, "sha512-aflIReBWhuXpjLLToWHr13zDCuEOvjebfdeGBS5qYfbUg93gJV6xakDNN51tNgCAiroeG6jkytDqrqRPj3ZkJw=="], - "@ankhorage/zora": ["@ankhorage/zora@2.8.7", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7", "@ankhorage/contracts": "^2.1.0", "@ankhorage/surface": "^2.0.3" }, "peerDependencies": { "@expo/vector-icons": ">=14.0.0", "@react-native-picker/picker": ">=2.0.0", "expo-font": "^55.0.6", "expo-linear-gradient": ">=14.0.0", "react": ">=18.2.0", "react-native": ">=0.72.0" }, "optionalPeers": ["@expo/vector-icons", "expo-font"] }, "sha512-pWEpJyyhsNhoeY6NdVmuwhcaS+JbQ76b7HRqL8Dht0opGVFFHB/rxGaYIm+7iJpdVYPU/dtY+ASeljX1WM+sZw=="], + "@ankhorage/zora": ["@ankhorage/zora@2.9.0", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7", "@ankhorage/contracts": "^2.1.0", "@ankhorage/surface": "^2.1.0" }, "peerDependencies": { "@expo/vector-icons": ">=14.0.0", "@react-native-picker/picker": ">=2.0.0", "expo-font": "^55.0.6", "expo-linear-gradient": ">=14.0.0", "react": ">=18.2.0", "react-native": ">=0.72.0" }, "optionalPeers": ["@expo/vector-icons", "expo-font"] }, "sha512-iiRaxYAXzkVgfYSy/xwvJf9N0U5f6IP/KhjUN5U3Cc2VEd95/oXKKfEjksnz1WLD3AkecQCTNvqP2jC8S65pUA=="], "@ankhorage/zora-chess": ["@ankhorage/zora-chess@0.1.2", "", { "dependencies": { "chess.js": "^1.4.0" }, "peerDependencies": { "@ankhorage/zora": ">=0.1.0", "react": ">=18.2.0", "react-native": ">=0.72.0" } }, "sha512-uVZyqJzvnyAzCNrrys4UGR/ioVgYxgcKbY/5ET0d7tyOt5joaKqiQMNEbQL+5gby34mqhLjmBSS3xt8mxZgRQg=="], @@ -2385,6 +2385,8 @@ "@ankhorage/templates/@ankhorage/zora/@ankhorage/contracts": ["@ankhorage/contracts@2.1.0", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7" } }, "sha512-QGG0atpGDe9+BUWzWvV83Kj309TBOK76yBRbWOoQli8g0LY8QLb7Z4UquD+qVhtWJTp3Y1jHUTEttQ0WjdhY/Q=="], + "@ankhorage/templates/@ankhorage/zora/@ankhorage/surface": ["@ankhorage/surface@2.0.3", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7", "@ankhorage/contracts": "^2.1.0" }, "peerDependencies": { "@expo/vector-icons": ">=14.0.0", "expo-font": "^55.0.6", "react": ">=18.2.0", "react-native": ">=0.72.0", "react-native-safe-area-context": "^5.0.0" }, "optionalPeers": ["@expo/vector-icons", "expo-font"] }, "sha512-F5uG1Hun98N0c8fYgXAOlliUoIgX1LRm5B5IyiN0I2GEiyW12mC1UOu2MNn+9PAjZNdH1tsNbHHikw/OJhh5jA=="], + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "@babel/highlight/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], diff --git a/package.json b/package.json index 2e6e5e6d..b976f235 100644 --- a/package.json +++ b/package.json @@ -240,7 +240,7 @@ "@ankhorage/supabase-auth": "^1.1.1", "@ankhorage/supabase-vault": "^0.2.0", "@ankhorage/templates": "^3.0.0", - "@ankhorage/zora": "^2.8.7", + "@ankhorage/zora": "^2.9.0", "@ankhorage/zora-chess": "^0.1.2", "@ankhorage/zora-tabletop": "^0.0.5", "@fastify/cors": "^11.2.0", diff --git a/paradox/badges/npm.svg b/paradox/badges/npm.svg index bda86b36..1cde34c5 100644 --- a/paradox/badges/npm.svg +++ b/paradox/badges/npm.svg @@ -1,7 +1,7 @@ - -npm: v0.10.5 + +npm: v0.10.9 npm -v0.10.5 +v0.10.9 diff --git a/paradox/diagrams/architecture-overview.mmd b/paradox/diagrams/architecture-overview.mmd index 793e57b3..b39dcb2d 100644 --- a/paradox/diagrams/architecture-overview.mmd +++ b/paradox/diagrams/architecture-overview.mmd @@ -259,6 +259,9 @@ graph TD module_src_host_layout_layoutGenerator_ts --> module_src_host_modules_layout_ts module_src_host_layout_layoutGenerator_ts --> module_src_index_ts module_src_host_layout_layoutGenerator_ts --> module_src_studioAdminRouteModel_ts + module_src_host_layout_templates_auth_adapter_test_ts["src/host/layout/templates/auth/adapter.test.ts"] + package__ankhorage_studio -.-> module_src_host_layout_templates_auth_adapter_test_ts + module_src_host_layout_templates_auth_adapter_test_ts --> module_src_host_layout_templates_auth_adapter_ts module_src_host_layout_templates_auth_adapter_ts["src/host/layout/templates/auth/adapter.ts"] package__ankhorage_studio -.-> module_src_host_layout_templates_auth_adapter_ts module_src_host_layout_templates_auth_callback_ts["src/host/layout/templates/auth/callback.ts"] @@ -295,9 +298,6 @@ graph TD module_src_host_layout_templates_redirect_ts["src/host/layout/templates/redirect.ts"] package__ankhorage_studio -.-> module_src_host_layout_templates_redirect_ts module_src_host_layout_templates_redirect_ts --> module_src_host_layout_utils_escapeStringLiteral_ts - module_src_host_layout_templates_rootLayout_selectionDom_test_ts["src/host/layout/templates/rootLayout.selectionDom.test.ts"] - package__ankhorage_studio -.-> module_src_host_layout_templates_rootLayout_selectionDom_test_ts - module_src_host_layout_templates_rootLayout_selectionDom_test_ts --> module_src_host_layout_templates_rootLayout_ts module_src_host_layout_templates_rootLayout_test_ts["src/host/layout/templates/rootLayout.test.ts"] package__ankhorage_studio -.-> module_src_host_layout_templates_rootLayout_test_ts module_src_host_layout_templates_rootLayout_test_ts --> module_src_host_layout_templates_rootLayout_ts @@ -593,6 +593,14 @@ graph TD package__ankhorage_studio -.-> module_src_runtime_appExtensionRegistry_ts module_src_runtime_index_ts["src/runtime/index.ts"] package__ankhorage_studio -.-> module_src_runtime_index_ts + module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"] + package__ankhorage_studio -.-> module_src_runtime_interactionPolicy_test_ts + module_src_runtime_interactionPolicy_test_ts --> module_src_runtime_interactionPolicyCore_ts + module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"] + package__ankhorage_studio -.-> module_src_runtime_interactionPolicy_ts + module_src_runtime_interactionPolicy_ts --> module_src_runtime_interactionPolicyCore_ts + module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"] + package__ankhorage_studio -.-> module_src_runtime_interactionPolicyCore_ts module_src_runtime_localizationPreview_ts["src/runtime/localizationPreview.ts"] package__ankhorage_studio -.-> module_src_runtime_localizationPreview_ts module_src_runtime_previewRegistry_ts["src/runtime/previewRegistry.ts"] @@ -601,6 +609,7 @@ graph TD module_src_runtime_previewRuntimeConfig_ts["src/runtime/previewRuntimeConfig.ts"] package__ankhorage_studio -.-> module_src_runtime_previewRuntimeConfig_ts module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_actionSuppression_ts + module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicy_ts module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_previewRegistry_ts module_src_runtime_registry_tsx["src/runtime/registry.tsx"] package__ankhorage_studio -.-> module_src_runtime_registry_tsx @@ -608,6 +617,16 @@ graph TD package__ankhorage_studio -.-> module_src_runtime_runtimeActions_ts module_src_runtime_runtimeSurface_test_ts["src/runtime/runtimeSurface.test.ts"] package__ankhorage_studio -.-> module_src_runtime_runtimeSurface_test_ts + module_src_runtime_stationarySelection_test_ts["src/runtime/stationarySelection.test.ts"] + package__ankhorage_studio -.-> module_src_runtime_stationarySelection_test_ts + module_src_runtime_stationarySelection_ts["src/runtime/stationarySelection.ts"] + package__ankhorage_studio -.-> module_src_runtime_stationarySelection_ts + module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionCoordinator_ts + module_src_runtime_stationarySelectionCoordinator_test_ts["src/runtime/stationarySelectionCoordinator.test.ts"] + package__ankhorage_studio -.-> module_src_runtime_stationarySelectionCoordinator_test_ts + module_src_runtime_stationarySelectionCoordinator_test_ts --> module_src_runtime_stationarySelectionCoordinator_ts + module_src_runtime_stationarySelectionCoordinator_ts["src/runtime/stationarySelectionCoordinator.ts"] + package__ankhorage_studio -.-> module_src_runtime_stationarySelectionCoordinator_ts module_src_runtime_useRuntimeAction_ts["src/runtime/useRuntimeAction.ts"] package__ankhorage_studio -.-> module_src_runtime_useRuntimeAction_ts module_src_runtime_useRuntimeAction_ts --> module_src_runtime_runtimeActions_ts diff --git a/paradox/diagrams/export-graph.mmd b/paradox/diagrams/export-graph.mmd index f664ba08..2d8ecb41 100644 --- a/paradox/diagrams/export-graph.mmd +++ b/paradox/diagrams/export-graph.mmd @@ -64,6 +64,7 @@ graph LR module_src_host_layout_generatedImportComposer_ts["src/host/layout/generatedImportComposer.ts"] module_src_host_layout_layoutGenerator_test_ts["src/host/layout/layoutGenerator.test.ts"] module_src_host_layout_layoutGenerator_ts["src/host/layout/layoutGenerator.ts"] + module_src_host_layout_templates_auth_adapter_test_ts["src/host/layout/templates/auth/adapter.test.ts"] module_src_host_layout_templates_auth_adapter_ts["src/host/layout/templates/auth/adapter.ts"] module_src_host_layout_templates_auth_callback_ts["src/host/layout/templates/auth/callback.ts"] module_src_host_layout_templates_auth_oauth_ts["src/host/layout/templates/auth/oauth.ts"] @@ -75,7 +76,6 @@ graph LR module_src_host_layout_templates_nestedLayout_test_ts["src/host/layout/templates/nestedLayout.test.ts"] module_src_host_layout_templates_nestedLayout_ts["src/host/layout/templates/nestedLayout.ts"] module_src_host_layout_templates_redirect_ts["src/host/layout/templates/redirect.ts"] - module_src_host_layout_templates_rootLayout_selectionDom_test_ts["src/host/layout/templates/rootLayout.selectionDom.test.ts"] module_src_host_layout_templates_rootLayout_test_ts["src/host/layout/templates/rootLayout.test.ts"] module_src_host_layout_templates_rootLayout_ts["src/host/layout/templates/rootLayout.ts"] module_src_host_layout_templates_screen_ts["src/host/layout/templates/screen.ts"] @@ -165,12 +165,19 @@ graph LR module_src_runtime_actionSuppression_ts["src/runtime/actionSuppression.ts"] module_src_runtime_appExtensionRegistry_ts["src/runtime/appExtensionRegistry.ts"] module_src_runtime_index_ts["src/runtime/index.ts"] + module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"] + module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"] + module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"] module_src_runtime_localizationPreview_ts["src/runtime/localizationPreview.ts"] module_src_runtime_previewRegistry_ts["src/runtime/previewRegistry.ts"] module_src_runtime_previewRuntimeConfig_ts["src/runtime/previewRuntimeConfig.ts"] module_src_runtime_registry_tsx["src/runtime/registry.tsx"] module_src_runtime_runtimeActions_ts["src/runtime/runtimeActions.ts"] module_src_runtime_runtimeSurface_test_ts["src/runtime/runtimeSurface.test.ts"] + module_src_runtime_stationarySelection_test_ts["src/runtime/stationarySelection.test.ts"] + module_src_runtime_stationarySelection_ts["src/runtime/stationarySelection.ts"] + module_src_runtime_stationarySelectionCoordinator_test_ts["src/runtime/stationarySelectionCoordinator.test.ts"] + module_src_runtime_stationarySelectionCoordinator_ts["src/runtime/stationarySelectionCoordinator.ts"] module_src_runtime_useRuntimeAction_ts["src/runtime/useRuntimeAction.ts"] module_src_runtimeDiagnosticsModel_test_ts["src/runtimeDiagnosticsModel.test.ts"] module_src_runtimeDiagnosticsModel_ts["src/runtimeDiagnosticsModel.ts"] diff --git a/paradox/diagrams/module-relationships.mmd b/paradox/diagrams/module-relationships.mmd index 1a6a9179..9d3c5184 100644 --- a/paradox/diagrams/module-relationships.mmd +++ b/paradox/diagrams/module-relationships.mmd @@ -64,6 +64,7 @@ graph LR module_src_host_layout_generatedImportComposer_ts["src/host/layout/generatedImportComposer.ts"] module_src_host_layout_layoutGenerator_test_ts["src/host/layout/layoutGenerator.test.ts"] module_src_host_layout_layoutGenerator_ts["src/host/layout/layoutGenerator.ts"] + module_src_host_layout_templates_auth_adapter_test_ts["src/host/layout/templates/auth/adapter.test.ts"] module_src_host_layout_templates_auth_adapter_ts["src/host/layout/templates/auth/adapter.ts"] module_src_host_layout_templates_auth_callback_ts["src/host/layout/templates/auth/callback.ts"] module_src_host_layout_templates_auth_oauth_ts["src/host/layout/templates/auth/oauth.ts"] @@ -75,7 +76,6 @@ graph LR module_src_host_layout_templates_nestedLayout_test_ts["src/host/layout/templates/nestedLayout.test.ts"] module_src_host_layout_templates_nestedLayout_ts["src/host/layout/templates/nestedLayout.ts"] module_src_host_layout_templates_redirect_ts["src/host/layout/templates/redirect.ts"] - module_src_host_layout_templates_rootLayout_selectionDom_test_ts["src/host/layout/templates/rootLayout.selectionDom.test.ts"] module_src_host_layout_templates_rootLayout_test_ts["src/host/layout/templates/rootLayout.test.ts"] module_src_host_layout_templates_rootLayout_ts["src/host/layout/templates/rootLayout.ts"] module_src_host_layout_templates_screen_ts["src/host/layout/templates/screen.ts"] @@ -165,12 +165,19 @@ graph LR module_src_runtime_actionSuppression_ts["src/runtime/actionSuppression.ts"] module_src_runtime_appExtensionRegistry_ts["src/runtime/appExtensionRegistry.ts"] module_src_runtime_index_ts["src/runtime/index.ts"] + module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"] + module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"] + module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"] module_src_runtime_localizationPreview_ts["src/runtime/localizationPreview.ts"] module_src_runtime_previewRegistry_ts["src/runtime/previewRegistry.ts"] module_src_runtime_previewRuntimeConfig_ts["src/runtime/previewRuntimeConfig.ts"] module_src_runtime_registry_tsx["src/runtime/registry.tsx"] module_src_runtime_runtimeActions_ts["src/runtime/runtimeActions.ts"] module_src_runtime_runtimeSurface_test_ts["src/runtime/runtimeSurface.test.ts"] + module_src_runtime_stationarySelection_test_ts["src/runtime/stationarySelection.test.ts"] + module_src_runtime_stationarySelection_ts["src/runtime/stationarySelection.ts"] + module_src_runtime_stationarySelectionCoordinator_test_ts["src/runtime/stationarySelectionCoordinator.test.ts"] + module_src_runtime_stationarySelectionCoordinator_ts["src/runtime/stationarySelectionCoordinator.ts"] module_src_runtime_useRuntimeAction_ts["src/runtime/useRuntimeAction.ts"] module_src_runtimeDiagnosticsModel_test_ts["src/runtimeDiagnosticsModel.test.ts"] module_src_runtimeDiagnosticsModel_ts["src/runtimeDiagnosticsModel.ts"] @@ -348,6 +355,7 @@ graph LR module_src_host_layout_layoutGenerator_ts --> module_src_host_modules_layout_ts module_src_host_layout_layoutGenerator_ts --> module_src_index_ts module_src_host_layout_layoutGenerator_ts --> module_src_studioAdminRouteModel_ts + module_src_host_layout_templates_auth_adapter_test_ts --> module_src_host_layout_templates_auth_adapter_ts module_src_host_layout_templates_auth_callback_ts --> module_src_host_layout_templates_utils_routes_ts module_src_host_layout_templates_auth_callback_ts --> module_src_host_layout_utils_escapeStringLiteral_ts module_src_host_layout_templates_auth_oauth_ts --> module_src_host_layout_auth_resolveAuthLayoutPlan_ts @@ -362,7 +370,6 @@ graph LR module_src_host_layout_templates_nestedLayout_ts --> module_src_host_layout_templates_navigation_ts module_src_host_layout_templates_nestedLayout_ts --> module_src_host_layout_templates_utils_strings_ts module_src_host_layout_templates_redirect_ts --> module_src_host_layout_utils_escapeStringLiteral_ts - module_src_host_layout_templates_rootLayout_selectionDom_test_ts --> module_src_host_layout_templates_rootLayout_ts module_src_host_layout_templates_rootLayout_test_ts --> module_src_host_layout_templates_rootLayout_ts module_src_host_layout_templates_rootLayout_ts --> module_src_host_layout_generatedImportComposer_ts module_src_host_layout_templates_rootLayout_ts --> module_src_host_layout_templates_navigation_ts @@ -481,9 +488,14 @@ graph LR module_src_projectSecretUsage_test_ts --> module_src_projectSecretUsage_ts module_src_routeUtils_public_test_ts --> module_src_routeUtils_ts module_src_routeUtils_test_ts --> module_src_routeUtils_ts + module_src_runtime_interactionPolicy_test_ts --> module_src_runtime_interactionPolicyCore_ts + module_src_runtime_interactionPolicy_ts --> module_src_runtime_interactionPolicyCore_ts module_src_runtime_previewRegistry_ts --> module_src_runtime_appExtensionRegistry_ts module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_actionSuppression_ts + module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicy_ts module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_previewRegistry_ts + module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionCoordinator_ts + module_src_runtime_stationarySelectionCoordinator_test_ts --> module_src_runtime_stationarySelectionCoordinator_ts module_src_runtime_useRuntimeAction_ts --> module_src_runtime_runtimeActions_ts module_src_runtimeDiagnosticsModel_test_ts --> module_src_runtimeDiagnosticsModel_ts module_src_runtimeLocalization_public_test_ts --> module_src_runtimeLocalization_ts diff --git a/paradox/index.html b/paradox/index.html index 05f9cc78..e12cb0af 100644 --- a/paradox/index.html +++ b/paradox/index.html @@ -438,7 +438,7 @@

@ankhorage/studio

data-target="src/host/generatedAdminExpoWeb.smoke.test.ts" > src/host/generatedAdminExpoWeb.smoke.test.ts - 28 functions + 30 functions
  • @@ -1237,6 +1237,26 @@

    @ankhorage/studio

    2 functions
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • @@ -1254,7 +1254,7 @@

    @ankhorage/studio

    data-target="src/runtime/interactionPolicyCore.ts" > src/runtime/interactionPolicyCore.ts - 4 functions + 7 functions
  • @@ -1296,7 +1296,7 @@

    @ankhorage/studio

    data-target="src/runtime/stationarySelection.ts" > src/runtime/stationarySelection.ts - 4 functions + 13 functions
  • @@ -1575,7 +1575,7 @@

    Package overview

    96public exports
    0components
    -
    229modules
    +
    230modules
    1entrypoints

    Entrypoints

    @@ -2779,14 +2779,26 @@

    src/host/oauthFixtureConsumer.smoke.test.ts

    +

    src/host/orchestrator/generatedAppFiles.test.ts

    +

    Referenced module

    +

    + Dependencies: + src/host/orchestrator/generatedAppFiles.ts +

    +

    Exports: None

    +
    +

    src/host/orchestrator/generatedAppFiles.ts

    Referenced module

    Dependencies: src/host/zoraExtensions/index.ts

    - Exports: GeneratedAppFilesOptions, - syncGeneratedAppFiles + Exports: createGeneratedAppExtensionRegistrySource, + GeneratedAppFilesOptions, syncGeneratedAppFiles

    src/runtime/actionSuppression.ts

    src/runtime/appExtensionRegistry.ts

    Referenced module

    Dependencies: None

    -

    Exports: STUDIO_APP_EXTENSION_COMPONENT_REGISTRY

    +

    + Exports: STUDIO_APP_EXTENSION_COMPONENT_REGISTRY, + STUDIO_APP_EXTENSION_INTERACTION_POLICY_SUPPORT +

    src/runtime/index.ts

    Referenced module

    Dependencies: None

    Exports: CommitSelectionResult, - ComponentRegistry, createInteractionPolicyResolver, + ComponentRegistry, composeInteractionPolicyResolver, + createInteractionPolicyResolver, createStationarySelectionCoordinator, createStudioActionSuppressionConfig, createStudioInteractionPolicyResolver, @@ -3826,11 +3842,14 @@

    src/runtime/index.ts

    createStudioPreviewRuntimeConfig, createStudioStationarySelectionWrapNode, executeRuntimeAction, InteractionPolicy, + InteractionPolicyNodePropsResolver, + InteractionPolicyResolveArgs, InteractionPolicyResolverArgs, isComponentSupported, resolveStudioLocalizationPreview, shouldSuppressStudioRuntimeActions, StationarySelectionCoordinator, StationaryTapSelector, STUDIO_APP_EXTENSION_COMPONENT_REGISTRY, + STUDIO_APP_EXTENSION_INTERACTION_POLICY_SUPPORT, STUDIO_PREVIEW_COMPONENT_REGISTRY, StudioInteractionPolicyResolverArgs, StudioLocalizationPreview, StudioLocalizationPreviewInput, @@ -3868,15 +3887,18 @@

    src/runtime/interactionPolicy.ts

    src/runtime/interactionPolicyCore.ts

    Referenced module

    Dependencies: None

    - Exports: createInteractionPolicyResolver, - InteractionPolicy, InteractionPolicyResolverArgs, - isComponentSupported, ThirdPartyComponentSupport + Exports: composeInteractionPolicyResolver, + createInteractionPolicyResolver, InteractionPolicy, + InteractionPolicyNodePropsResolver, + InteractionPolicyResolveArgs, + InteractionPolicyResolverArgs, isComponentSupported, + ThirdPartyComponentSupport

    src/runtime/previewRegistry.ts

    src/runtime/previewRuntimeConfig.ts

    Referenced module

    Dependencies: src/runtime/actionSuppression.ts, src/runtime/interactionPolicy.ts, + src/runtime/interactionPolicyCore.ts, src/runtime/previewRegistry.ts

    @@ -8847,6 +8870,11 @@

    Architecture overview

    module_src_host_orchestrator_projectManager_ts module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_templateRegistry_index_ts + module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"] + package__ankhorage_studio -.-> + module_src_host_orchestrator_generatedAppFiles_test_ts + module_src_host_orchestrator_generatedAppFiles_test_ts --> + module_src_host_orchestrator_generatedAppFiles_ts module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"] package__ankhorage_studio -.-> module_src_host_orchestrator_generatedAppFiles_ts module_src_host_orchestrator_generatedAppFiles_ts --> @@ -9185,6 +9213,8 @@

    Architecture overview

    module_src_runtime_actionSuppression_ts module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicy_ts module_src_runtime_previewRuntimeConfig_ts --> + module_src_runtime_interactionPolicyCore_ts + module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_previewRegistry_ts module_src_runtime_registry_tsx["src/runtime/registry.tsx"] package__ankhorage_studio -.-> module_src_runtime_registry_tsx @@ -9779,6 +9809,9 @@

    Architecture overview

    module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_templateRegistry_index_ts + module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"] + package__ankhorage_studio -.-> module_src_host_orchestrator_generatedAppFiles_test_ts + module_src_host_orchestrator_generatedAppFiles_test_ts --> module_src_host_orchestrator_generatedAppFiles_ts module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"] package__ankhorage_studio -.-> module_src_host_orchestrator_generatedAppFiles_ts module_src_host_orchestrator_generatedAppFiles_ts --> module_src_host_zoraExtensions_index_ts @@ -10044,6 +10077,7 @@

    Architecture overview

    package__ankhorage_studio -.-> module_src_runtime_previewRuntimeConfig_ts module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_actionSuppression_ts module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicy_ts + module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicyCore_ts module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_previewRegistry_ts module_src_runtime_registry_tsx["src/runtime/registry.tsx"] package__ankhorage_studio -.-> module_src_runtime_registry_tsx @@ -10342,6 +10376,7 @@

    Module relationships

    module_src_host_modules_layout_ts["src/host/modules/layout.ts"] module_src_host_modules_runtime_LocalFsTargetAdapter_ts["src/host/modules/runtime/LocalFsTargetAdapter.ts"] module_src_host_oauthFixtureConsumer_smoke_test_ts["src/host/oauthFixtureConsumer.smoke.test.ts"] + module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"] module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"] module_src_host_orchestrator_generatedRouteCleanup_ts["src/host/orchestrator/generatedRouteCleanup.ts"] module_src_host_orchestrator_infraGenerator_ts["src/host/orchestrator/infraGenerator.ts"] @@ -10709,6 +10744,8 @@

    Module relationships

    module_src_host_orchestrator_projectManager_ts module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_templateRegistry_index_ts + module_src_host_orchestrator_generatedAppFiles_test_ts --> + module_src_host_orchestrator_generatedAppFiles_ts module_src_host_orchestrator_generatedAppFiles_ts --> module_src_host_zoraExtensions_index_ts module_src_host_orchestrator_infraGenerator_ts --> @@ -10874,6 +10911,8 @@

    Module relationships

    module_src_runtime_actionSuppression_ts module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicy_ts module_src_runtime_previewRuntimeConfig_ts --> + module_src_runtime_interactionPolicyCore_ts + module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_previewRegistry_ts module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionCoordinator_ts module_src_runtime_stationarySelectionCoordinator_test_ts --> @@ -11086,6 +11125,7 @@

    Module relationships

    module_src_host_modules_layout_ts["src/host/modules/layout.ts"] module_src_host_modules_runtime_LocalFsTargetAdapter_ts["src/host/modules/runtime/LocalFsTargetAdapter.ts"] module_src_host_oauthFixtureConsumer_smoke_test_ts["src/host/oauthFixtureConsumer.smoke.test.ts"] + module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"] module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"] module_src_host_orchestrator_generatedRouteCleanup_ts["src/host/orchestrator/generatedRouteCleanup.ts"] module_src_host_orchestrator_infraGenerator_ts["src/host/orchestrator/infraGenerator.ts"] @@ -11382,6 +11422,7 @@

    Module relationships

    module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_templateRegistry_index_ts + module_src_host_orchestrator_generatedAppFiles_test_ts --> module_src_host_orchestrator_generatedAppFiles_ts module_src_host_orchestrator_generatedAppFiles_ts --> module_src_host_zoraExtensions_index_ts module_src_host_orchestrator_infraGenerator_ts --> module_src_host_orchestrator_infraValidation_ts module_src_host_orchestrator_infraRuntime_test_ts --> module_src_host_orchestrator_infraRuntime_ts @@ -11488,6 +11529,7 @@

    Module relationships

    module_src_runtime_previewRegistry_ts --> module_src_runtime_appExtensionRegistry_ts module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_actionSuppression_ts module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicy_ts + module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicyCore_ts module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_previewRegistry_ts module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionCoordinator_ts module_src_runtime_stationarySelectionCoordinator_test_ts --> module_src_runtime_stationarySelectionCoordinator_ts @@ -11669,6 +11711,7 @@

    Export graph

    module_src_host_modules_layout_ts["src/host/modules/layout.ts"] module_src_host_modules_runtime_LocalFsTargetAdapter_ts["src/host/modules/runtime/LocalFsTargetAdapter.ts"] module_src_host_oauthFixtureConsumer_smoke_test_ts["src/host/oauthFixtureConsumer.smoke.test.ts"] + module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"] module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"] module_src_host_orchestrator_generatedRouteCleanup_ts["src/host/orchestrator/generatedRouteCleanup.ts"] module_src_host_orchestrator_infraGenerator_ts["src/host/orchestrator/infraGenerator.ts"] @@ -12151,6 +12194,7 @@

    Export graph

    module_src_host_modules_layout_ts["src/host/modules/layout.ts"] module_src_host_modules_runtime_LocalFsTargetAdapter_ts["src/host/modules/runtime/LocalFsTargetAdapter.ts"] module_src_host_oauthFixtureConsumer_smoke_test_ts["src/host/oauthFixtureConsumer.smoke.test.ts"] + module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"] module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"] module_src_host_orchestrator_generatedRouteCleanup_ts["src/host/orchestrator/generatedRouteCleanup.ts"] module_src_host_orchestrator_infraGenerator_ts["src/host/orchestrator/infraGenerator.ts"] @@ -14335,7 +14379,7 @@

    src/host/generatedAdminExpoWeb.smoke.test.ts

    data-search="createAdminSmokeManifest src/host/generatedAdminExpoWeb.smoke.test.ts " >

    createAdminSmokeManifest

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:46:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:37:1

    No description available.

    createAdminSmokeManifest data-search="createScrollableRuntimeScreenRoot src/host/generatedAdminExpoWeb.smoke.test.ts " >

    createScrollableRuntimeScreenRoot

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:102:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:93:1

    No description available.

    -

    createGeneratedAdminProject

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:195:1

    +

    verifyDesktopSelectionAndUnsupportedGeometry

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:265:1

    No description available.

    -

    writeSmokeMetroConfig

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:233:1

    +

    waitForUnsupportedGeometry

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:401:1

    No description available.

    -

    reservePort

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:256:1

    +

    waitForStudioSmokeState

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:420:1

    No description available.

    -

    spawnExpoWeb

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:272:1

    +

    expectRectToMatch

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:439:1

    No description available.

    -

    startSmokeStudioApi

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:294:1

    +

    rectsOverlap

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:446:1

    No description available.

    -

    handleSmokeStudioApiRequest

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:319:1

    +

    createGeneratedAdminProject

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:453:1

    No description available.

    -

    listenOnLocalhost

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:359:1

    +

    installGeneratedProjectDependencies

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:491:1

    No description available.

    -

    closeHttpServer

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:373:1

    +

    stageLocalStudioPackage

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:525:1

    No description available.

    -

    readJsonRequest

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:382:1

    +

    writeSmokeRuntimeExtensions

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:554:1

    No description available.

    -

    writeJsonResponse

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:398:1

    +

    writeSmokeMetroConfig

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:633:1

    No description available.

    -

    writeCorsHeaders

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:405:1

    +

    reservePort

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:656:1

    No description available.

    -

    waitForExpoWebUrl

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:411:1

    +

    spawnExpoWeb

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:672:1

    +

    No description available.

    +
    +
    +

    startSmokeStudioApi

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:694:1

    +

    No description available.

    +
    +
    +

    handleSmokeStudioApiRequest

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:719:1

    No description available.

    +

    listenOnLocalhost

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:759:1

    +

    No description available.

    +
    +
    -

    linkSmokeNodeModules

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:424:1

    +

    closeHttpServer

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:773:1

    No description available.

    -

    linkNodeModuleEntries

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:437:1

    +

    readJsonRequest

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:782:1

    No description available.

    -

    linkScopedNodeModuleEntries

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:474:1

    +

    writeJsonResponse

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:798:1

    No description available.

    -

    symlinkIfMissing

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:489:1

    +

    writeCorsHeaders

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:805:1

    No description available.

    -

    isNodeErrorWithCode

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:498:1

    +

    waitForExpoWebUrl

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:811:1

    No description available.

    isNodeErrorWithCode data-search="waitForHttp src/host/generatedAdminExpoWeb.smoke.test.ts " >

    waitForHttp

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:502:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:824:1

    No description available.

    waitForHttp data-search="waitForBodyText src/host/generatedAdminExpoWeb.smoke.test.ts " >

    waitForBodyText

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:519:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:841:1

    No description available.

    waitForBodyText data-search="resolveChromePath src/host/generatedAdminExpoWeb.smoke.test.ts " >

    resolveChromePath

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:536:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:858:1

    No description available.

    resolveChromePath data-search="readEnvString src/host/generatedAdminExpoWeb.smoke.test.ts " >

    readEnvString

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:542:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:864:1

    No description available.

    readEnvString data-search="spawnChrome src/host/generatedAdminExpoWeb.smoke.test.ts " >

    spawnChrome

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:548:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:870:1

    No description available.

    spawnChrome data-search="openChromePage src/host/generatedAdminExpoWeb.smoke.test.ts " >

    openChromePage

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:564:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:886:1

    No description available.

    openChromePage data-search="parseChromeProtocolMessage src/host/generatedAdminExpoWeb.smoke.test.ts " >

    parseChromeProtocolMessage

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:669:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1191:1

    No description available.

    parseChromeProtocolMessage data-search="isRecord src/host/generatedAdminExpoWeb.smoke.test.ts " >

    isRecord

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:684:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1206:1

    No description available.

    isRecord data-search="stopProcess src/host/generatedAdminExpoWeb.smoke.test.ts " >

    stopProcess

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:688:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1210:1

    No description available.

    stopProcess data-search="collectProcessOutput src/host/generatedAdminExpoWeb.smoke.test.ts " >

    collectProcessOutput

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:697:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1219:1

    No description available.

    collectProcessOutput data-search="formatProcessOutput src/host/generatedAdminExpoWeb.smoke.test.ts " >

    formatProcessOutput

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:708:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1230:1

    No description available.

    @@ -15280,7 +15348,7 @@

    getPackageOwnedRuntimeImports

    data-search="getGeneratedRuntimeRegistryDeclarations src/host/layout/layoutGenerator.ts " >

    getGeneratedRuntimeRegistryDeclarations

    -

    src/host/layout/layoutGenerator.ts:61:1

    +

    src/host/layout/layoutGenerator.ts:65:1

    No description available.

    getGeneratedRuntimeRegistryDeclarations data-search="mergeRuntimeModuleDeclarations src/host/layout/layoutGenerator.ts " >

    mergeRuntimeModuleDeclarations

    -

    src/host/layout/layoutGenerator.ts:68:1

    +

    src/host/layout/layoutGenerator.ts:76:1

    No description available.

    mergeRuntimeModuleDeclarations data-search="addStudioAdminRouteFiles src/host/layout/layoutGenerator.ts " >

    addStudioAdminRouteFiles

    -

    src/host/layout/layoutGenerator.ts:91:11

    +

    src/host/layout/layoutGenerator.ts:99:11

    No description available.

    walk

    -

    src/host/layout/layoutGenerator.ts:103:11

    +

    src/host/layout/layoutGenerator.ts:111:11

    No description available.

    walk data-search="getStudioAdminLayoutTsx src/host/layout/layoutGenerator.ts " >

    getStudioAdminLayoutTsx

    -

    src/host/layout/layoutGenerator.ts:428:1

    +

    src/host/layout/layoutGenerator.ts:436:1

    No description available.

    getStudioAdminLayoutTsx data-search="createStudioAdminRouteGeneratedFiles src/host/layout/layoutGenerator.ts " >

    createStudioAdminRouteGeneratedFiles

    -

    src/host/layout/layoutGenerator.ts:442:1

    +

    src/host/layout/layoutGenerator.ts:450:1

    No description available.

    createStudioAdminRouteGeneratedFiles data-search="resolveStudioAdminRouteFilePath src/host/layout/layoutGenerator.ts " >

    resolveStudioAdminRouteFilePath

    -

    src/host/layout/layoutGenerator.ts:449:1

    +

    src/host/layout/layoutGenerator.ts:457:1

    No description available.

    resolveStudioAdminRouteFilePath data-search="getStudioAdminRouteTsx src/host/layout/layoutGenerator.ts " >

    getStudioAdminRouteTsx

    -

    src/host/layout/layoutGenerator.ts:472:1

    +

    src/host/layout/layoutGenerator.ts:480:1

    No description available.

    normalizeRel

    -

    src/host/layout/layoutGenerator.ts:486:1

    +

    src/host/layout/layoutGenerator.ts:494:1

    No description available.

    normalizeRel data-search="prepareNavigatorForGeneratedRoutes src/host/layout/layoutGenerator.ts " >

    prepareNavigatorForGeneratedRoutes

    -

    src/host/layout/layoutGenerator.ts:490:1

    +

    src/host/layout/layoutGenerator.ts:498:1

    No description available.

    prepareNavigatorForGeneratedRoutes data-search="prepareRouteForGeneratedRoutes src/host/layout/layoutGenerator.ts " >

    prepareRouteForGeneratedRoutes

    -

    src/host/layout/layoutGenerator.ts:534:1

    +

    src/host/layout/layoutGenerator.ts:542:1

    No description available.

    prepareRouteForGeneratedRoutes data-search="normalizeGeneratedRouteName src/host/layout/layoutGenerator.ts " >

    normalizeGeneratedRouteName

    -

    src/host/layout/layoutGenerator.ts:542:1

    +

    src/host/layout/layoutGenerator.ts:550:1

    No description available.

    normalizeGeneratedRouteName data-search="resolveValidGeneratedInitialRouteName src/host/layout/layoutGenerator.ts " >

    resolveValidGeneratedInitialRouteName

    -

    src/host/layout/layoutGenerator.ts:547:1

    +

    src/host/layout/layoutGenerator.ts:555:1

    No description available.

    @@ -15987,7 +16055,7 @@

    createGeneratedAppExtensionRegistrySource

    data-search="formatRegistryKey src/host/orchestrator/generatedAppFiles.ts " >

    formatRegistryKey

    -

    src/host/orchestrator/generatedAppFiles.ts:102:1

    +

    src/host/orchestrator/generatedAppFiles.ts:120:1

    No description available.

    formatRegistryKey data-search="removePath src/host/orchestrator/generatedAppFiles.ts " >

    removePath

    -

    src/host/orchestrator/generatedAppFiles.ts:110:1

    +

    src/host/orchestrator/generatedAppFiles.ts:128:1

    No description available.

    removePath data-search="assertNoForbiddenSpecifiers src/host/orchestrator/generatedAppFiles.ts " >

    assertNoForbiddenSpecifiers

    -

    src/host/orchestrator/generatedAppFiles.ts:114:1

    +

    src/host/orchestrator/generatedAppFiles.ts:132:1

    No description available.

    assertNoForbiddenSpecifiers data-search="listSourceFiles src/host/orchestrator/generatedAppFiles.ts " >

    listSourceFiles

    -

    src/host/orchestrator/generatedAppFiles.ts:133:1

    +

    src/host/orchestrator/generatedAppFiles.ts:151:1

    No description available.

    listSourceFiles data-search="isMissingPathError src/host/orchestrator/generatedAppFiles.ts " >

    isMissingPathError

    -

    src/host/orchestrator/generatedAppFiles.ts:156:1

    +

    src/host/orchestrator/generatedAppFiles.ts:174:1

    No description available.

    @@ -17672,7 +17740,7 @@

    src/host/zoraExtensions/index.ts

    data-search="resolveZoraExtensionsForTemplateSelection src/host/zoraExtensions/index.ts " >

    resolveZoraExtensionsForTemplateSelection

    -

    src/host/zoraExtensions/index.ts:36:1

    +

    src/host/zoraExtensions/index.ts:37:1

    No description available.

    resolveZoraExtensionsForTemplateSelection data-search="resolveZoraExtensionsFromDependencies src/host/zoraExtensions/index.ts " >

    resolveZoraExtensionsFromDependencies

    -

    src/host/zoraExtensions/index.ts:50:1

    +

    src/host/zoraExtensions/index.ts:51:1

    No description available.

    resolveZoraExtensionsFromDependencies data-search="mergeZoraExtensions src/host/zoraExtensions/index.ts " >

    mergeZoraExtensions

    -

    src/host/zoraExtensions/index.ts:56:1

    +

    src/host/zoraExtensions/index.ts:57:1

    No description available.

    mergeZoraExtensions data-search="collectZoraExtensionDependencies src/host/zoraExtensions/index.ts " >

    collectZoraExtensionDependencies

    -

    src/host/zoraExtensions/index.ts:70:1

    +

    src/host/zoraExtensions/index.ts:71:1

    No description available.

    @@ -19061,7 +19129,7 @@

    src/runtime/interactionPolicy.ts

    data-search="createStudioInteractionPolicyResolver src/runtime/interactionPolicy.ts " >

    createStudioInteractionPolicyResolver

    -

    src/runtime/interactionPolicy.ts:15:1

    +

    src/runtime/interactionPolicy.ts:18:1

    No description available.

    @@ -19087,7 +19155,7 @@

    classifyComponent

    data-search="isComponentSupported src/runtime/interactionPolicyCore.ts " >

    isComponentSupported

    -

    src/runtime/interactionPolicyCore.ts:30:1

    +

    src/runtime/interactionPolicyCore.ts:33:1

    No description available.

    isComponentSupported data-search="resolveInteractionPolicy src/runtime/interactionPolicyCore.ts " >

    resolveInteractionPolicy

    -

    src/runtime/interactionPolicyCore.ts:38:1

    +

    src/runtime/interactionPolicyCore.ts:41:1

    No description available.

    resolveInteractionPolicy data-search="createInteractionPolicyResolver src/runtime/interactionPolicyCore.ts " >

    createInteractionPolicyResolver

    -

    src/runtime/interactionPolicyCore.ts:55:1

    +

    src/runtime/interactionPolicyCore.ts:66:1

    +

    No description available.

    +
    +
    +

    composeInteractionPolicyResolver

    +

    src/runtime/interactionPolicyCore.ts:92:1

    +

    No description available.

    +
    +
    +

    composeInteractionPolicyResolver

    +

    src/runtime/interactionPolicyCore.ts:95:1

    +

    No description available.

    +
    +
    +

    composeInteractionPolicyResolver

    +

    src/runtime/interactionPolicyCore.ts:99:1

    No description available.

    @@ -19157,7 +19249,7 @@

    src/runtime/previewRuntimeConfig.ts

    data-search="createStudioPreviewRuntimeConfig src/runtime/previewRuntimeConfig.ts " >

    createStudioPreviewRuntimeConfig

    -

    src/runtime/previewRuntimeConfig.ts:12:14

    +

    src/runtime/previewRuntimeConfig.ts:15:14

    No description available.

    @@ -19185,20 +19277,75 @@

    executeRuntimeAction

    >

    src/runtime/stationarySelection.ts

    +
    +

    isWebElementLike

    +

    src/runtime/stationarySelection.ts:48:1

    +

    No description available.

    +
    +
    +

    toMeasuredRect

    +

    src/runtime/stationarySelection.ts:58:1

    +

    No description available.

    +
    +
    +

    unionRects

    +

    src/runtime/stationarySelection.ts:67:1

    +

    No description available.

    +
    +
    +

    measureRenderedBoxes

    +

    src/runtime/stationarySelection.ts:85:1

    +

    No description available.

    +
    +
    +

    measureNativeView

    +

    src/runtime/stationarySelection.ts:94:1

    +

    No description available.

    +
    +
    +

    measureRootView

    +

    src/runtime/stationarySelection.ts:102:1

    +

    No description available.

    +
    +
    +

    measureUnsupportedView

    +

    src/runtime/stationarySelection.ts:114:1

    +

    No description available.

    +
    +
    +

    areIndicatorRectsEqual

    +

    src/runtime/stationarySelection.ts:128:1

    +

    No description available.

    +

    StudioNodeTouchRecorder

    -

    src/runtime/stationarySelection.ts:21:1

    +

    src/runtime/stationarySelection.ts:151:1

    No description available.

    -

    handleTouchStart

    -

    src/runtime/stationarySelection.ts:31:3

    +

    handleInteractionStart

    +

    src/runtime/stationarySelection.ts:162:3

    No description available.

    handleTouchStart data-search="createStudioStationarySelectionWrapNode src/runtime/stationarySelection.ts " >

    createStudioStationarySelectionWrapNode

    -

    src/runtime/stationarySelection.ts:70:1

    +

    src/runtime/stationarySelection.ts:208:1

    No description available.

    createStudioStationarySelectionWrapNode data-search="StationaryTapSelector src/runtime/stationarySelection.ts " >

    StationaryTapSelector

    -

    src/runtime/stationarySelection.ts:105:1

    +

    src/runtime/stationarySelection.ts:242:1

    +

    No description available.

    +
    +
    +

    measureNextFrame

    +

    src/runtime/stationarySelection.ts:353:11

    No description available.

    diff --git a/paradox/paradox.json b/paradox/paradox.json index 16c3fc8e..a474f506 100644 --- a/paradox/paradox.json +++ b/paradox/paradox.json @@ -1011,11 +1011,21 @@ ], "exports": [] }, + { + "path": "src/host/orchestrator/generatedAppFiles.test.ts", + "isEntrypoint": false, + "dependencies": ["src/host/orchestrator/generatedAppFiles.ts"], + "exports": [] + }, { "path": "src/host/orchestrator/generatedAppFiles.ts", "isEntrypoint": false, "dependencies": ["src/host/zoraExtensions/index.ts"], - "exports": ["GeneratedAppFilesOptions", "syncGeneratedAppFiles"] + "exports": [ + "createGeneratedAppExtensionRegistrySource", + "GeneratedAppFilesOptions", + "syncGeneratedAppFiles" + ] }, { "path": "src/host/orchestrator/generatedRouteCleanup.ts", @@ -1905,7 +1915,10 @@ "path": "src/runtime/appExtensionRegistry.ts", "isEntrypoint": false, "dependencies": [], - "exports": ["STUDIO_APP_EXTENSION_COMPONENT_REGISTRY"] + "exports": [ + "STUDIO_APP_EXTENSION_COMPONENT_REGISTRY", + "STUDIO_APP_EXTENSION_INTERACTION_POLICY_SUPPORT" + ] }, { "path": "src/runtime/index.ts", @@ -1914,6 +1927,7 @@ "exports": [ "CommitSelectionResult", "ComponentRegistry", + "composeInteractionPolicyResolver", "createInteractionPolicyResolver", "createStationarySelectionCoordinator", "createStudioActionSuppressionConfig", @@ -1923,6 +1937,8 @@ "createStudioStationarySelectionWrapNode", "executeRuntimeAction", "InteractionPolicy", + "InteractionPolicyNodePropsResolver", + "InteractionPolicyResolveArgs", "InteractionPolicyResolverArgs", "isComponentSupported", "resolveStudioLocalizationPreview", @@ -1930,6 +1946,7 @@ "StationarySelectionCoordinator", "StationaryTapSelector", "STUDIO_APP_EXTENSION_COMPONENT_REGISTRY", + "STUDIO_APP_EXTENSION_INTERACTION_POLICY_SUPPORT", "STUDIO_PREVIEW_COMPONENT_REGISTRY", "StudioInteractionPolicyResolverArgs", "StudioLocalizationPreview", @@ -1964,8 +1981,11 @@ "isEntrypoint": false, "dependencies": [], "exports": [ + "composeInteractionPolicyResolver", "createInteractionPolicyResolver", "InteractionPolicy", + "InteractionPolicyNodePropsResolver", + "InteractionPolicyResolveArgs", "InteractionPolicyResolverArgs", "isComponentSupported", "ThirdPartyComponentSupport" @@ -1997,6 +2017,7 @@ "dependencies": [ "src/runtime/actionSuppression.ts", "src/runtime/interactionPolicy.ts", + "src/runtime/interactionPolicyCore.ts", "src/runtime/previewRegistry.ts" ], "exports": ["createStudioPreviewRuntimeConfig", "StudioPreviewRuntimeConfigOptions"] @@ -7922,7 +7943,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 46, + "line": 37, "column": 1 } }, @@ -7931,160 +7952,187 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 102, + "line": 93, "column": 1 } }, { - "name": "createGeneratedAdminProject", + "name": "verifyDesktopSelectionAndUnsupportedGeometry", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 195, + "line": 265, "column": 1 } }, { - "name": "writeSmokeMetroConfig", + "name": "waitForUnsupportedGeometry", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 233, + "line": 401, "column": 1 } }, { - "name": "reservePort", + "name": "waitForStudioSmokeState", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 256, + "line": 420, "column": 1 } }, { - "name": "spawnExpoWeb", + "name": "expectRectToMatch", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 272, + "line": 439, "column": 1 } }, { - "name": "startSmokeStudioApi", + "name": "rectsOverlap", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 294, + "line": 446, "column": 1 } }, { - "name": "handleSmokeStudioApiRequest", + "name": "createGeneratedAdminProject", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 319, + "line": 453, "column": 1 } }, { - "name": "listenOnLocalhost", + "name": "installGeneratedProjectDependencies", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 359, + "line": 491, "column": 1 } }, { - "name": "closeHttpServer", + "name": "stageLocalStudioPackage", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 373, + "line": 525, "column": 1 } }, { - "name": "readJsonRequest", + "name": "writeSmokeRuntimeExtensions", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 382, + "line": 554, "column": 1 } }, { - "name": "writeJsonResponse", + "name": "writeSmokeMetroConfig", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 398, + "line": 633, "column": 1 } }, { - "name": "writeCorsHeaders", + "name": "reservePort", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 405, + "line": 656, "column": 1 } }, { - "name": "waitForExpoWebUrl", + "name": "spawnExpoWeb", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 411, + "line": 672, "column": 1 } }, { - "name": "linkSmokeNodeModules", + "name": "startSmokeStudioApi", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 424, + "line": 694, "column": 1 } }, { - "name": "linkNodeModuleEntries", + "name": "handleSmokeStudioApiRequest", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 437, + "line": 719, "column": 1 } }, { - "name": "linkScopedNodeModuleEntries", + "name": "listenOnLocalhost", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 474, + "line": 759, "column": 1 } }, { - "name": "symlinkIfMissing", + "name": "closeHttpServer", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 489, + "line": 773, + "column": 1 + } + }, + { + "name": "readJsonRequest", + "description": null, + "sourceLocation": { + "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", + "line": 782, "column": 1 } }, { - "name": "isNodeErrorWithCode", + "name": "writeJsonResponse", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 498, + "line": 798, + "column": 1 + } + }, + { + "name": "writeCorsHeaders", + "description": null, + "sourceLocation": { + "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", + "line": 805, + "column": 1 + } + }, + { + "name": "waitForExpoWebUrl", + "description": null, + "sourceLocation": { + "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", + "line": 811, "column": 1 } }, @@ -8093,7 +8141,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 502, + "line": 824, "column": 1 } }, @@ -8102,7 +8150,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 519, + "line": 841, "column": 1 } }, @@ -8111,7 +8159,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 536, + "line": 858, "column": 1 } }, @@ -8120,7 +8168,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 542, + "line": 864, "column": 1 } }, @@ -8129,7 +8177,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 548, + "line": 870, "column": 1 } }, @@ -8138,7 +8186,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 564, + "line": 886, "column": 1 } }, @@ -8147,7 +8195,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 669, + "line": 1191, "column": 1 } }, @@ -8156,7 +8204,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 684, + "line": 1206, "column": 1 } }, @@ -8165,7 +8213,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 688, + "line": 1210, "column": 1 } }, @@ -8174,7 +8222,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 697, + "line": 1219, "column": 1 } }, @@ -8183,7 +8231,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 708, + "line": 1230, "column": 1 } }, @@ -8849,7 +8897,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/layout/layoutGenerator.ts", - "line": 61, + "line": 65, "column": 1 } }, @@ -8858,7 +8906,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/layout/layoutGenerator.ts", - "line": 68, + "line": 76, "column": 1 } }, @@ -8867,7 +8915,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/layout/layoutGenerator.ts", - "line": 91, + "line": 99, "column": 11 } }, @@ -8876,7 +8924,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/layout/layoutGenerator.ts", - "line": 103, + "line": 111, "column": 11 } }, @@ -8885,7 +8933,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/layout/layoutGenerator.ts", - "line": 428, + "line": 436, "column": 1 } }, @@ -8894,7 +8942,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/layout/layoutGenerator.ts", - "line": 442, + "line": 450, "column": 1 } }, @@ -8903,7 +8951,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/layout/layoutGenerator.ts", - "line": 449, + "line": 457, "column": 1 } }, @@ -8912,7 +8960,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/layout/layoutGenerator.ts", - "line": 472, + "line": 480, "column": 1 } }, @@ -8921,7 +8969,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/layout/layoutGenerator.ts", - "line": 486, + "line": 494, "column": 1 } }, @@ -8930,7 +8978,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/layout/layoutGenerator.ts", - "line": 490, + "line": 498, "column": 1 } }, @@ -8939,7 +8987,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/layout/layoutGenerator.ts", - "line": 534, + "line": 542, "column": 1 } }, @@ -8948,7 +8996,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/layout/layoutGenerator.ts", - "line": 542, + "line": 550, "column": 1 } }, @@ -8957,7 +9005,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/layout/layoutGenerator.ts", - "line": 547, + "line": 555, "column": 1 } }, @@ -9425,7 +9473,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/generatedAppFiles.ts", - "line": 102, + "line": 120, "column": 1 } }, @@ -9434,7 +9482,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/generatedAppFiles.ts", - "line": 110, + "line": 128, "column": 1 } }, @@ -9443,7 +9491,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/generatedAppFiles.ts", - "line": 114, + "line": 132, "column": 1 } }, @@ -9452,7 +9500,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/generatedAppFiles.ts", - "line": 133, + "line": 151, "column": 1 } }, @@ -9461,7 +9509,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/generatedAppFiles.ts", - "line": 156, + "line": 174, "column": 1 } }, @@ -11009,7 +11057,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/zoraExtensions/index.ts", - "line": 36, + "line": 37, "column": 1 } }, @@ -11018,7 +11066,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/zoraExtensions/index.ts", - "line": 50, + "line": 51, "column": 1 } }, @@ -11027,7 +11075,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/zoraExtensions/index.ts", - "line": 56, + "line": 57, "column": 1 } }, @@ -11036,7 +11084,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/zoraExtensions/index.ts", - "line": 70, + "line": 71, "column": 1 } }, @@ -12809,7 +12857,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/interactionPolicy.ts", - "line": 15, + "line": 18, "column": 1 } }, @@ -12827,7 +12875,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/interactionPolicyCore.ts", - "line": 30, + "line": 33, "column": 1 } }, @@ -12836,7 +12884,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/interactionPolicyCore.ts", - "line": 38, + "line": 41, "column": 1 } }, @@ -12845,7 +12893,34 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/interactionPolicyCore.ts", - "line": 55, + "line": 66, + "column": 1 + } + }, + { + "name": "composeInteractionPolicyResolver", + "description": null, + "sourceLocation": { + "filePath": "src/runtime/interactionPolicyCore.ts", + "line": 92, + "column": 1 + } + }, + { + "name": "composeInteractionPolicyResolver", + "description": null, + "sourceLocation": { + "filePath": "src/runtime/interactionPolicyCore.ts", + "line": 95, + "column": 1 + } + }, + { + "name": "composeInteractionPolicyResolver", + "description": null, + "sourceLocation": { + "filePath": "src/runtime/interactionPolicyCore.ts", + "line": 99, "column": 1 } }, @@ -12872,7 +12947,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/previewRuntimeConfig.ts", - "line": 12, + "line": 15, "column": 14 } }, @@ -12885,21 +12960,93 @@ "column": 1 } }, + { + "name": "isWebElementLike", + "description": null, + "sourceLocation": { + "filePath": "src/runtime/stationarySelection.ts", + "line": 48, + "column": 1 + } + }, + { + "name": "toMeasuredRect", + "description": null, + "sourceLocation": { + "filePath": "src/runtime/stationarySelection.ts", + "line": 58, + "column": 1 + } + }, + { + "name": "unionRects", + "description": null, + "sourceLocation": { + "filePath": "src/runtime/stationarySelection.ts", + "line": 67, + "column": 1 + } + }, + { + "name": "measureRenderedBoxes", + "description": null, + "sourceLocation": { + "filePath": "src/runtime/stationarySelection.ts", + "line": 85, + "column": 1 + } + }, + { + "name": "measureNativeView", + "description": null, + "sourceLocation": { + "filePath": "src/runtime/stationarySelection.ts", + "line": 94, + "column": 1 + } + }, + { + "name": "measureRootView", + "description": null, + "sourceLocation": { + "filePath": "src/runtime/stationarySelection.ts", + "line": 102, + "column": 1 + } + }, + { + "name": "measureUnsupportedView", + "description": null, + "sourceLocation": { + "filePath": "src/runtime/stationarySelection.ts", + "line": 114, + "column": 1 + } + }, + { + "name": "areIndicatorRectsEqual", + "description": null, + "sourceLocation": { + "filePath": "src/runtime/stationarySelection.ts", + "line": 128, + "column": 1 + } + }, { "name": "StudioNodeTouchRecorder", "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 21, + "line": 151, "column": 1 } }, { - "name": "handleTouchStart", + "name": "handleInteractionStart", "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 31, + "line": 162, "column": 3 } }, @@ -12908,7 +13055,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 70, + "line": 208, "column": 1 } }, @@ -12917,10 +13064,19 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 105, + "line": 242, "column": 1 } }, + { + "name": "measureNextFrame", + "description": null, + "sourceLocation": { + "filePath": "src/runtime/stationarySelection.ts", + "line": 353, + "column": 11 + } + }, { "name": "createStationarySelectionCoordinator", "description": null, @@ -15541,6 +15697,11 @@ "toPath": "src/runtime/interactionPolicy.ts", "sourcePath": "src/runtime/previewRuntimeConfig.ts" }, + { + "fromPath": "src/runtime/previewRuntimeConfig.ts", + "toPath": "src/runtime/interactionPolicyCore.ts", + "sourcePath": "src/runtime/previewRuntimeConfig.ts" + }, { "fromPath": "src/runtime/previewRuntimeConfig.ts", "toPath": "src/runtime/previewRegistry.ts", @@ -15856,6 +16017,11 @@ "toPath": "src/host/modules/layout.ts", "sourcePath": "src/host/modules/catalog.ts" }, + { + "fromPath": "src/host/orchestrator/generatedAppFiles.test.ts", + "toPath": "src/host/orchestrator/generatedAppFiles.ts", + "sourcePath": "src/host/orchestrator/generatedAppFiles.test.ts" + }, { "fromPath": "src/host/orchestrator/generatedAppFiles.ts", "toPath": "src/host/zoraExtensions/index.ts", @@ -22866,6 +23032,12 @@ "callExpression": "readFile", "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" }, + { + "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts", + "toSymbol": "realpath", + "callExpression": "realpath", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, { "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts", "toSymbol": "expect", @@ -22908,6 +23080,12 @@ "callExpression": "openChromePage", "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" }, + { + "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts", + "toSymbol": "blockHotReloadConnections", + "callExpression": "page.blockHotReloadConnections", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, { "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts", "toSymbol": "navigate", @@ -22926,6 +23104,18 @@ "callExpression": "page.readBodyText", "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" }, + { + "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts", + "toSymbol": "formatProcessOutput", + "callExpression": "formatProcessOutput", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts", + "toSymbol": "verifyDesktopSelectionAndUnsupportedGeometry", + "callExpression": "verifyDesktopSelectionAndUnsupportedGeometry", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, { "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts", "toSymbol": "close", @@ -22951,21 +23141,111 @@ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" }, { - "fromSymbol": "createGeneratedAdminProject", - "toSymbol": "mkdir", - "callExpression": "mkdir", + "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry", + "toSymbol": "waitForUnsupportedGeometry", + "callExpression": "waitForUnsupportedGeometry", "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" }, { - "fromSymbol": "createGeneratedAdminProject", - "toSymbol": "writeFile", - "callExpression": "writeFile", + "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry", + "toSymbol": "expectRectToMatch", + "callExpression": "expectRectToMatch", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry", + "toSymbol": "expect", + "callExpression": "expect", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry", + "toSymbol": "rectsOverlap", + "callExpression": "rectsOverlap", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry", + "toSymbol": "evaluate", + "callExpression": "page.evaluate", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry", + "toSymbol": "readRuntimeNodeCenter", + "callExpression": "page.readRuntimeNodeCenter", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry", + "toSymbol": "mouseClick", + "callExpression": "page.mouseClick", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry", + "toSymbol": "waitForStudioSmokeState", + "callExpression": "waitForStudioSmokeState", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry", + "toSymbol": "readSelectionRootId", + "callExpression": "page.readSelectionRootId", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry", + "toSymbol": "readStudioSmokeState", + "callExpression": "page.readStudioSmokeState", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry", + "toSymbol": "touchTap", + "callExpression": "page.touchTap", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry", + "toSymbol": "mouseDrag", + "callExpression": "page.mouseDrag", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "waitForUnsupportedGeometry", + "toSymbol": "readUnsupportedGeometry", + "callExpression": "page.readUnsupportedGeometry", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "waitForUnsupportedGeometry", + "toSymbol": "readUnsupportedGeometryDiagnostics", + "callExpression": "page.readUnsupportedGeometryDiagnostics", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "waitForStudioSmokeState", + "toSymbol": "readStudioSmokeState", + "callExpression": "page.readStudioSmokeState", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "expectRectToMatch", + "toSymbol": "expect", + "callExpression": "expect", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "createGeneratedAdminProject", + "toSymbol": "mkdir", + "callExpression": "mkdir", "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" }, { "fromSymbol": "createGeneratedAdminProject", - "toSymbol": "linkSmokeNodeModules", - "callExpression": "linkSmokeNodeModules", + "toSymbol": "writeFile", + "callExpression": "writeFile", "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" }, { @@ -23004,12 +23284,72 @@ "callExpression": "moduleManager.syncProject", "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" }, + { + "fromSymbol": "createGeneratedAdminProject", + "toSymbol": "writeSmokeRuntimeExtensions", + "callExpression": "writeSmokeRuntimeExtensions", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, { "fromSymbol": "createGeneratedAdminProject", "toSymbol": "writeSmokeMetroConfig", "callExpression": "writeSmokeMetroConfig", "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" }, + { + "fromSymbol": "createGeneratedAdminProject", + "toSymbol": "installGeneratedProjectDependencies", + "callExpression": "installGeneratedProjectDependencies", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "installGeneratedProjectDependencies", + "toSymbol": "readFile", + "callExpression": "readFile", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "installGeneratedProjectDependencies", + "toSymbol": "stageLocalStudioPackage", + "callExpression": "stageLocalStudioPackage", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "installGeneratedProjectDependencies", + "toSymbol": "writeFile", + "callExpression": "writeFile", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "stageLocalStudioPackage", + "toSymbol": "readFile", + "callExpression": "readFile", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "stageLocalStudioPackage", + "toSymbol": "mkdir", + "callExpression": "mkdir", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "stageLocalStudioPackage", + "toSymbol": "cp", + "callExpression": "cp", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "stageLocalStudioPackage", + "toSymbol": "writeFile", + "callExpression": "writeFile", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "writeSmokeRuntimeExtensions", + "toSymbol": "writeFile", + "callExpression": "writeFile", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, { "fromSymbol": "writeSmokeMetroConfig", "toSymbol": "writeFile", @@ -23106,84 +23446,6 @@ "callExpression": "formatProcessOutput", "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" }, - { - "fromSymbol": "linkSmokeNodeModules", - "toSymbol": "mkdir", - "callExpression": "mkdir", - "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" - }, - { - "fromSymbol": "linkSmokeNodeModules", - "toSymbol": "linkNodeModuleEntries", - "callExpression": "linkNodeModuleEntries", - "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" - }, - { - "fromSymbol": "linkNodeModuleEntries", - "toSymbol": "readdir", - "callExpression": "readdir", - "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" - }, - { - "fromSymbol": "linkNodeModuleEntries", - "toSymbol": "mkdir", - "callExpression": "mkdir", - "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" - }, - { - "fromSymbol": "linkNodeModuleEntries", - "toSymbol": "symlinkIfMissing", - "callExpression": "symlinkIfMissing", - "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" - }, - { - "fromSymbol": "linkNodeModuleEntries", - "toSymbol": "linkScopedNodeModuleEntries", - "callExpression": "linkScopedNodeModuleEntries", - "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" - }, - { - "fromSymbol": "linkScopedNodeModuleEntries", - "toSymbol": "mkdir", - "callExpression": "mkdir", - "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" - }, - { - "fromSymbol": "linkScopedNodeModuleEntries", - "toSymbol": "readdir", - "callExpression": "readdir", - "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" - }, - { - "fromSymbol": "linkScopedNodeModuleEntries", - "toSymbol": "symlinkIfMissing", - "callExpression": "symlinkIfMissing", - "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" - }, - { - "fromSymbol": "symlinkIfMissing", - "toSymbol": "symlink", - "callExpression": "symlink", - "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" - }, - { - "fromSymbol": "symlinkIfMissing", - "toSymbol": "realpath", - "callExpression": "realpath", - "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" - }, - { - "fromSymbol": "symlinkIfMissing", - "toSymbol": "isNodeErrorWithCode", - "callExpression": "isNodeErrorWithCode", - "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" - }, - { - "fromSymbol": "isNodeErrorWithCode", - "toSymbol": "isRecord", - "callExpression": "isRecord", - "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" - }, { "fromSymbol": "waitForHttp", "toSymbol": "getDiagnostics", @@ -23262,6 +23524,72 @@ "callExpression": "isRecord", "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" }, + { + "fromSymbol": "evaluate", + "toSymbol": "send", + "callExpression": "this.send", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "evaluate", + "toSymbol": "isRecord", + "callExpression": "isRecord", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "blockHotReloadConnections", + "toSymbol": "send", + "callExpression": "this.send", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "readStudioSmokeState", + "toSymbol": "evaluate", + "callExpression": "this.evaluate", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "readSelectionRootId", + "toSymbol": "evaluate", + "callExpression": "this.evaluate", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "readRuntimeNodeCenter", + "toSymbol": "evaluate", + "callExpression": "this.evaluate", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "readUnsupportedGeometry", + "toSymbol": "evaluate", + "callExpression": "this.evaluate", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "readUnsupportedGeometryDiagnostics", + "toSymbol": "evaluate", + "callExpression": "this.evaluate", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "mouseClick", + "toSymbol": "send", + "callExpression": "this.send", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "mouseDrag", + "toSymbol": "send", + "callExpression": "this.send", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "touchTap", + "toSymbol": "send", + "callExpression": "this.send", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, { "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts", "toSymbol": "parseChromeProtocolMessage", @@ -24504,6 +24832,12 @@ "callExpression": "isComponentSupported", "sourcePath": "src/runtime/interactionPolicy.test.ts" }, + { + "fromSymbol": "src/runtime/interactionPolicy.test.ts", + "toSymbol": "composeInteractionPolicyResolver", + "callExpression": "composeInteractionPolicyResolver", + "sourcePath": "src/runtime/interactionPolicy.test.ts" + }, { "fromSymbol": "src/runtime/interactionPolicy.test.ts", "toSymbol": "join", @@ -24523,15 +24857,9 @@ "sourcePath": "src/runtime/interactionPolicy.ts" }, { - "fromSymbol": "src/runtime/interactionPolicy.ts", - "toSymbol": "resolver", - "callExpression": "resolver", - "sourcePath": "src/runtime/interactionPolicy.ts" - }, - { - "fromSymbol": "src/runtime/interactionPolicy.ts", - "toSymbol": "existingResolver", - "callExpression": "existingResolver", + "fromSymbol": "createStudioInteractionPolicyResolver", + "toSymbol": "composeInteractionPolicyResolver", + "callExpression": "composeInteractionPolicyResolver", "sourcePath": "src/runtime/interactionPolicy.ts" }, { @@ -24558,6 +24886,18 @@ "callExpression": "resolveInteractionPolicy", "sourcePath": "src/runtime/interactionPolicyCore.ts" }, + { + "fromSymbol": "src/runtime/interactionPolicyCore.ts", + "toSymbol": "interactionPolicyResolver", + "callExpression": "interactionPolicyResolver", + "sourcePath": "src/runtime/interactionPolicyCore.ts" + }, + { + "fromSymbol": "src/runtime/interactionPolicyCore.ts", + "toSymbol": "existingResolver", + "callExpression": "existingResolver", + "sourcePath": "src/runtime/interactionPolicyCore.ts" + }, { "fromSymbol": "createStudioPreviewComponentRegistry", "toSymbol": "createComponentRegistry", @@ -24655,29 +24995,161 @@ "sourcePath": "src/runtime/stationarySelection.test.ts" }, { - "fromSymbol": "handleTouchStart", + "fromSymbol": "measureRenderedBoxes", + "toSymbol": "toMeasuredRect", + "callExpression": "toMeasuredRect", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "measureRenderedBoxes", + "toSymbol": "getBoundingClientRect", + "callExpression": "element.getBoundingClientRect", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "src/runtime/stationarySelection.ts", + "toSymbol": "measureRenderedBoxes", + "callExpression": "measureRenderedBoxes", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "src/runtime/stationarySelection.ts", + "toSymbol": "resolve", + "callExpression": "resolve", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "measureRootView", + "toSymbol": "isWebElementLike", + "callExpression": "isWebElementLike", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "measureRootView", + "toSymbol": "toMeasuredRect", + "callExpression": "toMeasuredRect", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "measureRootView", + "toSymbol": "getBoundingClientRect", + "callExpression": "view.getBoundingClientRect", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "measureRootView", + "toSymbol": "measureNativeView", + "callExpression": "measureNativeView", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "measureUnsupportedView", + "toSymbol": "isWebElementLike", + "callExpression": "isWebElementLike", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "measureUnsupportedView", + "toSymbol": "unionRects", + "callExpression": "unionRects", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "measureUnsupportedView", + "toSymbol": "measureNativeView", + "callExpression": "measureNativeView", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "handleInteractionStart", "toSymbol": "recordNode", "callExpression": "ctx.recordNode", "sourcePath": "src/runtime/stationarySelection.ts" }, + { + "fromSymbol": "src/runtime/stationarySelection.ts", + "toSymbol": "registerUnsupportedNode", + "callExpression": "ctx.registerUnsupportedNode", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "src/runtime/stationarySelection.ts", + "toSymbol": "measureUnsupportedView", + "callExpression": "measureUnsupportedView", + "sourcePath": "src/runtime/stationarySelection.ts" + }, { "fromSymbol": "StationaryTapSelector", "toSymbol": "createStationarySelectionCoordinator", "callExpression": "createStationarySelectionCoordinator", "sourcePath": "src/runtime/stationarySelection.ts" }, + { + "fromSymbol": "src/runtime/stationarySelection.ts", + "toSymbol": "setUnsupportedNodeVersion", + "callExpression": "setUnsupportedNodeVersion", + "sourcePath": "src/runtime/stationarySelection.ts" + }, { "fromSymbol": "recordNode", "toSymbol": "recordNode", "callExpression": "coordinator.recordNode", "sourcePath": "src/runtime/stationarySelection.ts" }, + { + "fromSymbol": "src/runtime/stationarySelection.ts", + "toSymbol": "setIndicatorRects", + "callExpression": "setIndicatorRects", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "src/runtime/stationarySelection.ts", + "toSymbol": "measureRootView", + "callExpression": "measureRootView", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "src/runtime/stationarySelection.ts", + "toSymbol": "measure", + "callExpression": "measure", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "src/runtime/stationarySelection.ts", + "toSymbol": "unionRects", + "callExpression": "unionRects", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "src/runtime/stationarySelection.ts", + "toSymbol": "areIndicatorRectsEqual", + "callExpression": "areIndicatorRectsEqual", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "measureNextFrame", + "toSymbol": "refreshIndicatorRects", + "callExpression": "refreshIndicatorRects", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "src/runtime/stationarySelection.ts", + "toSymbol": "measureNextFrame", + "callExpression": "measureNextFrame", + "sourcePath": "src/runtime/stationarySelection.ts" + }, { "fromSymbol": "src/runtime/stationarySelection.ts", "toSymbol": "beginTransaction", "callExpression": "coordinator.beginTransaction", "sourcePath": "src/runtime/stationarySelection.ts" }, + { + "fromSymbol": "src/runtime/stationarySelection.ts", + "toSymbol": "recordNode", + "callExpression": "coordinator.recordNode", + "sourcePath": "src/runtime/stationarySelection.ts" + }, { "fromSymbol": "src/runtime/stationarySelection.ts", "toSymbol": "commitSelection", @@ -26184,6 +26656,30 @@ "callExpression": "parseExpoGoogleFontsModuleConfig", "sourcePath": "src/host/modules/catalog.ts" }, + { + "fromSymbol": "src/host/orchestrator/generatedAppFiles.test.ts", + "toSymbol": "describe", + "callExpression": "describe", + "sourcePath": "src/host/orchestrator/generatedAppFiles.test.ts" + }, + { + "fromSymbol": "src/host/orchestrator/generatedAppFiles.test.ts", + "toSymbol": "it", + "callExpression": "it", + "sourcePath": "src/host/orchestrator/generatedAppFiles.test.ts" + }, + { + "fromSymbol": "src/host/orchestrator/generatedAppFiles.test.ts", + "toSymbol": "createGeneratedAppExtensionRegistrySource", + "callExpression": "createGeneratedAppExtensionRegistrySource", + "sourcePath": "src/host/orchestrator/generatedAppFiles.test.ts" + }, + { + "fromSymbol": "src/host/orchestrator/generatedAppFiles.test.ts", + "toSymbol": "expect", + "callExpression": "expect", + "sourcePath": "src/host/orchestrator/generatedAppFiles.test.ts" + }, { "fromSymbol": "syncGeneratedAppFiles", "toSymbol": "removePath", diff --git a/src/host/generatedAdminExpoWeb.smoke.test.ts b/src/host/generatedAdminExpoWeb.smoke.test.ts index 78dcf0f1..99d668d6 100644 --- a/src/host/generatedAdminExpoWeb.smoke.test.ts +++ b/src/host/generatedAdminExpoWeb.smoke.test.ts @@ -1,14 +1,5 @@ import { type ChildProcessWithoutNullStreams, spawn } from 'node:child_process'; -import { - mkdir, - mkdtemp, - readdir, - readFile, - realpath, - rm, - symlink, - writeFile, -} from 'node:fs/promises'; +import { cp, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises'; import { createServer as createHttpServer, type IncomingMessage, @@ -115,13 +106,55 @@ function createScrollableRuntimeScreenRoot(): UiNode { title: 'Scrollable Runtime Screen', description: 'Rendered through the generated-app runtime registry.', }, - children: Array.from({ length: 16 }, (_, index) => ({ - id: `dashboard-runtime-row-${index}`, - type: 'Text', - props: { - children: `Generated runtime row ${index + 1}`, + children: [ + { + id: 'desktop-pointer-parent', + type: 'Box', + props: { + p: 'm', + testID: 'desktop-pointer-parent', + }, + children: [ + { + id: 'desktop-pointer-target', + type: 'Text', + props: { + children: 'Desktop pointer target', + testID: 'desktop-pointer-target', + }, + }, + ], + }, + { + id: 'unsupported-runtime-target', + type: 'SmokeUnsupported', + props: { + label: 'Unsupported runtime target', + testID: 'unsupported-runtime-target', + }, + }, + { + id: 'supported-runtime-neighbor', + type: 'Text', + props: { + children: 'Supported runtime neighbor', + testID: 'supported-runtime-neighbor', + }, }, - })), + { + id: 'studio-smoke-probe', + type: 'SmokeStudioProbe', + props: {}, + }, + ...Array.from({ length: 16 }, (_, index) => ({ + id: `dashboard-runtime-row-${index}`, + type: 'Text', + props: { + children: `Generated runtime row ${index + 1}`, + testID: `dashboard-runtime-row-${index}`, + }, + })), + ], }, ], }; @@ -148,6 +181,21 @@ adminWebSmokeTest( path.join(projectRoot, 'src', 'app', '_layout.tsx'), 'utf8', ); + const generatedPackage = JSON.parse( + await readFile(path.join(projectRoot, 'package.json'), 'utf8'), + ) as { dependencies?: Record }; + const resolvedZoraPackage = JSON.parse( + await readFile( + path.join( + await realpath(path.join(projectRoot, 'node_modules', '@ankhorage', 'zora')), + 'package.json', + ), + 'utf8', + ), + ) as { version?: string }; + + expect(generatedPackage.dependencies?.['@ankhorage/zora']).toBe('^2.9.0'); + expect(resolvedZoraPackage.version).toBe('2.9.0'); expect(rootLayout).toContain('function GeneratedZoraThemeConfigSync'); expect(rootLayout).toContain('lastSyncedThemeConfigSignatureRef'); expect(rootLayout).not.toContain('}, [setThemeConfig, themeConfig]);'); @@ -160,7 +208,8 @@ adminWebSmokeTest( chromeProcess = spawnChrome(chromePath, debugPort); const page = await openChromePage(debugPort); try { - for (const route of ['/', '/dashboard', '/ankh', '/ankh/theme', '/ankh/auth/providers']) { + await page.blockHotReloadConnections(); + for (const route of ['/dashboard', '/ankh', '/ankh/theme', '/ankh/auth/providers']) { await page.navigate(`${appUrl}${route}`); await Bun.sleep(ROUTE_SETTLE_MS); const bodyText = @@ -173,8 +222,13 @@ adminWebSmokeTest( : await page.readBodyText(); expect(bodyText).not.toContain('Maximum update depth exceeded'); if (route === '/dashboard') { - expect(bodyText).toContain('Scrollable Runtime Screen'); + if (!bodyText.includes('Scrollable Runtime Screen')) { + throw new Error( + `Generated dashboard did not load.\nBody:\n${bodyText}\nChrome errors:\n${page.errors.join('\n')}${formatProcessOutput(expoOutput)}`, + ); + } expect(bodyText).toContain('Generated runtime row 16'); + await verifyDesktopSelectionAndUnsupportedGeometry(page); } expect(page.errors.join('\n')).not.toContain('Maximum update depth exceeded'); expect(page.errors.join('\n')).not.toContain('Cannot read properties of undefined'); @@ -192,6 +246,210 @@ adminWebSmokeTest( TEST_TIMEOUT_MS, ); +interface BrowserRect { + readonly left: number; + readonly top: number; + readonly right: number; + readonly bottom: number; + readonly width: number; + readonly height: number; +} + +interface UnsupportedGeometrySnapshot { + readonly indicator: BrowserRect; + readonly target: BrowserRect; + readonly neighbor: BrowserRect; + readonly pointerEvents: string; +} + +async function verifyDesktopSelectionAndUnsupportedGeometry(page: ChromePage): Promise { + const initialGeometry = await waitForUnsupportedGeometry(page, 15_000); + + expectRectToMatch(initialGeometry.indicator, initialGeometry.target); + expect(initialGeometry.pointerEvents).toBe('none'); + expect(rectsOverlap(initialGeometry.indicator, initialGeometry.neighbor)).toBe(false); + expect( + await page.evaluate( + `document.getElementById('studio-unsupported-indicator-supported-runtime-neighbor') !== null`, + ), + ).toBe(false); + + const scrolled = await page.evaluate(`(() => { + const wrapper = document.getElementById('studio-runtime-node-unsupported-runtime-target'); + const target = wrapper?.firstElementChild; + if (!(target instanceof HTMLElement)) return false; + let scrollParent = target.parentElement; + while (scrollParent) { + const style = getComputedStyle(scrollParent); + if ( + /(auto|scroll)/.test(style.overflowY) && + scrollParent.scrollHeight > scrollParent.clientHeight + ) { + scrollParent.scrollTop += 80; + scrollParent.dispatchEvent(new Event('scroll', { bubbles: true })); + return true; + } + scrollParent = scrollParent.parentElement; + } + return false; + })()`); + expect(scrolled).toBe(true); + await Bun.sleep(250); + + const scrolledGeometry = await waitForUnsupportedGeometry(page, 15_000); + expectRectToMatch(scrolledGeometry.indicator, scrolledGeometry.target); + expect(Math.abs(scrolledGeometry.target.top - initialGeometry.target.top)).toBeGreaterThan(20); + expect(rectsOverlap(scrolledGeometry.indicator, scrolledGeometry.neighbor)).toBe(false); + + await page.evaluate(`(() => { + const wrapper = document.getElementById('studio-runtime-node-unsupported-runtime-target'); + const target = wrapper?.firstElementChild; + if (!(target instanceof HTMLElement)) return; + let scrollParent = target.parentElement; + while (scrollParent) { + const style = getComputedStyle(scrollParent); + if ( + /(auto|scroll)/.test(style.overflowY) && + scrollParent.scrollHeight > scrollParent.clientHeight + ) { + scrollParent.scrollTop = 0; + scrollParent.dispatchEvent(new Event('scroll', { bubbles: true })); + return; + } + scrollParent = scrollParent.parentElement; + } + })()`); + await Bun.sleep(250); + + const desktopTarget = await page.readRuntimeNodeCenter('desktop-pointer-target'); + await page.mouseClick(desktopTarget.x, desktopTarget.y); + await waitForStudioSmokeState( + page, + 'mode=edit;selection=desktop-pointer-target;changes=1', + 15_000, + ); + expect(await page.readSelectionRootId()).toBe( + 'studio-stationary-selection-root:edit:desktop-pointer-target:1', + ); + + await page.mouseClick(desktopTarget.x, desktopTarget.y); + await Bun.sleep(250); + expect(await page.readStudioSmokeState()).toBe( + 'mode=edit;selection=desktop-pointer-target;changes=1', + ); + expect(await page.readSelectionRootId()).toBe( + 'studio-stationary-selection-root:edit:desktop-pointer-target:1', + ); + + const movedTarget = await page.readRuntimeNodeCenter('dashboard-runtime-row-0'); + await page.evaluate(`(() => { + globalThis.__studioSmokeInputTrace = []; + for (const type of ['touchstart', 'touchend', 'pointerdown', 'pointerup', 'click']) { + document.addEventListener(type, (event) => { + globalThis.__studioSmokeInputTrace.push({ + type, + pointerType: event.pointerType ?? null, + targetId: event.target?.id ?? null, + targetTestId: event.target?.getAttribute?.('data-testid') ?? null, + }); + }, { capture: true, once: true }); + } + return true; + })()`); + await page.touchTap(movedTarget.x, movedTarget.y); + await Bun.sleep(500); + const touchState = await page.readStudioSmokeState(); + if (touchState !== 'mode=edit;selection=dashboard-runtime-row-0;changes=2') { + throw new Error( + `Touch selection did not commit; state=${touchState}; trace=${await page.evaluate( + `JSON.stringify(globalThis.__studioSmokeInputTrace ?? [])`, + )}`, + ); + } + expect(await page.readSelectionRootId()).toBe( + 'studio-stationary-selection-root:edit:dashboard-runtime-row-0:2', + ); + + await page.mouseDrag(movedTarget.x, movedTarget.y, movedTarget.x + 24, movedTarget.y + 24); + await Bun.sleep(250); + expect(await page.readStudioSmokeState()).toBe( + 'mode=edit;selection=dashboard-runtime-row-0;changes=2', + ); + expect(await page.readSelectionRootId()).toBe( + 'studio-stationary-selection-root:edit:dashboard-runtime-row-0:2', + ); + + await page.evaluate(`globalThis.__studioSmokeTogglePreview?.()`); + await waitForStudioSmokeState( + page, + 'mode=preview;selection=dashboard-runtime-row-0;changes=2', + 15_000, + ); + expect( + await page.evaluate( + `document.getElementById('studio-unsupported-indicator-unsupported-runtime-target') === null`, + ), + ).toBe(true); + + await page.mouseClick(desktopTarget.x, desktopTarget.y); + await Bun.sleep(250); + expect(await page.readStudioSmokeState()).toBe( + 'mode=preview;selection=dashboard-runtime-row-0;changes=2', + ); +} + +async function waitForUnsupportedGeometry( + page: ChromePage, + timeoutMs: number, +): Promise { + const start = Date.now(); + + while (Date.now() - start < timeoutMs) { + const snapshot = await page.readUnsupportedGeometry(); + if (snapshot) { + return snapshot; + } + await Bun.sleep(100); + } + + throw new Error( + `Timed out waiting for unsupported Runtime indication geometry.\n${await page.readUnsupportedGeometryDiagnostics()}\nChrome errors:\n${page.errors.join('\n')}`, + ); +} + +async function waitForStudioSmokeState( + page: ChromePage, + expected: string, + timeoutMs: number, +): Promise { + const start = Date.now(); + + while (Date.now() - start < timeoutMs) { + if ((await page.readStudioSmokeState()) === expected) { + return; + } + await Bun.sleep(100); + } + + throw new Error( + `Timed out waiting for Studio smoke state ${expected}; received ${await page.readStudioSmokeState()}.`, + ); +} + +function expectRectToMatch(actual: BrowserRect, expected: BrowserRect): void { + expect(Math.abs(actual.left - expected.left)).toBeLessThanOrEqual(2); + expect(Math.abs(actual.top - expected.top)).toBeLessThanOrEqual(2); + expect(Math.abs(actual.width - expected.width)).toBeLessThanOrEqual(2); + expect(Math.abs(actual.height - expected.height)).toBeLessThanOrEqual(2); +} + +function rectsOverlap(left: BrowserRect, right: BrowserRect): boolean { + return ( + Math.min(left.right, right.right) > Math.max(left.left, right.left) && + Math.min(left.bottom, right.bottom) > Math.max(left.top, right.top) + ); +} + async function createGeneratedAdminProject(workspaceRoot: string): Promise { await mkdir(path.join(workspaceRoot, 'apps', 'studio'), { recursive: true }); await writeFile( @@ -202,7 +460,6 @@ async function createGeneratedAdminProject(workspaceRoot: string): Promise { + const packagePath = path.join(projectRoot, 'package.json'); + const generatedPackage = JSON.parse(await readFile(packagePath, 'utf8')) as { + dependencies?: Record; + }; + if (!generatedPackage.dependencies) { + throw new Error('Generated smoke project has no dependency map.'); + } + generatedPackage.dependencies['@ankhorage/studio'] = + `file:${await stageLocalStudioPackage(workspaceRoot)}`; + await writeFile(packagePath, `${JSON.stringify(generatedPackage, null, 2)}\n`, 'utf8'); + + const install = Bun.spawn(['bun', 'install', '--ignore-scripts'], { + cwd: workspaceRoot, + env: { + ...process.env, + CI: '1', + }, + stdout: 'pipe', + stderr: 'pipe', + }); + const [exitCode, stdout, stderr] = await Promise.all([ + install.exited, + new Response(install.stdout).text(), + new Response(install.stderr).text(), + ]); + if (exitCode !== 0) { + throw new Error(`Generated smoke dependency install failed.\n${stdout}\n${stderr}`); + } +} + +async function stageLocalStudioPackage(workspaceRoot: string): Promise { + const sourcePackage = JSON.parse( + await readFile(path.join(process.cwd(), 'package.json'), 'utf8'), + ) as Record; + const packageRoot = path.join(workspaceRoot, 'packages', 'studio'); + await mkdir(packageRoot, { recursive: true }); + await cp(path.join(process.cwd(), 'dist'), path.join(packageRoot, 'dist'), { + recursive: true, + }); + await writeFile( + path.join(packageRoot, 'package.json'), + `${JSON.stringify( + { + name: sourcePackage.name, + version: sourcePackage.version, + type: sourcePackage.type, + main: sourcePackage.main, + exports: sourcePackage.exports, + dependencies: sourcePackage.dependencies, + peerDependencies: sourcePackage.peerDependencies, + }, + null, + 2, + )}\n`, + 'utf8', + ); + return packageRoot; +} + +async function writeSmokeRuntimeExtensions(projectRoot: string): Promise { + const generatedRoot = path.join(projectRoot, 'src', 'generated'); + await writeFile( + path.join(generatedRoot, 'SmokeStudioComponents.tsx'), + `import { useStudio } from '@ankhorage/studio'; +import { Box, Text } from '@ankhorage/zora'; +import React, { useEffect, useRef, useState } from 'react'; +import { View } from 'react-native'; + +export function SmokeUnsupported({ + label, + testID, +}: { + label?: string; + testID?: string; +}) { + return ( + + {label ?? 'Unsupported runtime target'} + + ); +} + +export function SmokeStudioProbe() { + const studio = useStudio(); + const previousSelectedNodeIdRef = useRef(studio.selectedNodeId); + const [selectionChangeCount, setSelectionChangeCount] = useState(0); + + useEffect(() => { + const smokeGlobal = globalThis as typeof globalThis & { + __studioSmokeTogglePreview?: () => void; + }; + smokeGlobal.__studioSmokeTogglePreview = studio.togglePreviewMode; + return () => { + delete smokeGlobal.__studioSmokeTogglePreview; + }; + }, [studio.togglePreviewMode]); + + useEffect(() => { + if (previousSelectedNodeIdRef.current === studio.selectedNodeId) return; + previousSelectedNodeIdRef.current = studio.selectedNodeId; + setSelectionChangeCount((count) => count + 1); + }, [studio.selectedNodeId]); + + return ( + + + {\`mode=\${studio.previewMode ? 'preview' : 'edit'};selection=\${studio.selectedNodeId ?? 'none'};changes=\${selectionChangeCount}\`} + + + ); +} +`, + 'utf8', + ); + await writeFile( + path.join(generatedRoot, 'appExtensionRegistry.ts'), + `import type { ComponentRegistry } from '@ankhorage/runtime'; +import { SmokeStudioProbe, SmokeUnsupported } from './SmokeStudioComponents'; + +export const APP_EXTENSION_COMPONENT_REGISTRY: ComponentRegistry = { + SmokeStudioProbe, + SmokeUnsupported, +}; + +export const APP_EXTENSION_INTERACTION_POLICY_SUPPORT = {} as const; +`, + 'utf8', + ); +} + async function writeSmokeMetroConfig(projectRoot: string): Promise { const repositoryRoot = process.cwd(); await writeFile( @@ -421,84 +821,6 @@ async function waitForExpoWebUrl(output: readonly string[], timeoutMs: number): throw new Error(`Timed out waiting for Expo web URL.${formatProcessOutput(output)}`); } -async function linkSmokeNodeModules(workspaceRoot: string): Promise { - const nodeModulesRoot = path.join(workspaceRoot, 'node_modules'); - await mkdir(nodeModulesRoot, { recursive: true }); - await linkNodeModuleEntries({ - sourceRoot: path.join(process.cwd(), 'node_modules', '.bun', 'node_modules'), - targetRoot: nodeModulesRoot, - }); - await linkNodeModuleEntries({ - sourceRoot: path.join(process.cwd(), 'apps', 'studio', 'node_modules'), - targetRoot: nodeModulesRoot, - }); -} - -async function linkNodeModuleEntries(args: { - readonly sourceRoot: string; - readonly targetRoot: string; - readonly packageNames?: readonly string[]; -}): Promise { - const packageNames = args.packageNames ?? (await readdir(args.sourceRoot)); - for (const packageName of packageNames) { - if (packageName.startsWith('.')) continue; - if (packageName.startsWith('@') && packageName.includes('/')) { - const [scopeName, scopedPackageName] = packageName.split('/'); - if (!scopeName || !scopedPackageName) { - throw new Error(`Invalid scoped package name: ${packageName}`); - } - const targetScopeRoot = path.join(args.targetRoot, scopeName); - await mkdir(targetScopeRoot, { recursive: true }); - await symlinkIfMissing( - path.join(args.sourceRoot, scopeName, scopedPackageName), - path.join(targetScopeRoot, scopedPackageName), - ); - continue; - } - if (packageName.startsWith('@')) { - await linkScopedNodeModuleEntries({ - sourceRoot: path.join(args.sourceRoot, packageName), - targetRoot: path.join(args.targetRoot, packageName), - scopeName: packageName, - }); - continue; - } - - await symlinkIfMissing( - path.join(args.sourceRoot, packageName), - path.join(args.targetRoot, packageName), - ); - } -} - -async function linkScopedNodeModuleEntries(args: { - readonly sourceRoot: string; - readonly targetRoot: string; - readonly scopeName: string; -}): Promise { - await mkdir(args.targetRoot, { recursive: true }); - const packageNames = await readdir(args.sourceRoot); - for (const packageName of packageNames) { - await symlinkIfMissing( - path.join(args.sourceRoot, packageName), - path.join(args.targetRoot, packageName), - ); - } -} - -async function symlinkIfMissing(source: string, target: string): Promise { - try { - await symlink(await realpath(source), target, 'dir'); - } catch (error) { - if (isNodeErrorWithCode(error, 'EEXIST')) return; - throw error; - } -} - -function isNodeErrorWithCode(error: unknown, code: string): boolean { - return isRecord(error) && error.code === code; -} - async function waitForHttp( url: string, timeoutMs: number, @@ -613,6 +935,206 @@ class ChromePage { return typeof value === 'string' ? value : ''; } + async evaluate(expression: string): Promise { + const result = await this.send('Runtime.evaluate', { + expression, + returnByValue: true, + awaitPromise: true, + }); + if (!isRecord(result) || !isRecord(result.result)) { + throw new Error(`Chrome evaluation returned no value for: ${expression}`); + } + return ('value' in result.result ? result.result.value : undefined) as T; + } + + async blockHotReloadConnections(): Promise { + await this.send('Network.enable'); + await this.send('Network.setBlockedURLs', { + urls: ['ws://127.0.0.1:*/hot*', 'ws://localhost:*/hot*'], + }); + await this.send('Emulation.setDeviceMetricsOverride', { + width: 1280, + height: 900, + deviceScaleFactor: 1, + mobile: true, + }); + await this.send('Emulation.setTouchEmulationEnabled', { + enabled: true, + maxTouchPoints: 1, + }); + } + + async readStudioSmokeState(): Promise { + return this.evaluate( + `document.querySelector('[data-testid="studio-smoke-state"]')?.textContent ?? ''`, + ); + } + + async readSelectionRootId(): Promise { + return this.evaluate( + `document.querySelector('[data-testid="studio-stationary-selection-root"]')?.id ?? ''`, + ); + } + + async readRuntimeNodeCenter(nodeId: string): Promise<{ readonly x: number; readonly y: number }> { + const encodedNodeId = encodeURIComponent(nodeId); + const center = await this.evaluate<{ readonly x: number; readonly y: number } | null>(`(() => { + const wrapper = document.getElementById(${JSON.stringify( + `studio-runtime-node-${encodedNodeId}`, + )}); + const findRenderedElement = (element) => { + const queue = element ? [...element.children] : []; + while (queue.length > 0) { + const candidate = queue.shift(); + if (!(candidate instanceof HTMLElement)) continue; + const rect = candidate.getBoundingClientRect(); + if (rect.width > 0 && rect.height > 0) return candidate; + queue.unshift(...candidate.children); + } + return null; + }; + const target = findRenderedElement(wrapper); + if (!(target instanceof HTMLElement)) return null; + const rect = target.getBoundingClientRect(); + return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; + })()`); + if (!center) { + throw new Error(`Could not resolve browser center for Runtime node ${nodeId}.`); + } + return center; + } + + async readUnsupportedGeometry(): Promise { + return this.evaluate(`(() => { + const wrapper = document.getElementById( + 'studio-runtime-node-unsupported-runtime-target', + ); + const findRenderedElement = (element) => { + const queue = element ? [...element.children] : []; + while (queue.length > 0) { + const candidate = queue.shift(); + if (!(candidate instanceof HTMLElement)) continue; + const rect = candidate.getBoundingClientRect(); + if (rect.width > 0 && rect.height > 0) return candidate; + queue.unshift(...candidate.children); + } + return null; + }; + const target = findRenderedElement(wrapper); + const indicator = document.getElementById( + 'studio-unsupported-indicator-unsupported-runtime-target', + ); + const neighborWrapper = document.getElementById( + 'studio-runtime-node-supported-runtime-neighbor', + ); + const neighbor = findRenderedElement(neighborWrapper); + if ( + !(target instanceof HTMLElement) || + !(indicator instanceof HTMLElement) || + !(neighbor instanceof HTMLElement) + ) { + return null; + } + const toRect = (element) => { + const rect = element.getBoundingClientRect(); + return { + left: rect.left, + top: rect.top, + right: rect.right, + bottom: rect.bottom, + width: rect.width, + height: rect.height, + }; + }; + return { + target: toRect(target), + indicator: toRect(indicator), + neighbor: toRect(neighbor), + pointerEvents: getComputedStyle(indicator).pointerEvents, + }; + })()`); + } + + async readUnsupportedGeometryDiagnostics(): Promise { + return this.evaluate(`JSON.stringify({ + state: document.querySelector('[data-testid="studio-smoke-state"]')?.textContent ?? null, + root: document.querySelector('[data-testid="studio-stationary-selection-root"]')?.outerHTML.slice(0, 300) ?? null, + roots: [...document.querySelectorAll('[data-testid="studio-stationary-selection-root"]')].map((element) => element.id), + wrapper: document.getElementById('studio-runtime-node-unsupported-runtime-target')?.outerHTML.slice(0, 300) ?? null, + wrapperRoot: document.getElementById('studio-runtime-node-unsupported-runtime-target')?.closest('[data-testid="studio-stationary-selection-root"]')?.id ?? null, + unsupportedRecorders: [...document.querySelectorAll('[data-testid^="studio-unsupported-recorder-"]')].map((element) => element.getAttribute('data-testid')), + indicator: document.getElementById('studio-unsupported-indicator-unsupported-runtime-target')?.outerHTML ?? null, + neighbor: document.getElementById('studio-runtime-node-supported-runtime-neighbor')?.outerHTML.slice(0, 300) ?? null, + })`); + } + + async mouseClick(x: number, y: number): Promise { + await this.send('Input.dispatchMouseEvent', { + type: 'mouseMoved', + x, + y, + }); + await this.send('Input.dispatchMouseEvent', { + type: 'mousePressed', + x, + y, + button: 'left', + buttons: 1, + clickCount: 1, + }); + await this.send('Input.dispatchMouseEvent', { + type: 'mouseReleased', + x, + y, + button: 'left', + buttons: 0, + clickCount: 1, + }); + } + + async mouseDrag(startX: number, startY: number, endX: number, endY: number): Promise { + await this.send('Input.dispatchMouseEvent', { + type: 'mouseMoved', + x: startX, + y: startY, + }); + await this.send('Input.dispatchMouseEvent', { + type: 'mousePressed', + x: startX, + y: startY, + button: 'left', + buttons: 1, + clickCount: 1, + }); + await this.send('Input.dispatchMouseEvent', { + type: 'mouseMoved', + x: endX, + y: endY, + button: 'left', + buttons: 1, + }); + await this.send('Input.dispatchMouseEvent', { + type: 'mouseReleased', + x: endX, + y: endY, + button: 'left', + buttons: 0, + clickCount: 1, + }); + } + + async touchTap(x: number, y: number): Promise { + await this.send('Input.dispatchTouchEvent', { + type: 'touchStart', + touchPoints: [{ x, y, radiusX: 1, radiusY: 1, force: 1, id: 1 }], + }); + await Bun.sleep(100); + await this.send('Input.dispatchTouchEvent', { + type: 'touchEnd', + touchPoints: [], + }); + } + send(method: string, params?: Readonly>): Promise { const id = this.nextId; this.nextId += 1; diff --git a/src/host/layout/layoutGenerator.test.ts b/src/host/layout/layoutGenerator.test.ts index 2cc607f5..98f9a1c2 100644 --- a/src/host/layout/layoutGenerator.test.ts +++ b/src/host/layout/layoutGenerator.test.ts @@ -205,13 +205,21 @@ describe('GeneratedAppFileGenerator', () => { ); expect(rootLayout).toContain('createComponentRegistry'); expect(rootLayout).toContain('STUDIO_APP_EXTENSION_COMPONENT_REGISTRY'); + expect(rootLayout).toContain('STUDIO_APP_EXTENSION_INTERACTION_POLICY_SUPPORT'); expect(rootLayout).toContain( - "import { APP_EXTENSION_COMPONENT_REGISTRY as GENERATED_APP_EXTENSION_COMPONENT_REGISTRY } from '@/generated/appExtensionRegistry';", + 'APP_EXTENSION_COMPONENT_REGISTRY as GENERATED_APP_EXTENSION_COMPONENT_REGISTRY', + ); + expect(rootLayout).toContain( + 'APP_EXTENSION_INTERACTION_POLICY_SUPPORT as GENERATED_APP_EXTENSION_INTERACTION_POLICY_SUPPORT', ); expect(rootLayout).toContain(`const APP_EXTENSION_COMPONENT_REGISTRY = createComponentRegistry( STUDIO_APP_EXTENSION_COMPONENT_REGISTRY, GENERATED_APP_EXTENSION_COMPONENT_REGISTRY, );`); + expect(rootLayout).toContain(`const APP_EXTENSION_INTERACTION_POLICY_SUPPORT = { + ...STUDIO_APP_EXTENSION_INTERACTION_POLICY_SUPPORT, + ...GENERATED_APP_EXTENSION_INTERACTION_POLICY_SUPPORT, +} as const;`); expect(rootLayout).toContain(`const runtimeComponentRegistry = createComponentRegistry( ZORA_COMPONENT_REGISTRY, APP_EXTENSION_COMPONENT_REGISTRY, diff --git a/src/host/layout/layoutGenerator.ts b/src/host/layout/layoutGenerator.ts index 3edcdd8e..2034bd32 100644 --- a/src/host/layout/layoutGenerator.ts +++ b/src/host/layout/layoutGenerator.ts @@ -53,16 +53,24 @@ function getPackageOwnedRuntimeImports(): string { } from '@ankhorage/runtime'; import { STUDIO_APP_EXTENSION_COMPONENT_REGISTRY, + STUDIO_APP_EXTENSION_INTERACTION_POLICY_SUPPORT, useRuntimeAction, } from '@ankhorage/studio/runtime'; -import { APP_EXTENSION_COMPONENT_REGISTRY as GENERATED_APP_EXTENSION_COMPONENT_REGISTRY } from '@/generated/appExtensionRegistry';`; +import { + APP_EXTENSION_COMPONENT_REGISTRY as GENERATED_APP_EXTENSION_COMPONENT_REGISTRY, + APP_EXTENSION_INTERACTION_POLICY_SUPPORT as GENERATED_APP_EXTENSION_INTERACTION_POLICY_SUPPORT, +} from '@/generated/appExtensionRegistry';`; } function getGeneratedRuntimeRegistryDeclarations(): string { return `const APP_EXTENSION_COMPONENT_REGISTRY = createComponentRegistry( STUDIO_APP_EXTENSION_COMPONENT_REGISTRY, GENERATED_APP_EXTENSION_COMPONENT_REGISTRY, -);`; +); +const APP_EXTENSION_INTERACTION_POLICY_SUPPORT = { + ...STUDIO_APP_EXTENSION_INTERACTION_POLICY_SUPPORT, + ...GENERATED_APP_EXTENSION_INTERACTION_POLICY_SUPPORT, +} as const;`; } function mergeRuntimeModuleDeclarations(...declarations: readonly string[]): string { diff --git a/src/host/layout/templates/rootLayout.test.ts b/src/host/layout/templates/rootLayout.test.ts index 76e04c9d..fe2389eb 100644 --- a/src/host/layout/templates/rootLayout.test.ts +++ b/src/host/layout/templates/rootLayout.test.ts @@ -154,7 +154,7 @@ test('generates a root stationary tap selector for edit mode and excludes old Pr expect(generated).toContain('wrapNode: studioWrapNode'); expect(generated).toContain('selectedNodeId'); expect(generated).toContain('selectNode'); - expect(generated).toContain('STUDIO_EMPTY_THIRD_PARTY_SUPPORT'); + expect(generated).toContain('APP_EXTENSION_INTERACTION_POLICY_SUPPORT'); expect(generated).toContain('ZORA_COMPONENT_REGISTRY'); expect(generated).not.toContain(' { expect(studioShellSource).toContain('const studioRuntimeConfig = useMemo('); expect(studioShellSource).toContain('const studioWrapNode = useMemo('); expect(studioShellSource).toContain('createStudioStationarySelectionWrapNode'); - expect(studioShellSource).toContain('STUDIO_EMPTY_THIRD_PARTY_SUPPORT'); + expect(studioShellSource).toContain('APP_EXTENSION_INTERACTION_POLICY_SUPPORT'); expect(studioShellSource).toContain('const studioResolveNodeProps = useMemo('); expect(studioShellSource).toContain('createStudioInteractionPolicyResolver'); expect(studioShellSource).toContain('disableActions: !previewMode'); diff --git a/src/host/layout/templates/rootLayout.ts b/src/host/layout/templates/rootLayout.ts index 93d11df4..e0602c34 100644 --- a/src/host/layout/templates/rootLayout.ts +++ b/src/host/layout/templates/rootLayout.ts @@ -436,7 +436,6 @@ function resolveRuntimeOperationCredential(credential: { readonly kind?: string runtimeOperationHelpers.trim(), authRuntimeConstants.trim(), includeStudio ? appHeaderHelpers.trim() : '', - includeStudio ? 'const STUDIO_EMPTY_THIRD_PARTY_SUPPORT = {} as const;' : '', innerNavigation.declarations.trim(), ] .filter(Boolean) @@ -620,7 +619,7 @@ function StudioShell({ () => createStudioStationarySelectionWrapNode({ previewMode, - thirdPartySupport: STUDIO_EMPTY_THIRD_PARTY_SUPPORT, + thirdPartySupport: APP_EXTENSION_INTERACTION_POLICY_SUPPORT, }), [previewMode], ); @@ -628,7 +627,7 @@ function StudioShell({ () => createStudioInteractionPolicyResolver({ previewMode, - thirdPartySupport: STUDIO_EMPTY_THIRD_PARTY_SUPPORT, + thirdPartySupport: APP_EXTENSION_INTERACTION_POLICY_SUPPORT, }), [previewMode], ); diff --git a/src/host/orchestrator/generatedAppFiles.test.ts b/src/host/orchestrator/generatedAppFiles.test.ts new file mode 100644 index 00000000..767426a4 --- /dev/null +++ b/src/host/orchestrator/generatedAppFiles.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'bun:test'; + +import { createGeneratedAppExtensionRegistrySource } from './generatedAppFiles'; + +describe('generated app extension interaction-policy support', () => { + it('emits an explicit support map beside the generated component registry', () => { + const source = createGeneratedAppExtensionRegistrySource({ + usesExpoBarcodeScannerAdapter: false, + zoraExtensions: [ + { + packageName: '@example/widgets', + components: { + DeclaredWidget: 'DeclaredWidget', + RegisteredOnlyWidget: 'RegisteredOnlyWidget', + }, + interactionPolicySupportedComponents: ['DeclaredWidget'], + }, + ], + }); + + expect(source).toContain('DeclaredWidget: DeclaredWidget'); + expect(source).toContain('RegisteredOnlyWidget: RegisteredOnlyWidget'); + expect(source).toContain('export const APP_EXTENSION_INTERACTION_POLICY_SUPPORT = {'); + expect(source).toContain('DeclaredWidget: true'); + expect(source).not.toContain('RegisteredOnlyWidget: true'); + }); + + it('rejects support declarations for node types absent from the registry', () => { + expect(() => + createGeneratedAppExtensionRegistrySource({ + usesExpoBarcodeScannerAdapter: false, + zoraExtensions: [ + { + packageName: '@example/widgets', + components: { RegisteredWidget: 'RegisteredWidget' }, + interactionPolicySupportedComponents: ['MissingWidget'], + }, + ], + }), + ).toThrow('declares interaction-policy support for unregistered component MissingWidget'); + }); + + it('emits an empty declaration instead of inferring support from registry membership', () => { + const source = createGeneratedAppExtensionRegistrySource({ + usesExpoBarcodeScannerAdapter: false, + zoraExtensions: [ + { + packageName: '@example/widgets', + components: { RegisteredOnlyWidget: 'RegisteredOnlyWidget' }, + }, + ], + }); + + expect(source).toMatch( + /export const APP_EXTENSION_INTERACTION_POLICY_SUPPORT = \{\s*\} as const;/, + ); + expect(source).not.toContain('RegisteredOnlyWidget: true'); + }); +}); diff --git a/src/host/orchestrator/generatedAppFiles.ts b/src/host/orchestrator/generatedAppFiles.ts index 7f8aaac0..8ad34d83 100644 --- a/src/host/orchestrator/generatedAppFiles.ts +++ b/src/host/orchestrator/generatedAppFiles.ts @@ -57,16 +57,26 @@ export async function syncGeneratedAppFiles( await assertNoForbiddenSpecifiers(targetProjectPath); } -function createGeneratedAppExtensionRegistrySource(args: { +export function createGeneratedAppExtensionRegistrySource(args: { usesExpoBarcodeScannerAdapter: boolean; zoraExtensions: readonly ZoraExtensionDefinition[]; }): string { const importLines = new Set(); + const interactionPolicySupportedComponents = new Set(); const entries = args.zoraExtensions .flatMap((extension) => { const exportNames = Array.from(new Set(Object.values(extension.components))).sort(); importLines.add(`import { ${exportNames.join(', ')} } from '${extension.packageName}';`); + for (const componentName of extension.interactionPolicySupportedComponents ?? []) { + if (!Object.prototype.hasOwnProperty.call(extension.components, componentName)) { + throw new Error( + `ZORA extension ${extension.packageName} declares interaction-policy support for unregistered component ${componentName}.`, + ); + } + interactionPolicySupportedComponents.add(componentName); + } + return Object.entries(extension.components).map(([componentName, exportName]) => ({ componentName, exportName, @@ -85,6 +95,10 @@ function createGeneratedAppExtensionRegistrySource(args: { const registryEntries = entries .map(({ componentName, exportName }) => ` ${formatRegistryKey(componentName)}: ${exportName},`) .join('\n'); + const interactionPolicySupportEntries = [...interactionPolicySupportedComponents] + .sort() + .map((componentName) => ` ${formatRegistryKey(componentName)}: true,`) + .join('\n'); return [ "import type { ComponentRegistry } from '@ankhorage/runtime';", @@ -94,6 +108,10 @@ function createGeneratedAppExtensionRegistrySource(args: { registryEntries, '};', '', + 'export const APP_EXTENSION_INTERACTION_POLICY_SUPPORT = {', + interactionPolicySupportEntries, + '} as const;', + '', ] .filter((line, index, lines) => line.length > 0 || lines[index - 1]?.length !== 0) .join('\n'); diff --git a/src/host/orchestrator/templates.test.ts b/src/host/orchestrator/templates.test.ts index ceddaa75..4080d4bd 100644 --- a/src/host/orchestrator/templates.test.ts +++ b/src/host/orchestrator/templates.test.ts @@ -12,12 +12,19 @@ describe('generated OAuth scaffold templates', () => { expect(dependencies['@ankhorage/contracts']).toBe('^4.0.0'); expect(dependencies['@ankhorage/runtime']).toBe('^0.3.0'); - expect(dependencies['@ankhorage/zora']).toBe('^2.8.7'); + expect(dependencies['@ankhorage/zora']).toBe('^2.9.0'); expect(dependencies['@ankhorage/supabase-auth']).toBe('^1.0.0'); expect(dependencies['expo-secure-store']).toBe('~15.0.8'); expect(dependencies['expo-web-browser']).toBe('~15.0.11'); }); + it('requires the first ZORA release that enforces interactionPolicy', () => { + const pkg = getPackageJson({ name: 'studio-enabled-app', includeStudio: true }); + const dependencies = pkg.dependencies as Record; + + expect(dependencies['@ankhorage/zora']).toBe('^2.9.0'); + }); + it('omits OAuth-specific packages when auth is not generated', () => { const pkg = getPackageJson({ name: 'public-app' }); const dependencies = pkg.dependencies as Record; diff --git a/src/host/orchestrator/templates.ts b/src/host/orchestrator/templates.ts index f4f67b6c..18433aaf 100644 --- a/src/host/orchestrator/templates.ts +++ b/src/host/orchestrator/templates.ts @@ -12,7 +12,7 @@ const EXPO_MODULES_CORE_VERSION = '~3.0.30'; const CONTRACTS_VERSION = '^4.0.0'; const RUNTIME_VERSION = '^0.3.0'; const SUPABASE_AUTH_VERSION = '^1.0.0'; -const ZORA_VERSION = '^2.8.7'; +const ZORA_VERSION = '^2.9.0'; const EXPO_SECURE_STORE_VERSION = '~15.0.8'; const EXPO_WEB_BROWSER_VERSION = '~15.0.11'; diff --git a/src/host/zoraExtensions/index.ts b/src/host/zoraExtensions/index.ts index 1c0015f3..48b06b42 100644 --- a/src/host/zoraExtensions/index.ts +++ b/src/host/zoraExtensions/index.ts @@ -4,6 +4,7 @@ export interface ZoraExtensionDefinition { packageName: string; components: Record; dependencies?: Record; + interactionPolicySupportedComponents?: readonly string[]; } const ZORA_CHESS_EXTENSION = { diff --git a/src/runtime/appExtensionRegistry.ts b/src/runtime/appExtensionRegistry.ts index d385f9a3..2fdc8c02 100644 --- a/src/runtime/appExtensionRegistry.ts +++ b/src/runtime/appExtensionRegistry.ts @@ -7,3 +7,10 @@ export const STUDIO_APP_EXTENSION_COMPONENT_REGISTRY: ComponentRegistry = { OpeningBook, TabletopTable, }; + +/** + * Exact Studio-owned extension node types that accept and enforce interactionPolicy. + * + * Registry membership alone does not opt a component in. + */ +export const STUDIO_APP_EXTENSION_INTERACTION_POLICY_SUPPORT = {} as const; diff --git a/src/runtime/interactionPolicy.test.ts b/src/runtime/interactionPolicy.test.ts index f46622c4..de387462 100644 --- a/src/runtime/interactionPolicy.test.ts +++ b/src/runtime/interactionPolicy.test.ts @@ -3,7 +3,11 @@ import { join } from 'node:path'; import { describe, expect, it } from 'bun:test'; -import { createInteractionPolicyResolver, isComponentSupported } from './interactionPolicyCore'; +import { + composeInteractionPolicyResolver, + createInteractionPolicyResolver, + isComponentSupported, +} from './interactionPolicyCore'; describe('interactionPolicyCore', () => { it('assigns passive policy to supported components in Edit mode', () => { @@ -103,6 +107,21 @@ describe('interactionPolicyCore', () => { expect(result.interactionPolicy).toBeUndefined(); }); + it('does not infer support from third-party registry membership', () => { + const registeredThirdPartyTypes = new Set(['RegisteredOnlyComponent']); + const resolver = createInteractionPolicyResolver({ + previewMode: false, + isSupportedNodeType: () => false, + }); + const result = resolver({ + node: { type: 'RegisteredOnlyComponent' }, + props: { registered: registeredThirdPartyTypes.has('RegisteredOnlyComponent') }, + }); + + expect(result.registered).toBe(true); + expect(result.interactionPolicy).toBeUndefined(); + }); + it('assigns no policy to unknown types', () => { const resolver = createInteractionPolicyResolver({ previewMode: false, @@ -164,6 +183,29 @@ describe('interactionPolicyCore', () => { it('isComponentSupported returns false for unsupported', () => { expect(isComponentSupported('Unknown', {}, () => false)).toBe(false); }); + + it('preserves existing Runtime resolver output while adding policy', () => { + const policyResolver = createInteractionPolicyResolver({ + previewMode: false, + thirdPartySupport: { CustomWidget: true }, + isSupportedNodeType: () => false, + }); + const resolver = composeInteractionPolicyResolver(policyResolver, ({ props }) => ({ + ...props, + existingResolver: 'preserved', + })); + + expect( + resolver({ + node: { type: 'CustomWidget' }, + props: { authored: 'preserved' }, + }), + ).toEqual({ + authored: 'preserved', + existingResolver: 'preserved', + interactionPolicy: 'passive', + }); + }); }); describe('interactionPolicy production adapter', () => { @@ -180,5 +222,21 @@ describe('interactionPolicy production adapter', () => { it('delegates classification to the pure core', () => { expect(source).toContain('createInteractionPolicyResolver'); + expect(source).toContain('composeInteractionPolicyResolver'); + }); +}); + +describe('preview Runtime config production wiring', () => { + const sourcePath = join(import.meta.dir, 'previewRuntimeConfig.ts'); + const source = readFileSync(sourcePath, 'utf8'); + + it('threads support and inherited resolver options independently from registry membership', () => { + expect(source).toContain('readonly components?: ComponentRegistry;'); + expect(source).toContain('readonly interactionPolicySupport?: ThirdPartyComponentSupport;'); + expect(source).toContain( + "readonly resolveNodeProps?: RuntimeRendererConfig['resolveNodeProps'];", + ); + expect(source).toContain('thirdPartySupport: options.interactionPolicySupport'); + expect(source).toContain('existingResolver: options.resolveNodeProps'); }); }); diff --git a/src/runtime/interactionPolicy.ts b/src/runtime/interactionPolicy.ts index 26669dda..a3394094 100644 --- a/src/runtime/interactionPolicy.ts +++ b/src/runtime/interactionPolicy.ts @@ -2,7 +2,10 @@ import { type RuntimeNodePropsResolver } from '@ankhorage/runtime'; import { ZORA_COMPONENT_REGISTRY } from '@ankhorage/zora'; import type { ThirdPartyComponentSupport } from './interactionPolicyCore.js'; -import { createInteractionPolicyResolver } from './interactionPolicyCore.js'; +import { + composeInteractionPolicyResolver, + createInteractionPolicyResolver, +} from './interactionPolicyCore.js'; export type { InteractionPolicy, ThirdPartyComponentSupport } from './interactionPolicyCore.js'; @@ -24,22 +27,7 @@ export function createStudioInteractionPolicyResolver( Object.prototype.hasOwnProperty.call(ZORA_COMPONENT_REGISTRY, nodeType), }); - if (!existingResolver) { - return (resolveArgs) => { - const result = resolver({ - node: resolveArgs.node, - props: resolveArgs.props, - }); - return result; - }; - } - - return (resolveArgs) => { - const baseProps = existingResolver(resolveArgs); - const result = resolver({ - node: resolveArgs.node, - props: baseProps, - }); - return result; - }; + return existingResolver + ? composeInteractionPolicyResolver(resolver, existingResolver) + : composeInteractionPolicyResolver(resolver); } diff --git a/src/runtime/interactionPolicyCore.ts b/src/runtime/interactionPolicyCore.ts index 8337c9c5..a46db62b 100644 --- a/src/runtime/interactionPolicyCore.ts +++ b/src/runtime/interactionPolicyCore.ts @@ -20,7 +20,10 @@ function classifyComponent( return 'zora-builtin'; } - if (Object.prototype.hasOwnProperty.call(thirdPartySupport, nodeType)) { + if ( + Object.prototype.hasOwnProperty.call(thirdPartySupport, nodeType) && + thirdPartySupport[nodeType] === true + ) { return 'third-party-supported'; } @@ -52,12 +55,17 @@ function resolveInteractionPolicy( const POLICY_PROP = 'interactionPolicy'; +export interface InteractionPolicyResolveArgs { + readonly node: TNode; + readonly props: Record; +} + +export type InteractionPolicyNodePropsResolver = + (resolveArgs: InteractionPolicyResolveArgs) => Record; + export function createInteractionPolicyResolver( args: InteractionPolicyResolverArgs, -): (resolveArgs: { - node: { type: string }; - props: Record; -}) => Record { +): InteractionPolicyNodePropsResolver { const { previewMode, thirdPartySupport = EMPTY_THIRD_PARTY, isSupportedNodeType } = args; return (resolveArgs) => { @@ -80,3 +88,25 @@ export function createInteractionPolicyResolver( }; }; } + +export function composeInteractionPolicyResolver( + interactionPolicyResolver: InteractionPolicyNodePropsResolver, +): InteractionPolicyNodePropsResolver; +export function composeInteractionPolicyResolver( + interactionPolicyResolver: InteractionPolicyNodePropsResolver, + existingResolver: InteractionPolicyNodePropsResolver, +): InteractionPolicyNodePropsResolver; +export function composeInteractionPolicyResolver( + interactionPolicyResolver: InteractionPolicyNodePropsResolver, + existingResolver?: InteractionPolicyNodePropsResolver, +): InteractionPolicyNodePropsResolver { + if (!existingResolver) { + return interactionPolicyResolver; + } + + return (resolveArgs) => + interactionPolicyResolver({ + node: resolveArgs.node, + props: existingResolver(resolveArgs), + }); +} diff --git a/src/runtime/previewRuntimeConfig.ts b/src/runtime/previewRuntimeConfig.ts index 5d9135d9..0d040e27 100644 --- a/src/runtime/previewRuntimeConfig.ts +++ b/src/runtime/previewRuntimeConfig.ts @@ -2,11 +2,14 @@ import type { ComponentRegistry, RuntimeRendererConfig } from '@ankhorage/runtim import { shouldSuppressStudioRuntimeActions } from './actionSuppression'; import { createStudioInteractionPolicyResolver } from './interactionPolicy'; +import type { ThirdPartyComponentSupport } from './interactionPolicyCore'; import { createStudioPreviewComponentRegistry } from './previewRegistry'; export interface StudioPreviewRuntimeConfigOptions { readonly previewMode: boolean; readonly components?: ComponentRegistry; + readonly interactionPolicySupport?: ThirdPartyComponentSupport; + readonly resolveNodeProps?: RuntimeRendererConfig['resolveNodeProps']; } export const createStudioPreviewRuntimeConfig = ( @@ -16,5 +19,7 @@ export const createStudioPreviewRuntimeConfig = ( registry: createStudioPreviewComponentRegistry({ components: options.components }), resolveNodeProps: createStudioInteractionPolicyResolver({ previewMode: options.previewMode, + thirdPartySupport: options.interactionPolicySupport, + existingResolver: options.resolveNodeProps, }), }); diff --git a/src/runtime/stationarySelection.test.ts b/src/runtime/stationarySelection.test.ts index 9ea88b10..212e7c70 100644 --- a/src/runtime/stationarySelection.test.ts +++ b/src/runtime/stationarySelection.test.ts @@ -69,22 +69,37 @@ describe('stationarySelection RN integration', () => { expect(source).toContain("display: 'contents'"); }); - it('preserves unsupported indicator with pointerEvents none', () => { + it('renders root-owned unsupported indicators with pointerEvents none', () => { + expect(source).toContain('...indicatorRects.map((rect) =>'); expect(source).toContain("pointerEvents: 'none'"); }); + it('retains duplicate live wrapper measurements for the same Runtime node', () => { + expect(source).toContain('new Map>()'); + expect(source).toContain('measurements.add(measure)'); + expect(source).toContain('ref: setViewRef'); + }); + it('places pointerEvents as a View prop, not inside style', () => { - const indicatorMatch = /React\.createElement\(View,\s*\{[^}]*style:\s*\{[^}]*\}[^}]*\}\)/.exec( - source, - ); - expect(indicatorMatch).not.toBeNull(); - const indicatorBlock = indicatorMatch?.[0] ?? ''; + const indicatorStart = source.indexOf('...indicatorRects.map((rect) =>'); + const indicatorBlock = source.slice(indicatorStart); expect(indicatorBlock).toContain("pointerEvents: 'none'"); - const styleBlock = /style:\s*\{([^}]*)\}/.exec(indicatorBlock); + const styleBlock = /style:\s*\{([\s\S]*?)\n\s*\},/.exec(indicatorBlock); expect(styleBlock).toBeTruthy(); expect(styleBlock?.[1]).not.toContain('pointerEvents'); }); + it('does not position an indicator beneath the display-contents recorder', () => { + const recorderStart = source.indexOf('function StudioNodeTouchRecorder'); + const wrapperFactoryStart = source.indexOf( + 'export function createStudioStationarySelectionWrapNode', + ); + const recorderSource = source.slice(recorderStart, wrapperFactoryStart); + + expect(recorderSource).not.toContain("position: 'absolute'"); + expect(recorderSource).not.toContain('borderColor'); + }); + it('uses lazy coordinator creation', () => { expect(source).toContain( 'const coordinatorRef = React.useRef(null);', @@ -93,11 +108,24 @@ describe('stationarySelection RN integration', () => { expect(source).not.toContain('useRef(createStationarySelectionCoordinator())'); }); - it('records touch with generation validation', () => { + it('records pointer and touch input with generation validation', () => { expect(source).toContain('ctx.recordNode(nodeId)'); + expect(source).toContain('onPointerDown: handleInteractionStart'); + expect(source).toContain('onTouchStart: handleInteractionStart'); + expect(source).toContain('pendingNodeIdsRef.current.push(nodeId)'); + expect(source).toContain('for (const nodeId of pendingNodeIdsRef.current)'); + }); + + it('uses public element geometry only for web indication measurement', () => { + expect(source).toContain("Platform.OS === 'web'"); + expect(source).toContain('getBoundingClientRect'); + expect(source).toContain('measureRenderedBoxes(child)'); + expect(source).not.toContain('findNodeHandle'); + expect(source).not.toContain('ReactNativePrivateInterface'); }); - it('uses bubble phase onTouchStart', () => { - expect(source).toContain('onTouchStart: handleTouchStart'); + it('continuously refreshes indication geometry across scroll and layout changes', () => { + expect(source).toContain('requestAnimationFrame(measureNextFrame)'); + expect(source).toContain('cancelAnimationFrame(frameId)'); }); }); diff --git a/src/runtime/stationarySelection.ts b/src/runtime/stationarySelection.ts index 5c67650a..a36bc12f 100644 --- a/src/runtime/stationarySelection.ts +++ b/src/runtime/stationarySelection.ts @@ -1,6 +1,6 @@ import { ZORA_COMPONENT_REGISTRY } from '@ankhorage/zora'; import React from 'react'; -import { View } from 'react-native'; +import { Platform, View } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import type { StationarySelectionCoordinator } from './stationarySelectionCoordinator.js'; @@ -12,12 +12,142 @@ export type { TransactionState, } from './stationarySelectionCoordinator.js'; +interface MeasuredRect { + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; +} + +interface UnsupportedIndicatorRect extends MeasuredRect { + readonly nodeId: string; +} + +type ViewRef = React.ElementRef; +type MeasureUnsupportedNode = () => Promise; + interface TrackerContextValue { readonly recordNode: (nodeId: string) => void; + readonly registerUnsupportedNode: (nodeId: string, measure: MeasureUnsupportedNode) => () => void; } const TrackerContext = React.createContext(null); +interface WebRectLike { + readonly left: number; + readonly top: number; + readonly width: number; + readonly height: number; +} + +interface WebElementLike { + readonly children: ArrayLike; + getBoundingClientRect(): WebRectLike; +} + +function isWebElementLike(value: unknown): value is WebElementLike { + return ( + typeof value === 'object' && + value !== null && + 'children' in value && + 'getBoundingClientRect' in value && + typeof value.getBoundingClientRect === 'function' + ); +} + +function toMeasuredRect(rect: WebRectLike): MeasuredRect { + return { + x: rect.left, + y: rect.top, + width: rect.width, + height: rect.height, + }; +} + +function unionRects(rects: readonly MeasuredRect[]): MeasuredRect | null { + if (rects.length === 0) { + return null; + } + + const left = Math.min(...rects.map((rect) => rect.x)); + const top = Math.min(...rects.map((rect) => rect.y)); + const right = Math.max(...rects.map((rect) => rect.x + rect.width)); + const bottom = Math.max(...rects.map((rect) => rect.y + rect.height)); + + return { + x: left, + y: top, + width: right - left, + height: bottom - top, + }; +} + +function measureRenderedBoxes(element: WebElementLike): readonly MeasuredRect[] { + const rect = toMeasuredRect(element.getBoundingClientRect()); + if (rect.width > 0 && rect.height > 0) { + return [rect]; + } + + return Array.from(element.children).flatMap((child) => measureRenderedBoxes(child)); +} + +function measureNativeView(view: ViewRef): Promise { + return new Promise((resolve) => { + view.measureInWindow((x, y, width, height) => { + resolve(width > 0 && height > 0 ? { x, y, width, height } : null); + }); + }); +} + +function measureRootView(view: ViewRef | null): Promise { + if (!view) { + return Promise.resolve(null); + } + + if (Platform.OS === 'web' && isWebElementLike(view)) { + return Promise.resolve(toMeasuredRect(view.getBoundingClientRect())); + } + + return measureNativeView(view); +} + +function measureUnsupportedView(view: ViewRef | null): Promise { + if (!view) { + return Promise.resolve(null); + } + + if (Platform.OS === 'web' && isWebElementLike(view)) { + return Promise.resolve( + unionRects(Array.from(view.children).flatMap((child) => measureRenderedBoxes(child))), + ); + } + + return measureNativeView(view); +} + +function areIndicatorRectsEqual( + left: readonly UnsupportedIndicatorRect[], + right: readonly UnsupportedIndicatorRect[], +): boolean { + if (left.length !== right.length) { + return false; + } + + return left.every((rect, index) => { + const candidate = right[index]; + if (!candidate) { + return false; + } + return ( + rect.nodeId === candidate.nodeId && + Math.abs(rect.x - candidate.x) < 0.5 && + Math.abs(rect.y - candidate.y) < 0.5 && + Math.abs(rect.width - candidate.width) < 0.5 && + Math.abs(rect.height - candidate.height) < 0.5 + ); + }); +} + function StudioNodeTouchRecorder(props: { readonly nodeId: string | undefined; readonly showUnsupportedIndicator: boolean; @@ -25,10 +155,11 @@ function StudioNodeTouchRecorder(props: { }): React.JSX.Element { const ctx = React.useContext(TrackerContext); const nodeIdRef = React.useRef(props.nodeId); + const unregisterMeasurementRef = React.useRef<(() => void) | null>(null); nodeIdRef.current = props.nodeId; - function handleTouchStart(): void { + function handleInteractionStart(): void { if (!ctx) { return; } @@ -42,27 +173,34 @@ function StudioNodeTouchRecorder(props: { ctx.recordNode(nodeId); } + const setViewRef = React.useCallback( + (view: ViewRef | null) => { + unregisterMeasurementRef.current?.(); + unregisterMeasurementRef.current = null; + if (view && ctx && props.nodeId && props.showUnsupportedIndicator) { + unregisterMeasurementRef.current = ctx.registerUnsupportedNode(props.nodeId, () => + measureUnsupportedView(view), + ); + } + }, + [ctx, props.nodeId, props.showUnsupportedIndicator], + ); + return React.createElement( View, { + ref: setViewRef, + nativeID: props.nodeId + ? `studio-runtime-node-${encodeURIComponent(props.nodeId)}` + : undefined, + testID: + props.nodeId && props.showUnsupportedIndicator + ? `studio-unsupported-recorder-${props.nodeId}` + : undefined, style: { display: 'contents' }, - onTouchStart: handleTouchStart, + onPointerDown: handleInteractionStart, + onTouchStart: handleInteractionStart, }, - props.showUnsupportedIndicator - ? React.createElement(View, { - style: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - borderWidth: 1, - borderColor: '#ef4444', - borderStyle: 'dashed', - }, - pointerEvents: 'none', - }) - : null, props.children, ); } @@ -90,13 +228,12 @@ export function createStudioStationarySelectionWrapNode(options?: { const isSupported = Object.prototype.hasOwnProperty.call(ZORA_COMPONENT_REGISTRY, args.node.type) || (thirdPartySupport != null && - Object.prototype.hasOwnProperty.call(thirdPartySupport, args.node.type)); - - const showUnsupportedIndicator = isEditMode && !isSupported; + Object.prototype.hasOwnProperty.call(thirdPartySupport, args.node.type) && + thirdPartySupport[args.node.type] === true); return React.createElement(StudioNodeTouchRecorder, { nodeId: args.node.id, - showUnsupportedIndicator: showUnsupportedIndicator, + showUnsupportedIndicator: isEditMode && !isSupported, children: args.rendered, }); }; @@ -109,6 +246,12 @@ function StationaryTapSelector(props: { readonly children: React.ReactNode; }): React.JSX.Element { const coordinatorRef = React.useRef(null); + const rootViewRef = React.useRef(null); + const unsupportedNodesRef = React.useRef(new Map>()); + const [unsupportedNodeVersion, setUnsupportedNodeVersion] = React.useState(0); + const [indicatorRects, setIndicatorRects] = React.useState( + [], + ); coordinatorRef.current ??= createStationarySelectionCoordinator(); @@ -117,19 +260,114 @@ function StationaryTapSelector(props: { const isEditModeRef = React.useRef(props.isEditMode); const selectedNodeIdRef = React.useRef(props.selectedNodeId); const selectNodeRef = React.useRef(props.selectNode); + const selectionCommitCountRef = React.useRef(0); const generationRef = React.useRef(0); + const pendingNodeIdsRef = React.useRef([]); isEditModeRef.current = props.isEditMode; selectedNodeIdRef.current = props.selectedNodeId; selectNodeRef.current = props.selectNode; + const registerUnsupportedNode = React.useCallback( + (nodeId: string, measure: MeasureUnsupportedNode) => { + const measurements = unsupportedNodesRef.current.get(nodeId) ?? new Set(); + measurements.add(measure); + unsupportedNodesRef.current.set(nodeId, measurements); + setUnsupportedNodeVersion((version) => version + 1); + + return () => { + const registeredMeasurements = unsupportedNodesRef.current.get(nodeId); + if (!registeredMeasurements?.delete(measure)) { + return; + } + if (registeredMeasurements.size === 0) { + unsupportedNodesRef.current.delete(nodeId); + } + setUnsupportedNodeVersion((version) => version + 1); + }; + }, + [], + ); + const contextValue = React.useMemo( () => ({ - recordNode: (nodeId: string) => coordinator.recordNode(nodeId, generationRef.current), + recordNode: (nodeId: string) => { + if (generationRef.current === 0) { + if (!pendingNodeIdsRef.current.includes(nodeId)) { + pendingNodeIdsRef.current.push(nodeId); + } + return; + } + coordinator.recordNode(nodeId, generationRef.current); + }, + registerUnsupportedNode, }), - [coordinator], + [coordinator, registerUnsupportedNode], ); + const refreshIndicatorRects = React.useCallback(async () => { + if (!isEditModeRef.current || unsupportedNodesRef.current.size === 0) { + setIndicatorRects((current) => (current.length === 0 ? current : [])); + return; + } + + const rootRect = await measureRootView(rootViewRef.current); + if (!rootRect) { + return; + } + + const measurements = await Promise.all( + [...unsupportedNodesRef.current.entries()].map(async ([nodeId, nodeMeasurements]) => { + const rects = await Promise.all([...nodeMeasurements].map((measure) => measure())); + return { + nodeId, + rect: unionRects(rects.filter((rect): rect is MeasuredRect => rect !== null)), + }; + }), + ); + const nextRects = measurements + .flatMap(({ nodeId, rect }) => + rect + ? [ + { + nodeId, + x: rect.x - rootRect.x, + y: rect.y - rootRect.y, + width: rect.width, + height: rect.height, + }, + ] + : [], + ) + .sort((left, right) => left.nodeId.localeCompare(right.nodeId)); + + setIndicatorRects((current) => + areIndicatorRectsEqual(current, nextRects) ? current : nextRects, + ); + }, []); + + React.useEffect(() => { + let active = true; + let frameId: number | null = null; + + const measureNextFrame = () => { + void refreshIndicatorRects().finally(() => { + if (active) { + frameId = requestAnimationFrame(measureNextFrame); + } + }); + }; + + measureNextFrame(); + + return () => { + active = false; + if (frameId !== null) { + cancelAnimationFrame(frameId); + } + }; + }, [props.isEditMode, unsupportedNodeVersion, refreshIndicatorRects]); + const tapGesture = React.useMemo(() => { return Gesture.Tap() .maxDeltaX(5) @@ -138,6 +376,10 @@ function StationaryTapSelector(props: { .runOnJS(true) .onBegin(() => { generationRef.current = coordinator.beginTransaction(); + for (const nodeId of pendingNodeIdsRef.current) { + coordinator.recordNode(nodeId, generationRef.current); + } + pendingNodeIdsRef.current = []; }) .onEnd((_event, success) => { if (!success) { @@ -147,26 +389,67 @@ function StationaryTapSelector(props: { coordinator.commitSelection( isEditModeRef.current, selectedNodeIdRef.current, - selectNodeRef.current, + (nodeId) => { + selectionCommitCountRef.current += 1; + selectNodeRef.current(nodeId); + }, generationRef.current, ); }) .onFinalize(() => { coordinator.clearTransaction(generationRef.current); + generationRef.current = 0; + pendingNodeIdsRef.current = []; }); }, [coordinator]); React.useEffect(() => { coordinator.clearTransaction(); + generationRef.current = 0; + pendingNodeIdsRef.current = []; }, [props.isEditMode, coordinator]); + const selectionStateId = [ + 'studio-stationary-selection-root', + props.isEditMode ? 'edit' : 'preview', + encodeURIComponent(props.selectedNodeId ?? 'none'), + String(selectionCommitCountRef.current), + ].join(':'); + return React.createElement( TrackerContext.Provider, { value: contextValue }, React.createElement( GestureDetector, { gesture: tapGesture }, - React.createElement(View, { style: { flex: 1 } }, props.children), + React.createElement( + View, + { + ref: rootViewRef, + nativeID: selectionStateId, + testID: 'studio-stationary-selection-root', + style: { flex: 1, position: 'relative' }, + }, + props.children, + ...indicatorRects.map((rect) => + React.createElement(View, { + key: rect.nodeId, + nativeID: `studio-unsupported-indicator-${encodeURIComponent(rect.nodeId)}`, + testID: `studio-unsupported-indicator-${rect.nodeId}`, + pointerEvents: 'none', + style: { + position: 'absolute', + left: rect.x, + top: rect.y, + width: rect.width, + height: rect.height, + borderWidth: 1, + borderColor: '#ef4444', + borderStyle: 'dashed', + }, + }), + ), + ), ), ); } From b89dceda99fd55b1b49f7143e897d5583d725483 Mon Sep 17 00:00:00 2001 From: artiphishle Date: Thu, 30 Jul 2026 22:11:31 +0200 Subject: [PATCH 7/8] fix: complete Studio selection validation --- ...-issue-167-interaction-policy-selection.md | 5 +- paradox/diagrams/architecture-overview.mmd | 18 + paradox/diagrams/export-graph.mmd | 6 + paradox/diagrams/module-relationships.mmd | 12 + paradox/index.html | 621 +++++++++++-- paradox/paradox.json | 819 ++++++++++++++++-- src/authFlowContract.test.ts | 3 + src/host/generatedAdminExpoWeb.smoke.test.ts | 105 ++- src/host/orchestrator/semverRange.test.ts | 16 + src/host/orchestrator/semverRange.ts | 43 + src/host/orchestrator/templates.test.ts | 29 +- src/host/orchestrator/templates.ts | 11 +- .../indicatorRefreshCoordinator.test.ts | 75 ++ src/runtime/indicatorRefreshCoordinator.ts | 43 + src/runtime/stationarySelection.test.ts | 39 +- src/runtime/stationarySelection.ts | 280 ++++-- .../stationarySelectionInputState.test.ts | 134 +++ src/runtime/stationarySelectionInputState.ts | 94 ++ 18 files changed, 2097 insertions(+), 256 deletions(-) create mode 100644 src/host/orchestrator/semverRange.test.ts create mode 100644 src/host/orchestrator/semverRange.ts create mode 100644 src/runtime/indicatorRefreshCoordinator.test.ts create mode 100644 src/runtime/indicatorRefreshCoordinator.ts create mode 100644 src/runtime/stationarySelectionInputState.test.ts create mode 100644 src/runtime/stationarySelectionInputState.ts diff --git a/.changeset/2026-07-28-issue-167-interaction-policy-selection.md b/.changeset/2026-07-28-issue-167-interaction-policy-selection.md index f222b176..b73b5ea5 100644 --- a/.changeset/2026-07-28-issue-167-interaction-policy-selection.md +++ b/.changeset/2026-07-28-issue-167-interaction-policy-selection.md @@ -3,5 +3,6 @@ --- Add public interaction-policy and stationary-selection runtime helpers, require ZORA 2.9 in -generated apps, support explicit extension policy declarations, and provide desktop pointer/touch -selection with root-owned unsupported-component overlays. +generated apps, support explicit extension policy declarations, and provide stale-safe +pointer/touch selection with event-driven root-owned unsupported-component overlays. Generate the +Expo SDK 54 animation stack with the compatible Reanimated 4.3.0 and Worklets 0.8.3 pair. diff --git a/paradox/diagrams/architecture-overview.mmd b/paradox/diagrams/architecture-overview.mmd index c4bd310b..32a7d955 100644 --- a/paradox/diagrams/architecture-overview.mmd +++ b/paradox/diagrams/architecture-overview.mmd @@ -177,6 +177,7 @@ graph TD package__ankhorage_studio -.-> module_src_host_generatedAdminExpoWeb_smoke_test_ts module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts + module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_semverRange_ts module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_templateRegistry_index_ts module_src_host_generatedAuthAdapterRuntime_smoke_test_ts["src/host/generatedAuthAdapterRuntime.smoke.test.ts"] package__ankhorage_studio -.-> module_src_host_generatedAuthAdapterRuntime_smoke_test_ts @@ -422,6 +423,11 @@ graph TD module_src_host_orchestrator_scaffolder_ts --> module_src_host_orchestrator_templates_ts module_src_host_orchestrator_scaffolder_ts --> module_src_host_templateRegistry_index_ts module_src_host_orchestrator_scaffolder_ts --> module_src_host_zoraExtensions_index_ts + module_src_host_orchestrator_semverRange_test_ts["src/host/orchestrator/semverRange.test.ts"] + package__ankhorage_studio -.-> module_src_host_orchestrator_semverRange_test_ts + module_src_host_orchestrator_semverRange_test_ts --> module_src_host_orchestrator_semverRange_ts + module_src_host_orchestrator_semverRange_ts["src/host/orchestrator/semverRange.ts"] + package__ankhorage_studio -.-> module_src_host_orchestrator_semverRange_ts module_src_host_orchestrator_studioInfraUp_ts["src/host/orchestrator/studioInfraUp.ts"] package__ankhorage_studio -.-> module_src_host_orchestrator_studioInfraUp_ts module_src_host_orchestrator_studioInfraUp_ts --> module_src_host_orchestrator_infraRuntime_ts @@ -596,6 +602,11 @@ graph TD package__ankhorage_studio -.-> module_src_runtime_appExtensionRegistry_ts module_src_runtime_index_ts["src/runtime/index.ts"] package__ankhorage_studio -.-> module_src_runtime_index_ts + module_src_runtime_indicatorRefreshCoordinator_test_ts["src/runtime/indicatorRefreshCoordinator.test.ts"] + package__ankhorage_studio -.-> module_src_runtime_indicatorRefreshCoordinator_test_ts + module_src_runtime_indicatorRefreshCoordinator_test_ts --> module_src_runtime_indicatorRefreshCoordinator_ts + module_src_runtime_indicatorRefreshCoordinator_ts["src/runtime/indicatorRefreshCoordinator.ts"] + package__ankhorage_studio -.-> module_src_runtime_indicatorRefreshCoordinator_ts module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"] package__ankhorage_studio -.-> module_src_runtime_interactionPolicy_test_ts module_src_runtime_interactionPolicy_test_ts --> module_src_runtime_interactionPolicyCore_ts @@ -625,12 +636,19 @@ graph TD package__ankhorage_studio -.-> module_src_runtime_stationarySelection_test_ts module_src_runtime_stationarySelection_ts["src/runtime/stationarySelection.ts"] package__ankhorage_studio -.-> module_src_runtime_stationarySelection_ts + module_src_runtime_stationarySelection_ts --> module_src_runtime_indicatorRefreshCoordinator_ts module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionCoordinator_ts + module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionInputState_ts module_src_runtime_stationarySelectionCoordinator_test_ts["src/runtime/stationarySelectionCoordinator.test.ts"] package__ankhorage_studio -.-> module_src_runtime_stationarySelectionCoordinator_test_ts module_src_runtime_stationarySelectionCoordinator_test_ts --> module_src_runtime_stationarySelectionCoordinator_ts module_src_runtime_stationarySelectionCoordinator_ts["src/runtime/stationarySelectionCoordinator.ts"] package__ankhorage_studio -.-> module_src_runtime_stationarySelectionCoordinator_ts + module_src_runtime_stationarySelectionInputState_test_ts["src/runtime/stationarySelectionInputState.test.ts"] + package__ankhorage_studio -.-> module_src_runtime_stationarySelectionInputState_test_ts + module_src_runtime_stationarySelectionInputState_test_ts --> module_src_runtime_stationarySelectionInputState_ts + module_src_runtime_stationarySelectionInputState_ts["src/runtime/stationarySelectionInputState.ts"] + package__ankhorage_studio -.-> module_src_runtime_stationarySelectionInputState_ts module_src_runtime_useRuntimeAction_ts["src/runtime/useRuntimeAction.ts"] package__ankhorage_studio -.-> module_src_runtime_useRuntimeAction_ts module_src_runtime_useRuntimeAction_ts --> module_src_runtime_runtimeActions_ts diff --git a/paradox/diagrams/export-graph.mmd b/paradox/diagrams/export-graph.mmd index f500f845..1abed6a4 100644 --- a/paradox/diagrams/export-graph.mmd +++ b/paradox/diagrams/export-graph.mmd @@ -109,6 +109,8 @@ graph LR module_src_host_orchestrator_projectStoreSummary_test_ts["src/host/orchestrator/projectStoreSummary.test.ts"] module_src_host_orchestrator_resolveMutations_ts["src/host/orchestrator/resolveMutations.ts"] module_src_host_orchestrator_scaffolder_ts["src/host/orchestrator/scaffolder.ts"] + module_src_host_orchestrator_semverRange_test_ts["src/host/orchestrator/semverRange.test.ts"] + module_src_host_orchestrator_semverRange_ts["src/host/orchestrator/semverRange.ts"] module_src_host_orchestrator_studioInfraUp_ts["src/host/orchestrator/studioInfraUp.ts"] module_src_host_orchestrator_templates_test_ts["src/host/orchestrator/templates.test.ts"] module_src_host_orchestrator_templates_ts["src/host/orchestrator/templates.ts"] @@ -166,6 +168,8 @@ graph LR module_src_runtime_actionSuppression_ts["src/runtime/actionSuppression.ts"] module_src_runtime_appExtensionRegistry_ts["src/runtime/appExtensionRegistry.ts"] module_src_runtime_index_ts["src/runtime/index.ts"] + module_src_runtime_indicatorRefreshCoordinator_test_ts["src/runtime/indicatorRefreshCoordinator.test.ts"] + module_src_runtime_indicatorRefreshCoordinator_ts["src/runtime/indicatorRefreshCoordinator.ts"] module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"] module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"] module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"] @@ -179,6 +183,8 @@ graph LR module_src_runtime_stationarySelection_ts["src/runtime/stationarySelection.ts"] module_src_runtime_stationarySelectionCoordinator_test_ts["src/runtime/stationarySelectionCoordinator.test.ts"] module_src_runtime_stationarySelectionCoordinator_ts["src/runtime/stationarySelectionCoordinator.ts"] + module_src_runtime_stationarySelectionInputState_test_ts["src/runtime/stationarySelectionInputState.test.ts"] + module_src_runtime_stationarySelectionInputState_ts["src/runtime/stationarySelectionInputState.ts"] module_src_runtime_useRuntimeAction_ts["src/runtime/useRuntimeAction.ts"] module_src_runtimeDiagnosticsModel_test_ts["src/runtimeDiagnosticsModel.test.ts"] module_src_runtimeDiagnosticsModel_ts["src/runtimeDiagnosticsModel.ts"] diff --git a/paradox/diagrams/module-relationships.mmd b/paradox/diagrams/module-relationships.mmd index 8d8c67be..12a55dce 100644 --- a/paradox/diagrams/module-relationships.mmd +++ b/paradox/diagrams/module-relationships.mmd @@ -109,6 +109,8 @@ graph LR module_src_host_orchestrator_projectStoreSummary_test_ts["src/host/orchestrator/projectStoreSummary.test.ts"] module_src_host_orchestrator_resolveMutations_ts["src/host/orchestrator/resolveMutations.ts"] module_src_host_orchestrator_scaffolder_ts["src/host/orchestrator/scaffolder.ts"] + module_src_host_orchestrator_semverRange_test_ts["src/host/orchestrator/semverRange.test.ts"] + module_src_host_orchestrator_semverRange_ts["src/host/orchestrator/semverRange.ts"] module_src_host_orchestrator_studioInfraUp_ts["src/host/orchestrator/studioInfraUp.ts"] module_src_host_orchestrator_templates_test_ts["src/host/orchestrator/templates.test.ts"] module_src_host_orchestrator_templates_ts["src/host/orchestrator/templates.ts"] @@ -166,6 +168,8 @@ graph LR module_src_runtime_actionSuppression_ts["src/runtime/actionSuppression.ts"] module_src_runtime_appExtensionRegistry_ts["src/runtime/appExtensionRegistry.ts"] module_src_runtime_index_ts["src/runtime/index.ts"] + module_src_runtime_indicatorRefreshCoordinator_test_ts["src/runtime/indicatorRefreshCoordinator.test.ts"] + module_src_runtime_indicatorRefreshCoordinator_ts["src/runtime/indicatorRefreshCoordinator.ts"] module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"] module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"] module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"] @@ -179,6 +183,8 @@ graph LR module_src_runtime_stationarySelection_ts["src/runtime/stationarySelection.ts"] module_src_runtime_stationarySelectionCoordinator_test_ts["src/runtime/stationarySelectionCoordinator.test.ts"] module_src_runtime_stationarySelectionCoordinator_ts["src/runtime/stationarySelectionCoordinator.ts"] + module_src_runtime_stationarySelectionInputState_test_ts["src/runtime/stationarySelectionInputState.test.ts"] + module_src_runtime_stationarySelectionInputState_ts["src/runtime/stationarySelectionInputState.ts"] module_src_runtime_useRuntimeAction_ts["src/runtime/useRuntimeAction.ts"] module_src_runtimeDiagnosticsModel_test_ts["src/runtimeDiagnosticsModel.test.ts"] module_src_runtimeDiagnosticsModel_ts["src/runtimeDiagnosticsModel.ts"] @@ -310,6 +316,7 @@ graph LR module_src_host_createStudioHost_ts --> module_src_host_orchestrator_projectManager_ts module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts + module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_semverRange_ts module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_templateRegistry_index_ts module_src_host_generatedAuthAdapterRuntime_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts module_src_host_generatedAuthAdapterRuntime_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts @@ -429,6 +436,7 @@ graph LR module_src_host_orchestrator_scaffolder_ts --> module_src_host_orchestrator_templates_ts module_src_host_orchestrator_scaffolder_ts --> module_src_host_templateRegistry_index_ts module_src_host_orchestrator_scaffolder_ts --> module_src_host_zoraExtensions_index_ts + module_src_host_orchestrator_semverRange_test_ts --> module_src_host_orchestrator_semverRange_ts module_src_host_orchestrator_studioInfraUp_ts --> module_src_host_orchestrator_infraRuntime_ts module_src_host_orchestrator_studioInfraUp_ts --> module_src_host_orchestrator_projectManager_ts module_src_host_orchestrator_studioInfraUp_ts --> module_src_host_secrets_trustedOAuthInfraEnvironment_ts @@ -490,6 +498,7 @@ graph LR module_src_projectSecretUsage_test_ts --> module_src_projectSecretUsage_ts module_src_routeUtils_public_test_ts --> module_src_routeUtils_ts module_src_routeUtils_test_ts --> module_src_routeUtils_ts + module_src_runtime_indicatorRefreshCoordinator_test_ts --> module_src_runtime_indicatorRefreshCoordinator_ts module_src_runtime_interactionPolicy_test_ts --> module_src_runtime_interactionPolicyCore_ts module_src_runtime_interactionPolicy_ts --> module_src_runtime_interactionPolicyCore_ts module_src_runtime_previewRegistry_ts --> module_src_runtime_appExtensionRegistry_ts @@ -497,8 +506,11 @@ graph LR module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicy_ts module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicyCore_ts module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_previewRegistry_ts + module_src_runtime_stationarySelection_ts --> module_src_runtime_indicatorRefreshCoordinator_ts module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionCoordinator_ts + module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionInputState_ts module_src_runtime_stationarySelectionCoordinator_test_ts --> module_src_runtime_stationarySelectionCoordinator_ts + module_src_runtime_stationarySelectionInputState_test_ts --> module_src_runtime_stationarySelectionInputState_ts module_src_runtime_useRuntimeAction_ts --> module_src_runtime_runtimeActions_ts module_src_runtimeDiagnosticsModel_test_ts --> module_src_runtimeDiagnosticsModel_ts module_src_runtimeLocalization_public_test_ts --> module_src_runtimeLocalization_ts diff --git a/paradox/index.html b/paradox/index.html index 09534478..20c3a6a0 100644 --- a/paradox/index.html +++ b/paradox/index.html @@ -438,7 +438,7 @@

    @ankhorage/studio

    data-target="src/host/generatedAdminExpoWeb.smoke.test.ts" > src/host/generatedAdminExpoWeb.smoke.test.ts - 33 functions + 34 functions
  • @@ -911,6 +911,16 @@

    @ankhorage/studio

    4 functions
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • @@ -1309,6 +1339,26 @@

    @ankhorage/studio

    9 functions
  • +
  • + +
  • +
  • + +
  • @@ -495,6 +495,26 @@

    @ankhorage/studio

    1 function
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • @@ -1625,7 +1665,7 @@

    Package overview

    96public exports
    0components
    -
    236modules
    +
    242modules
    1entrypoints

    Entrypoints

    @@ -2247,12 +2287,13 @@

    src/host/createStudioHost.ts

    src/host/generatedAdminExpoWeb.smoke.test.ts

    Referenced module

    - Dependencies: src/host/orchestrator/moduleManager.ts, + Dependencies: src/host/installedPackageProvenance.ts, + src/host/orchestrator/moduleManager.ts, src/host/orchestrator/projectManager.ts, src/host/orchestrator/semverRange.ts, src/host/templateRegistry/index.ts @@ -2415,6 +2456,30 @@

    src/host/index.ts

    startStudioHostServerWithSecrets, StudioHost

    +
    +

    src/host/installedPackageProvenance.test.ts

    +

    Referenced module

    +

    + Dependencies: src/host/installedPackageProvenance.ts +

    +

    Exports: None

    +
    +
    +

    src/host/installedPackageProvenance.ts

    +

    Referenced module

    +

    Dependencies: None

    +

    + Exports: bunLockfileReferencesPackageVersion, + InstalledPackageProvenance, isPathInsideResolved, + resolveInstalledPackageProvenance +

    +
    src/host/oauthFixtureConsumer.smoke.test.ts

    Exports: None

    +
    +

    src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts

    +

    Referenced module

    +

    + Dependencies: + src/host/orchestrator/expoSdk54AnimationCompatibility.ts, + src/host/orchestrator/templates.ts +

    +

    Exports: None

    +
    +
    +

    src/host/orchestrator/expoSdk54AnimationCompatibility.ts

    +

    Referenced module

    +

    Dependencies: None

    +

    Exports: EXPO_SDK_54_ANIMATION_COMPATIBILITY

    +
    src/host/orchestrator/studioInfraUp.ts

    src/host/orchestrator/templates.test.ts

    Referenced module

    -

    Dependencies: src/host/orchestrator/templates.ts

    +

    + Dependencies: + src/host/orchestrator/expoSdk54AnimationCompatibility.ts, + src/host/orchestrator/templates.ts +

    Exports: None

    src/host/orchestrator/templates.ts

    Referenced module

    -

    Dependencies: None

    - Exports: GENERATED_ANIMATION_DEPENDENCY_PAIR, - GeneratedAuthProvider, GeneratedStorageProvider, - getAppConfigTs, getBabelConfigJs, - getEslintConfigMjs, getIndexJs, - getMetroConfigJs, getPackageJson, + Dependencies: + src/host/orchestrator/expoSdk54AnimationCompatibility.ts +

    +

    + Exports: GeneratedAuthProvider, + GeneratedStorageProvider, getAppConfigTs, + getBabelConfigJs, getEslintConfigMjs, + getIndexJs, getMetroConfigJs, getPackageJson, getPrettierRcJs, getTsConfigJson

    @@ -3898,7 +3991,7 @@

    src/runtime/appExtensionRegistry.ts

    src/runtime/index.ts

    Referenced module

    @@ -3928,7 +4021,8 @@

    src/runtime/index.ts

    StudioPreviewRegistryOptions, StudioPreviewRuntimeConfigOptions, ThirdPartyComponentSupport, TransactionState, - useRuntimeAction, UseRuntimeActionOptions + useRuntimeAction, UseRuntimeActionOptions, + useStudioUnsupportedNodeMeasurement

    src/runtime/indicatorRefreshCoordinator.ts IndicatorRefreshCoordinator

    +
    +

    src/runtime/indicatorSettleCoordinator.test.ts

    +

    Referenced module

    +

    + Dependencies: + src/runtime/indicatorSettleCoordinator.ts +

    +

    Exports: None

    +
    +
    +

    src/runtime/indicatorSettleCoordinator.ts

    +

    Referenced module

    +

    Dependencies: None

    +

    + Exports: createIndicatorSettleCoordinator, + IndicatorSettleCoordinator, + IndicatorSettleCoordinatorOptions +

    +
    src/runtime/stationarySelection.test.ts

    src/runtime/stationarySelection.ts

    Referenced module

    Dependencies: src/runtime/indicatorRefreshCoordinator.ts, + src/runtime/indicatorSettleCoordinator.ts, src/runtime/stationarySelectionCoordinator.ts, src/runtime/stationarySelectionInputState.ts

    @@ -4086,7 +4206,7 @@

    src/runtime/stationarySelection.ts

    Exports: CommitSelectionResult, createStudioStationarySelectionWrapNode, StationarySelectionCoordinator, StationaryTapSelector, - TransactionState + TransactionState, useStudioUnsupportedNodeMeasurement

    Architecture overview package__ankhorage_studio -.-> module_src_host_generatedAdminExpoWeb_smoke_test_ts module_src_host_generatedAdminExpoWeb_smoke_test_ts --> + module_src_host_installedPackageProvenance_ts + module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts @@ -8838,6 +8960,12 @@

    Architecture overview

    module_src_host_http_standalone_ts --> module_src_host_http_serverWithSecrets_ts module_src_host_index_ts["src/host/index.ts"] package__ankhorage_studio -.-> module_src_host_index_ts + module_src_host_installedPackageProvenance_test_ts["src/host/installedPackageProvenance.test.ts"] + package__ankhorage_studio -.-> module_src_host_installedPackageProvenance_test_ts + module_src_host_installedPackageProvenance_test_ts --> + module_src_host_installedPackageProvenance_ts + module_src_host_installedPackageProvenance_ts["src/host/installedPackageProvenance.ts"] + package__ankhorage_studio -.-> module_src_host_installedPackageProvenance_ts module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts["src/host/layout/auth/resolveAuthLayoutPlan.test.ts"] package__ankhorage_studio -.-> module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts @@ -8996,6 +9124,16 @@

    Architecture overview

    module_src_host_orchestrator_projectManager_ts module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_templateRegistry_index_ts + module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts"] + package__ankhorage_studio -.-> + module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts + module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts --> + module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts + module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts --> + module_src_host_orchestrator_templates_ts + module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.ts"] + package__ankhorage_studio -.-> + module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"] package__ankhorage_studio -.-> module_src_host_orchestrator_generatedAppFiles_test_ts @@ -9130,9 +9268,13 @@

    Architecture overview

    module_src_host_orchestrator_templates_test_ts["src/host/orchestrator/templates.test.ts"] package__ankhorage_studio -.-> module_src_host_orchestrator_templates_test_ts module_src_host_orchestrator_templates_test_ts --> + module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts + module_src_host_orchestrator_templates_test_ts --> module_src_host_orchestrator_templates_ts module_src_host_orchestrator_templates_ts["src/host/orchestrator/templates.ts"] package__ankhorage_studio -.-> module_src_host_orchestrator_templates_ts + module_src_host_orchestrator_templates_ts --> + module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts module_src_host_orchestrator_workspaceRuntime_ts["src/host/orchestrator/workspaceRuntime.ts"] package__ankhorage_studio -.-> module_src_host_orchestrator_workspaceRuntime_ts module_src_host_secrets_bunSupabaseVaultClient_ts["src/host/secrets/bunSupabaseVaultClient.ts"] @@ -9330,6 +9472,13 @@

    Architecture overview

    module_src_runtime_indicatorRefreshCoordinator_ts module_src_runtime_indicatorRefreshCoordinator_ts["src/runtime/indicatorRefreshCoordinator.ts"] package__ankhorage_studio -.-> module_src_runtime_indicatorRefreshCoordinator_ts + module_src_runtime_indicatorSettleCoordinator_test_ts["src/runtime/indicatorSettleCoordinator.test.ts"] + package__ankhorage_studio -.-> + module_src_runtime_indicatorSettleCoordinator_test_ts + module_src_runtime_indicatorSettleCoordinator_test_ts --> + module_src_runtime_indicatorSettleCoordinator_ts + module_src_runtime_indicatorSettleCoordinator_ts["src/runtime/indicatorSettleCoordinator.ts"] + package__ankhorage_studio -.-> module_src_runtime_indicatorSettleCoordinator_ts module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"] package__ankhorage_studio -.-> module_src_runtime_interactionPolicy_test_ts module_src_runtime_interactionPolicy_test_ts --> @@ -9368,6 +9517,8 @@

    Architecture overview

    module_src_runtime_stationarySelection_ts --> module_src_runtime_indicatorRefreshCoordinator_ts module_src_runtime_stationarySelection_ts --> + module_src_runtime_indicatorSettleCoordinator_ts + module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionCoordinator_ts module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionInputState_ts @@ -9790,6 +9941,7 @@

    Architecture overview

    module_src_host_createStudioHost_ts --> module_src_host_orchestrator_projectManager_ts module_src_host_generatedAdminExpoWeb_smoke_test_ts["src/host/generatedAdminExpoWeb.smoke.test.ts"] package__ankhorage_studio -.-> module_src_host_generatedAdminExpoWeb_smoke_test_ts + module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_installedPackageProvenance_ts module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_semverRange_ts @@ -9850,6 +10002,11 @@

    Architecture overview

    module_src_host_http_standalone_ts --> module_src_host_http_serverWithSecrets_ts module_src_host_index_ts["src/host/index.ts"] package__ankhorage_studio -.-> module_src_host_index_ts + module_src_host_installedPackageProvenance_test_ts["src/host/installedPackageProvenance.test.ts"] + package__ankhorage_studio -.-> module_src_host_installedPackageProvenance_test_ts + module_src_host_installedPackageProvenance_test_ts --> module_src_host_installedPackageProvenance_ts + module_src_host_installedPackageProvenance_ts["src/host/installedPackageProvenance.ts"] + package__ankhorage_studio -.-> module_src_host_installedPackageProvenance_ts module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts["src/host/layout/auth/resolveAuthLayoutPlan.test.ts"] package__ankhorage_studio -.-> module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts --> module_src_host_layout_auth_resolveAuthLayoutPlan_ts @@ -9961,6 +10118,12 @@

    Architecture overview

    module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_templateRegistry_index_ts + module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts"] + package__ankhorage_studio -.-> module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts + module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts --> module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts + module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts --> module_src_host_orchestrator_templates_ts + module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.ts"] + package__ankhorage_studio -.-> module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"] package__ankhorage_studio -.-> module_src_host_orchestrator_generatedAppFiles_test_ts module_src_host_orchestrator_generatedAppFiles_test_ts --> module_src_host_orchestrator_generatedAppFiles_ts @@ -10050,9 +10213,11 @@

    Architecture overview

    module_src_host_orchestrator_studioInfraUp_ts --> module_src_host_secrets_trustedOAuthInfraEnvironment_ts module_src_host_orchestrator_templates_test_ts["src/host/orchestrator/templates.test.ts"] package__ankhorage_studio -.-> module_src_host_orchestrator_templates_test_ts + module_src_host_orchestrator_templates_test_ts --> module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts module_src_host_orchestrator_templates_test_ts --> module_src_host_orchestrator_templates_ts module_src_host_orchestrator_templates_ts["src/host/orchestrator/templates.ts"] package__ankhorage_studio -.-> module_src_host_orchestrator_templates_ts + module_src_host_orchestrator_templates_ts --> module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts module_src_host_orchestrator_workspaceRuntime_ts["src/host/orchestrator/workspaceRuntime.ts"] package__ankhorage_studio -.-> module_src_host_orchestrator_workspaceRuntime_ts module_src_host_secrets_bunSupabaseVaultClient_ts["src/host/secrets/bunSupabaseVaultClient.ts"] @@ -10222,6 +10387,11 @@

    Architecture overview

    module_src_runtime_indicatorRefreshCoordinator_test_ts --> module_src_runtime_indicatorRefreshCoordinator_ts module_src_runtime_indicatorRefreshCoordinator_ts["src/runtime/indicatorRefreshCoordinator.ts"] package__ankhorage_studio -.-> module_src_runtime_indicatorRefreshCoordinator_ts + module_src_runtime_indicatorSettleCoordinator_test_ts["src/runtime/indicatorSettleCoordinator.test.ts"] + package__ankhorage_studio -.-> module_src_runtime_indicatorSettleCoordinator_test_ts + module_src_runtime_indicatorSettleCoordinator_test_ts --> module_src_runtime_indicatorSettleCoordinator_ts + module_src_runtime_indicatorSettleCoordinator_ts["src/runtime/indicatorSettleCoordinator.ts"] + package__ankhorage_studio -.-> module_src_runtime_indicatorSettleCoordinator_ts module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"] package__ankhorage_studio -.-> module_src_runtime_interactionPolicy_test_ts module_src_runtime_interactionPolicy_test_ts --> module_src_runtime_interactionPolicyCore_ts @@ -10252,6 +10422,7 @@

    Architecture overview

    module_src_runtime_stationarySelection_ts["src/runtime/stationarySelection.ts"] package__ankhorage_studio -.-> module_src_runtime_stationarySelection_ts module_src_runtime_stationarySelection_ts --> module_src_runtime_indicatorRefreshCoordinator_ts + module_src_runtime_stationarySelection_ts --> module_src_runtime_indicatorSettleCoordinator_ts module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionCoordinator_ts module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionInputState_ts module_src_runtime_stationarySelectionCoordinator_test_ts["src/runtime/stationarySelectionCoordinator.test.ts"] @@ -10511,6 +10682,8 @@

    Module relationships

    module_src_host_http_serverWithSecrets_ts["src/host/http/serverWithSecrets.ts"] module_src_host_http_standalone_ts["src/host/http/standalone.ts"] module_src_host_index_ts["src/host/index.ts"] + module_src_host_installedPackageProvenance_test_ts["src/host/installedPackageProvenance.test.ts"] + module_src_host_installedPackageProvenance_ts["src/host/installedPackageProvenance.ts"] module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts["src/host/layout/auth/resolveAuthLayoutPlan.test.ts"] module_src_host_layout_auth_resolveAuthLayoutPlan_ts["src/host/layout/auth/resolveAuthLayoutPlan.ts"] module_src_host_layout_authRootBootstrap_test_ts["src/host/layout/authRootBootstrap.test.ts"] @@ -10545,6 +10718,8 @@

    Module relationships

    module_src_host_modules_layout_ts["src/host/modules/layout.ts"] module_src_host_modules_runtime_LocalFsTargetAdapter_ts["src/host/modules/runtime/LocalFsTargetAdapter.ts"] module_src_host_oauthFixtureConsumer_smoke_test_ts["src/host/oauthFixtureConsumer.smoke.test.ts"] + module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts"] + module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.ts"] module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"] module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"] module_src_host_orchestrator_generatedRouteCleanup_ts["src/host/orchestrator/generatedRouteCleanup.ts"] @@ -10624,6 +10799,8 @@

    Module relationships

    module_src_runtime_index_ts["src/runtime/index.ts"] module_src_runtime_indicatorRefreshCoordinator_test_ts["src/runtime/indicatorRefreshCoordinator.test.ts"] module_src_runtime_indicatorRefreshCoordinator_ts["src/runtime/indicatorRefreshCoordinator.ts"] + module_src_runtime_indicatorSettleCoordinator_test_ts["src/runtime/indicatorSettleCoordinator.test.ts"] + module_src_runtime_indicatorSettleCoordinator_ts["src/runtime/indicatorSettleCoordinator.ts"] module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"] module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"] module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"] @@ -10791,6 +10968,8 @@

    Module relationships

    module_src_host_createStudioHost_ts --> module_src_host_orchestrator_projectManager_ts module_src_host_generatedAdminExpoWeb_smoke_test_ts --> + module_src_host_installedPackageProvenance_ts + module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts @@ -10844,6 +11023,8 @@

    Module relationships

    module_src_host_http_serverWithSecrets_ts --> module_src_host_utils_workspaceRoot_ts module_src_host_http_standalone_ts --> module_src_host_http_serverWithSecrets_ts + module_src_host_installedPackageProvenance_test_ts --> + module_src_host_installedPackageProvenance_ts module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts --> module_src_host_layout_auth_resolveAuthLayoutPlan_ts module_src_host_layout_authRootBootstrap_test_ts --> @@ -10921,6 +11102,10 @@

    Module relationships

    module_src_host_orchestrator_projectManager_ts module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_templateRegistry_index_ts + module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts --> + module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts + module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts --> + module_src_host_orchestrator_templates_ts module_src_host_orchestrator_generatedAppFiles_test_ts --> module_src_host_orchestrator_generatedAppFiles_ts module_src_host_orchestrator_generatedAppFiles_ts --> @@ -11004,7 +11189,10 @@

    Module relationships

    module_src_host_orchestrator_studioInfraUp_ts --> module_src_host_secrets_trustedOAuthInfraEnvironment_ts module_src_host_orchestrator_templates_test_ts --> - module_src_host_orchestrator_templates_ts + module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts + module_src_host_orchestrator_templates_test_ts --> + module_src_host_orchestrator_templates_ts module_src_host_orchestrator_templates_ts + --> module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts module_src_host_secrets_projectSecretService_test_ts --> module_src_authSettings_ts module_src_host_secrets_projectSecretService_test_ts --> module_src_host_secrets_projectSecretService_ts @@ -11083,6 +11271,8 @@

    Module relationships

    module_src_routeUtils_test_ts --> module_src_routeUtils_ts module_src_runtime_indicatorRefreshCoordinator_test_ts --> module_src_runtime_indicatorRefreshCoordinator_ts + module_src_runtime_indicatorSettleCoordinator_test_ts --> + module_src_runtime_indicatorSettleCoordinator_ts module_src_runtime_interactionPolicy_test_ts --> module_src_runtime_interactionPolicyCore_ts module_src_runtime_interactionPolicy_ts --> module_src_runtime_interactionPolicyCore_ts @@ -11097,6 +11287,8 @@

    Module relationships

    module_src_runtime_previewRegistry_ts module_src_runtime_stationarySelection_ts --> module_src_runtime_indicatorRefreshCoordinator_ts module_src_runtime_stationarySelection_ts --> + module_src_runtime_indicatorSettleCoordinator_ts + module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionCoordinator_ts module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionInputState_ts @@ -11278,6 +11470,8 @@

    Module relationships

    module_src_host_http_serverWithSecrets_ts["src/host/http/serverWithSecrets.ts"] module_src_host_http_standalone_ts["src/host/http/standalone.ts"] module_src_host_index_ts["src/host/index.ts"] + module_src_host_installedPackageProvenance_test_ts["src/host/installedPackageProvenance.test.ts"] + module_src_host_installedPackageProvenance_ts["src/host/installedPackageProvenance.ts"] module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts["src/host/layout/auth/resolveAuthLayoutPlan.test.ts"] module_src_host_layout_auth_resolveAuthLayoutPlan_ts["src/host/layout/auth/resolveAuthLayoutPlan.ts"] module_src_host_layout_authRootBootstrap_test_ts["src/host/layout/authRootBootstrap.test.ts"] @@ -11312,6 +11506,8 @@

    Module relationships

    module_src_host_modules_layout_ts["src/host/modules/layout.ts"] module_src_host_modules_runtime_LocalFsTargetAdapter_ts["src/host/modules/runtime/LocalFsTargetAdapter.ts"] module_src_host_oauthFixtureConsumer_smoke_test_ts["src/host/oauthFixtureConsumer.smoke.test.ts"] + module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts"] + module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.ts"] module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"] module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"] module_src_host_orchestrator_generatedRouteCleanup_ts["src/host/orchestrator/generatedRouteCleanup.ts"] @@ -11391,6 +11587,8 @@

    Module relationships

    module_src_runtime_index_ts["src/runtime/index.ts"] module_src_runtime_indicatorRefreshCoordinator_test_ts["src/runtime/indicatorRefreshCoordinator.test.ts"] module_src_runtime_indicatorRefreshCoordinator_ts["src/runtime/indicatorRefreshCoordinator.ts"] + module_src_runtime_indicatorSettleCoordinator_test_ts["src/runtime/indicatorSettleCoordinator.test.ts"] + module_src_runtime_indicatorSettleCoordinator_ts["src/runtime/indicatorSettleCoordinator.ts"] module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"] module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"] module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"] @@ -11535,6 +11733,7 @@

    Module relationships

    module_src_host_createStudioHost_ts --> module_src_host_orchestrator_infraRuntime_ts module_src_host_createStudioHost_ts --> module_src_host_orchestrator_moduleManager_ts module_src_host_createStudioHost_ts --> module_src_host_orchestrator_projectManager_ts + module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_installedPackageProvenance_ts module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_semverRange_ts @@ -11573,6 +11772,7 @@

    Module relationships

    module_src_host_http_serverWithSecrets_ts --> module_src_host_orchestrator_projectManager_ts module_src_host_http_serverWithSecrets_ts --> module_src_host_utils_workspaceRoot_ts module_src_host_http_standalone_ts --> module_src_host_http_serverWithSecrets_ts + module_src_host_installedPackageProvenance_test_ts --> module_src_host_installedPackageProvenance_ts module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts --> module_src_host_layout_auth_resolveAuthLayoutPlan_ts module_src_host_layout_authRootBootstrap_test_ts --> module_src_host_layout_layoutGenerator_ts module_src_host_layout_generatedImportComposer_test_ts --> module_src_host_layout_generatedImportComposer_ts @@ -11616,6 +11816,8 @@

    Module relationships

    module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_templateRegistry_index_ts + module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts --> module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts + module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts --> module_src_host_orchestrator_templates_ts module_src_host_orchestrator_generatedAppFiles_test_ts --> module_src_host_orchestrator_generatedAppFiles_ts module_src_host_orchestrator_generatedAppFiles_ts --> module_src_host_zoraExtensions_index_ts module_src_host_orchestrator_infraGenerator_ts --> module_src_host_orchestrator_infraValidation_ts @@ -11661,7 +11863,9 @@

    Module relationships

    module_src_host_orchestrator_studioInfraUp_ts --> module_src_host_orchestrator_infraRuntime_ts module_src_host_orchestrator_studioInfraUp_ts --> module_src_host_orchestrator_projectManager_ts module_src_host_orchestrator_studioInfraUp_ts --> module_src_host_secrets_trustedOAuthInfraEnvironment_ts + module_src_host_orchestrator_templates_test_ts --> module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts module_src_host_orchestrator_templates_test_ts --> module_src_host_orchestrator_templates_ts + module_src_host_orchestrator_templates_ts --> module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts module_src_host_secrets_projectSecretService_test_ts --> module_src_authSettings_ts module_src_host_secrets_projectSecretService_test_ts --> module_src_host_secrets_projectSecretService_ts module_src_host_secrets_projectSecretService_ts --> module_src_host_orchestrator_projectManager_ts @@ -11720,6 +11924,7 @@

    Module relationships

    module_src_routeUtils_public_test_ts --> module_src_routeUtils_ts module_src_routeUtils_test_ts --> module_src_routeUtils_ts module_src_runtime_indicatorRefreshCoordinator_test_ts --> module_src_runtime_indicatorRefreshCoordinator_ts + module_src_runtime_indicatorSettleCoordinator_test_ts --> module_src_runtime_indicatorSettleCoordinator_ts module_src_runtime_interactionPolicy_test_ts --> module_src_runtime_interactionPolicyCore_ts module_src_runtime_interactionPolicy_ts --> module_src_runtime_interactionPolicyCore_ts module_src_runtime_previewRegistry_ts --> module_src_runtime_appExtensionRegistry_ts @@ -11728,6 +11933,7 @@

    Module relationships

    module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicyCore_ts module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_previewRegistry_ts module_src_runtime_stationarySelection_ts --> module_src_runtime_indicatorRefreshCoordinator_ts + module_src_runtime_stationarySelection_ts --> module_src_runtime_indicatorSettleCoordinator_ts module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionCoordinator_ts module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionInputState_ts module_src_runtime_stationarySelectionCoordinator_test_ts --> module_src_runtime_stationarySelectionCoordinator_ts @@ -11876,6 +12082,8 @@

    Export graph

    module_src_host_http_serverWithSecrets_ts["src/host/http/serverWithSecrets.ts"] module_src_host_http_standalone_ts["src/host/http/standalone.ts"] module_src_host_index_ts["src/host/index.ts"] + module_src_host_installedPackageProvenance_test_ts["src/host/installedPackageProvenance.test.ts"] + module_src_host_installedPackageProvenance_ts["src/host/installedPackageProvenance.ts"] module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts["src/host/layout/auth/resolveAuthLayoutPlan.test.ts"] module_src_host_layout_auth_resolveAuthLayoutPlan_ts["src/host/layout/auth/resolveAuthLayoutPlan.ts"] module_src_host_layout_authRootBootstrap_test_ts["src/host/layout/authRootBootstrap.test.ts"] @@ -11910,6 +12118,8 @@

    Export graph

    module_src_host_modules_layout_ts["src/host/modules/layout.ts"] module_src_host_modules_runtime_LocalFsTargetAdapter_ts["src/host/modules/runtime/LocalFsTargetAdapter.ts"] module_src_host_oauthFixtureConsumer_smoke_test_ts["src/host/oauthFixtureConsumer.smoke.test.ts"] + module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts"] + module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.ts"] module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"] module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"] module_src_host_orchestrator_generatedRouteCleanup_ts["src/host/orchestrator/generatedRouteCleanup.ts"] @@ -11989,6 +12199,8 @@

    Export graph

    module_src_runtime_index_ts["src/runtime/index.ts"] module_src_runtime_indicatorRefreshCoordinator_test_ts["src/runtime/indicatorRefreshCoordinator.test.ts"] module_src_runtime_indicatorRefreshCoordinator_ts["src/runtime/indicatorRefreshCoordinator.ts"] + module_src_runtime_indicatorSettleCoordinator_test_ts["src/runtime/indicatorSettleCoordinator.test.ts"] + module_src_runtime_indicatorSettleCoordinator_ts["src/runtime/indicatorSettleCoordinator.ts"] module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"] module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"] module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"] @@ -12365,6 +12577,8 @@

    Export graph

    module_src_host_http_serverWithSecrets_ts["src/host/http/serverWithSecrets.ts"] module_src_host_http_standalone_ts["src/host/http/standalone.ts"] module_src_host_index_ts["src/host/index.ts"] + module_src_host_installedPackageProvenance_test_ts["src/host/installedPackageProvenance.test.ts"] + module_src_host_installedPackageProvenance_ts["src/host/installedPackageProvenance.ts"] module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts["src/host/layout/auth/resolveAuthLayoutPlan.test.ts"] module_src_host_layout_auth_resolveAuthLayoutPlan_ts["src/host/layout/auth/resolveAuthLayoutPlan.ts"] module_src_host_layout_authRootBootstrap_test_ts["src/host/layout/authRootBootstrap.test.ts"] @@ -12399,6 +12613,8 @@

    Export graph

    module_src_host_modules_layout_ts["src/host/modules/layout.ts"] module_src_host_modules_runtime_LocalFsTargetAdapter_ts["src/host/modules/runtime/LocalFsTargetAdapter.ts"] module_src_host_oauthFixtureConsumer_smoke_test_ts["src/host/oauthFixtureConsumer.smoke.test.ts"] + module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts"] + module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.ts"] module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"] module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"] module_src_host_orchestrator_generatedRouteCleanup_ts["src/host/orchestrator/generatedRouteCleanup.ts"] @@ -12478,6 +12694,8 @@

    Export graph

    module_src_runtime_index_ts["src/runtime/index.ts"] module_src_runtime_indicatorRefreshCoordinator_test_ts["src/runtime/indicatorRefreshCoordinator.test.ts"] module_src_runtime_indicatorRefreshCoordinator_ts["src/runtime/indicatorRefreshCoordinator.ts"] + module_src_runtime_indicatorSettleCoordinator_test_ts["src/runtime/indicatorSettleCoordinator.test.ts"] + module_src_runtime_indicatorSettleCoordinator_ts["src/runtime/indicatorSettleCoordinator.ts"] module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"] module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"] module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"] @@ -14590,7 +14808,7 @@

    src/host/generatedAdminExpoWeb.smoke.test.ts

    data-search="createAdminSmokeManifest src/host/generatedAdminExpoWeb.smoke.test.ts " >

    createAdminSmokeManifest

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:38:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:43:1

    No description available.

    createAdminSmokeManifest data-search="createScrollableRuntimeScreenRoot src/host/generatedAdminExpoWeb.smoke.test.ts " >

    createScrollableRuntimeScreenRoot

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:94:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:99:1

    +

    No description available.

    +
    +
    +

    visit

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:343:9

    No description available.

    createScrollableRuntimeScreenRoot data-search="verifyDesktopSelectionAndUnsupportedGeometry src/host/generatedAdminExpoWeb.smoke.test.ts " >

    verifyDesktopSelectionAndUnsupportedGeometry

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:272:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:503:1

    +

    No description available.

    +
    +
    +

    expectCapturedLayoutsToMatch

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:708:1

    No description available.

    verifyDesktopSelectionAndUnsupportedGeometry data-search="waitForUnsupportedGeometry src/host/generatedAdminExpoWeb.smoke.test.ts " >

    waitForUnsupportedGeometry

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:452:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:735:1

    No description available.

    waitForUnsupportedGeometry data-search="waitForStudioSmokeState src/host/generatedAdminExpoWeb.smoke.test.ts " >

    waitForStudioSmokeState

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:471:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:754:1

    No description available.

    waitForStudioSmokeState data-search="expectRectToMatch src/host/generatedAdminExpoWeb.smoke.test.ts " >

    expectRectToMatch

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:490:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:773:1

    No description available.

    expectRectToMatch data-search="rectsOverlap src/host/generatedAdminExpoWeb.smoke.test.ts " >

    rectsOverlap

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:497:1

    -

    No description available.

    -
    -
    -

    isPathInside

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:504:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:780:1

    No description available.

    isPathInside data-search="createGeneratedAdminProject src/host/generatedAdminExpoWeb.smoke.test.ts " >

    createGeneratedAdminProject

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:509:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:787:1

    No description available.

    createGeneratedAdminProject data-search="installGeneratedProjectDependencies src/host/generatedAdminExpoWeb.smoke.test.ts " >

    installGeneratedProjectDependencies

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:547:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:825:1

    No description available.

    installGeneratedProjectDependencies data-search="stageLocalStudioPackage src/host/generatedAdminExpoWeb.smoke.test.ts " >

    stageLocalStudioPackage

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:581:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:859:1

    No description available.

    stageLocalStudioPackage data-search="writeSmokeRuntimeExtensions src/host/generatedAdminExpoWeb.smoke.test.ts " >

    writeSmokeRuntimeExtensions

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:610:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:888:1

    No description available.

    writeSmokeRuntimeExtensions data-search="writeSmokeMetroConfig src/host/generatedAdminExpoWeb.smoke.test.ts " >

    writeSmokeMetroConfig

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:689:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1159:1

    No description available.

    writeSmokeMetroConfig data-search="reservePort src/host/generatedAdminExpoWeb.smoke.test.ts " >

    reservePort

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:712:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1182:1

    No description available.

    reservePort data-search="spawnExpoWeb src/host/generatedAdminExpoWeb.smoke.test.ts " >

    spawnExpoWeb

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:728:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1198:1

    No description available.

    spawnExpoWeb data-search="startSmokeStudioApi src/host/generatedAdminExpoWeb.smoke.test.ts " >

    startSmokeStudioApi

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:750:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1220:1

    No description available.

    startSmokeStudioApi data-search="handleSmokeStudioApiRequest src/host/generatedAdminExpoWeb.smoke.test.ts " >

    handleSmokeStudioApiRequest

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:775:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1245:1

    No description available.

    handleSmokeStudioApiRequest data-search="listenOnLocalhost src/host/generatedAdminExpoWeb.smoke.test.ts " >

    listenOnLocalhost

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:815:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1285:1

    No description available.

    listenOnLocalhost data-search="closeHttpServer src/host/generatedAdminExpoWeb.smoke.test.ts " >

    closeHttpServer

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:829:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1299:1

    No description available.

    closeHttpServer data-search="readJsonRequest src/host/generatedAdminExpoWeb.smoke.test.ts " >

    readJsonRequest

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:838:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1308:1

    No description available.

    readJsonRequest data-search="writeJsonResponse src/host/generatedAdminExpoWeb.smoke.test.ts " >

    writeJsonResponse

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:854:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1324:1

    No description available.

    writeJsonResponse data-search="writeCorsHeaders src/host/generatedAdminExpoWeb.smoke.test.ts " >

    writeCorsHeaders

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:861:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1331:1

    No description available.

    writeCorsHeaders data-search="waitForExpoWebUrl src/host/generatedAdminExpoWeb.smoke.test.ts " >

    waitForExpoWebUrl

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:867:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1337:1

    No description available.

    waitForExpoWebUrl data-search="waitForHttp src/host/generatedAdminExpoWeb.smoke.test.ts " >

    waitForHttp

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:880:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1350:1

    No description available.

    waitForHttp data-search="waitForBodyText src/host/generatedAdminExpoWeb.smoke.test.ts " >

    waitForBodyText

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:897:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1367:1

    No description available.

    waitForBodyText data-search="resolveChromePath src/host/generatedAdminExpoWeb.smoke.test.ts " >

    resolveChromePath

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:914:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1384:1

    No description available.

    resolveChromePath data-search="readEnvString src/host/generatedAdminExpoWeb.smoke.test.ts " >

    readEnvString

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:920:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1390:1

    No description available.

    readEnvString data-search="spawnChrome src/host/generatedAdminExpoWeb.smoke.test.ts " >

    spawnChrome

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:926:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1396:1

    No description available.

    spawnChrome data-search="openChromePage src/host/generatedAdminExpoWeb.smoke.test.ts " >

    openChromePage

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:942:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1412:1

    No description available.

    openChromePage data-search="parseChromeProtocolMessage src/host/generatedAdminExpoWeb.smoke.test.ts " >

    parseChromeProtocolMessage

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:1252:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1732:1

    No description available.

    parseChromeProtocolMessage data-search="isRecord src/host/generatedAdminExpoWeb.smoke.test.ts " >

    isRecord

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:1267:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1747:1

    No description available.

    isRecord data-search="stopProcess src/host/generatedAdminExpoWeb.smoke.test.ts " >

    stopProcess

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:1271:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1751:1

    No description available.

    stopProcess data-search="collectProcessOutput src/host/generatedAdminExpoWeb.smoke.test.ts " >

    collectProcessOutput

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:1280:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1760:1

    No description available.

    collectProcessOutput data-search="formatProcessOutput src/host/generatedAdminExpoWeb.smoke.test.ts " >

    formatProcessOutput

    -

    src/host/generatedAdminExpoWeb.smoke.test.ts:1291:1

    +

    src/host/generatedAdminExpoWeb.smoke.test.ts:1771:1

    No description available.

    @@ -15148,6 +15371,58 @@

    startStudioHostServerWithSecrets

    + +
    src/host/orchestrator/templates.ts data-search="serializeStringLiteral src/host/orchestrator/templates.ts " >

    serializeStringLiteral

    -

    src/host/orchestrator/templates.ts:97:1

    +

    src/host/orchestrator/templates.ts:94:1

    No description available.

    serializeStringLiteral data-search="serializeJsValue src/host/orchestrator/templates.ts " >

    serializeJsValue

    -

    src/host/orchestrator/templates.ts:105:1

    +

    src/host/orchestrator/templates.ts:102:1

    No description available.

    serializeJsValue data-search="serializeSplashScreenPlugin src/host/orchestrator/templates.ts " >

    serializeSplashScreenPlugin

    -

    src/host/orchestrator/templates.ts:147:1

    +

    src/host/orchestrator/templates.ts:144:1

    No description available.

    serializeSplashScreenPlugin data-search="serializeRuntimePlugin src/host/orchestrator/templates.ts " >

    serializeRuntimePlugin

    -

    src/host/orchestrator/templates.ts:161:1

    +

    src/host/orchestrator/templates.ts:158:1

    No description available.

    serializeRuntimePlugin data-search="serializePluginsWithRuntimePlan src/host/orchestrator/templates.ts " >

    serializePluginsWithRuntimePlan

    -

    src/host/orchestrator/templates.ts:174:1

    +

    src/host/orchestrator/templates.ts:171:1

    No description available.

    serializePluginsWithRuntimePlan data-search="createNativeIdentifierSegment src/host/orchestrator/templates.ts " >

    createNativeIdentifierSegment

    -

    src/host/orchestrator/templates.ts:196:1

    +

    src/host/orchestrator/templates.ts:193:1

    No description available.

    createNativeIdentifierSegment data-search="createNativeApplicationId src/host/orchestrator/templates.ts " >

    createNativeApplicationId

    -

    src/host/orchestrator/templates.ts:206:1

    +

    src/host/orchestrator/templates.ts:203:1

    No description available.

    createNativeApplicationId data-search="createExpoScheme src/host/orchestrator/templates.ts " >

    createExpoScheme

    -

    src/host/orchestrator/templates.ts:210:1

    +

    src/host/orchestrator/templates.ts:207:1

    No description available.

    createExpoScheme data-search="serializeAndroidConfig src/host/orchestrator/templates.ts " >

    serializeAndroidConfig

    -

    src/host/orchestrator/templates.ts:215:1

    +

    src/host/orchestrator/templates.ts:212:1

    No description available.

    getAppConfigTs

    -

    src/host/orchestrator/templates.ts:229:1

    +

    src/host/orchestrator/templates.ts:226:1

    No description available.

    getAppConfigTs data-search="getMetroConfigJs src/host/orchestrator/templates.ts " >

    getMetroConfigJs

    -

    src/host/orchestrator/templates.ts:265:1

    +

    src/host/orchestrator/templates.ts:262:1

    No description available.

    getMetroConfigJs data-search="getBabelConfigJs src/host/orchestrator/templates.ts " >

    getBabelConfigJs

    -

    src/host/orchestrator/templates.ts:273:1

    +

    src/host/orchestrator/templates.ts:270:1

    No description available.

    getPackageJson

    -

    src/host/orchestrator/templates.ts:297:1

    +

    src/host/orchestrator/templates.ts:294:1

    No description available.

    getPackageJson data-search="getEslintConfigMjs src/host/orchestrator/templates.ts " >

    getEslintConfigMjs

    -

    src/host/orchestrator/templates.ts:386:1

    +

    src/host/orchestrator/templates.ts:383:1

    No description available.

    getPrettierRcJs

    -

    src/host/orchestrator/templates.ts:410:1

    +

    src/host/orchestrator/templates.ts:407:1

    No description available.

    getIndexJs

    -

    src/host/orchestrator/templates.ts:415:1

    +

    src/host/orchestrator/templates.ts:412:1

    No description available.

    getTsConfigJson

    -

    src/host/orchestrator/templates.ts:430:1

    +

    src/host/orchestrator/templates.ts:427:1

    No description available.

    @@ -19421,6 +19696,71 @@

    cancelPendingRefresh

    + +
    src/runtime/stationarySelection.ts data-search="isWebElementLike src/runtime/stationarySelection.ts " >

    isWebElementLike

    -

    src/runtime/stationarySelection.ts:75:1

    +

    src/runtime/stationarySelection.ts:89:1

    No description available.

    isWebElementLike data-search="getWebResizeTarget src/runtime/stationarySelection.ts " >

    getWebResizeTarget

    -

    src/runtime/stationarySelection.ts:85:1

    +

    src/runtime/stationarySelection.ts:99:1

    No description available.

    getWebResizeTarget data-search="getWebDescendantResizeTargets src/runtime/stationarySelection.ts " >

    getWebDescendantResizeTargets

    -

    src/runtime/stationarySelection.ts:89:1

    +

    src/runtime/stationarySelection.ts:103:1

    No description available.

    toMeasuredRect

    -

    src/runtime/stationarySelection.ts:100:1

    +

    src/runtime/stationarySelection.ts:114:1

    No description available.

    unionRects

    -

    src/runtime/stationarySelection.ts:109:1

    +

    src/runtime/stationarySelection.ts:123:1

    No description available.

    unionRects data-search="measureRenderedBoxes src/runtime/stationarySelection.ts " >

    measureRenderedBoxes

    -

    src/runtime/stationarySelection.ts:127:1

    +

    src/runtime/stationarySelection.ts:141:1

    No description available.

    measureRenderedBoxes data-search="measureNativeView src/runtime/stationarySelection.ts " >

    measureNativeView

    -

    src/runtime/stationarySelection.ts:136:1

    +

    src/runtime/stationarySelection.ts:150:1

    No description available.

    measureRootView

    -

    src/runtime/stationarySelection.ts:144:1

    +

    src/runtime/stationarySelection.ts:158:1

    No description available.

    -

    measureUnsupportedView

    -

    src/runtime/stationarySelection.ts:156:1

    +

    measureUnsupportedWebView

    +

    src/runtime/stationarySelection.ts:170:1

    No description available.

    measureUnsupportedView data-search="areIndicatorRectsEqual src/runtime/stationarySelection.ts " >

    areIndicatorRectsEqual

    -

    src/runtime/stationarySelection.ts:170:1

    +

    src/runtime/stationarySelection.ts:184:1

    +

    No description available.

    +
    +
    +

    useStudioUnsupportedNodeMeasurement

    +

    src/runtime/stationarySelection.ts:207:1

    No description available.

    areIndicatorRectsEqual data-search="StudioNodeTouchRecorder src/runtime/stationarySelection.ts " >

    StudioNodeTouchRecorder

    -

    src/runtime/stationarySelection.ts:193:1

    +

    src/runtime/stationarySelection.ts:240:1

    No description available.

    StudioNodeTouchRecorder data-search="recordInteraction src/runtime/stationarySelection.ts " >

    recordInteraction

    -

    src/runtime/stationarySelection.ts:204:3

    +

    src/runtime/stationarySelection.ts:251:3

    No description available.

    recordInteraction data-search="handlePointerDown src/runtime/stationarySelection.ts " >

    handlePointerDown

    -

    src/runtime/stationarySelection.ts:218:3

    +

    src/runtime/stationarySelection.ts:265:3

    No description available.

    handlePointerDown data-search="handleTouchStart src/runtime/stationarySelection.ts " >

    handleTouchStart

    -

    src/runtime/stationarySelection.ts:229:3

    +

    src/runtime/stationarySelection.ts:276:3

    No description available.

    handleTouchStart data-search="handleInteractionCompletion src/runtime/stationarySelection.ts " >

    handleInteractionCompletion

    -

    src/runtime/stationarySelection.ts:238:3

    +

    src/runtime/stationarySelection.ts:285:3

    No description available.

    handleLayout

    -

    src/runtime/stationarySelection.ts:242:3

    +

    src/runtime/stationarySelection.ts:289:3

    No description available.

    handleLayout data-search="createStudioStationarySelectionWrapNode src/runtime/stationarySelection.ts " >

    createStudioStationarySelectionWrapNode

    -

    src/runtime/stationarySelection.ts:288:1

    +

    src/runtime/stationarySelection.ts:353:1

    No description available.

    createStudioStationarySelectionWrapNode data-search="StationaryTapSelector src/runtime/stationarySelection.ts " >

    StationaryTapSelector

    -

    src/runtime/stationarySelection.ts:322:1

    +

    src/runtime/stationarySelection.ts:387:1

    No description available.

    handleScroll

    -

    src/runtime/stationarySelection.ts:517:11

    +

    src/runtime/stationarySelection.ts:603:11

    No description available.

    handleResize

    -

    src/runtime/stationarySelection.ts:518:11

    +

    src/runtime/stationarySelection.ts:604:11

    No description available.

    diff --git a/paradox/paradox.json b/paradox/paradox.json index e975355c..b53ddd6a 100644 --- a/paradox/paradox.json +++ b/paradox/paradox.json @@ -596,6 +596,7 @@ "path": "src/host/generatedAdminExpoWeb.smoke.test.ts", "isEntrypoint": false, "dependencies": [ + "src/host/installedPackageProvenance.ts", "src/host/orchestrator/moduleManager.ts", "src/host/orchestrator/projectManager.ts", "src/host/orchestrator/semverRange.ts", @@ -737,6 +738,23 @@ "StudioHost" ] }, + { + "path": "src/host/installedPackageProvenance.test.ts", + "isEntrypoint": false, + "dependencies": ["src/host/installedPackageProvenance.ts"], + "exports": [] + }, + { + "path": "src/host/installedPackageProvenance.ts", + "isEntrypoint": false, + "dependencies": [], + "exports": [ + "bunLockfileReferencesPackageVersion", + "InstalledPackageProvenance", + "isPathInsideResolved", + "resolveInstalledPackageProvenance" + ] + }, { "path": "src/host/layout/auth/resolveAuthLayoutPlan.test.ts", "isEntrypoint": false, @@ -1012,6 +1030,21 @@ ], "exports": [] }, + { + "path": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts", + "isEntrypoint": false, + "dependencies": [ + "src/host/orchestrator/expoSdk54AnimationCompatibility.ts", + "src/host/orchestrator/templates.ts" + ], + "exports": [] + }, + { + "path": "src/host/orchestrator/expoSdk54AnimationCompatibility.ts", + "isEntrypoint": false, + "dependencies": [], + "exports": ["EXPO_SDK_54_ANIMATION_COMPATIBILITY"] + }, { "path": "src/host/orchestrator/generatedAppFiles.test.ts", "isEntrypoint": false, @@ -1203,15 +1236,17 @@ { "path": "src/host/orchestrator/templates.test.ts", "isEntrypoint": false, - "dependencies": ["src/host/orchestrator/templates.ts"], + "dependencies": [ + "src/host/orchestrator/expoSdk54AnimationCompatibility.ts", + "src/host/orchestrator/templates.ts" + ], "exports": [] }, { "path": "src/host/orchestrator/templates.ts", "isEntrypoint": false, - "dependencies": [], + "dependencies": ["src/host/orchestrator/expoSdk54AnimationCompatibility.ts"], "exports": [ - "GENERATED_ANIMATION_DEPENDENCY_PAIR", "GeneratedAuthProvider", "GeneratedStorageProvider", "getAppConfigTs", @@ -1970,7 +2005,8 @@ "ThirdPartyComponentSupport", "TransactionState", "useRuntimeAction", - "UseRuntimeActionOptions" + "UseRuntimeActionOptions", + "useStudioUnsupportedNodeMeasurement" ] }, { @@ -1989,6 +2025,22 @@ "IndicatorRefreshCoordinator" ] }, + { + "path": "src/runtime/indicatorSettleCoordinator.test.ts", + "isEntrypoint": false, + "dependencies": ["src/runtime/indicatorSettleCoordinator.ts"], + "exports": [] + }, + { + "path": "src/runtime/indicatorSettleCoordinator.ts", + "isEntrypoint": false, + "dependencies": [], + "exports": [ + "createIndicatorSettleCoordinator", + "IndicatorSettleCoordinator", + "IndicatorSettleCoordinatorOptions" + ] + }, { "path": "src/runtime/interactionPolicy.test.ts", "isEntrypoint": false, @@ -2081,6 +2133,7 @@ "isEntrypoint": false, "dependencies": [ "src/runtime/indicatorRefreshCoordinator.ts", + "src/runtime/indicatorSettleCoordinator.ts", "src/runtime/stationarySelectionCoordinator.ts", "src/runtime/stationarySelectionInputState.ts" ], @@ -2089,7 +2142,8 @@ "createStudioStationarySelectionWrapNode", "StationarySelectionCoordinator", "StationaryTapSelector", - "TransactionState" + "TransactionState", + "useStudioUnsupportedNodeMeasurement" ] }, { @@ -7995,7 +8049,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 38, + "line": 43, "column": 1 } }, @@ -8004,61 +8058,70 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 94, + "line": 99, "column": 1 } }, + { + "name": "visit", + "description": null, + "sourceLocation": { + "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", + "line": 343, + "column": 9 + } + }, { "name": "verifyDesktopSelectionAndUnsupportedGeometry", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 272, + "line": 503, "column": 1 } }, { - "name": "waitForUnsupportedGeometry", + "name": "expectCapturedLayoutsToMatch", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 452, + "line": 708, "column": 1 } }, { - "name": "waitForStudioSmokeState", + "name": "waitForUnsupportedGeometry", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 471, + "line": 735, "column": 1 } }, { - "name": "expectRectToMatch", + "name": "waitForStudioSmokeState", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 490, + "line": 754, "column": 1 } }, { - "name": "rectsOverlap", + "name": "expectRectToMatch", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 497, + "line": 773, "column": 1 } }, { - "name": "isPathInside", + "name": "rectsOverlap", "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 504, + "line": 780, "column": 1 } }, @@ -8067,7 +8130,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 509, + "line": 787, "column": 1 } }, @@ -8076,7 +8139,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 547, + "line": 825, "column": 1 } }, @@ -8085,7 +8148,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 581, + "line": 859, "column": 1 } }, @@ -8094,7 +8157,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 610, + "line": 888, "column": 1 } }, @@ -8103,7 +8166,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 689, + "line": 1159, "column": 1 } }, @@ -8112,7 +8175,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 712, + "line": 1182, "column": 1 } }, @@ -8121,7 +8184,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 728, + "line": 1198, "column": 1 } }, @@ -8130,7 +8193,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 750, + "line": 1220, "column": 1 } }, @@ -8139,7 +8202,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 775, + "line": 1245, "column": 1 } }, @@ -8148,7 +8211,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 815, + "line": 1285, "column": 1 } }, @@ -8157,7 +8220,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 829, + "line": 1299, "column": 1 } }, @@ -8166,7 +8229,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 838, + "line": 1308, "column": 1 } }, @@ -8175,7 +8238,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 854, + "line": 1324, "column": 1 } }, @@ -8184,7 +8247,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 861, + "line": 1331, "column": 1 } }, @@ -8193,7 +8256,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 867, + "line": 1337, "column": 1 } }, @@ -8202,7 +8265,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 880, + "line": 1350, "column": 1 } }, @@ -8211,7 +8274,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 897, + "line": 1367, "column": 1 } }, @@ -8220,7 +8283,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 914, + "line": 1384, "column": 1 } }, @@ -8229,7 +8292,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 920, + "line": 1390, "column": 1 } }, @@ -8238,7 +8301,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 926, + "line": 1396, "column": 1 } }, @@ -8247,7 +8310,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 942, + "line": 1412, "column": 1 } }, @@ -8256,7 +8319,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 1252, + "line": 1732, "column": 1 } }, @@ -8265,7 +8328,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 1267, + "line": 1747, "column": 1 } }, @@ -8274,7 +8337,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 1271, + "line": 1751, "column": 1 } }, @@ -8283,7 +8346,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 1280, + "line": 1760, "column": 1 } }, @@ -8292,7 +8355,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "line": 1291, + "line": 1771, "column": 1 } }, @@ -8557,6 +8620,42 @@ "column": 1 } }, + { + "name": "createTemporaryRoot", + "description": null, + "sourceLocation": { + "filePath": "src/host/installedPackageProvenance.test.ts", + "line": 23, + "column": 1 + } + }, + { + "name": "isPathInsideResolved", + "description": null, + "sourceLocation": { + "filePath": "src/host/installedPackageProvenance.ts", + "line": 9, + "column": 1 + } + }, + { + "name": "resolveInstalledPackageProvenance", + "description": null, + "sourceLocation": { + "filePath": "src/host/installedPackageProvenance.ts", + "line": 22, + "column": 1 + } + }, + { + "name": "bunLockfileReferencesPackageVersion", + "description": null, + "sourceLocation": { + "filePath": "src/host/installedPackageProvenance.ts", + "line": 37, + "column": 1 + } + }, { "name": "createManifest", "description": null, @@ -10281,7 +10380,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/templates.ts", - "line": 97, + "line": 94, "column": 1 } }, @@ -10290,7 +10389,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/templates.ts", - "line": 105, + "line": 102, "column": 1 } }, @@ -10299,7 +10398,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/templates.ts", - "line": 147, + "line": 144, "column": 1 } }, @@ -10308,7 +10407,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/templates.ts", - "line": 161, + "line": 158, "column": 1 } }, @@ -10317,7 +10416,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/templates.ts", - "line": 174, + "line": 171, "column": 1 } }, @@ -10326,7 +10425,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/templates.ts", - "line": 196, + "line": 193, "column": 1 } }, @@ -10335,7 +10434,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/templates.ts", - "line": 206, + "line": 203, "column": 1 } }, @@ -10344,7 +10443,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/templates.ts", - "line": 210, + "line": 207, "column": 1 } }, @@ -10353,7 +10452,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/templates.ts", - "line": 215, + "line": 212, "column": 1 } }, @@ -10362,7 +10461,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/templates.ts", - "line": 229, + "line": 226, "column": 1 } }, @@ -10371,7 +10470,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/templates.ts", - "line": 265, + "line": 262, "column": 1 } }, @@ -10380,7 +10479,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/templates.ts", - "line": 273, + "line": 270, "column": 1 } }, @@ -10389,7 +10488,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/templates.ts", - "line": 297, + "line": 294, "column": 1 } }, @@ -10398,7 +10497,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/templates.ts", - "line": 386, + "line": 383, "column": 1 } }, @@ -10407,7 +10506,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/templates.ts", - "line": 410, + "line": 407, "column": 1 } }, @@ -10416,7 +10515,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/templates.ts", - "line": 415, + "line": 412, "column": 1 } }, @@ -10425,7 +10524,7 @@ "description": null, "sourceLocation": { "filePath": "src/host/orchestrator/templates.ts", - "line": 430, + "line": 427, "column": 1 } }, @@ -12976,6 +13075,60 @@ "column": 3 } }, + { + "name": "createFakeScheduler", + "description": null, + "sourceLocation": { + "filePath": "src/runtime/indicatorSettleCoordinator.test.ts", + "line": 5, + "column": 1 + } + }, + { + "name": "createIndicatorSettleCoordinator", + "description": null, + "sourceLocation": { + "filePath": "src/runtime/indicatorSettleCoordinator.ts", + "line": 21, + "column": 1 + } + }, + { + "name": "clearPending", + "description": null, + "sourceLocation": { + "filePath": "src/runtime/indicatorSettleCoordinator.ts", + "line": 32, + "column": 3 + } + }, + { + "name": "cancel", + "description": null, + "sourceLocation": { + "filePath": "src/runtime/indicatorSettleCoordinator.ts", + "line": 40, + "column": 3 + } + }, + { + "name": "scheduleNext", + "description": null, + "sourceLocation": { + "filePath": "src/runtime/indicatorSettleCoordinator.ts", + "line": 49, + "column": 3 + } + }, + { + "name": "sampleNext", + "description": null, + "sourceLocation": { + "filePath": "src/runtime/indicatorSettleCoordinator.ts", + "line": 59, + "column": 3 + } + }, { "name": "createStudioInteractionPolicyResolver", "description": null, @@ -13089,7 +13242,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 75, + "line": 89, "column": 1 } }, @@ -13098,7 +13251,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 85, + "line": 99, "column": 1 } }, @@ -13107,7 +13260,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 89, + "line": 103, "column": 1 } }, @@ -13116,7 +13269,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 100, + "line": 114, "column": 1 } }, @@ -13125,7 +13278,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 109, + "line": 123, "column": 1 } }, @@ -13134,7 +13287,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 127, + "line": 141, "column": 1 } }, @@ -13143,7 +13296,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 136, + "line": 150, "column": 1 } }, @@ -13152,16 +13305,16 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 144, + "line": 158, "column": 1 } }, { - "name": "measureUnsupportedView", + "name": "measureUnsupportedWebView", "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 156, + "line": 170, "column": 1 } }, @@ -13170,7 +13323,16 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 170, + "line": 184, + "column": 1 + } + }, + { + "name": "useStudioUnsupportedNodeMeasurement", + "description": null, + "sourceLocation": { + "filePath": "src/runtime/stationarySelection.ts", + "line": 207, "column": 1 } }, @@ -13179,7 +13341,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 193, + "line": 240, "column": 1 } }, @@ -13188,7 +13350,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 204, + "line": 251, "column": 3 } }, @@ -13197,7 +13359,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 218, + "line": 265, "column": 3 } }, @@ -13206,7 +13368,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 229, + "line": 276, "column": 3 } }, @@ -13215,7 +13377,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 238, + "line": 285, "column": 3 } }, @@ -13224,7 +13386,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 242, + "line": 289, "column": 3 } }, @@ -13233,7 +13395,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 288, + "line": 353, "column": 1 } }, @@ -13242,7 +13404,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 322, + "line": 387, "column": 1 } }, @@ -13251,7 +13413,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 517, + "line": 603, "column": 11 } }, @@ -13260,7 +13422,7 @@ "description": null, "sourceLocation": { "filePath": "src/runtime/stationarySelection.ts", - "line": 518, + "line": 604, "column": 11 } }, @@ -15704,6 +15866,11 @@ "toPath": "src/host/orchestrator/projectManager.ts", "sourcePath": "src/host/createStudioHost.ts" }, + { + "fromPath": "src/host/generatedAdminExpoWeb.smoke.test.ts", + "toPath": "src/host/installedPackageProvenance.ts", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, { "fromPath": "src/host/generatedAdminExpoWeb.smoke.test.ts", "toPath": "src/host/orchestrator/moduleManager.ts", @@ -15849,6 +16016,11 @@ "toPath": "src/host/utils/workspaceRoot.ts", "sourcePath": "src/host/index.ts" }, + { + "fromPath": "src/host/installedPackageProvenance.test.ts", + "toPath": "src/host/installedPackageProvenance.ts", + "sourcePath": "src/host/installedPackageProvenance.test.ts" + }, { "fromPath": "src/host/oauthFixtureConsumer.smoke.test.ts", "toPath": "src/host/orchestrator/moduleManager.ts", @@ -15959,6 +16131,11 @@ "toPath": "src/runtime/indicatorRefreshCoordinator.ts", "sourcePath": "src/runtime/indicatorRefreshCoordinator.test.ts" }, + { + "fromPath": "src/runtime/indicatorSettleCoordinator.test.ts", + "toPath": "src/runtime/indicatorSettleCoordinator.ts", + "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts" + }, { "fromPath": "src/runtime/interactionPolicy.test.ts", "toPath": "src/runtime/interactionPolicyCore.ts", @@ -16004,6 +16181,11 @@ "toPath": "src/runtime/indicatorRefreshCoordinator.ts", "sourcePath": "src/runtime/stationarySelection.ts" }, + { + "fromPath": "src/runtime/stationarySelection.ts", + "toPath": "src/runtime/indicatorSettleCoordinator.ts", + "sourcePath": "src/runtime/stationarySelection.ts" + }, { "fromPath": "src/runtime/stationarySelection.ts", "toPath": "src/runtime/stationarySelectionCoordinator.ts", @@ -16319,6 +16501,16 @@ "toPath": "src/host/modules/layout.ts", "sourcePath": "src/host/modules/catalog.ts" }, + { + "fromPath": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts", + "toPath": "src/host/orchestrator/expoSdk54AnimationCompatibility.ts", + "sourcePath": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts" + }, + { + "fromPath": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts", + "toPath": "src/host/orchestrator/templates.ts", + "sourcePath": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts" + }, { "fromPath": "src/host/orchestrator/generatedAppFiles.test.ts", "toPath": "src/host/orchestrator/generatedAppFiles.ts", @@ -16544,11 +16736,21 @@ "toPath": "src/host/orchestrator/projectManager.ts", "sourcePath": "src/host/orchestrator/studioInfraUp.ts" }, + { + "fromPath": "src/host/orchestrator/templates.test.ts", + "toPath": "src/host/orchestrator/expoSdk54AnimationCompatibility.ts", + "sourcePath": "src/host/orchestrator/templates.test.ts" + }, { "fromPath": "src/host/orchestrator/templates.test.ts", "toPath": "src/host/orchestrator/templates.ts", "sourcePath": "src/host/orchestrator/templates.test.ts" }, + { + "fromPath": "src/host/orchestrator/templates.ts", + "toPath": "src/host/orchestrator/expoSdk54AnimationCompatibility.ts", + "sourcePath": "src/host/orchestrator/templates.ts" + }, { "fromPath": "src/host/secrets/projectSecretService.test.ts", "toPath": "src/authSettings.ts", @@ -23291,6 +23493,30 @@ "callExpression": "createScrollableRuntimeScreenRoot", "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" }, + { + "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts", + "toSymbol": "test", + "callExpression": "test", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts", + "toSymbol": "createScrollableRuntimeScreenRoot", + "callExpression": "createScrollableRuntimeScreenRoot", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts", + "toSymbol": "visit", + "callExpression": "visit", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts", + "toSymbol": "expect", + "callExpression": "expect", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, { "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts", "toSymbol": "adminWebSmokeTest", @@ -23341,14 +23567,14 @@ }, { "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "toSymbol": "realpath", - "callExpression": "realpath", + "toSymbol": "resolveInstalledPackageProvenance", + "callExpression": "resolveInstalledPackageProvenance", "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" }, { "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "toSymbol": "expect", - "callExpression": "expect", + "toSymbol": "realpath", + "callExpression": "realpath", "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" }, { @@ -23359,8 +23585,14 @@ }, { "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts", - "toSymbol": "isPathInside", - "callExpression": "isPathInside", + "toSymbol": "isPathInsideResolved", + "callExpression": "isPathInsideResolved", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts", + "toSymbol": "bunLockfileReferencesPackageVersion", + "callExpression": "bunLockfileReferencesPackageVersion", "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" }, { @@ -23531,6 +23763,24 @@ "callExpression": "page.mouseDrag", "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" }, + { + "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry", + "toSymbol": "captureSmokeLayout", + "callExpression": "page.captureSmokeLayout", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry", + "toSymbol": "expectCapturedLayoutsToMatch", + "callExpression": "expectCapturedLayoutsToMatch", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, + { + "fromSymbol": "expectCapturedLayoutsToMatch", + "toSymbol": "expect", + "callExpression": "expect", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, { "fromSymbol": "waitForUnsupportedGeometry", "toSymbol": "readUnsupportedGeometry", @@ -23873,6 +24123,12 @@ "callExpression": "this.evaluate", "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" }, + { + "fromSymbol": "captureSmokeLayout", + "toSymbol": "evaluate", + "callExpression": "this.evaluate", + "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts" + }, { "fromSymbol": "readRuntimeNodeCenter", "toSymbol": "evaluate", @@ -24329,6 +24585,102 @@ "callExpression": "resolveWorkspaceRoot", "sourcePath": "src/host/hostOwnership.test.ts" }, + { + "fromSymbol": "src/host/installedPackageProvenance.test.ts", + "toSymbol": "afterEach", + "callExpression": "afterEach", + "sourcePath": "src/host/installedPackageProvenance.test.ts" + }, + { + "fromSymbol": "src/host/installedPackageProvenance.test.ts", + "toSymbol": "rm", + "callExpression": "rm", + "sourcePath": "src/host/installedPackageProvenance.test.ts" + }, + { + "fromSymbol": "createTemporaryRoot", + "toSymbol": "mkdtemp", + "callExpression": "mkdtemp", + "sourcePath": "src/host/installedPackageProvenance.test.ts" + }, + { + "fromSymbol": "createTemporaryRoot", + "toSymbol": "tmpdir", + "callExpression": "tmpdir", + "sourcePath": "src/host/installedPackageProvenance.test.ts" + }, + { + "fromSymbol": "src/host/installedPackageProvenance.test.ts", + "toSymbol": "describe", + "callExpression": "describe", + "sourcePath": "src/host/installedPackageProvenance.test.ts" + }, + { + "fromSymbol": "src/host/installedPackageProvenance.test.ts", + "toSymbol": "it", + "callExpression": "it", + "sourcePath": "src/host/installedPackageProvenance.test.ts" + }, + { + "fromSymbol": "src/host/installedPackageProvenance.test.ts", + "toSymbol": "createTemporaryRoot", + "callExpression": "createTemporaryRoot", + "sourcePath": "src/host/installedPackageProvenance.test.ts" + }, + { + "fromSymbol": "src/host/installedPackageProvenance.test.ts", + "toSymbol": "mkdir", + "callExpression": "mkdir", + "sourcePath": "src/host/installedPackageProvenance.test.ts" + }, + { + "fromSymbol": "src/host/installedPackageProvenance.test.ts", + "toSymbol": "resolveInstalledPackageProvenance", + "callExpression": "resolveInstalledPackageProvenance", + "sourcePath": "src/host/installedPackageProvenance.test.ts" + }, + { + "fromSymbol": "src/host/installedPackageProvenance.test.ts", + "toSymbol": "expect", + "callExpression": "expect", + "sourcePath": "src/host/installedPackageProvenance.test.ts" + }, + { + "fromSymbol": "src/host/installedPackageProvenance.test.ts", + "toSymbol": "isPathInsideResolved", + "callExpression": "isPathInsideResolved", + "sourcePath": "src/host/installedPackageProvenance.test.ts" + }, + { + "fromSymbol": "src/host/installedPackageProvenance.test.ts", + "toSymbol": "symlink", + "callExpression": "symlink", + "sourcePath": "src/host/installedPackageProvenance.test.ts" + }, + { + "fromSymbol": "src/host/installedPackageProvenance.test.ts", + "toSymbol": "realpath", + "callExpression": "realpath", + "sourcePath": "src/host/installedPackageProvenance.test.ts" + }, + { + "fromSymbol": "src/host/installedPackageProvenance.test.ts", + "toSymbol": "writeFile", + "callExpression": "writeFile", + "sourcePath": "src/host/installedPackageProvenance.test.ts" + }, + { + "fromSymbol": "src/host/installedPackageProvenance.test.ts", + "toSymbol": "bunLockfileReferencesPackageVersion", + "callExpression": "bunLockfileReferencesPackageVersion", + "sourcePath": "src/host/installedPackageProvenance.test.ts" + }, + { + "fromSymbol": "resolveInstalledPackageProvenance", + "toSymbol": "realpath", + "callExpression": "realpath", + "sourcePath": "src/host/installedPackageProvenance.ts" + }, { "fromSymbol": "collectRelativeFiles", "toSymbol": "readdir", @@ -25187,6 +25539,120 @@ "callExpression": "scheduler.cancel", "sourcePath": "src/runtime/indicatorRefreshCoordinator.ts" }, + { + "fromSymbol": "flushNext", + "toSymbol": "__type", + "callExpression": "entry[1]", + "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts" + }, + { + "fromSymbol": "src/runtime/indicatorSettleCoordinator.test.ts", + "toSymbol": "describe", + "callExpression": "describe", + "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts" + }, + { + "fromSymbol": "src/runtime/indicatorSettleCoordinator.test.ts", + "toSymbol": "it", + "callExpression": "it", + "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts" + }, + { + "fromSymbol": "src/runtime/indicatorSettleCoordinator.test.ts", + "toSymbol": "createFakeScheduler", + "callExpression": "createFakeScheduler", + "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts" + }, + { + "fromSymbol": "src/runtime/indicatorSettleCoordinator.test.ts", + "toSymbol": "createIndicatorSettleCoordinator", + "callExpression": "createIndicatorSettleCoordinator", + "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts" + }, + { + "fromSymbol": "src/runtime/indicatorSettleCoordinator.test.ts", + "toSymbol": "expect", + "callExpression": "expect", + "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts" + }, + { + "fromSymbol": "src/runtime/indicatorSettleCoordinator.test.ts", + "toSymbol": "trigger", + "callExpression": "coordinator.trigger", + "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts" + }, + { + "fromSymbol": "src/runtime/indicatorSettleCoordinator.test.ts", + "toSymbol": "isActive", + "callExpression": "coordinator.isActive", + "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts" + }, + { + "fromSymbol": "src/runtime/indicatorSettleCoordinator.test.ts", + "toSymbol": "flushNext", + "callExpression": "scheduler.flushNext", + "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts" + }, + { + "fromSymbol": "src/runtime/indicatorSettleCoordinator.test.ts", + "toSymbol": "cancel", + "callExpression": "coordinator.cancel", + "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts" + }, + { + "fromSymbol": "clearPending", + "toSymbol": "cancel", + "callExpression": "options.scheduler.cancel", + "sourcePath": "src/runtime/indicatorSettleCoordinator.ts" + }, + { + "fromSymbol": "cancel", + "toSymbol": "clearPending", + "callExpression": "clearPending", + "sourcePath": "src/runtime/indicatorSettleCoordinator.ts" + }, + { + "fromSymbol": "scheduleNext", + "toSymbol": "schedule", + "callExpression": "options.scheduler.schedule", + "sourcePath": "src/runtime/indicatorSettleCoordinator.ts" + }, + { + "fromSymbol": "src/runtime/indicatorSettleCoordinator.ts", + "toSymbol": "sampleNext", + "callExpression": "sampleNext", + "sourcePath": "src/runtime/indicatorSettleCoordinator.ts" + }, + { + "fromSymbol": "sampleNext", + "toSymbol": "sample", + "callExpression": "options.sample", + "sourcePath": "src/runtime/indicatorSettleCoordinator.ts" + }, + { + "fromSymbol": "sampleNext", + "toSymbol": "scheduleNext", + "callExpression": "scheduleNext", + "sourcePath": "src/runtime/indicatorSettleCoordinator.ts" + }, + { + "fromSymbol": "sampleNext", + "toSymbol": "cancel", + "callExpression": "cancel", + "sourcePath": "src/runtime/indicatorSettleCoordinator.ts" + }, + { + "fromSymbol": "sampleNext", + "toSymbol": "areEqual", + "callExpression": "options.areEqual", + "sourcePath": "src/runtime/indicatorSettleCoordinator.ts" + }, + { + "fromSymbol": "trigger", + "toSymbol": "scheduleNext", + "callExpression": "scheduleNext", + "sourcePath": "src/runtime/indicatorSettleCoordinator.ts" + }, { "fromSymbol": "src/runtime/interactionPolicy.test.ts", "toSymbol": "describe", @@ -25458,21 +25924,27 @@ "sourcePath": "src/runtime/stationarySelection.ts" }, { - "fromSymbol": "measureUnsupportedView", + "fromSymbol": "measureUnsupportedWebView", "toSymbol": "isWebElementLike", "callExpression": "isWebElementLike", "sourcePath": "src/runtime/stationarySelection.ts" }, { - "fromSymbol": "measureUnsupportedView", + "fromSymbol": "measureUnsupportedWebView", "toSymbol": "unionRects", "callExpression": "unionRects", "sourcePath": "src/runtime/stationarySelection.ts" }, { - "fromSymbol": "measureUnsupportedView", - "toSymbol": "measureNativeView", - "callExpression": "measureNativeView", + "fromSymbol": "src/runtime/stationarySelection.ts", + "toSymbol": "registerView", + "callExpression": "context.registerView", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "src/runtime/stationarySelection.ts", + "toSymbol": "requestRefresh", + "callExpression": "context?.requestRefresh", "sourcePath": "src/runtime/stationarySelection.ts" }, { @@ -25513,8 +25985,20 @@ }, { "fromSymbol": "measure", - "toSymbol": "measureUnsupportedView", - "callExpression": "measureUnsupportedView", + "toSymbol": "measureUnsupportedWebView", + "callExpression": "measureUnsupportedWebView", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "registerView", + "toSymbol": "registerUnsupportedNode", + "callExpression": "ctx.registerUnsupportedNode", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "measure", + "toSymbol": "measureNativeView", + "callExpression": "measureNativeView", "sourcePath": "src/runtime/stationarySelection.ts" }, { @@ -25553,6 +26037,24 @@ "callExpression": "requestIndicatorRefresh", "sourcePath": "src/runtime/stationarySelection.ts" }, + { + "fromSymbol": "src/runtime/stationarySelection.ts", + "toSymbol": "createIndicatorSettleCoordinator", + "callExpression": "createIndicatorSettleCoordinator", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "src/runtime/stationarySelection.ts", + "toSymbol": "trigger", + "callExpression": "settleCoordinatorRef.current.trigger", + "sourcePath": "src/runtime/stationarySelection.ts" + }, + { + "fromSymbol": "src/runtime/stationarySelection.ts", + "toSymbol": "cancel", + "callExpression": "settleCoordinatorRef.current?.cancel", + "sourcePath": "src/runtime/stationarySelection.ts" + }, { "fromSymbol": "completePendingInteraction", "toSymbol": "completePendingInteraction", @@ -25595,12 +26097,6 @@ "callExpression": "areIndicatorRectsEqual", "sourcePath": "src/runtime/stationarySelection.ts" }, - { - "fromSymbol": "src/runtime/stationarySelection.ts", - "toSymbol": "refreshIndicatorRects", - "callExpression": "refreshIndicatorRects", - "sourcePath": "src/runtime/stationarySelection.ts" - }, { "fromSymbol": "src/runtime/stationarySelection.ts", "toSymbol": "cancelPendingRefresh", @@ -27275,6 +27771,42 @@ "callExpression": "parseExpoGoogleFontsModuleConfig", "sourcePath": "src/host/modules/catalog.ts" }, + { + "fromSymbol": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts", + "toSymbol": "describe", + "callExpression": "describe", + "sourcePath": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts" + }, + { + "fromSymbol": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts", + "toSymbol": "it", + "callExpression": "it", + "sourcePath": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts" + }, + { + "fromSymbol": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts", + "toSymbol": "readFile", + "callExpression": "readFile", + "sourcePath": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts" + }, + { + "fromSymbol": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts", + "toSymbol": "getPackageJson", + "callExpression": "getPackageJson", + "sourcePath": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts" + }, + { + "fromSymbol": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts", + "toSymbol": "expect", + "callExpression": "expect", + "sourcePath": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts" + }, + { + "fromSymbol": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts", + "toSymbol": "getBabelConfigJs", + "callExpression": "getBabelConfigJs", + "sourcePath": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts" + }, { "fromSymbol": "src/host/orchestrator/generatedAppFiles.test.ts", "toSymbol": "describe", diff --git a/src/host/generatedAdminExpoWeb.smoke.test.ts b/src/host/generatedAdminExpoWeb.smoke.test.ts index b46e0fad..416b9259 100644 --- a/src/host/generatedAdminExpoWeb.smoke.test.ts +++ b/src/host/generatedAdminExpoWeb.smoke.test.ts @@ -13,6 +13,11 @@ import path from 'node:path'; import type { AppManifest, UiNode } from '@ankhorage/contracts'; import { expect, test } from 'bun:test'; +import { + bunLockfileReferencesPackageVersion, + isPathInsideResolved, + resolveInstalledPackageProvenance, +} from './installedPackageProvenance'; import { ModuleManager } from './orchestrator/moduleManager'; import { ProjectManager } from './orchestrator/projectManager'; import { satisfiesCaretSemverRange } from './orchestrator/semverRange'; @@ -147,6 +152,177 @@ function createScrollableRuntimeScreenRoot(): UiNode { type: 'SmokeStudioProbe', props: {}, }, + { + id: 'native-layout-fixture', + type: 'Box', + props: { + testID: 'native-layout-fixture', + style: { gap: 12 }, + }, + children: [ + { + id: 'native-row-parent', + type: 'SmokeLayoutBox', + props: { + testID: 'native-row-parent', + style: { + alignItems: 'center', + flexDirection: 'row', + height: 72, + width: 320, + }, + }, + children: [ + { + id: 'native-row-sibling-a', + type: 'SmokeLayoutBox', + props: { + label: 'Row A', + testID: 'native-row-sibling-a', + style: { height: 44, width: 70 }, + }, + }, + { + id: 'native-row-target', + type: 'SmokeLayoutBox', + props: { + label: 'Row target', + stateful: true, + testID: 'native-row-target', + style: { height: 52, width: 130 }, + }, + }, + { + id: 'native-row-sibling-b', + type: 'SmokeLayoutBox', + props: { + label: 'Row B', + testID: 'native-row-sibling-b', + style: { height: 40, width: 80 }, + }, + }, + ], + }, + { + id: 'native-flex-parent', + type: 'SmokeLayoutBox', + props: { + testID: 'native-flex-parent', + style: { flexDirection: 'row', height: 64, width: 320 }, + }, + children: [ + { + id: 'native-flex-target', + type: 'SmokeLayoutBox', + props: { + label: 'Flex grow', + testID: 'native-flex-target', + style: { flexGrow: 1, height: 52 }, + }, + }, + { + id: 'native-flex-sibling', + type: 'SmokeLayoutBox', + props: { + label: 'Fixed', + testID: 'native-flex-sibling', + style: { height: 52, width: 88 }, + }, + }, + ], + }, + { + id: 'native-align-parent', + type: 'SmokeLayoutBox', + props: { + testID: 'native-align-parent', + style: { height: 90, width: 320 }, + }, + children: [ + { + id: 'native-align-target', + type: 'SmokeLayoutBox', + props: { + label: 'Align self', + testID: 'native-align-target', + style: { alignSelf: 'flex-end', height: 42, width: 140 }, + }, + }, + ], + }, + { + id: 'native-percentage-parent', + type: 'SmokeLayoutBox', + props: { + testID: 'native-percentage-parent', + style: { height: 60, width: 300 }, + }, + children: [ + { + id: 'native-percentage-target', + type: 'SmokeLayoutBox', + props: { + label: 'Fifty percent', + testID: 'native-percentage-target', + style: { height: 44, width: '50%' }, + }, + }, + ], + }, + { + id: 'native-absolute-parent', + type: 'SmokeLayoutBox', + props: { + testID: 'native-absolute-parent', + style: { height: 110, position: 'relative', width: 320 }, + }, + children: [ + { + id: 'native-absolute-sibling', + type: 'SmokeLayoutBox', + props: { + label: 'Flow sibling', + testID: 'native-absolute-sibling', + style: { height: 40, width: 100 }, + }, + }, + { + id: 'native-absolute-target', + type: 'SmokeLayoutBox', + props: { + label: 'Absolute', + testID: 'native-absolute-target', + style: { + height: 52, + left: 150, + position: 'absolute', + top: 24, + width: 130, + }, + }, + }, + ], + }, + { + id: 'native-nested-scroll', + type: 'SmokeNestedScroll', + props: { + testID: 'native-nested-scroll', + }, + children: [ + ...Array.from({ length: 12 }, (_, index) => ({ + id: `native-nested-scroll-row-${index}`, + type: 'SmokeLayoutBox', + props: { + label: `Nested row ${index + 1}`, + testID: `native-nested-scroll-row-${index}`, + style: { height: 64, width: 260 }, + }, + })), + ], + }, + ], + }, ...Array.from({ length: 16 }, (_, index) => ({ id: `dashboard-runtime-row-${index}`, type: 'Text', @@ -161,10 +337,40 @@ function createScrollableRuntimeScreenRoot(): UiNode { }; } +test('native unsupported layout fixture covers layout-sensitive Runtime relationships', () => { + const root = createScrollableRuntimeScreenRoot(); + const nodes = new Map(); + const visit = (node: UiNode): void => { + nodes.set(node.id, node); + node.children?.forEach(visit); + }; + visit(root); + + expect(nodes.get('native-row-parent')?.children?.length).toBe(3); + expect(nodes.get('native-flex-target')?.props?.style).toMatchObject({ flexGrow: 1 }); + expect(nodes.get('native-align-target')?.props?.style).toMatchObject({ + alignSelf: 'flex-end', + }); + expect(nodes.get('native-percentage-target')?.props?.style).toMatchObject({ + width: '50%', + }); + expect(nodes.get('native-absolute-target')?.props?.style).toMatchObject({ + position: 'absolute', + }); + expect(nodes.get('native-row-target')?.props?.stateful).toBe(true); + expect(nodes.get('native-nested-scroll')?.children?.length).toBe(12); +}); + adminWebSmokeTest( 'loads generated Studio admin routes through Expo web without a theme update loop', async () => { - const workspaceRoot = await mkdtemp(path.join(tmpdir(), 'ankh-admin-web-smoke-')); + const rawPreservedWorkspaceRoot: unknown = process.env.ANKH_STUDIO_ADMIN_WEB_SMOKE_WORKSPACE; + const preservedWorkspaceRoot = + typeof rawPreservedWorkspaceRoot === 'string' && rawPreservedWorkspaceRoot.length > 0 + ? rawPreservedWorkspaceRoot + : undefined; + const workspaceRoot = + preservedWorkspaceRoot ?? (await mkdtemp(path.join(tmpdir(), 'ankh-admin-web-smoke-'))); const debugPort = await reservePort(); let expoProcess: ChildProcessWithoutNullStreams | null = null; let chromeProcess: ChildProcessWithoutNullStreams | null = null; @@ -187,7 +393,11 @@ adminWebSmokeTest( ) as { dependencies?: Record }; const generatedZoraRange = generatedPackage.dependencies?.['@ankhorage/zora']; const installedZoraRoot = path.join(projectRoot, 'node_modules', '@ankhorage', 'zora'); - const resolvedZoraRoot = await realpath(installedZoraRoot); + const { resolvedCandidatePath: resolvedZoraRoot, resolvedWorkspacePath } = + await resolveInstalledPackageProvenance(workspaceRoot, installedZoraRoot); + const resolvedRepositoryZoraRoot = await realpath( + path.join(process.cwd(), 'node_modules', '@ankhorage', 'zora'), + ); const resolvedZoraPackage = JSON.parse( await readFile(path.join(resolvedZoraRoot, 'package.json'), 'utf8'), ) as { version?: string }; @@ -198,11 +408,15 @@ adminWebSmokeTest( expect( satisfiesCaretSemverRange(resolvedZoraPackage.version ?? '', generatedZoraRange ?? ''), ).toBe(true); - expect(isPathInside(workspaceRoot, installedZoraRoot)).toBe(true); - expect(isPathInside(process.cwd(), installedZoraRoot)).toBe(false); - expect(generatedLockfile).toContain( - `"@ankhorage/zora": ["@ankhorage/zora@${resolvedZoraPackage.version}"`, - ); + expect(isPathInsideResolved(resolvedWorkspacePath, resolvedZoraRoot)).toBe(true); + expect(resolvedZoraRoot).not.toBe(resolvedRepositoryZoraRoot); + expect( + bunLockfileReferencesPackageVersion( + generatedLockfile, + '@ankhorage/zora', + resolvedZoraPackage.version ?? '', + ), + ).toBe(true); expect(rootLayout).toContain('function GeneratedZoraThemeConfigSync'); expect(rootLayout).toContain('lastSyncedThemeConfigSignatureRef'); expect(rootLayout).not.toContain('}, [setThemeConfig, themeConfig]);'); @@ -247,7 +461,9 @@ adminWebSmokeTest( await studioApi?.close(); stopProcess(chromeProcess); stopProcess(expoProcess); - await rm(workspaceRoot, { force: true, recursive: true }); + if (!preservedWorkspaceRoot) { + await rm(workspaceRoot, { force: true, recursive: true }); + } } }, TEST_TIMEOUT_MS, @@ -269,6 +485,21 @@ interface UnsupportedGeometrySnapshot { readonly pointerEvents: string; } +interface CapturedLayoutRect { + readonly height: number; + readonly width: number; + readonly x: number; + readonly y: number; +} + +interface CapturedLayoutSnapshot { + readonly instances: Readonly>; + readonly rects: Readonly>; + readonly scrollContentSizes: Readonly< + Record + >; +} + async function verifyDesktopSelectionAndUnsupportedGeometry(page: ChromePage): Promise { const initialGeometry = await waitForUnsupportedGeometry(page, 15_000); @@ -430,25 +661,77 @@ async function verifyDesktopSelectionAndUnsupportedGeometry(page: ChromePage): P 'studio-stationary-selection-root:edit:dashboard-runtime-row-1:3', ); - await page.evaluate(`globalThis.__studioSmokeTogglePreview?.()`); - await waitForStudioSmokeState( - page, - 'mode=preview;selection=dashboard-runtime-row-1;changes=3', - 15_000, + const privateStateBefore = await page.evaluate( + `document.querySelector('[data-testid="native-row-target-private-state"]')?.textContent ?? ''`, + ); + const statefulTarget = await page.readRuntimeNodeCenter('native-row-target'); + await page.touchTap(statefulTarget.x, statefulTarget.y); + await waitForStudioSmokeState(page, 'mode=edit;selection=native-row-target;changes=4', 15_000); + const privateStateAfterInteraction = await page.evaluate( + `document.querySelector('[data-testid="native-row-target-private-state"]')?.textContent ?? ''`, ); + expect(privateStateAfterInteraction).not.toBe(privateStateBefore); + + const editLayoutSnapshot = await page.captureSmokeLayout(); + expect(editLayoutSnapshot.rects['native-row-parent']).not.toBeNull(); + expect(editLayoutSnapshot.rects['native-flex-target']).not.toBeNull(); + expect(editLayoutSnapshot.rects['native-align-target']).not.toBeNull(); + expect(editLayoutSnapshot.rects['native-percentage-target']).not.toBeNull(); + expect(editLayoutSnapshot.rects['native-absolute-target']).not.toBeNull(); + expect(editLayoutSnapshot.scrollContentSizes['native-nested-scroll']?.height).toBeGreaterThan( + 220, + ); + + await page.evaluate(`globalThis.__studioSmokeTogglePreview?.()`); + await waitForStudioSmokeState(page, 'mode=preview;selection=native-row-target;changes=4', 15_000); expect( await page.evaluate( `document.getElementById('studio-unsupported-indicator-unsupported-runtime-target') === null`, ), ).toBe(true); + const previewLayoutSnapshot = await page.captureSmokeLayout(); + expectCapturedLayoutsToMatch(previewLayoutSnapshot, editLayoutSnapshot); + expect( + await page.evaluate( + `document.querySelector('[data-testid="native-row-target-private-state"]')?.textContent ?? ''`, + ), + ).toBe(privateStateAfterInteraction); - await page.mouseClick(desktopTarget.x, desktopTarget.y); + const previewDesktopTarget = await page.readRuntimeNodeCenter('desktop-pointer-target'); + await page.mouseClick(previewDesktopTarget.x, previewDesktopTarget.y); await Bun.sleep(250); expect(await page.readStudioSmokeState()).toBe( - 'mode=preview;selection=dashboard-runtime-row-1;changes=3', + 'mode=preview;selection=native-row-target;changes=4', ); } +function expectCapturedLayoutsToMatch( + actual: CapturedLayoutSnapshot, + expected: CapturedLayoutSnapshot, +): void { + const actualAnchor = actual.rects['native-row-parent']; + const expectedAnchor = expected.rects['native-row-parent']; + expect(actualAnchor).not.toBeNull(); + expect(expectedAnchor).not.toBeNull(); + const viewportOffsetX = actualAnchor && expectedAnchor ? actualAnchor.x - expectedAnchor.x : 0; + const viewportOffsetY = actualAnchor && expectedAnchor ? actualAnchor.y - expectedAnchor.y : 0; + + expect(actual.instances).toEqual(expected.instances); + expect(actual.scrollContentSizes).toEqual(expected.scrollContentSizes); + expect(Object.keys(actual.rects).sort()).toEqual(Object.keys(expected.rects).sort()); + for (const [testID, expectedRect] of Object.entries(expected.rects)) { + const actualRect = actual.rects[testID]; + expect(actualRect == null).toBe(expectedRect == null); + if (!actualRect || !expectedRect) { + continue; + } + expect(Math.abs(actualRect.x - viewportOffsetX - expectedRect.x)).toBeLessThanOrEqual(2); + expect(Math.abs(actualRect.y - viewportOffsetY - expectedRect.y)).toBeLessThanOrEqual(2); + expect(Math.abs(actualRect.width - expectedRect.width)).toBeLessThanOrEqual(2); + expect(Math.abs(actualRect.height - expectedRect.height)).toBeLessThanOrEqual(2); + } +} + async function waitForUnsupportedGeometry( page: ChromePage, timeoutMs: number, @@ -501,11 +784,6 @@ function rectsOverlap(left: BrowserRect, right: BrowserRect): boolean { ); } -function isPathInside(parentPath: string, candidatePath: string): boolean { - const relativePath = path.relative(parentPath, candidatePath); - return relativePath !== '' && !relativePath.startsWith(`..${path.sep}`) && relativePath !== '..'; -} - async function createGeneratedAdminProject(workspaceRoot: string): Promise { await mkdir(path.join(workspaceRoot, 'apps', 'studio'), { recursive: true }); await writeFile( @@ -612,9 +890,89 @@ async function writeSmokeRuntimeExtensions(projectRoot: string): Promise { await writeFile( path.join(generatedRoot, 'SmokeStudioComponents.tsx'), `import { useStudio } from '@ankhorage/studio'; +import { useStudioUnsupportedNodeMeasurement } from '@ankhorage/studio/runtime'; import { Box, Text } from '@ankhorage/zora'; -import React, { useEffect, useRef, useState } from 'react'; -import { View } from 'react-native'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { + Pressable, + ScrollView, + Text as NativeText, + View, + type ViewStyle, +} from 'react-native'; + +type ViewRef = React.ElementRef; + +interface SmokeRect { + readonly height: number; + readonly width: number; + readonly x: number; + readonly y: number; +} + +const smokeMeasurements = new Map Promise>(); +const smokeInstances = new Map(); +const smokeScrollContentSizes = new Map(); +let nextSmokeInstanceId = 1; + +function useSmokeViewRegistration(testID: string | undefined) { + const measurement = useStudioUnsupportedNodeMeasurement(); + const viewRef = useRef(null); + const instanceIdRef = useRef(nextSmokeInstanceId++); + + const setViewRef = useCallback( + (view: ViewRef | null) => { + viewRef.current = view; + measurement.ref(view); + }, + [measurement.ref], + ); + + useEffect(() => { + if (!testID) return; + const measure = () => + new Promise((resolve) => { + const view = viewRef.current; + if (!view) { + resolve(null); + return; + } + view.measureInWindow((x, y, width, height) => { + resolve({ x, y, width, height }); + }); + }); + smokeMeasurements.set(testID, measure); + smokeInstances.set(testID, instanceIdRef.current); + return () => { + if (smokeMeasurements.get(testID) === measure) { + smokeMeasurements.delete(testID); + smokeInstances.delete(testID); + } + }; + }, [testID]); + + return { + instanceId: instanceIdRef.current, + onLayout: measurement.onLayout, + ref: setViewRef, + }; +} + +async function captureSmokeLayout() { + const rects = Object.fromEntries( + await Promise.all( + [...smokeMeasurements.entries()].map(async ([testID, measure]) => [ + testID, + await measure(), + ]), + ), + ); + return { + instances: Object.fromEntries(smokeInstances), + rects, + scrollContentSizes: Object.fromEntries(smokeScrollContentSizes), + }; +} export function SmokeUnsupported({ label, @@ -623,8 +981,11 @@ export function SmokeUnsupported({ label?: string; testID?: string; }) { + const registration = useSmokeViewRegistration(testID); return ( setPrivateCount((count) => count + 1) : undefined} + testID={testID} + style={[ + { + backgroundColor: '#e2e8f0', + borderColor: '#64748b', + borderWidth: 1, + padding: 6, + }, + style, + ]} + > + {label ? {label} : null} + {stateful ? ( + + {\`instance=\${registration.instanceId};private=\${privateCount}\`} + + ) : null} + {children} + + ); +} + +export function SmokeNestedScroll({ + children, + testID, +}: { + children?: React.ReactNode; + testID?: string; +}) { + const registration = useSmokeViewRegistration(testID); + return ( + + { + if (testID) smokeScrollContentSizes.set(testID, { width, height }); + }} + testID={testID ? \`\${testID}-scroll-view\` : undefined} + contentContainerStyle={{ gap: 8, padding: 8 }} + style={{ borderColor: '#94a3b8', borderWidth: 1 }} + > + {children} + + + ); +} + export function SmokeStudioProbe() { const studio = useStudio(); const previousSelectedNodeIdRef = useRef(studio.selectedNodeId); const [selectionChangeCount, setSelectionChangeCount] = useState(0); + const [layoutSnapshot, setLayoutSnapshot] = useState('not-captured'); useEffect(() => { const smokeGlobal = globalThis as typeof globalThis & { + __studioSmokeCaptureLayout?: typeof captureSmokeLayout; __studioSmokeTogglePreview?: () => void; }; + smokeGlobal.__studioSmokeCaptureLayout = captureSmokeLayout; smokeGlobal.__studioSmokeTogglePreview = studio.togglePreviewMode; return () => { + delete smokeGlobal.__studioSmokeCaptureLayout; delete smokeGlobal.__studioSmokeTogglePreview; }; }, [studio.togglePreviewMode]); @@ -664,6 +1102,31 @@ export function SmokeStudioProbe() { {\`mode=\${studio.previewMode ? 'preview' : 'edit'};selection=\${studio.selectedNodeId ?? 'none'};changes=\${selectionChangeCount}\`} + + Toggle Edit Preview + + { + void captureSmokeLayout().then((snapshot) => { + setLayoutSnapshot(JSON.stringify(snapshot)); + }); + }} + testID="studio-smoke-capture-layout" + > + Capture native layout + + + + {layoutSnapshot === 'not-captured' ? layoutSnapshot : 'captured'} + + ); } @@ -673,9 +1136,16 @@ export function SmokeStudioProbe() { await writeFile( path.join(generatedRoot, 'appExtensionRegistry.ts'), `import type { ComponentRegistry } from '@ankhorage/runtime'; -import { SmokeStudioProbe, SmokeUnsupported } from './SmokeStudioComponents'; +import { + SmokeLayoutBox, + SmokeNestedScroll, + SmokeStudioProbe, + SmokeUnsupported, +} from './SmokeStudioComponents'; export const APP_EXTENSION_COMPONENT_REGISTRY: ComponentRegistry = { + SmokeLayoutBox, + SmokeNestedScroll, SmokeStudioProbe, SmokeUnsupported, }; @@ -1032,6 +1502,12 @@ class ChromePage { ); } + async captureSmokeLayout(): Promise { + return this.evaluate( + `globalThis.__studioSmokeCaptureLayout?.() ?? Promise.reject(new Error('Smoke layout capture unavailable'))`, + ); + } + async readRuntimeNodeCenter(nodeId: string): Promise<{ readonly x: number; readonly y: number }> { const encodedNodeId = encodeURIComponent(nodeId); const center = await this.evaluate<{ readonly x: number; readonly y: number } | null>(`(() => { @@ -1051,7 +1527,11 @@ class ChromePage { }; const target = findRenderedElement(wrapper); if (!(target instanceof HTMLElement)) return null; - const rect = target.getBoundingClientRect(); + let rect = target.getBoundingClientRect(); + if (rect.top < 0 || rect.bottom > window.innerHeight) { + target.scrollIntoView({ block: 'center' }); + rect = target.getBoundingClientRect(); + } return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; })()`); if (!center) { diff --git a/src/host/installedPackageProvenance.test.ts b/src/host/installedPackageProvenance.test.ts new file mode 100644 index 00000000..8becb791 --- /dev/null +++ b/src/host/installedPackageProvenance.test.ts @@ -0,0 +1,83 @@ +import { mkdir, mkdtemp, realpath, rm, symlink, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; + +import { afterEach, describe, expect, it } from 'bun:test'; + +import { + bunLockfileReferencesPackageVersion, + isPathInsideResolved, + resolveInstalledPackageProvenance, +} from './installedPackageProvenance'; + +const temporaryRoots: string[] = []; + +afterEach(async () => { + await Promise.all( + temporaryRoots + .splice(0) + .map((temporaryRoot) => rm(temporaryRoot, { force: true, recursive: true })), + ); +}); + +async function createTemporaryRoot(): Promise { + const temporaryRoot = await mkdtemp(path.join(tmpdir(), 'studio-package-provenance-')); + temporaryRoots.push(temporaryRoot); + return temporaryRoot; +} + +describe('installed package provenance', () => { + it('accepts a real installed package directory inside the resolved workspace', async () => { + const temporaryRoot = await createTemporaryRoot(); + const workspaceRoot = path.join(temporaryRoot, 'workspace'); + const packageRoot = path.join(workspaceRoot, 'node_modules', '@ankhorage', 'zora'); + await mkdir(packageRoot, { recursive: true }); + + const provenance = await resolveInstalledPackageProvenance(workspaceRoot, packageRoot); + + expect( + isPathInsideResolved(provenance.resolvedWorkspacePath, provenance.resolvedCandidatePath), + ).toBe(true); + }); + + it('rejects a workspace symlink that resolves to a package outside the workspace', async () => { + const temporaryRoot = await createTemporaryRoot(); + const workspaceRoot = path.join(temporaryRoot, 'workspace'); + const externalPackageRoot = path.join(temporaryRoot, 'external', 'zora'); + const installedPackageRoot = path.join(workspaceRoot, 'node_modules', '@ankhorage', 'zora'); + await mkdir(path.dirname(installedPackageRoot), { recursive: true }); + await mkdir(externalPackageRoot, { recursive: true }); + await symlink(externalPackageRoot, installedPackageRoot); + + const provenance = await resolveInstalledPackageProvenance(workspaceRoot, installedPackageRoot); + + expect( + isPathInsideResolved(provenance.resolvedWorkspacePath, provenance.resolvedCandidatePath), + ).toBe(false); + }); + + it('rejects traversal and sibling paths that only share a string prefix', async () => { + const temporaryRoot = await createTemporaryRoot(); + const workspaceRoot = path.join(temporaryRoot, 'workspace'); + const siblingRoot = path.join(temporaryRoot, 'workspace-copy'); + await mkdir(workspaceRoot, { recursive: true }); + await mkdir(siblingRoot, { recursive: true }); + const [resolvedWorkspaceRoot, resolvedSiblingRoot] = await Promise.all([ + realpath(workspaceRoot), + realpath(path.join(workspaceRoot, '..', 'workspace-copy')), + ]); + + expect(isPathInsideResolved(resolvedWorkspaceRoot, resolvedSiblingRoot)).toBe(false); + expect(resolvedSiblingRoot.startsWith(resolvedWorkspaceRoot)).toBe(true); + }); + + it('matches the exact installed package version in the generated lockfile', async () => { + const temporaryRoot = await createTemporaryRoot(); + const lockfilePath = path.join(temporaryRoot, 'bun.lock'); + const lockfile = '"@ankhorage/zora": ["@ankhorage/zora@2.9.1", "", {}, "sha512-test"]\n'; + await writeFile(lockfilePath, lockfile); + + expect(bunLockfileReferencesPackageVersion(lockfile, '@ankhorage/zora', '2.9.1')).toBe(true); + expect(bunLockfileReferencesPackageVersion(lockfile, '@ankhorage/zora', '2.9.0')).toBe(false); + }); +}); diff --git a/src/host/installedPackageProvenance.ts b/src/host/installedPackageProvenance.ts new file mode 100644 index 00000000..4a039521 --- /dev/null +++ b/src/host/installedPackageProvenance.ts @@ -0,0 +1,45 @@ +import { realpath } from 'node:fs/promises'; +import path from 'node:path'; + +export interface InstalledPackageProvenance { + readonly resolvedCandidatePath: string; + readonly resolvedWorkspacePath: string; +} + +export function isPathInsideResolved( + resolvedParentPath: string, + resolvedCandidatePath: string, +): boolean { + const relativePath = path.relative(resolvedParentPath, resolvedCandidatePath); + return ( + relativePath !== '' && + relativePath !== '..' && + !relativePath.startsWith(`..${path.sep}`) && + !path.isAbsolute(relativePath) + ); +} + +export async function resolveInstalledPackageProvenance( + workspacePath: string, + candidatePath: string, +): Promise { + const [resolvedWorkspacePath, resolvedCandidatePath] = await Promise.all([ + realpath(workspacePath), + realpath(candidatePath), + ]); + + return { + resolvedCandidatePath, + resolvedWorkspacePath, + }; +} + +export function bunLockfileReferencesPackageVersion( + lockfile: string, + packageName: string, + version: string, +): boolean { + return lockfile.includes( + `${JSON.stringify(packageName)}: [${JSON.stringify(`${packageName}@${version}`)}`, + ); +} diff --git a/src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts b/src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts new file mode 100644 index 00000000..05c8925b --- /dev/null +++ b/src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts @@ -0,0 +1,54 @@ +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; + +import { describe, expect, it } from 'bun:test'; + +import { EXPO_SDK_54_ANIMATION_COMPATIBILITY } from './expoSdk54AnimationCompatibility'; +import { getBabelConfigJs, getPackageJson } from './templates'; + +interface StudioAppPackage { + readonly dependencies?: Readonly>; +} + +describe('Expo SDK 54 first-party animation compatibility', () => { + it('keeps generated apps and the supported Studio Expo app on one canonical stack', async () => { + const repositoryRoot = path.resolve(import.meta.dir, '..', '..', '..'); + const studioPackage = JSON.parse( + await readFile(path.join(repositoryRoot, 'apps', 'studio', 'package.json'), 'utf8'), + ) as StudioAppPackage; + const studioBabelConfig = await readFile( + path.join(repositoryRoot, 'apps', 'studio', 'babel.config.js'), + 'utf8', + ); + const generatedPackage = getPackageJson({ + name: 'generated-sdk-54-app', + includeStudio: true, + }); + const generatedDependencies = generatedPackage.dependencies as Record; + const studioDependencies = studioPackage.dependencies ?? {}; + + expect( + generatedDependencies.expo?.startsWith(`~${EXPO_SDK_54_ANIMATION_COMPATIBILITY.expoSdk}.`), + ).toBe(true); + expect( + studioDependencies.expo?.startsWith(`~${EXPO_SDK_54_ANIMATION_COMPATIBILITY.expoSdk}.`), + ).toBe(true); + expect(generatedDependencies['react-native-reanimated']).toBe( + EXPO_SDK_54_ANIMATION_COMPATIBILITY.reanimated, + ); + expect(generatedDependencies['react-native-worklets']).toBe( + EXPO_SDK_54_ANIMATION_COMPATIBILITY.worklets, + ); + expect(studioDependencies['react-native-reanimated']).toBe( + EXPO_SDK_54_ANIMATION_COMPATIBILITY.reanimated, + ); + expect(studioDependencies['react-native-worklets']).toBe( + EXPO_SDK_54_ANIMATION_COMPATIBILITY.worklets, + ); + expect(getBabelConfigJs()).toContain(`'${EXPO_SDK_54_ANIMATION_COMPATIBILITY.babelPlugin}'`); + expect(studioBabelConfig).toContain(`'${EXPO_SDK_54_ANIMATION_COMPATIBILITY.babelPlugin}'`); + expect(studioBabelConfig).not.toContain("'react-native-reanimated/plugin'"); + expect(Object.values(studioDependencies)).not.toContain('~4.1.1'); + expect(Object.values(studioDependencies)).not.toContain('0.5.1'); + }); +}); diff --git a/src/host/orchestrator/expoSdk54AnimationCompatibility.ts b/src/host/orchestrator/expoSdk54AnimationCompatibility.ts new file mode 100644 index 00000000..51fab6e8 --- /dev/null +++ b/src/host/orchestrator/expoSdk54AnimationCompatibility.ts @@ -0,0 +1,6 @@ +export const EXPO_SDK_54_ANIMATION_COMPATIBILITY = { + babelPlugin: 'react-native-worklets/plugin', + expoSdk: '54', + reanimated: '4.3.0', + worklets: '0.8.3', +} as const; diff --git a/src/host/orchestrator/templates.test.ts b/src/host/orchestrator/templates.test.ts index 83fa1be1..d1fa0151 100644 --- a/src/host/orchestrator/templates.test.ts +++ b/src/host/orchestrator/templates.test.ts @@ -1,11 +1,7 @@ import { describe, expect, it } from 'bun:test'; -import { - GENERATED_ANIMATION_DEPENDENCY_PAIR, - getAppConfigTs, - getBabelConfigJs, - getPackageJson, -} from './templates'; +import { EXPO_SDK_54_ANIMATION_COMPATIBILITY } from './expoSdk54AnimationCompatibility'; +import { getAppConfigTs, getBabelConfigJs, getPackageJson } from './templates'; describe('generated OAuth scaffold templates', () => { it('pins the canonical runtime, ZORA, Supabase auth, and Expo persistence dependencies', () => { @@ -34,21 +30,22 @@ describe('generated OAuth scaffold templates', () => { const pkg = getPackageJson({ name: 'native-app', includeStudio: true }); const dependencies = pkg.dependencies as Record; - expect(GENERATED_ANIMATION_DEPENDENCY_PAIR).toEqual({ + expect(EXPO_SDK_54_ANIMATION_COMPATIBILITY).toEqual({ + babelPlugin: 'react-native-worklets/plugin', expoSdk: '54', reanimated: '4.3.0', worklets: '0.8.3', }); expect(dependencies['react-native-reanimated']).toBe( - GENERATED_ANIMATION_DEPENDENCY_PAIR.reanimated, + EXPO_SDK_54_ANIMATION_COMPATIBILITY.reanimated, ); expect(dependencies['react-native-worklets']).toBe( - GENERATED_ANIMATION_DEPENDENCY_PAIR.worklets, + EXPO_SDK_54_ANIMATION_COMPATIBILITY.worklets, ); expect(getPackageJson({ name: 'second-native-app', includeStudio: true }).dependencies).toEqual( pkg.dependencies, ); - expect(getBabelConfigJs()).toContain("'react-native-worklets/plugin'"); + expect(getBabelConfigJs()).toContain(`'${EXPO_SDK_54_ANIMATION_COMPATIBILITY.babelPlugin}'`); expect(getBabelConfigJs()).not.toContain("'react-native-reanimated/plugin'"); }); diff --git a/src/host/orchestrator/templates.ts b/src/host/orchestrator/templates.ts index 024e7ea3..373164a1 100644 --- a/src/host/orchestrator/templates.ts +++ b/src/host/orchestrator/templates.ts @@ -6,6 +6,8 @@ import { resolveExpoRuntimeNativeOutput, } from '@ankhorage/expo-runtime/planning'; +import { EXPO_SDK_54_ANIMATION_COMPATIBILITY } from './expoSdk54AnimationCompatibility.js'; + export type GeneratedAuthProvider = 'supabase' | null; export type GeneratedStorageProvider = 'supabase' | null; const EXPO_MODULES_CORE_VERSION = '~3.0.30'; @@ -15,11 +17,6 @@ const SUPABASE_AUTH_VERSION = '^1.0.0'; const ZORA_VERSION = '^2.9.0'; const EXPO_SECURE_STORE_VERSION = '~15.0.8'; const EXPO_WEB_BROWSER_VERSION = '~15.0.11'; -export const GENERATED_ANIMATION_DEPENDENCY_PAIR = { - expoSdk: '54', - reanimated: '4.3.0', - worklets: '0.8.3', -} as const; const RESERVED_NATIVE_IDENTIFIER_SEGMENTS = new Set( [ @@ -287,7 +284,7 @@ export function getBabelConfigJs() { }, }, ], - 'react-native-worklets/plugin', + '${EXPO_SDK_54_ANIMATION_COMPATIBILITY.babelPlugin}', ], }; }; @@ -362,12 +359,12 @@ export function getPackageJson(args: { 'react-dom': '19.1.0', 'react-native': '0.81.5', 'react-native-gesture-handler': '~2.28.0', - 'react-native-reanimated': GENERATED_ANIMATION_DEPENDENCY_PAIR.reanimated, + 'react-native-reanimated': EXPO_SDK_54_ANIMATION_COMPATIBILITY.reanimated, 'react-native-safe-area-context': '~5.6.0', 'react-native-screens': '~4.16.0', 'react-native-svg': '~15.12.1', 'react-native-web': '^0.21.2', - 'react-native-worklets': GENERATED_ANIMATION_DEPENDENCY_PAIR.worklets, + 'react-native-worklets': EXPO_SDK_54_ANIMATION_COMPATIBILITY.worklets, 'reanimated-color-picker': '^4.2.0', }, devDependencies: { diff --git a/src/runtime/indicatorSettleCoordinator.test.ts b/src/runtime/indicatorSettleCoordinator.test.ts new file mode 100644 index 00000000..6cd0524f --- /dev/null +++ b/src/runtime/indicatorSettleCoordinator.test.ts @@ -0,0 +1,173 @@ +import { describe, expect, it } from 'bun:test'; + +import { createIndicatorSettleCoordinator } from './indicatorSettleCoordinator'; + +function createFakeScheduler() { + let nextHandle = 1; + const callbacks = new Map void>(); + + return { + callbacks, + scheduler: { + cancel: (handle: number) => { + callbacks.delete(handle); + }, + schedule: (callback: () => void, _delayMs: number) => { + const handle = nextHandle++; + callbacks.set(handle, callback); + return handle; + }, + }, + flushNext: async () => { + const entry = callbacks.entries().next().value as readonly [number, () => void] | undefined; + if (!entry) { + return; + } + callbacks.delete(entry[0]); + entry[1](); + await Promise.resolve(); + await Promise.resolve(); + }, + }; +} + +describe('indicator settle coordinator', () => { + it('keeps rapid triggers in one active settle sequence', () => { + const scheduler = createFakeScheduler(); + const coordinator = createIndicatorSettleCoordinator({ + areEqual: Object.is, + intervalMs: 60, + maxSamples: 10, + sample: () => Promise.resolve(1), + scheduler: scheduler.scheduler, + stableSampleCount: 3, + }); + + expect(coordinator.trigger()).toBe(true); + expect(coordinator.trigger()).toBe(false); + expect(coordinator.trigger()).toBe(false); + expect(scheduler.callbacks.size).toBe(1); + expect(coordinator.isActive()).toBe(true); + }); + + it('restarts the same sequence when new input arrives', async () => { + const scheduler = createFakeScheduler(); + let nextSnapshot = 1; + const coordinator = createIndicatorSettleCoordinator({ + areEqual: Object.is, + intervalMs: 60, + maxSamples: 10, + sample: () => Promise.resolve(nextSnapshot), + scheduler: scheduler.scheduler, + stableSampleCount: 2, + }); + + coordinator.trigger(); + await scheduler.flushNext(); + expect(scheduler.callbacks.size).toBe(1); + + nextSnapshot = 2; + expect(coordinator.trigger()).toBe(false); + expect(scheduler.callbacks.size).toBe(1); + await scheduler.flushNext(); + + expect(coordinator.isActive()).toBe(true); + expect(scheduler.callbacks.size).toBe(1); + }); + + it('stops after the required stable samples', async () => { + const scheduler = createFakeScheduler(); + let sampleCount = 0; + const coordinator = createIndicatorSettleCoordinator({ + areEqual: Object.is, + intervalMs: 60, + maxSamples: 10, + sample: () => { + sampleCount += 1; + return Promise.resolve(4); + }, + scheduler: scheduler.scheduler, + stableSampleCount: 3, + }); + + coordinator.trigger(); + await scheduler.flushNext(); + await scheduler.flushNext(); + await scheduler.flushNext(); + + expect(sampleCount).toBe(3); + expect(coordinator.isActive()).toBe(false); + expect(scheduler.callbacks.size).toBe(0); + }); + + it('stops at the maximum bound when geometry never stabilizes', async () => { + const scheduler = createFakeScheduler(); + let snapshot = 0; + const coordinator = createIndicatorSettleCoordinator({ + areEqual: Object.is, + intervalMs: 60, + maxSamples: 4, + sample: () => Promise.resolve(++snapshot), + scheduler: scheduler.scheduler, + stableSampleCount: 3, + }); + + coordinator.trigger(); + await scheduler.flushNext(); + await scheduler.flushNext(); + await scheduler.flushNext(); + await scheduler.flushNext(); + + expect(snapshot).toBe(4); + expect(coordinator.isActive()).toBe(false); + expect(scheduler.callbacks.size).toBe(0); + }); + + it('cancels all pending work for Preview or unmount cleanup', async () => { + const scheduler = createFakeScheduler(); + let sampleCount = 0; + const coordinator = createIndicatorSettleCoordinator({ + areEqual: Object.is, + intervalMs: 60, + maxSamples: 10, + sample: () => Promise.resolve(++sampleCount), + scheduler: scheduler.scheduler, + stableSampleCount: 3, + }); + + coordinator.trigger(); + coordinator.cancel(); + await scheduler.flushNext(); + expect(sampleCount).toBe(0); + expect(coordinator.isActive()).toBe(false); + + coordinator.trigger(); + coordinator.cancel(); + await scheduler.flushNext(); + expect(sampleCount).toBe(0); + expect(scheduler.callbacks.size).toBe(0); + }); + + it('stops without rescheduling when measurement reports no unsupported nodes', async () => { + const scheduler = createFakeScheduler(); + let sampleCount = 0; + const coordinator = createIndicatorSettleCoordinator({ + areEqual: Object.is, + intervalMs: 60, + maxSamples: 10, + sample: () => { + sampleCount += 1; + return Promise.resolve(null); + }, + scheduler: scheduler.scheduler, + stableSampleCount: 3, + }); + + coordinator.trigger(); + await scheduler.flushNext(); + + expect(sampleCount).toBe(1); + expect(coordinator.isActive()).toBe(false); + expect(scheduler.callbacks.size).toBe(0); + }); +}); diff --git a/src/runtime/indicatorSettleCoordinator.ts b/src/runtime/indicatorSettleCoordinator.ts new file mode 100644 index 00000000..162a9596 --- /dev/null +++ b/src/runtime/indicatorSettleCoordinator.ts @@ -0,0 +1,108 @@ +interface IndicatorSettleScheduler { + readonly cancel: (handle: Handle) => void; + readonly schedule: (callback: () => void, delayMs: number) => Handle; +} + +export interface IndicatorSettleCoordinator { + cancel(): void; + isActive(): boolean; + trigger(): boolean; +} + +export interface IndicatorSettleCoordinatorOptions { + readonly areEqual: (left: Snapshot, right: Snapshot) => boolean; + readonly intervalMs: number; + readonly maxSamples: number; + readonly sample: () => Promise; + readonly scheduler: IndicatorSettleScheduler; + readonly stableSampleCount: number; +} + +export function createIndicatorSettleCoordinator( + options: IndicatorSettleCoordinatorOptions, +): IndicatorSettleCoordinator { + let active = false; + let pendingHandle: Handle | null = null; + let sampling = false; + let revision = 0; + let sampleCount = 0; + let stableSampleCount = 0; + let previousSnapshot: Snapshot | null = null; + + function clearPending(): void { + if (pendingHandle === null) { + return; + } + options.scheduler.cancel(pendingHandle); + pendingHandle = null; + } + + function cancel(): void { + active = false; + revision += 1; + sampleCount = 0; + stableSampleCount = 0; + previousSnapshot = null; + clearPending(); + } + + function scheduleNext(delayMs: number): void { + if (!active || sampling || pendingHandle !== null) { + return; + } + pendingHandle = options.scheduler.schedule(() => { + pendingHandle = null; + void sampleNext(); + }, delayMs); + } + + async function sampleNext(): Promise { + if (!active || sampling) { + return; + } + + const sampleRevision = revision; + sampling = true; + const snapshot = await options.sample(); + sampling = false; + + if (sampleRevision !== revision) { + scheduleNext(0); + return; + } + if (snapshot === null) { + cancel(); + return; + } + + sampleCount += 1; + if (previousSnapshot !== null && options.areEqual(previousSnapshot, snapshot)) { + stableSampleCount += 1; + } else { + stableSampleCount = 1; + } + previousSnapshot = snapshot; + + if (stableSampleCount >= options.stableSampleCount || sampleCount >= options.maxSamples) { + cancel(); + return; + } + + scheduleNext(options.intervalMs); + } + + return { + cancel, + isActive: () => active, + trigger: () => { + const wasActive = active; + active = true; + revision += 1; + sampleCount = 0; + stableSampleCount = 0; + previousSnapshot = null; + scheduleNext(0); + return !wasActive; + }, + }; +} diff --git a/src/runtime/stationarySelection.test.ts b/src/runtime/stationarySelection.test.ts index c95ce162..0e09fa78 100644 --- a/src/runtime/stationarySelection.test.ts +++ b/src/runtime/stationarySelection.test.ts @@ -65,16 +65,27 @@ describe('stationarySelection RN integration', () => { expect(source).not.toContain('onResponderReject'); }); - it('uses display contents on web and a retained unstyled native measurement view', () => { - expect(source).toContain("Platform.OS === 'web' || !props.showUnsupportedIndicator"); - expect(source).toContain('collapsable: false'); + it('keeps the per-node recorder layout-neutral on every platform', () => { + expect(source).toContain("style: { display: 'contents' }"); + expect(source).not.toContain('collapsable: false'); + }); + + it('measures unsupported native content through its existing public View ref', () => { + expect(source).toContain('export function useStudioUnsupportedNodeMeasurement()'); + expect(source).toContain('measure: () => measureNativeView(view)'); + expect(source).toContain('UnsupportedNodeMeasurementContext.Provider'); + expect(source).not.toContain('measureUnsupportedView'); }); it('renders root-owned unsupported indicators with pointerEvents none', () => { - expect(source).toContain('...indicatorRects.map((rect) =>'); + expect(source).toContain('...(props.isEditMode ? indicatorRects : []).map((rect) =>'); expect(source).toContain("pointerEvents: 'none'"); }); + it('hides unsupported indicators synchronously in Preview', () => { + expect(source).toContain('...(props.isEditMode ? indicatorRects : []).map((rect) =>'); + }); + it('retains duplicate live wrapper measurements for the same Runtime node', () => { expect(source).toContain('new Map>()'); expect(source).toContain('measurements.add(measurement)'); @@ -82,7 +93,9 @@ describe('stationarySelection RN integration', () => { }); it('places pointerEvents as a View prop, not inside style', () => { - const indicatorStart = source.indexOf('...indicatorRects.map((rect) =>'); + const indicatorStart = source.indexOf( + '...(props.isEditMode ? indicatorRects : []).map((rect) =>', + ); const indicatorBlock = source.slice(indicatorStart); expect(indicatorBlock).toContain("pointerEvents: 'none'"); const styleBlock = /style:\s*\{([\s\S]*?)\n\s*\},/.exec(indicatorBlock); @@ -133,7 +146,7 @@ describe('stationarySelection RN integration', () => { expect(source).toContain('new ResizeObserver(() => requestIndicatorRefresh())'); expect(source).toContain('onLayout: requestIndicatorRefresh'); expect(source).toContain('onTouchMove: requestScrollIndicatorRefresh'); - expect(source).toContain('}, NATIVE_SCROLL_SETTLE_MS)'); + expect(source).toContain('createIndicatorSettleCoordinator'); expect(source).not.toContain('measureNextFrame'); }); diff --git a/src/runtime/stationarySelection.ts b/src/runtime/stationarySelection.ts index 56946ad2..885a8f20 100644 --- a/src/runtime/stationarySelection.ts +++ b/src/runtime/stationarySelection.ts @@ -13,6 +13,10 @@ import { createIndicatorRefreshCoordinator, type IndicatorRefreshCoordinator, } from './indicatorRefreshCoordinator.js'; +import { + createIndicatorSettleCoordinator, + type IndicatorSettleCoordinator, +} from './indicatorSettleCoordinator.js'; import type { StationarySelectionCoordinator } from './stationarySelectionCoordinator.js'; import { createStationarySelectionCoordinator } from './stationarySelectionCoordinator.js'; import { @@ -58,7 +62,17 @@ interface TrackerContextValue { } const TrackerContext = React.createContext(null); -const NATIVE_SCROLL_SETTLE_MS = 800; +const NATIVE_SETTLE_INTERVAL_MS = 60; +const NATIVE_SETTLE_MAX_SAMPLES = 80; +const NATIVE_SETTLE_STABLE_SAMPLE_COUNT = 3; + +interface UnsupportedNodeMeasurementContextValue { + readonly registerView: (view: ViewRef) => () => void; + readonly requestRefresh: () => void; +} + +const UnsupportedNodeMeasurementContext = + React.createContext(null); interface WebRectLike { readonly left: number; @@ -153,18 +167,18 @@ function measureRootView(view: ViewRef | null): Promise { return measureNativeView(view); } -function measureUnsupportedView(view: ViewRef | null): Promise { +function measureUnsupportedWebView(view: ViewRef | null): Promise { if (!view) { return Promise.resolve(null); } - if (Platform.OS === 'web' && isWebElementLike(view)) { + if (isWebElementLike(view)) { return Promise.resolve( unionRects(Array.from(view.children).flatMap((child) => measureRenderedBoxes(child))), ); } - return measureNativeView(view); + return Promise.resolve(null); } function areIndicatorRectsEqual( @@ -190,6 +204,39 @@ function areIndicatorRectsEqual( }); } +export function useStudioUnsupportedNodeMeasurement(): { + readonly onLayout: (_event: LayoutChangeEvent) => void; + readonly ref: (view: ViewRef | null) => void; +} { + const context = React.useContext(UnsupportedNodeMeasurementContext); + const unregisterRef = React.useRef<(() => void) | null>(null); + + const setViewRef = React.useCallback( + (view: ViewRef | null) => { + unregisterRef.current?.(); + unregisterRef.current = view && context ? context.registerView(view) : null; + }, + [context], + ); + + React.useEffect( + () => () => { + unregisterRef.current?.(); + unregisterRef.current = null; + }, + [], + ); + + const onLayout = React.useCallback( + (_event: LayoutChangeEvent) => { + context?.requestRefresh(); + }, + [context], + ); + + return React.useMemo(() => ({ onLayout, ref: setViewRef }), [onLayout, setViewRef]); +} + function StudioNodeTouchRecorder(props: { readonly nodeId: string | undefined; readonly showUnsupportedIndicator: boolean; @@ -247,9 +294,9 @@ function StudioNodeTouchRecorder(props: { (view: ViewRef | null) => { unregisterMeasurementRef.current?.(); unregisterMeasurementRef.current = null; - if (view && ctx && props.nodeId && props.showUnsupportedIndicator) { + if (Platform.OS === 'web' && view && ctx && props.nodeId && props.showUnsupportedIndicator) { unregisterMeasurementRef.current = ctx.registerUnsupportedNode(props.nodeId, { - measure: () => measureUnsupportedView(view), + measure: () => measureUnsupportedWebView(view), resizeTargets: getWebDescendantResizeTargets(view), }); } @@ -257,31 +304,49 @@ function StudioNodeTouchRecorder(props: { [ctx, props.nodeId, props.showUnsupportedIndicator], ); + const nativeMeasurementContext = + React.useMemo(() => { + const { nodeId } = props; + if (Platform.OS === 'web' || !ctx || !nodeId || !props.showUnsupportedIndicator) { + return null; + } + + return { + registerView: (view) => + ctx.registerUnsupportedNode(nodeId, { + measure: () => measureNativeView(view), + resizeTargets: [], + }), + requestRefresh: ctx.requestIndicatorRefresh, + }; + }, [ctx, props.nodeId, props.showUnsupportedIndicator]); + return React.createElement( - View, - { - collapsable: false, - ref: setViewRef, - nativeID: props.nodeId - ? `studio-runtime-node-${encodeURIComponent(props.nodeId)}` - : undefined, - testID: - props.nodeId && props.showUnsupportedIndicator - ? `studio-unsupported-recorder-${props.nodeId}` - : undefined, - style: - Platform.OS === 'web' || !props.showUnsupportedIndicator - ? { display: 'contents' } - : undefined, - onLayout: handleLayout, - onPointerCancel: handleInteractionCompletion, - onPointerDown: handlePointerDown, - onPointerUp: handleInteractionCompletion, - onTouchCancel: handleInteractionCompletion, - onTouchEnd: handleInteractionCompletion, - onTouchStart: handleTouchStart, - }, - props.children, + UnsupportedNodeMeasurementContext.Provider, + { value: nativeMeasurementContext }, + React.createElement( + View, + { + ref: setViewRef, + nativeID: + Platform.OS === 'web' && props.nodeId + ? `studio-runtime-node-${encodeURIComponent(props.nodeId)}` + : undefined, + testID: + Platform.OS === 'web' && props.nodeId && props.showUnsupportedIndicator + ? `studio-unsupported-recorder-${props.nodeId}` + : undefined, + style: { display: 'contents' }, + onLayout: handleLayout, + onPointerCancel: handleInteractionCompletion, + onPointerDown: handlePointerDown, + onPointerUp: handleInteractionCompletion, + onTouchCancel: handleInteractionCompletion, + onTouchEnd: handleInteractionCompletion, + onTouchStart: handleTouchStart, + }, + props.children, + ), ); } @@ -332,6 +397,7 @@ function StationaryTapSelector(props: { const [indicatorRects, setIndicatorRects] = React.useState( [], ); + const latestIndicatorRectsRef = React.useRef([]); coordinatorRef.current ??= createStationarySelectionCoordinator(); @@ -344,10 +410,12 @@ function StationaryTapSelector(props: { const generationRef = React.useRef(0); const inputStateRef = React.useRef(null); const refreshCoordinatorRef = React.useRef(null); - const refreshIndicatorRectsRef = React.useRef<() => void>(() => undefined); + const settleCoordinatorRef = React.useRef(null); + const refreshIndicatorRectsRef = React.useRef< + () => Promise + >(() => Promise.resolve(null)); const geometryRevisionRef = React.useRef(0); const mountedRef = React.useRef(true); - const settledScrollRefreshTimerRef = React.useRef | null>(null); isEditModeRef.current = props.isEditMode; selectedNodeIdRef.current = props.selectedNodeId; @@ -362,9 +430,10 @@ function StationaryTapSelector(props: { const inputState = inputStateRef.current; const requestIndicatorRefresh = React.useCallback(() => { - geometryRevisionRef.current += 1; refreshCoordinatorRef.current ??= createIndicatorRefreshCoordinator( - () => refreshIndicatorRectsRef.current(), + () => { + void refreshIndicatorRectsRef.current(); + }, { request: (callback) => requestAnimationFrame(callback), cancel: (frameId) => cancelAnimationFrame(frameId), @@ -375,13 +444,24 @@ function StationaryTapSelector(props: { const requestScrollIndicatorRefresh = React.useCallback(() => { requestIndicatorRefresh(); - if (settledScrollRefreshTimerRef.current !== null) { - clearTimeout(settledScrollRefreshTimerRef.current); + if (Platform.OS === 'web' || !isEditModeRef.current || unsupportedNodesRef.current.size === 0) { + return; } - settledScrollRefreshTimerRef.current = setTimeout(() => { - settledScrollRefreshTimerRef.current = null; - requestIndicatorRefresh(); - }, NATIVE_SCROLL_SETTLE_MS); + settleCoordinatorRef.current ??= createIndicatorSettleCoordinator< + readonly UnsupportedIndicatorRect[], + ReturnType + >({ + areEqual: areIndicatorRectsEqual, + intervalMs: NATIVE_SETTLE_INTERVAL_MS, + maxSamples: NATIVE_SETTLE_MAX_SAMPLES, + sample: () => refreshIndicatorRectsRef.current(), + scheduler: { + cancel: (handle) => clearTimeout(handle), + schedule: (callback, delayMs) => setTimeout(callback, delayMs), + }, + stableSampleCount: NATIVE_SETTLE_STABLE_SAMPLE_COUNT, + }); + settleCoordinatorRef.current.trigger(); }, [requestIndicatorRefresh]); const registerUnsupportedNode = React.useCallback( @@ -405,6 +485,9 @@ function StationaryTapSelector(props: { if (registeredMeasurements.size === 0) { unsupportedNodesRef.current.delete(nodeId); } + if (unsupportedNodesRef.current.size === 0) { + settleCoordinatorRef.current?.cancel(); + } requestIndicatorRefresh(); }; }, @@ -424,16 +507,19 @@ function StationaryTapSelector(props: { [inputState, registerUnsupportedNode, requestIndicatorRefresh, requestScrollIndicatorRefresh], ); - const refreshIndicatorRects = React.useCallback(async () => { - const geometryRevision = geometryRevisionRef.current; + const refreshIndicatorRects = React.useCallback(async (): Promise< + readonly UnsupportedIndicatorRect[] | null + > => { + const geometryRevision = ++geometryRevisionRef.current; if (!isEditModeRef.current || unsupportedNodesRef.current.size === 0) { + latestIndicatorRectsRef.current = []; setIndicatorRects((current) => (current.length === 0 ? current : [])); - return; + return null; } const rootRect = await measureRootView(rootViewRef.current); if (!rootRect) { - return; + return latestIndicatorRectsRef.current; } const measurements = await Promise.all( @@ -464,18 +550,21 @@ function StationaryTapSelector(props: { .sort((left, right) => left.nodeId.localeCompare(right.nodeId)); if (!mountedRef.current || geometryRevision !== geometryRevisionRef.current) { - return; + return mountedRef.current ? latestIndicatorRectsRef.current : null; } + latestIndicatorRectsRef.current = nextRects; setIndicatorRects((current) => areIndicatorRectsEqual(current, nextRects) ? current : nextRects, ); + return nextRects; }, []); - refreshIndicatorRectsRef.current = () => { - void refreshIndicatorRects(); - }; + refreshIndicatorRectsRef.current = refreshIndicatorRects; React.useEffect(() => { + if (!props.isEditMode) { + settleCoordinatorRef.current?.cancel(); + } requestIndicatorRefresh(); }, [props.isEditMode, requestIndicatorRefresh]); @@ -485,10 +574,7 @@ function StationaryTapSelector(props: { mountedRef.current = false; geometryRevisionRef.current += 1; refreshCoordinatorRef.current?.cancelPendingRefresh(); - if (settledScrollRefreshTimerRef.current !== null) { - clearTimeout(settledScrollRefreshTimerRef.current); - settledScrollRefreshTimerRef.current = null; - } + settleCoordinatorRef.current?.cancel(); }; }, []); @@ -609,7 +695,7 @@ function StationaryTapSelector(props: { style: { flex: 1, position: 'relative' }, }, props.children, - ...indicatorRects.map((rect) => + ...(props.isEditMode ? indicatorRects : []).map((rect) => React.createElement(View, { key: rect.nodeId, nativeID: `studio-unsupported-indicator-${encodeURIComponent(rect.nodeId)}`,