embedded-react 0.4.0 → 0.5.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/aot/compile.mjs CHANGED
@@ -879,7 +879,11 @@ function collectSvgImports(program) {
879
879
  * @param {string} baseDir Directory the import paths are resolved against (the app's dir).
880
880
  * @returns {Promise<Object>} name → { ops, paints, gradients, width, height }. */
881
881
  export async function bakeSvgArtifacts(src, baseDir) {
882
- const program = parse(src, {sourceType: 'module', plugins: ['jsx']}).program;
882
+ // Parse with the TS plugin too (harmless for plain JSX) so a .tsx app's <Svg source> imports are found.
883
+ const program = parse(src, {
884
+ sourceType: 'module',
885
+ plugins: ['jsx', 'typescript'],
886
+ }).program;
883
887
  const imports = collectSvgImports(program);
884
888
  if (imports.size === 0) return {};
885
889
  const {svgToVector, svgToRaster, writeRasterPng} =
@@ -4285,14 +4289,126 @@ function compileKeyboardConfig(program, out) {
4285
4289
  // directly; the CLI entry at the bottom of the file wraps it with the file read/write.
4286
4290
  // ---------------------------------------------------------------------------------------------------
4287
4291
 
4292
+ // --- TypeScript support (Flow B) ---------------------------------------------------------------------
4293
+ // The compiler walks a plain JS+JSX AST. TypeScript apps (App.tsx) parse with the `typescript` plugin and
4294
+ // are then scrubbed of all type-only syntax IN PLACE before the walker runs: the runtime nodes keep their
4295
+ // original source locations, so code-frame errors still point at the user's real source. This is a faithful
4296
+ // type strip — the generated C for an App.tsx is identical to its untyped App.jsx twin.
4297
+
4298
+ /** Is this app a TypeScript entry? Driven by the filename extension, or an explicit opts.ts (for tests). */
4299
+ const isTsEntry = opts => opts.ts ?? /\.[cm]?tsx$/.test(opts.filename || '');
4300
+
4301
+ const parserPlugins = ts => (ts ? ['jsx', 'typescript'] : ['jsx']);
4302
+ // Type-only expression wrappers: `x as T`, `x satisfies T`, `x!`, `<T>x`, `f<T>` — unwrap to the inner expr.
4303
+ const TS_EXPR_WRAPPERS = new Set([
4304
+ 'TSAsExpression',
4305
+ 'TSSatisfiesExpression',
4306
+ 'TSNonNullExpression',
4307
+ 'TSTypeAssertion',
4308
+ 'TSInstantiationExpression',
4309
+ ]);
4310
+ // Type-only declarations (no runtime presence) — dropped from any statement/body list.
4311
+ const TS_TYPE_DECLS = new Set([
4312
+ 'TSInterfaceDeclaration',
4313
+ 'TSTypeAliasDeclaration',
4314
+ 'TSDeclareFunction',
4315
+ ]);
4316
+ // Type-only fields hung off otherwise-runtime nodes — deleted so nothing downstream traverses into them.
4317
+ const TS_TYPE_FIELDS = [
4318
+ 'typeAnnotation',
4319
+ 'returnType',
4320
+ 'typeParameters',
4321
+ 'typeArguments',
4322
+ 'accessibility',
4323
+ 'definite',
4324
+ 'declare',
4325
+ 'readonly',
4326
+ 'override',
4327
+ 'abstract',
4328
+ ];
4329
+ // Keys that never hold child AST nodes — skip them so the scrub stays cheap and never mangles metadata.
4330
+ const TS_SKIP_KEYS = new Set([
4331
+ 'loc',
4332
+ 'start',
4333
+ 'end',
4334
+ 'range',
4335
+ 'leadingComments',
4336
+ 'trailingComments',
4337
+ 'innerComments',
4338
+ 'comments',
4339
+ 'extra',
4340
+ 'tokens',
4341
+ ]);
4342
+
4343
+ /** True for nodes that carry no runtime meaning and must be removed from a statement/specifier list. */
4344
+ const isTypeOnly = node =>
4345
+ !!node &&
4346
+ (TS_TYPE_DECLS.has(node.type) ||
4347
+ // `import type ... ` / `export type ...`, and per-specifier `import { type X }`.
4348
+ ((node.type === 'ImportDeclaration' || node.type === 'ImportSpecifier') &&
4349
+ node.importKind === 'type') ||
4350
+ ((node.type === 'ExportNamedDeclaration' ||
4351
+ node.type === 'ExportSpecifier') &&
4352
+ node.exportKind === 'type'));
4353
+
4354
+ /** Follow a chain of type-only expression wrappers to the runtime expression underneath. */
4355
+ const unwrapTs = node => {
4356
+ while (node && TS_EXPR_WRAPPERS.has(node.type)) node = node.expression;
4357
+ return node;
4358
+ };
4359
+
4360
+ /**
4361
+ * Strip every TypeScript-only construct from a parsed AST, in place: drop type declarations and type
4362
+ * imports, unwrap `as`/`!`/`<T>` expression wrappers, and delete type-annotation fields. Remaining nodes
4363
+ * keep their .loc, so the compiler's error code-frames stay accurate.
4364
+ */
4365
+ function stripTypeScript(root) {
4366
+ const visit = node => {
4367
+ if (!node || typeof node !== 'object') return;
4368
+ for (const f of TS_TYPE_FIELDS) if (f in node) delete node[f];
4369
+ for (const key of Object.keys(node)) {
4370
+ if (TS_SKIP_KEYS.has(key)) continue;
4371
+ let val = node[key];
4372
+ if (Array.isArray(val)) {
4373
+ const kept = [];
4374
+ for (let el of val) {
4375
+ if (isTypeOnly(el)) continue;
4376
+ if (el && TS_EXPR_WRAPPERS.has(el.type)) el = unwrapTs(el);
4377
+ kept.push(el);
4378
+ visit(el);
4379
+ }
4380
+ node[key] = kept;
4381
+ } else if (val && typeof val.type === 'string') {
4382
+ if (TS_EXPR_WRAPPERS.has(val.type)) {
4383
+ val = unwrapTs(val);
4384
+ node[key] = val;
4385
+ }
4386
+ visit(val);
4387
+ } else if (val && typeof val === 'object') {
4388
+ visit(val);
4389
+ }
4390
+ }
4391
+ };
4392
+ visit(root);
4393
+ return root;
4394
+ }
4395
+
4396
+ /** Parse an app entry to a JS+JSX AST, transparently stripping TypeScript when the entry is .ts/.tsx. */
4397
+ function parseApp(src, opts = {}) {
4398
+ const ts = isTsEntry(opts);
4399
+ const ast = parse(src, {sourceType: 'module', plugins: parserPlugins(ts)});
4400
+ if (ts) stripTypeScript(ast);
4401
+ return ast;
4402
+ }
4403
+
4288
4404
  /**
4289
- * Compiles a Flow B app's JSX source to C.
4290
- * @param {string} src The App.jsx source text.
4405
+ * Compiles a Flow B app's JSX (or TSX) source to C.
4406
+ * @param {string} src The App.jsx/App.tsx source text.
4291
4407
  * @param {string} demo Demo name (only used in the generated-by header comment).
4292
4408
  * @returns {{c: string, h: string, nodes: number, state: number, handlers: number, updates: number}}
4293
4409
  */
4294
4410
  function compileSourceImpl(src, demo = 'app', opts = {}) {
4295
- const ast = parse(src, {sourceType: 'module', plugins: ['jsx']});
4411
+ const ast = parseApp(src, opts);
4296
4412
 
4297
4413
  const screen = opts.screen ?? {width: SCREEN_W, height: SCREEN_H};
4298
4414
  // Image imports first, so their asset-name strings seed the module scope BEFORE its consts fold (a const
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "embedded-react",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "React Native-style component package + reconciler that drives the embedded-react C engine through the QuickJS NativeUI bridge (Flow A).",
6
6
  "license": "Apache-2.0",
@@ -31,8 +31,12 @@
31
31
  "engines": {
32
32
  "node": ">=18"
33
33
  },
34
+ "types": "./src/embedded-react/index.d.ts",
34
35
  "exports": {
35
- ".": "./src/embedded-react/index.js"
36
+ ".": {
37
+ "types": "./src/embedded-react/index.d.ts",
38
+ "default": "./src/embedded-react/index.js"
39
+ }
36
40
  },
37
41
  "bin": {
38
42
  "embedded-react": "cli.mjs"
@@ -71,6 +75,7 @@
71
75
  "test:watch": "vitest",
72
76
  "test:runtime": "node test/runtime/run.mjs",
73
77
  "test:bytecode": "node test/runtime/run.mjs --bytecode",
78
+ "typecheck": "tsc -p tsconfig.types.json",
74
79
  "format": "cd ../../.. && prettier --write \"**/*.{js,jsx,mjs,cjs}\"",
75
80
  "format:check": "cd ../../.. && prettier --check \"**/*.{js,jsx,mjs,cjs}\""
76
81
  },
@@ -86,7 +91,9 @@
86
91
  "svgson": "^5.3.1"
87
92
  },
88
93
  "devDependencies": {
94
+ "@types/react": "18.3.1",
89
95
  "prettier": "^3.4.2",
96
+ "typescript": "^5.5.0",
90
97
  "vitest": "^3.2.4"
91
98
  },
92
99
  "overrides": {
@@ -120,18 +120,31 @@ export function shouldPersist(absPath, projectRootNorm) {
120
120
  /**
121
121
  * Applies the persist transform to a module's source.
122
122
  *
123
- * @param {string} code Module source (JSX allowed).
123
+ * TypeScript entries (.ts/.tsx) are parsed with the `typescript` plugin enabled; the type syntax is
124
+ * preserved in the output and stripped downstream by esbuild's ts/tsx loader (see sim-server.mjs).
125
+ *
126
+ * @param {string} code Module source (JSX and/or TypeScript allowed).
124
127
  * @param {string} moduleId Stable module identifier (used in keys; e.g. a path relative to the app).
125
- * @returns {string} Transformed source (JSX preserved for esbuild to handle).
128
+ * @returns {string} Transformed source (JSX/TS preserved for esbuild to handle).
126
129
  */
127
130
  export function transformPersist(code, moduleId) {
131
+ // Enable the babel parser plugins this module's extension needs. `.tsx` is jsx+typescript; `.ts` is
132
+ // typescript-only (the jsx plugin would misparse `<T>` type assertions); everything else is jsx.
133
+ const isTs = /\.tsx?$/.test(moduleId);
134
+ const isTsx = /\.tsx$/.test(moduleId);
135
+ const parserPlugins = isTsx
136
+ ? ['jsx', 'typescript']
137
+ : isTs
138
+ ? ['typescript']
139
+ : ['jsx'];
128
140
  const out = transformSync(code, {
129
141
  filename: moduleId,
130
142
  babelrc: false,
131
143
  configFile: false,
132
144
  sourceType: 'module',
133
- parserOpts: {plugins: ['jsx']},
134
- plugins: [syntaxJsx, [persistPlugin, {moduleId}]],
145
+ parserOpts: {plugins: parserPlugins},
146
+ // syntaxJsx only enables jsx parsing; for ts/tsx we drive the parser via parserOpts above instead.
147
+ plugins: [...(isTs ? [] : [syntaxJsx]), [persistPlugin, {moduleId}]],
135
148
  });
136
149
  return out.code;
137
150
  }
Binary file
package/sim-server.mjs CHANGED
@@ -177,12 +177,19 @@ function createBundle({
177
177
  // node_modules is what stops the library's usePersistentState being rewritten to call itself).
178
178
  if (!persist || !shouldPersist(a.path, projNorm)) return undefined;
179
179
  try {
180
+ // Match the esbuild loader to the source extension so TS type syntax (preserved by the
181
+ // persist transform) is stripped: .tsx → tsx, .ts → ts, .jsx/.js → jsx.
182
+ const loader = a.path.endsWith('.tsx')
183
+ ? 'tsx'
184
+ : a.path.endsWith('.ts')
185
+ ? 'ts'
186
+ : 'jsx';
180
187
  return {
181
188
  contents: transformPersist(
182
189
  readFileSync(a.path, 'utf8'),
183
190
  relative(projectRoot, a.path).replace(/\\/g, '/'),
184
191
  ),
185
- loader: 'jsx',
192
+ loader,
186
193
  };
187
194
  } catch (e) {
188
195
  return {errors: [{text: `persist transform: ${e.message}`}]};
@@ -0,0 +1,211 @@
1
+ /*
2
+ * Copyright 2026 Cory Lamming
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ // Public type declarations for the `embedded-react` package — the React Native analog. These cover the
18
+ // common surface (View / Text / Pressable / Image / StyleSheet / Animated / hooks); the SVG primitives are
19
+ // typed loosely for now. The runtime is JavaScript and ignores types entirely — these only power editors
20
+ // and `tsc`. React hooks (useState, useEffect, …) still come from 'react', as in React Native.
21
+
22
+ import type {ReactNode} from 'react';
23
+
24
+ // --- Styling ---------------------------------------------------------------
25
+ /** A single style object. Properties mirror the React Native subset the engine supports. */
26
+ export interface ViewStyle {
27
+ [key: string]: string | number | undefined | TransformStyle[];
28
+ flex?: number;
29
+ flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
30
+ alignItems?: 'flex-start' | 'flex-end' | 'center' | 'stretch';
31
+ justifyContent?:
32
+ | 'flex-start'
33
+ | 'flex-end'
34
+ | 'center'
35
+ | 'space-between'
36
+ | 'space-around'
37
+ | 'space-evenly';
38
+ width?: number | string;
39
+ height?: number | string;
40
+ maxWidth?: number | string;
41
+ maxHeight?: number | string;
42
+ margin?: number;
43
+ marginTop?: number;
44
+ marginBottom?: number;
45
+ marginLeft?: number;
46
+ marginRight?: number;
47
+ padding?: number;
48
+ paddingVertical?: number;
49
+ paddingHorizontal?: number;
50
+ gap?: number;
51
+ backgroundColor?: string;
52
+ borderRadius?: number;
53
+ borderWidth?: number;
54
+ borderColor?: string;
55
+ opacity?: number;
56
+ transform?: TransformStyle[];
57
+ }
58
+
59
+ export interface TextStyle extends ViewStyle {
60
+ color?: string;
61
+ fontSize?: number;
62
+ fontFamily?: string;
63
+ fontWeight?: string | number;
64
+ textAlign?: 'auto' | 'left' | 'right' | 'center';
65
+ }
66
+
67
+ export type TransformStyle =
68
+ | {scale: number | AnimatedValue}
69
+ | {scaleX: number | AnimatedValue}
70
+ | {scaleY: number | AnimatedValue}
71
+ | {translateX: number | AnimatedValue}
72
+ | {translateY: number | AnimatedValue}
73
+ | {rotate: string | AnimatedValue};
74
+
75
+ export type StyleProp<T> = T | false | null | undefined | StyleProp<T>[];
76
+
77
+ // --- Components ------------------------------------------------------------
78
+ export interface ViewProps {
79
+ style?: StyleProp<ViewStyle>;
80
+ children?: ReactNode;
81
+ }
82
+
83
+ export interface TextProps {
84
+ style?: StyleProp<TextStyle>;
85
+ children?: ReactNode;
86
+ numberOfLines?: number;
87
+ }
88
+
89
+ export interface PressableProps {
90
+ style?: StyleProp<ViewStyle>;
91
+ children?: ReactNode;
92
+ onPress?: () => void;
93
+ disabled?: boolean;
94
+ }
95
+
96
+ export type ImageSource = number | string | {uri: string};
97
+
98
+ export interface ImageProps {
99
+ source: ImageSource;
100
+ style?: StyleProp<ViewStyle>;
101
+ }
102
+
103
+ export interface ScrollViewProps extends ViewProps {
104
+ horizontal?: boolean;
105
+ }
106
+
107
+ export interface TextInputProps {
108
+ style?: StyleProp<TextStyle>;
109
+ value?: string;
110
+ placeholder?: string;
111
+ onChangeText?: (text: string) => void;
112
+ }
113
+
114
+ export interface SwitchProps {
115
+ value?: boolean;
116
+ onValueChange?: (value: boolean) => void;
117
+ }
118
+
119
+ export const View: (props: ViewProps) => JSX.Element;
120
+ export const Text: (props: TextProps) => JSX.Element;
121
+ export const Image: (props: ImageProps) => JSX.Element;
122
+ export const Pressable: (props: PressableProps) => JSX.Element;
123
+ export const TouchableOpacity: (props: PressableProps) => JSX.Element;
124
+ export const ScrollView: (props: ScrollViewProps) => JSX.Element;
125
+ export const FlatList: (props: Record<string, unknown>) => JSX.Element;
126
+ export const TextInput: (props: TextInputProps) => JSX.Element;
127
+ export const Switch: (props: SwitchProps) => JSX.Element;
128
+ export const ActivityIndicator: (props: ViewProps) => JSX.Element;
129
+ export const Modal: (props: ViewProps) => JSX.Element;
130
+
131
+ // SVG primitives (see the repo for their full prop sets).
132
+ export const Svg: (props: Record<string, unknown>) => JSX.Element;
133
+ export const Path: (props: Record<string, unknown>) => JSX.Element;
134
+ export const Circle: (props: Record<string, unknown>) => JSX.Element;
135
+ export const Ellipse: (props: Record<string, unknown>) => JSX.Element;
136
+ export const Rect: (props: Record<string, unknown>) => JSX.Element;
137
+ export const Line: (props: Record<string, unknown>) => JSX.Element;
138
+ export const G: (props: Record<string, unknown>) => JSX.Element;
139
+ export const Arc: (props: Record<string, unknown>) => JSX.Element;
140
+
141
+ // --- StyleSheet ------------------------------------------------------------
142
+ export const StyleSheet: {
143
+ create<T extends Record<string, ViewStyle | TextStyle>>(styles: T): T;
144
+ };
145
+
146
+ // --- Platform --------------------------------------------------------------
147
+ export const Platform: {
148
+ OS: string;
149
+ select<T>(specifics: Record<string, T>): T | undefined;
150
+ };
151
+
152
+ // --- AppRegistry -----------------------------------------------------------
153
+ export const AppRegistry: {
154
+ registerComponent(
155
+ appKey: string,
156
+ componentProvider: () => (props: any) => JSX.Element,
157
+ ): void;
158
+ };
159
+
160
+ // --- Animated --------------------------------------------------------------
161
+ // A handle to an engine-side animated float. Not importable directly — obtain one from `useAnimatedValue`
162
+ // or `new Animated.Value(...)`, so this is a type, not a runtime export.
163
+ export interface AnimatedValue {
164
+ setValue(value: number): void;
165
+ interpolate(config: {
166
+ inputRange: number[];
167
+ outputRange: number[] | string[];
168
+ }): AnimatedValue;
169
+ }
170
+
171
+ export interface AnimationConfig {
172
+ toValue: number;
173
+ duration?: number;
174
+ delay?: number;
175
+ easing?: (t: number) => number;
176
+ useNativeDriver?: boolean;
177
+ }
178
+
179
+ export interface Animation {
180
+ start(callback?: (result: {finished: boolean}) => void): void;
181
+ stop?(): void;
182
+ }
183
+
184
+ export const Animated: {
185
+ Value: new (initial?: number) => AnimatedValue;
186
+ View: (props: ViewProps) => JSX.Element;
187
+ Text: (props: TextProps) => JSX.Element;
188
+ Image: (props: ImageProps) => JSX.Element;
189
+ timing(value: AnimatedValue, config: AnimationConfig): Animation;
190
+ spring(value: AnimatedValue, config: AnimationConfig): Animation;
191
+ decay(value: AnimatedValue, config: Record<string, unknown>): Animation;
192
+ sequence(animations: Animation[]): Animation;
193
+ parallel(animations: Animation[], config?: {stopTogether?: boolean}): Animation;
194
+ stagger(delay: number, animations: Animation[]): Animation;
195
+ loop(animation: Animation, config?: {iterations?: number}): Animation;
196
+ delay(ms: number): Animation;
197
+ };
198
+
199
+ /** Creates an AnimatedValue tied to the component lifecycle (destroyed on unmount). */
200
+ export function useAnimatedValue(initial?: number): AnimatedValue;
201
+
202
+ /** Like useState, but the value survives a dev hot reload. */
203
+ export function usePersistentState<S>(
204
+ initialState: S | (() => S),
205
+ ): [S, (value: S | ((prev: S) => S)) => void];
206
+
207
+ export const Easing: Record<string, (t: number) => number>;
208
+ export const LayoutAnimation: Record<string, unknown>;
209
+ export function updateVector(...args: unknown[]): void;
210
+ export function updateText(...args: unknown[]): void;
211
+ export function setKeyboardConfig(...args: unknown[]): void;