nitrogen 0.35.9 → 0.36.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,4 +1,4 @@
1
- <a href="https://margelo.com">
1
+ <a href="https://nitro.margelo.com/docs/concepts/nitrogen">
2
2
  <picture>
3
3
  <source media="(prefers-color-scheme: dark)" srcset="../../docs/static/img/banner-nitrogen-dark.png" />
4
4
  <source media="(prefers-color-scheme: light)" srcset="../../docs/static/img/banner-nitrogen-light.png" />
@@ -80,13 +80,13 @@ function getHybridObjectSpec(type, language) {
80
80
  if (Node.isPropertySignature(declaration)) {
81
81
  const t = declaration.getType();
82
82
  const isOptional = prop.isOptional() || t.getUnionTypes().some((u) => u.isUndefined());
83
- const propType = createType(language, t, isOptional);
83
+ const propType = createType(language, t, isOptional, declaration.getTypeNode());
84
84
  properties.push(new Property(prop.getName(), propType, declaration.isReadonly()));
85
85
  }
86
86
  else if (Node.isMethodSignature(declaration)) {
87
87
  const returnType = declaration.getReturnType();
88
88
  const isOptional = returnType.getUnionTypes().some((t) => t.isUndefined());
89
- const methodReturnType = createType(language, returnType, isOptional);
89
+ const methodReturnType = createType(language, returnType, isOptional, declaration.getReturnTypeNode());
90
90
  const methodParameters = declaration
91
91
  .getParameters()
92
92
  .map((p) => new Parameter(p, language));
@@ -18,7 +18,7 @@ export class Parameter {
18
18
  const name = param.getSymbolOrThrow().getEscapedName();
19
19
  const type = param.getType();
20
20
  const isOptional = param.hasQuestionToken() || param.isOptional() || type.isNullable();
21
- this.type = createNamedType(language, name, type, isOptional);
21
+ this.type = createNamedType(language, name, type, isOptional, param.getTypeNode());
22
22
  }
23
23
  else {
24
24
  // constructor(...)???
@@ -1,8 +1,9 @@
1
- import { Type as TSMorphType } from 'ts-morph';
1
+ import { Node, ts, Type as TSMorphType } from 'ts-morph';
2
2
  import type { Type } from './types/Type.js';
3
3
  import { NamedWrappingType } from './types/NamedWrappingType.js';
4
4
  import { type Language } from '../getPlatformSpecs.js';
5
- export declare function createNamedType(language: Language, name: string, type: TSMorphType, isOptional: boolean): NamedWrappingType<Type>;
5
+ type TypeNode = Node<ts.TypeNode>;
6
+ export declare function createNamedType(language: Language, name: string, type: TSMorphType, isOptional: boolean, typeNode?: TypeNode): NamedWrappingType<Type>;
6
7
  export declare function createVoidType(): Type;
7
8
  /**
8
9
  * Get a list of all currently known complex types.
@@ -12,4 +13,5 @@ export declare function addKnownType(key: string, type: Type, language: Language
12
13
  /**
13
14
  * Create a new type (or return it from cache if it is already known)
14
15
  */
15
- export declare function createType(language: Language, type: TSMorphType, isOptional: boolean): Type;
16
+ export declare function createType(language: Language, type: TSMorphType, isOptional: boolean, typeNode?: TypeNode): Type;
17
+ export {};
@@ -1,4 +1,4 @@
1
- import { ts, Type as TSMorphType } from 'ts-morph';
1
+ import { Node, ts, Type as TSMorphType } from 'ts-morph';
2
2
  import { BooleanType } from './types/BooleanType.js';
3
3
  import { NumberType } from './types/NumberType.js';
4
4
  import { StringType } from './types/StringType.js';
@@ -51,6 +51,34 @@ function removeDuplicates(types) {
51
51
  return firstIndexOfType === index;
52
52
  });
53
53
  }
54
+ function getAliasTypeNode(type) {
55
+ const symbol = type.getNonNullableType().getAliasSymbol();
56
+ const declaration = symbol?.getDeclarations()[0];
57
+ if (declaration != null && Node.isTypeAliasDeclaration(declaration)) {
58
+ return declaration.getTypeNode();
59
+ }
60
+ return undefined;
61
+ }
62
+ function getUnionTypeNodes(type, typeNode) {
63
+ if (typeNode != null && Node.isUnionTypeNode(typeNode)) {
64
+ return typeNode.getTypeNodes();
65
+ }
66
+ const aliasTypeNode = getAliasTypeNode(type);
67
+ if (aliasTypeNode != null && Node.isUnionTypeNode(aliasTypeNode)) {
68
+ return aliasTypeNode.getTypeNodes();
69
+ }
70
+ return undefined;
71
+ }
72
+ function getUnionConstituents(type, typeNode) {
73
+ const typeNodes = getUnionTypeNodes(type, typeNode);
74
+ if (typeNodes != null) {
75
+ return typeNodes.map((node) => ({
76
+ type: node.getType(),
77
+ typeNode: node,
78
+ }));
79
+ }
80
+ return type.getUnionTypes().map((unionType) => ({ type: unionType }));
81
+ }
54
82
  function getArguments(type, typename, count) {
55
83
  const typeArguments = type.getTypeArguments();
56
84
  if (typeArguments.length === count) {
@@ -62,11 +90,11 @@ function getArguments(type, typename, count) {
62
90
  }
63
91
  throw new Error(`Type ${type.getText()} looks like a ${typename}, but has ${typeArguments.length} or ${aliasTypeArguments.length} type arguments instead of ${count}!`);
64
92
  }
65
- export function createNamedType(language, name, type, isOptional) {
93
+ export function createNamedType(language, name, type, isOptional, typeNode) {
66
94
  if (name.startsWith('__')) {
67
95
  throw new Error(`Name cannot start with two underscores (__) as this is reserved syntax for Nitrogen! (In ${type.getText()})`);
68
96
  }
69
- return new NamedWrappingType(name, createType(language, type, isOptional));
97
+ return new NamedWrappingType(name, createType(language, type, isOptional, typeNode));
70
98
  }
71
99
  export function createVoidType() {
72
100
  return new VoidType();
@@ -110,7 +138,7 @@ export function addKnownType(key, type, language) {
110
138
  /**
111
139
  * Create a new type (or return it from cache if it is already known)
112
140
  */
113
- export function createType(language, type, isOptional) {
141
+ export function createType(language, type, isOptional, typeNode) {
114
142
  const key = getTypeId(type, isOptional);
115
143
  if (key != null && knownTypes[language].has(key)) {
116
144
  const known = knownTypes[language].get(key);
@@ -120,7 +148,7 @@ export function createType(language, type, isOptional) {
120
148
  }
121
149
  const get = () => {
122
150
  if (isOptional) {
123
- const wrapping = createType(language, type, false);
151
+ const wrapping = createType(language, type, false, typeNode);
124
152
  return new OptionalType(wrapping);
125
153
  }
126
154
  if (type.isNull()) {
@@ -232,8 +260,10 @@ export function createType(language, type, isOptional) {
232
260
  // - of string literals (then it's an enum)
233
261
  // - of type `T | undefined` (then it's just optional `T`)
234
262
  // - of different types (then it's a variant `A | B | C`)
235
- const types = type.getUnionTypes();
236
- const nonNullTypes = types.filter((t) => !t.isNull() && !t.isUndefined() && !t.isVoid());
263
+ const unionConstituents = getUnionConstituents(type, typeNode);
264
+ const nonNullTypes = unionConstituents
265
+ .map((c) => c.type)
266
+ .filter((t) => !t.isNull() && !t.isUndefined() && !t.isVoid());
237
267
  const isEnumUnion = nonNullTypes.every((t) => t.isStringLiteral());
238
268
  if (isEnumUnion) {
239
269
  // It consists only of string literaly - that means it's describing an enum!
@@ -248,11 +278,10 @@ export function createType(language, type, isOptional) {
248
278
  }
249
279
  else {
250
280
  // It consists of different types - that means it's a variant!
251
- let variants = type
252
- .getUnionTypes()
281
+ let variants = unionConstituents
253
282
  // Filter out any undefineds/voids, as those are already treated as `isOptional`.
254
- .filter((t) => !t.isUndefined() && !t.isVoid())
255
- .map((t) => createType(language, t, false))
283
+ .filter(({ type: t }) => !t.isUndefined() && !t.isVoid())
284
+ .map(({ type: t, typeNode: node }) => createType(language, t, false, node))
256
285
  .toSorted(compareLooselyness);
257
286
  variants = removeDuplicates(variants);
258
287
  if (variants.length === 1) {
@@ -1,4 +1,4 @@
1
- import type { ts, Type } from 'ts-morph';
1
+ import { type ts, type Type } from 'ts-morph';
2
2
  import type { NamedType } from './types/Type.js';
3
3
  import type { Language } from '../getPlatformSpecs.js';
4
4
  export declare function getInterfaceProperties(language: Language, interfaceType: Type<ts.ObjectType>): NamedType[];
@@ -1,9 +1,13 @@
1
+ import { Node } from 'ts-morph';
1
2
  import { createNamedType } from './createType.js';
2
3
  export function getInterfaceProperties(language, interfaceType) {
3
4
  const symbol = interfaceType.getAliasSymbol() ?? interfaceType.getSymbol();
4
5
  if (symbol == null)
5
6
  throw new Error(`Interface "${interfaceType.getText()}" does not have a Symbol!`);
6
7
  return interfaceType.getProperties().map((prop) => {
8
+ const propDeclaration = prop
9
+ .getDeclarations()
10
+ .find((declaration) => Node.isPropertySignature(declaration));
7
11
  let propType = prop.getDeclaredType();
8
12
  if (propType.isAny() || propType.isUnknown()) {
9
13
  // the interface is aliased/merged - we need to look into the actual declaration
@@ -15,7 +19,7 @@ export function getInterfaceProperties(language, interfaceType) {
15
19
  }
16
20
  }
17
21
  }
18
- const refType = createNamedType(language, prop.getName(), propType, prop.isOptional() || propType.isNullable());
22
+ const refType = createNamedType(language, prop.getName(), propType, prop.isOptional() || propType.isNullable(), propDeclaration?.getTypeNode());
19
23
  return refType;
20
24
  });
21
25
  }
@@ -40,7 +40,7 @@ namespace ${cxxNamespace} {
40
40
  using namespace facebook;
41
41
 
42
42
  /**
43
- * The C++ JNI bridge between the C++ enum "${enumType.enumName}" and the the Kotlin enum "${enumType.enumName}".
43
+ * The C++ JNI bridge between the C++ enum "${enumType.enumName}" and the Kotlin enum "${enumType.enumName}".
44
44
  */
45
45
  struct J${enumType.enumName} final: public jni::JavaClass<J${enumType.enumName}> {
46
46
  public:
@@ -109,7 +109,7 @@ namespace ${cxxNamespace} {
109
109
  using namespace facebook;
110
110
 
111
111
  /**
112
- * The C++ JNI bridge between the C++ struct "${structType.structName}" and the the Kotlin data class "${structType.structName}".
112
+ * The C++ JNI bridge between the C++ struct "${structType.structName}" and the Kotlin data class "${structType.structName}".
113
113
  */
114
114
  struct J${structType.structName} final: public jni::JavaClass<J${structType.structName}> {
115
115
  public:
@@ -210,7 +210,7 @@ namespace ${namespace} {
210
210
 
211
211
  ${descriptorClassName}::${descriptorClassName}(const react::ComponentDescriptorParameters& parameters)
212
212
  : ConcreteComponentDescriptor(parameters,
213
- react::RawPropsParser(/* enableJsiParser */ true)) {}
213
+ react::RawPropsParser()) {}
214
214
 
215
215
  std::shared_ptr<const react::Props> ${descriptorClassName}::cloneProps(const react::PropsParserContext& context,
216
216
  ${descriptorIndent} const std::shared_ptr<const react::Props>& props,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nitrogen",
3
- "version": "0.35.9",
4
- "description": "The code-generator for react-native-nitro-modules.",
3
+ "version": "0.36.0",
4
+ "description": "Code generator for React Native Nitro Modules that turns TypeScript specs into C++, Swift, and Kotlin bindings.",
5
5
  "main": "lib/index",
6
6
  "types": "lib/index.d.ts",
7
7
  "source": "src/index",
@@ -21,7 +21,7 @@
21
21
  "bugs": {
22
22
  "url": "https://github.com/mrousavy/nitro/issues"
23
23
  },
24
- "homepage": "https://github.com/mrousavy/nitro#readme",
24
+ "homepage": "https://nitro.margelo.com/docs/concepts/nitrogen",
25
25
  "publishConfig": {
26
26
  "registry": "https://registry.npmjs.org/"
27
27
  },
@@ -30,18 +30,18 @@
30
30
  "start": "tsc && node lib/index.js",
31
31
  "typecheck": "tsc --noEmit",
32
32
  "lint": "eslint \"**/*.{js,ts,tsx}\" --fix",
33
- "lint-ci": "eslint \"**/*.{js,ts,tsx}\" -f @jamesacarr/github-actions",
33
+ "lint-ci": "eslint \"**/*.{js,ts,tsx}\" -f @jamesacarr/github-actions --max-warnings 0",
34
34
  "release": "release-it"
35
35
  },
36
36
  "dependencies": {
37
37
  "chalk": "^5.3.0",
38
- "react-native-nitro-modules": "^0.35.9",
38
+ "react-native-nitro-modules": "^0.36.0",
39
39
  "ts-morph": "^28.0.0",
40
40
  "yargs": "^18.0.0",
41
- "zod": "^4.0.5"
41
+ "zod": "^4.4.3"
42
42
  },
43
43
  "devDependencies": {
44
- "@types/node": "^25.7.0"
44
+ "@types/node": "^26.0.0"
45
45
  },
46
46
  "release-it": {
47
47
  "npm": {
@@ -113,14 +113,24 @@ function getHybridObjectSpec(type: Type, language: Language): HybridObjectSpec {
113
113
  const t = declaration.getType()
114
114
  const isOptional =
115
115
  prop.isOptional() || t.getUnionTypes().some((u) => u.isUndefined())
116
- const propType = createType(language, t, isOptional)
116
+ const propType = createType(
117
+ language,
118
+ t,
119
+ isOptional,
120
+ declaration.getTypeNode()
121
+ )
117
122
  properties.push(
118
123
  new Property(prop.getName(), propType, declaration.isReadonly())
119
124
  )
120
125
  } else if (Node.isMethodSignature(declaration)) {
121
126
  const returnType = declaration.getReturnType()
122
127
  const isOptional = returnType.getUnionTypes().some((t) => t.isUndefined())
123
- const methodReturnType = createType(language, returnType, isOptional)
128
+ const methodReturnType = createType(
129
+ language,
130
+ returnType,
131
+ isOptional,
132
+ declaration.getReturnTypeNode()
133
+ )
124
134
  const methodParameters = declaration
125
135
  .getParameters()
126
136
  .map((p) => new Parameter(p, language))
@@ -26,7 +26,13 @@ export class Parameter implements CodeNode {
26
26
  const type = param.getType()
27
27
  const isOptional =
28
28
  param.hasQuestionToken() || param.isOptional() || type.isNullable()
29
- this.type = createNamedType(language, name, type, isOptional)
29
+ this.type = createNamedType(
30
+ language,
31
+ name,
32
+ type,
33
+ isOptional,
34
+ param.getTypeNode()
35
+ )
30
36
  } else {
31
37
  // constructor(...)???
32
38
  throw new Error(`Invalid constructor! Arguments: ${args}`)
@@ -1,4 +1,4 @@
1
- import { ts, Type as TSMorphType, type Signature } from 'ts-morph'
1
+ import { Node, ts, Type as TSMorphType, type Signature } from 'ts-morph'
2
2
  import type { Type } from './types/Type.js'
3
3
  import { BooleanType } from './types/BooleanType.js'
4
4
  import { NumberType } from './types/NumberType.js'
@@ -83,6 +83,54 @@ type Tuple<
83
83
  N extends number,
84
84
  R extends unknown[] = [],
85
85
  > = R['length'] extends N ? R : Tuple<T, N, [T, ...R]>
86
+
87
+ type TypeNode = Node<ts.TypeNode>
88
+
89
+ interface UnionConstituent {
90
+ type: TSMorphType
91
+ typeNode?: TypeNode
92
+ }
93
+
94
+ function getAliasTypeNode(type: TSMorphType): TypeNode | undefined {
95
+ const symbol = type.getNonNullableType().getAliasSymbol()
96
+ const declaration = symbol?.getDeclarations()[0]
97
+ if (declaration != null && Node.isTypeAliasDeclaration(declaration)) {
98
+ return declaration.getTypeNode()
99
+ }
100
+ return undefined
101
+ }
102
+
103
+ function getUnionTypeNodes(
104
+ type: TSMorphType,
105
+ typeNode?: TypeNode
106
+ ): TypeNode[] | undefined {
107
+ if (typeNode != null && Node.isUnionTypeNode(typeNode)) {
108
+ return typeNode.getTypeNodes()
109
+ }
110
+
111
+ const aliasTypeNode = getAliasTypeNode(type)
112
+ if (aliasTypeNode != null && Node.isUnionTypeNode(aliasTypeNode)) {
113
+ return aliasTypeNode.getTypeNodes()
114
+ }
115
+
116
+ return undefined
117
+ }
118
+
119
+ function getUnionConstituents(
120
+ type: TSMorphType,
121
+ typeNode?: TypeNode
122
+ ): UnionConstituent[] {
123
+ const typeNodes = getUnionTypeNodes(type, typeNode)
124
+ if (typeNodes != null) {
125
+ return typeNodes.map((node) => ({
126
+ type: node.getType(),
127
+ typeNode: node,
128
+ }))
129
+ }
130
+
131
+ return type.getUnionTypes().map((unionType) => ({ type: unionType }))
132
+ }
133
+
86
134
  function getArguments<N extends number>(
87
135
  type: TSMorphType,
88
136
  typename: string,
@@ -107,14 +155,18 @@ export function createNamedType(
107
155
  language: Language,
108
156
  name: string,
109
157
  type: TSMorphType,
110
- isOptional: boolean
158
+ isOptional: boolean,
159
+ typeNode?: TypeNode
111
160
  ) {
112
161
  if (name.startsWith('__')) {
113
162
  throw new Error(
114
163
  `Name cannot start with two underscores (__) as this is reserved syntax for Nitrogen! (In ${type.getText()})`
115
164
  )
116
165
  }
117
- return new NamedWrappingType(name, createType(language, type, isOptional))
166
+ return new NamedWrappingType(
167
+ name,
168
+ createType(language, type, isOptional, typeNode)
169
+ )
118
170
  }
119
171
 
120
172
  export function createVoidType(): Type {
@@ -171,7 +223,8 @@ export function addKnownType(
171
223
  export function createType(
172
224
  language: Language,
173
225
  type: TSMorphType,
174
- isOptional: boolean
226
+ isOptional: boolean,
227
+ typeNode?: TypeNode
175
228
  ): Type {
176
229
  const key = getTypeId(type, isOptional)
177
230
  if (key != null && knownTypes[language].has(key)) {
@@ -183,7 +236,7 @@ export function createType(
183
236
 
184
237
  const get = () => {
185
238
  if (isOptional) {
186
- const wrapping = createType(language, type, false)
239
+ const wrapping = createType(language, type, false, typeNode)
187
240
  return new OptionalType(wrapping)
188
241
  }
189
242
 
@@ -286,10 +339,10 @@ export function createType(
286
339
  // - of string literals (then it's an enum)
287
340
  // - of type `T | undefined` (then it's just optional `T`)
288
341
  // - of different types (then it's a variant `A | B | C`)
289
- const types = type.getUnionTypes()
290
- const nonNullTypes = types.filter(
291
- (t) => !t.isNull() && !t.isUndefined() && !t.isVoid()
292
- )
342
+ const unionConstituents = getUnionConstituents(type, typeNode)
343
+ const nonNullTypes = unionConstituents
344
+ .map((c) => c.type)
345
+ .filter((t) => !t.isNull() && !t.isUndefined() && !t.isVoid())
293
346
  const isEnumUnion = nonNullTypes.every((t) => t.isStringLiteral())
294
347
  if (isEnumUnion) {
295
348
  // It consists only of string literaly - that means it's describing an enum!
@@ -305,11 +358,12 @@ export function createType(
305
358
  return new EnumType(typename, type)
306
359
  } else {
307
360
  // It consists of different types - that means it's a variant!
308
- let variants = type
309
- .getUnionTypes()
361
+ let variants = unionConstituents
310
362
  // Filter out any undefineds/voids, as those are already treated as `isOptional`.
311
- .filter((t) => !t.isUndefined() && !t.isVoid())
312
- .map((t) => createType(language, t, false))
363
+ .filter(({ type: t }) => !t.isUndefined() && !t.isVoid())
364
+ .map(({ type: t, typeNode: node }) =>
365
+ createType(language, t, false, node)
366
+ )
313
367
  .toSorted(compareLooselyness)
314
368
  variants = removeDuplicates(variants)
315
369
 
@@ -1,4 +1,4 @@
1
- import type { ts, Type } from 'ts-morph'
1
+ import { Node, type ts, type Type } from 'ts-morph'
2
2
  import type { NamedType } from './types/Type.js'
3
3
  import { createNamedType } from './createType.js'
4
4
  import type { Language } from '../getPlatformSpecs.js'
@@ -13,6 +13,9 @@ export function getInterfaceProperties(
13
13
  `Interface "${interfaceType.getText()}" does not have a Symbol!`
14
14
  )
15
15
  return interfaceType.getProperties().map((prop) => {
16
+ const propDeclaration = prop
17
+ .getDeclarations()
18
+ .find((declaration) => Node.isPropertySignature(declaration))
16
19
  let propType = prop.getDeclaredType()
17
20
  if (propType.isAny() || propType.isUnknown()) {
18
21
  // the interface is aliased/merged - we need to look into the actual declaration
@@ -29,7 +32,8 @@ export function getInterfaceProperties(
29
32
  language,
30
33
  prop.getName(),
31
34
  propType,
32
- prop.isOptional() || propType.isNullable()
35
+ prop.isOptional() || propType.isNullable(),
36
+ propDeclaration?.getTypeNode()
33
37
  )
34
38
  return refType
35
39
  })
@@ -49,7 +49,7 @@ namespace ${cxxNamespace} {
49
49
  using namespace facebook;
50
50
 
51
51
  /**
52
- * The C++ JNI bridge between the C++ enum "${enumType.enumName}" and the the Kotlin enum "${enumType.enumName}".
52
+ * The C++ JNI bridge between the C++ enum "${enumType.enumName}" and the Kotlin enum "${enumType.enumName}".
53
53
  */
54
54
  struct J${enumType.enumName} final: public jni::JavaClass<J${enumType.enumName}> {
55
55
  public:
@@ -133,7 +133,7 @@ namespace ${cxxNamespace} {
133
133
  using namespace facebook;
134
134
 
135
135
  /**
136
- * The C++ JNI bridge between the C++ struct "${structType.structName}" and the the Kotlin data class "${structType.structName}".
136
+ * The C++ JNI bridge between the C++ struct "${structType.structName}" and the Kotlin data class "${structType.structName}".
137
137
  */
138
138
  struct J${structType.structName} final: public jni::JavaClass<J${structType.structName}> {
139
139
  public:
@@ -257,7 +257,7 @@ namespace ${namespace} {
257
257
 
258
258
  ${descriptorClassName}::${descriptorClassName}(const react::ComponentDescriptorParameters& parameters)
259
259
  : ConcreteComponentDescriptor(parameters,
260
- react::RawPropsParser(/* enableJsiParser */ true)) {}
260
+ react::RawPropsParser()) {}
261
261
 
262
262
  std::shared_ptr<const react::Props> ${descriptorClassName}::cloneProps(const react::PropsParserContext& context,
263
263
  ${descriptorIndent} const std::shared_ptr<const react::Props>& props,