waypoint-expo-template 1.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,68 +1,45 @@
1
1
  # waypoint-expo-template
2
2
 
3
- The Expo/React Native preview-app scaffold code-builders' `coder` specialist
4
- bootstraps every new app from. Published to npm as `waypoint-expo-template`;
5
- a `bootstrap_app_cli` `run_tool` call in the sandbox does `npm pack
6
- waypoint-expo-template@latest`, extracts the tarball into `/workspace`,
7
- substitutes the app's name, and runs `npm install` — real npm install of
8
- this package's own dependencies, not a copied file tree.
3
+ The Expo app that `bootstrap_app_cli` installs into a fresh workspace. It is a
4
+ `create-expo-app` scaffold, reduced to the smallest thing that renders inside a
5
+ Waypoint device-preview tile.
9
6
 
10
- ## Why this exists
7
+ ## What is here
11
8
 
12
- The `coder` specialist has no shell — it cannot run `create-expo-app` or
13
- `npx expo install` itself. This package **is** the output of those real
14
- commands (`npx create-expo-app@latest --template blank-typescript`, then
15
- `npx expo install react-dom react-native-web @expo/metro-runtime`), so the
16
- agent gets a genuinely correct, currently-supported Expo SDK 57 scaffold
17
- without needing to run anything — and without anyone hand-picking dependency
18
- versions, which is how a real session once shipped an incompatible set and
19
- crashed Metro on startup.
9
+ ```
10
+ src/app/_layout.tsx plain Stack, headers off
11
+ src/app/index.tsx the entry screen — `/` by expo-router convention
12
+ src/navigation.ts which control leads where (links only)
20
13
 
21
- Two things are added on top of the stock Expo output — the minimum needed
22
- for this platform's preview canvas, not a rewrite of it:
14
+ platform/ platform-owned — do not edit
15
+ app.config.js two lines; delegates to platform/
16
+ ```
23
17
 
24
- - `src/lib/previewBridge.ts` — the postMessage IPC contract with the canvas
25
- host (not something `create-expo-app` produces).
26
- - `App.tsx` calls `reportRoutes(buildRouteManifest(...))` on mount and hard-
27
- fails on an unrecognized route, instead of silently rendering a fallback
28
- screen — both verified, live, to be the difference between a working
29
- preview and one that renders the same screen for every route with the
30
- canvas showing no navigation edges at all.
18
+ `platform/` holds the files the platform maintains, not you. Today that is the
19
+ base URL: the preview is embedded in the control plane under
20
+ `/api/v1/preview/<appId>/`, and expo-router matches routes against the
21
+ browser's pathname, so it has to be told that prefix or every screen renders
22
+ "Unmatched Route". It comes from `EXPO_BASE_URL`, set when the platform starts
23
+ Metro — unset (running the app yourself) the base is empty and it serves at the
24
+ root, as normal.
31
25
 
32
- Everything else — `package.json`, `app.json`, `index.ts`, `tsconfig.json`,
33
- `assets/` — is unmodified `create-expo-app` output.
26
+ `app.config.js` has to sit at the root because Expo resolves its dynamic config
27
+ from `<projectRoot>/app.config` and nowhere else. It contains no logic.
34
28
 
35
- ## What you get after bootstrapping
29
+ ## What is deliberately NOT here
36
30
 
37
- ```
38
- package.json # name substituted to the app's own name
39
- app.json # expo.name/expo.slug substituted; web.output: "single"
40
- index.ts # @expo/metro-runtime imported first (required for HMR)
41
- tsconfig.json
42
- assets/ # Expo's default icon set
43
- App.tsx # single "home" route; add cases as screens are added
44
- src/
45
- lib/previewBridge.ts # host protocol — only buildRouteManifest() should change
46
- screens/
47
- HomeScreen.tsx # replace with real content
48
- HomeScreen.preview.md
49
- ```
31
+ No preview manifests, no compile step, no canvas protocol module, and no
32
+ declared screen list or control coordinates.
50
33
 
51
- From there, the agent writes the app's actual business logic — screens,
52
- ports, navigation — following the `expo-app` and `workflow-preview` skills.
34
+ The preview canvas derives its flow graph itself: screens come from the route
35
+ files under `src/app`, positions are measured from the running app, and button
36
+ presses are observed directly because the preview iframe is same-origin. The
37
+ app is just an app. See ADR-039.
53
38
 
54
- ## Publishing
39
+ ## Adding a screen
55
40
 
56
- CI publishes automatically (`.github/workflows/expo-template.yml`) on every
57
- push to `main` that touches this package, whenever `package.json`'s version
58
- differs from what's on npm — bump the version and push; no manual publish
59
- step, and the bootstrap tool always resolves whatever is currently tagged
60
- `latest`, so it never needs a code change when a new version ships.
41
+ 1. `src/app/<name>.tsx` — put a `testID` on every control that navigates
42
+ 2. add a `links` entry in `src/navigation.ts` for each of those controls
43
+ 3. `npx tsc --noEmit`
61
44
 
62
- To test a change locally before pushing:
63
-
64
- ```
65
- cd packages/expo-template
66
- npm install
67
- npx tsc --noEmit
68
- ```
45
+ There is no step 4. Nothing to compile, nothing to regenerate.
package/app.config.js ADDED
@@ -0,0 +1,10 @@
1
+ // Expo resolves its dynamic config from `<projectRoot>/app.config` and nowhere
2
+ // else — @expo/config's getDynamicConfigFilePath does
3
+ // `resolveFrom(projectRoot, './app.config')`. So this file cannot live in
4
+ // platform/; it stays here as the entry point Expo requires, and holds no
5
+ // logic of its own.
6
+ //
7
+ // Everything the platform maintains is in platform/. Do not edit either.
8
+ const { withPlatformBaseUrl } = require('./platform/base-url');
9
+
10
+ module.exports = ({ config }) => withPlatformBaseUrl(config);
package/app.json CHANGED
@@ -1,26 +1,42 @@
1
1
  {
2
2
  "expo": {
3
- "name": "expo-template",
4
- "slug": "expo-template",
3
+ "name": "waypoint-expo-template",
4
+ "slug": "waypoint-expo-template",
5
5
  "version": "1.0.0",
6
6
  "orientation": "portrait",
7
- "icon": "./assets/icon.png",
8
- "userInterfaceStyle": "light",
7
+ "icon": "./assets/images/icon.png",
8
+ "scheme": "waypointexpotemplate",
9
+ "userInterfaceStyle": "automatic",
9
10
  "ios": {
10
- "supportsTablet": true
11
+ "icon": "./assets/expo.icon"
11
12
  },
12
13
  "android": {
13
14
  "adaptiveIcon": {
14
15
  "backgroundColor": "#E6F4FE",
15
- "foregroundImage": "./assets/android-icon-foreground.png",
16
- "backgroundImage": "./assets/android-icon-background.png",
17
- "monochromeImage": "./assets/android-icon-monochrome.png"
16
+ "foregroundImage": "./assets/images/android-icon-foreground.png",
17
+ "backgroundImage": "./assets/images/android-icon-background.png",
18
+ "monochromeImage": "./assets/images/android-icon-monochrome.png"
18
19
  },
19
20
  "predictiveBackGestureEnabled": false
20
21
  },
21
22
  "web": {
22
- "favicon": "./assets/favicon.png",
23
- "output": "single"
23
+ "output": "static",
24
+ "favicon": "./assets/images/favicon.png"
25
+ },
26
+ "plugins": [
27
+ "expo-router",
28
+ [
29
+ "expo-splash-screen",
30
+ {
31
+ "backgroundColor": "#208AEF",
32
+ "image": "./assets/images/splash-icon.png",
33
+ "imageWidth": 76
34
+ }
35
+ ]
36
+ ],
37
+ "experiments": {
38
+ "typedRoutes": true,
39
+ "reactCompiler": true
24
40
  }
25
41
  }
26
42
  }
@@ -0,0 +1,3 @@
1
+ <svg width="652" height="606" viewBox="0 0 652 606" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M353.554 0H298.446C273.006 0 249.684 14.6347 237.962 37.9539L4.37994 502.646C-1.04325 513.435 -1.45067 526.178 3.2716 537.313L22.6123 582.918C34.6475 611.297 72.5404 614.156 88.4414 587.885L309.863 222.063C313.34 216.317 319.439 212.826 326 212.826C332.561 212.826 338.659 216.317 342.137 222.063L563.559 587.885C579.46 614.156 617.352 611.297 629.388 582.918L648.728 537.313C653.451 526.178 653.043 513.435 647.62 502.646L414.038 37.9539C402.316 14.6347 378.994 0 353.554 0Z" fill="white"/>
3
+ </svg>
Binary file
@@ -0,0 +1,40 @@
1
+ {
2
+ "fill" : {
3
+ "automatic-gradient" : "extended-srgb:0.00000,0.47843,1.00000,1.00000"
4
+ },
5
+ "groups" : [
6
+ {
7
+ "layers" : [
8
+ {
9
+ "image-name" : "expo-symbol 2.svg",
10
+ "name" : "expo-symbol 2",
11
+ "position" : {
12
+ "scale" : 1,
13
+ "translation-in-points" : [
14
+ 1.1008400065293245e-05,
15
+ -16.046875
16
+ ]
17
+ }
18
+ },
19
+ {
20
+ "image-name" : "grid.png",
21
+ "name" : "grid"
22
+ }
23
+ ],
24
+ "shadow" : {
25
+ "kind" : "neutral",
26
+ "opacity" : 0.5
27
+ },
28
+ "translucency" : {
29
+ "enabled" : true,
30
+ "value" : 0.5
31
+ }
32
+ }
33
+ ],
34
+ "supported-platforms" : {
35
+ "circles" : [
36
+ "watchOS"
37
+ ],
38
+ "squares" : "shared"
39
+ }
40
+ }
Binary file
Binary file
package/package.json CHANGED
@@ -1,19 +1,22 @@
1
1
  {
2
2
  "name": "waypoint-expo-template",
3
- "version": "1.0.0",
4
- "description": "Expo preview-app scaffold for code-builders' coder specialist — bootstrapped into a fresh sandbox workspace via npm, not vendored/copied. See src/lib/previewBridge.ts and App.tsx for the Waypoint-specific host IPC wiring; everything else is unmodified `create-expo-app --template blank-typescript` output plus the web-enablement packages `npx expo install` adds.",
5
- "main": "index.ts",
6
- "publishConfig": {
7
- "access": "public"
8
- },
3
+ "main": "expo-router/entry",
4
+ "version": "2.1.0",
9
5
  "dependencies": {
10
- "@expo/metro-runtime": "~57.0.15",
11
- "expo": "~57.0.20",
6
+ "expo": "~57.0.21",
7
+ "expo-constants": "~57.0.17",
8
+ "expo-font": "~57.0.3",
9
+ "expo-linking": "~57.0.9",
10
+ "expo-router": "~57.0.20",
11
+ "expo-splash-screen": "~57.0.8",
12
12
  "expo-status-bar": "~57.0.1",
13
+ "expo-system-ui": "~57.0.3",
13
14
  "react": "19.2.3",
14
15
  "react-dom": "19.2.3",
15
16
  "react-native": "0.86.3",
16
- "react-native-web": "^0.21.2"
17
+ "react-native-safe-area-context": "~5.7.0",
18
+ "react-native-screens": "~4.26.0",
19
+ "react-native-web": "~0.21.0"
17
20
  },
18
21
  "devDependencies": {
19
22
  "@types/react": "~19.2.2",
@@ -23,7 +26,7 @@
23
26
  "start": "expo start",
24
27
  "android": "expo start --android",
25
28
  "ios": "expo start --ios",
26
- "web": "expo start --web",
27
- "compile": "node scripts/compile-preview.js"
28
- }
29
+ "web": "expo start --web"
30
+ },
31
+ "description": "Expo app template for Waypoint app previews."
29
32
  }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The subpath this app is served under, when the platform is serving it.
3
+ *
4
+ * WHY THIS EXISTS
5
+ *
6
+ * The preview is embedded in the control plane as a SAME-ORIGIN iframe
7
+ * (ADR-033), served under `/api/v1/preview/<appId>/`. Same-origin is not
8
+ * incidental: the canvas reads this app's DOM directly to measure controls,
9
+ * observe presses and detect readiness (ADR-039), and all three throw across
10
+ * origins. So the app cannot be given its own origin to escape the prefix —
11
+ * it has to be told the prefix instead.
12
+ *
13
+ * expo-router resolves routes against `window.location.pathname`. Without a
14
+ * base it compares that whole prefixed path against the route files and
15
+ * matches nothing, so EVERY screen — the entry one included — renders
16
+ * "Unmatched Route". Verified live before this existed.
17
+ *
18
+ * ON `experiments`
19
+ *
20
+ * `experiments.baseUrl` is Expo's only mechanism for subpath hosting, and it
21
+ * is typed and documented in @expo/config-types ("Export a website relative
22
+ * to a subpath of a domain"). This project already depends on that namespace
23
+ * — `typedRoutes` in app.json — which in Expo is a grab-bag of build-pipeline
24
+ * flags rather than a staging area.
25
+ *
26
+ * The exposure is a rename on an SDK upgrade. The symptom is unmistakable
27
+ * (every route renders "Unmatched Route") and the fix is this one field;
28
+ * check @expo/config-types' ExpoConfig for where it moved.
29
+ */
30
+
31
+ /**
32
+ * Empty unless the platform set it. An app run directly (`npm run web`) has
33
+ * no prefix and serves at the origin root, which is what an empty base means.
34
+ * The variable is set by the harness when it starts Metro, from the pod's own
35
+ * app id — never written into the workspace, because the base URL is a
36
+ * deployment fact the platform owns and app.json belongs to you.
37
+ */
38
+ function resolveBaseUrl() {
39
+ return process.env.EXPO_BASE_URL || '';
40
+ }
41
+
42
+ /** Applies the base to an Expo config, leaving everything else untouched. */
43
+ function withPlatformBaseUrl(config) {
44
+ return {
45
+ ...config,
46
+ experiments: {
47
+ ...config.experiments,
48
+ baseUrl: resolveBaseUrl(),
49
+ },
50
+ };
51
+ }
52
+
53
+ module.exports = { resolveBaseUrl, withPlatformBaseUrl };
@@ -0,0 +1,16 @@
1
+ import { Stack } from 'expo-router';
2
+
3
+ /**
4
+ * A plain Stack, deliberately.
5
+ *
6
+ * `create-expo-app` scaffolds a tab layout. A tab bar is wrong here: every
7
+ * screen is rendered inside a device-preview tile on the canvas, and a
8
+ * persistent tab bar would appear in all of them and offer a second way to
9
+ * navigate that the flow graph knows nothing about.
10
+ *
11
+ * `headerShown: false` for the same reason — the canvas labels each tile
12
+ * itself, so a header would be a duplicate title inside the device frame.
13
+ */
14
+ export default function RootLayout() {
15
+ return <Stack screenOptions={{ headerShown: false }} />;
16
+ }
@@ -0,0 +1,22 @@
1
+ import { StyleSheet, Text, View } from 'react-native';
2
+
3
+ /**
4
+ * The entry screen — `src/app/index.tsx` is `/` by expo-router's own
5
+ * convention, which is also how the preview canvas knows it is the flow's
6
+ * starting point. Nothing declares that.
7
+ *
8
+ * Replace this with the real first screen. Put a `testID` on any control that
9
+ * navigates, and declare where it leads in `src/navigation.ts`.
10
+ */
11
+ export default function Index() {
12
+ return (
13
+ <View style={styles.container} testID="screen-index">
14
+ <Text style={styles.text}>Home</Text>
15
+ </View>
16
+ );
17
+ }
18
+
19
+ const styles = StyleSheet.create({
20
+ container: { flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: '#ffffff' },
21
+ text: { fontSize: 18 },
22
+ });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Which control leads where — and nothing else.
3
+ *
4
+ * The screens themselves are NOT declared here. With file-based routing they
5
+ * are the files under `src/app`: `index.tsx` is `/`, `settings.tsx` is
6
+ * `/settings`, and the preview canvas reads that directly. Writing the list
7
+ * down again would create a second copy that can disagree with the first.
8
+ *
9
+ * What the filesystem cannot show is which button leads where — that lives
10
+ * inside a handler, and `router.push('/settings')` behind an async call is
11
+ * not something anything can read statically. This file is that missing edge.
12
+ *
13
+ * Each entry:
14
+ * from the screen the control is on (its route id: `/` is `index`,
15
+ * `/user/settings` is `user-settings`)
16
+ * via the control's `testID` (must match the prop in the component)
17
+ * to the screen it leads to
18
+ * label optional; names the edge on the canvas
19
+ *
20
+ * Declare links in the direction the user travels, INCLUDING back. A link
21
+ * returning to an earlier screen is written plainly — the canvas materializes
22
+ * the return frame itself. Do not add a duplicate "return" screen for it.
23
+ *
24
+ * Never declare where a control sits. Positions are measured from the running
25
+ * app, so restyling a button needs no change here.
26
+ */
27
+ export const links: Array<{
28
+ from: string;
29
+ via: string;
30
+ to: string;
31
+ label?: string;
32
+ }> = [
33
+ // { from: 'index', via: 'btn-open-settings', to: 'settings', label: 'Open settings' },
34
+ ];
package/tsconfig.json CHANGED
@@ -1,6 +1,20 @@
1
1
  {
2
2
  "extends": "expo/tsconfig.base",
3
3
  "compilerOptions": {
4
- "strict": true
5
- }
4
+ "strict": true,
5
+ "paths": {
6
+ "@/*": [
7
+ "./src/*"
8
+ ],
9
+ "@/assets/*": [
10
+ "./assets/*"
11
+ ]
12
+ }
13
+ },
14
+ "include": [
15
+ "**/*.ts",
16
+ "**/*.tsx",
17
+ ".expo/types/**/*.ts",
18
+ "expo-env.d.ts"
19
+ ]
6
20
  }
package/App.tsx DELETED
@@ -1,61 +0,0 @@
1
- import { useState, useEffect } from 'react';
2
- import { StyleSheet, View } from 'react-native';
3
- import { StatusBar } from 'expo-status-bar';
4
- import { getAppParams, onHostMessage, postToHost, buildRouteManifest, reportRoutes, type HostMessage } from './src/lib/previewBridge';
5
- import { HomeScreen } from './src/screens/HomeScreen';
6
-
7
- // Add each new screen's id here as you create one (see src/lib/previewBridge.ts
8
- // and the workflow-preview skill for the accompanying *.preview.md manifest).
9
- type ScreenRoute = 'home';
10
-
11
- export default function App() {
12
- const [params] = useState(() => getAppParams());
13
- const initialRoute = (params.route?.replace(/^\//, '') || 'home') as ScreenRoute;
14
- const [currentScreen, setCurrentScreen] = useState<ScreenRoute>(initialRoute);
15
-
16
- useEffect(() => {
17
- postToHost({ type: 'expo-app:ready', appId: params.appId ?? undefined });
18
- // Required — without this the preview canvas falls back to a static
19
- // source scan that cannot see this navigation pattern's links and
20
- // derives differently-cased route paths from screen filenames. Keep
21
- // this call as you add screens; only buildRouteManifest() itself needs
22
- // editing.
23
- reportRoutes(buildRouteManifest(params.appId));
24
- return onHostMessage((message: HostMessage) => {
25
- if (message.type === 'expo-app:navigate' && typeof message.route === 'string') {
26
- setCurrentScreen(message.route.replace(/^\//, '') as ScreenRoute);
27
- }
28
- });
29
- }, [params]);
30
-
31
- // Navigation helper — updates state AND notifies host.
32
- const navigateTo = (screen: ScreenRoute) => {
33
- setCurrentScreen(screen);
34
- postToHost({ type: 'expo-app:navigation-change', route: `/${screen}` });
35
- };
36
-
37
- const renderScreen = () => {
38
- switch (currentScreen) {
39
- case 'home':
40
- return <HomeScreen />;
41
- default:
42
- // Hard fail — never silently render a fallback screen for an
43
- // unmatched route. A route reaching here is a real bug (a case is
44
- // missing for a route reportRoutes/navigateTo/the host actually
45
- // named) and must surface immediately, not render an unrelated
46
- // screen with no visible error.
47
- throw new Error(`Unhandled ScreenRoute: "${currentScreen}" — no case in App.tsx's renderScreen() switch.`);
48
- }
49
- };
50
-
51
- return (
52
- <View style={styles.container}>
53
- <StatusBar style="auto" />
54
- {renderScreen()}
55
- </View>
56
- );
57
- }
58
-
59
- const styles = StyleSheet.create({
60
- container: { flex: 1, backgroundColor: '#ffffff' },
61
- });
package/assets/icon.png DELETED
Binary file
Binary file
package/index.ts DELETED
@@ -1,13 +0,0 @@
1
- // Must be the first import — pulls Metro's Fast Refresh client into the web
2
- // bundle. Without it the bundle contains no HMR client at all and edits
3
- // never reach the running preview (verified: the bundle grows measurably
4
- // and gains an HMRClient once this import is present).
5
- import '@expo/metro-runtime';
6
- import { registerRootComponent } from 'expo';
7
-
8
- import App from './App';
9
-
10
- // registerRootComponent calls AppRegistry.registerComponent('main', () => App);
11
- // It also ensures that whether you load the app in Expo Go or in a native build,
12
- // the environment is set up appropriately
13
- registerRootComponent(App);
package/preview.json DELETED
@@ -1,42 +0,0 @@
1
- {
2
- "name": "waypoint-expo-template-workflow",
3
- "version": "1",
4
- "preset": "iphone-16-pro",
5
- "nodes": [
6
- {
7
- "id": "preview-home",
8
- "type": "devicePreview",
9
- "position": {
10
- "x": 0,
11
- "y": 0
12
- },
13
- "data": {
14
- "type": "devicePreview",
15
- "route": "/home",
16
- "label": "Home",
17
- "isInitial": true,
18
- "presetId": "iphone-16-pro",
19
- "hideControls": true,
20
- "actionPorts": [],
21
- "action": {
22
- "after": [
23
- {
24
- "stepId": "await-nav-home",
25
- "actionType": "step/await-navigation",
26
- "semantics": {
27
- "label": "Await navigation from Home",
28
- "description": "Pause and wait for user to trigger a navigation event from this screen"
29
- },
30
- "config": {
31
- "screenId": "home",
32
- "route": "/home"
33
- }
34
- }
35
- ]
36
- },
37
- "apiCalls": []
38
- }
39
- }
40
- ],
41
- "edges": []
42
- }