@astryxdesign/native 0.7.5-canary.51c62ca → 0.7.5-canary.f2dafed
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 +0 -16
- package/dist/contract/fromComponentDoc.d.ts +5 -43
- package/dist/contract/fromComponentDoc.d.ts.map +1 -1
- package/dist/contract/fromComponentDoc.js +4 -46
- package/dist/contract/generateSwift.d.ts.map +1 -1
- package/dist/contract/generateSwift.js +0 -11
- package/dist/contract/generateTypeScript.d.ts.map +1 -1
- package/dist/contract/generateTypeScript.js +0 -2
- package/dist/contract/index.d.ts +3 -7
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +1 -2
- package/dist/contract/parseDocType.d.ts +2 -5
- package/dist/contract/parseDocType.d.ts.map +1 -1
- package/dist/contract/parseDocType.js +5 -34
- package/dist/contract/rules.d.ts +2 -5
- package/dist/contract/rules.d.ts.map +1 -1
- package/dist/contract/rules.js +4 -8
- package/dist/contract/types.d.ts +0 -7
- package/dist/contract/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/contract/fromComponentDoc.ts +9 -104
- package/src/contract/generateSwift.ts +0 -16
- package/src/contract/generateTypeScript.ts +0 -2
- package/src/contract/index.ts +4 -13
- package/src/contract/parseDocType.test.ts +0 -45
- package/src/contract/parseDocType.ts +6 -41
- package/src/contract/rules.ts +4 -11
- package/src/contract/types.ts +0 -4
- package/src/contract/fromComponentDoc.test.ts +0 -97
package/README.md
CHANGED
|
@@ -34,22 +34,6 @@ const props: SelectorProps = {
|
|
|
34
34
|
};
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
How core documentation becomes a contract:
|
|
38
|
-
|
|
39
|
-
- A callback whose arguments are values followed by the DOM event that caused
|
|
40
|
-
them, such as `(checked: boolean, e: ChangeEvent) => void`, keeps the values
|
|
41
|
-
in the core; the event stays on the web.
|
|
42
|
-
- A callback that carries only an event stays on the web, unless the component
|
|
43
|
-
names it in `callbacks`, as a button's `onClick` would. Its core form takes no
|
|
44
|
-
arguments. A callback with an event before a value stays on the web either
|
|
45
|
-
way.
|
|
46
|
-
- Free-form content becomes a typed slot only where a component declares one
|
|
47
|
-
in `slots`; `text` is a string, as for a badge's label or a heading's words.
|
|
48
|
-
- A closed set of numbers, such as a heading level, is a Swift enum with raw
|
|
49
|
-
values.
|
|
50
|
-
- A core prop may share a platform toolkit's name only when the component
|
|
51
|
-
declares in `sameMeaning` that it means the same thing there.
|
|
52
|
-
|
|
53
37
|
## Working on it
|
|
54
38
|
|
|
55
39
|
```sh
|
|
@@ -1,56 +1,19 @@
|
|
|
1
1
|
import type { BaseTarget, ComponentContract, PropMap } from './types.js';
|
|
2
2
|
import type { NativePlatform } from '../platforms.js';
|
|
3
|
-
|
|
4
|
-
readonly name: string;
|
|
5
|
-
readonly type?: unknown;
|
|
6
|
-
readonly required?: boolean;
|
|
7
|
-
readonly description?: unknown;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* The subset of a ComponentDoc this reader needs. A file that documents
|
|
11
|
-
* several components, such as Stack with HStack and VStack, lists them in
|
|
12
|
-
* `components`.
|
|
13
|
-
*/
|
|
3
|
+
/** The subset of a ComponentDoc this reader needs. */
|
|
14
4
|
export interface ComponentDocLike {
|
|
15
5
|
readonly name: string;
|
|
16
|
-
readonly props?: ReadonlyArray<
|
|
17
|
-
readonly components?: ReadonlyArray<{
|
|
6
|
+
readonly props?: ReadonlyArray<{
|
|
18
7
|
readonly name: string;
|
|
19
|
-
readonly
|
|
8
|
+
readonly type?: unknown;
|
|
9
|
+
readonly required?: boolean;
|
|
10
|
+
readonly description?: unknown;
|
|
20
11
|
}>;
|
|
21
12
|
}
|
|
22
|
-
/**
|
|
23
|
-
* Typed slots (AST-082 FR4) for documented free-form content. `text` is a
|
|
24
|
-
* string in the core contract, as a badge's label or a heading's words.
|
|
25
|
-
*/
|
|
26
|
-
export type SlotKind = 'text';
|
|
27
13
|
/** What the native package adds to one core component. */
|
|
28
14
|
export interface NativeAdditions {
|
|
29
15
|
readonly name: string;
|
|
30
16
|
readonly baseTarget: BaseTarget;
|
|
31
|
-
/**
|
|
32
|
-
* Where core documents the component when it is not `<Name>/<Name>.doc.mjs`:
|
|
33
|
-
* `file` relative to core's `src` without `.doc.mjs`, and `component` when
|
|
34
|
-
* the file documents several components.
|
|
35
|
-
*/
|
|
36
|
-
readonly doc?: {
|
|
37
|
-
readonly file: string;
|
|
38
|
-
readonly component?: string;
|
|
39
|
-
};
|
|
40
|
-
/** Documented free-form content props, given a typed slot in the core. */
|
|
41
|
-
readonly slots?: Readonly<Record<string, SlotKind>>;
|
|
42
|
-
/**
|
|
43
|
-
* Callbacks whose only documented argument is a DOM event but which name a
|
|
44
|
-
* moment every platform has, such as a button's press. Their core form
|
|
45
|
-
* takes no arguments.
|
|
46
|
-
*/
|
|
47
|
-
readonly callbacks?: readonly string[];
|
|
48
|
-
/**
|
|
49
|
-
* Core props whose name a platform toolkit also uses, with the same meaning
|
|
50
|
-
* (AST-082 FR8 forbids only a different meaning), and why: a button's
|
|
51
|
-
* `onClick` is a press in Compose too.
|
|
52
|
-
*/
|
|
53
|
-
readonly sameMeaning?: Readonly<Record<string, string>>;
|
|
54
17
|
/**
|
|
55
18
|
* Portable definitions for named types the documentation references. Fields
|
|
56
19
|
* the web type has that are not portable are left out on purpose.
|
|
@@ -66,5 +29,4 @@ export interface NativeAdditions {
|
|
|
66
29
|
readonly webOnly?: Readonly<Record<string, string>>;
|
|
67
30
|
}
|
|
68
31
|
export declare function fromComponentDoc(doc: ComponentDocLike, additions: NativeAdditions): ComponentContract;
|
|
69
|
-
export {};
|
|
70
32
|
//# sourceMappingURL=fromComponentDoc.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fromComponentDoc.d.ts","sourceRoot":"","sources":["../../src/contract/fromComponentDoc.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EACjB,OAAO,
|
|
1
|
+
{"version":3,"file":"fromComponentDoc.d.ts","sourceRoot":"","sources":["../../src/contract/fromComponentDoc.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EACjB,OAAO,EAGR,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,iBAAiB,CAAC;AAEpD,sDAAsD;AACtD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;QAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;QACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;QAC5B,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;KAChC,CAAC,CAAC;CACJ;AAED,0DAA0D;AAC1D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACnD,gFAAgF;IAChF,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACxE;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACrD;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,gBAAgB,EACrB,SAAS,EAAE,eAAe,GACzB,iBAAiB,CA0EnB"}
|
|
@@ -10,32 +10,11 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { parseDocType } from './parseDocType.js';
|
|
12
12
|
import { coreNameProblem } from './rules.js';
|
|
13
|
-
/** The documented props of the component the additions name. */
|
|
14
|
-
function documentedProps(doc, additions) {
|
|
15
|
-
const wanted = additions.doc?.component;
|
|
16
|
-
if (wanted == null) {
|
|
17
|
-
return doc.props ?? [];
|
|
18
|
-
}
|
|
19
|
-
const entry = doc.components?.find(component => component.name === wanted);
|
|
20
|
-
if (!entry) {
|
|
21
|
-
throw new Error(`${additions.name}: core documentation "${doc.name}" does not document "${wanted}"`);
|
|
22
|
-
}
|
|
23
|
-
return entry.props ?? [];
|
|
24
|
-
}
|
|
25
|
-
const SLOT_TYPES = {
|
|
26
|
-
text: { kind: 'string' },
|
|
27
|
-
};
|
|
28
13
|
export function fromComponentDoc(doc, additions) {
|
|
29
14
|
const core = {};
|
|
30
15
|
const webOnly = [];
|
|
31
16
|
const types = additions.types ?? {};
|
|
32
|
-
|
|
33
|
-
for (const prop of documentedProps(doc, additions)) {
|
|
34
|
-
// `id` and `isHidden` are shared base props on every platform (FR7), so a
|
|
35
|
-
// documented prop of either name is the base prop, not a core prop.
|
|
36
|
-
if (prop.name === 'id' || prop.name === 'isHidden') {
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
17
|
+
for (const prop of doc.props ?? []) {
|
|
39
18
|
const declaredWebOnly = additions.webOnly?.[prop.name];
|
|
40
19
|
if (declaredWebOnly) {
|
|
41
20
|
webOnly.push({ name: prop.name, reason: declaredWebOnly });
|
|
@@ -50,33 +29,13 @@ export function fromComponentDoc(doc, additions) {
|
|
|
50
29
|
webOnly.push({ name: prop.name, reason: 'HTML attribute' });
|
|
51
30
|
continue;
|
|
52
31
|
}
|
|
53
|
-
const
|
|
54
|
-
const slotNameProblem = slot &&
|
|
55
|
-
coreNameProblem(prop.name, prop.name in (additions.sameMeaning ?? {}));
|
|
56
|
-
if (slotNameProblem) {
|
|
57
|
-
webOnly.push({ name: prop.name, reason: slotNameProblem });
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
if (slot) {
|
|
61
|
-
core[prop.name] = {
|
|
62
|
-
type: SLOT_TYPES[slot],
|
|
63
|
-
optional: prop.required !== true,
|
|
64
|
-
...describe(prop),
|
|
65
|
-
};
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
let parsed = parseDocType(String(prop.type ?? ''));
|
|
69
|
-
if (!parsed.ok &&
|
|
70
|
-
parsed.eventOnly === true &&
|
|
71
|
-
additions.callbacks?.includes(prop.name)) {
|
|
72
|
-
parsed = { ok: true, type: { kind: 'fn', args: [] } };
|
|
73
|
-
}
|
|
32
|
+
const parsed = parseDocType(String(prop.type ?? ''));
|
|
74
33
|
if (!parsed.ok) {
|
|
75
34
|
webOnly.push({ name: prop.name, reason: parsed.reason });
|
|
76
35
|
continue;
|
|
77
36
|
}
|
|
78
37
|
// A portable type can still carry a name another platform owns.
|
|
79
|
-
const nameProblem = coreNameProblem(prop.name
|
|
38
|
+
const nameProblem = coreNameProblem(prop.name);
|
|
80
39
|
if (nameProblem) {
|
|
81
40
|
webOnly.push({ name: prop.name, reason: nameProblem });
|
|
82
41
|
continue;
|
|
@@ -108,7 +67,7 @@ export function fromComponentDoc(doc, additions) {
|
|
|
108
67
|
core[prop.name] = {
|
|
109
68
|
type,
|
|
110
69
|
optional: prop.required !== true,
|
|
111
|
-
...
|
|
70
|
+
...(typeof prop.description === 'string' ? { doc: prop.description } : {}),
|
|
112
71
|
};
|
|
113
72
|
}
|
|
114
73
|
return {
|
|
@@ -118,6 +77,5 @@ export function fromComponentDoc(doc, additions) {
|
|
|
118
77
|
core,
|
|
119
78
|
platforms: additions.platforms ?? {},
|
|
120
79
|
webOnly,
|
|
121
|
-
sameMeaning: Object.keys(additions.sameMeaning ?? {}).filter(name => name in core),
|
|
122
80
|
};
|
|
123
81
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generateSwift.d.ts","sourceRoot":"","sources":["../../src/contract/generateSwift.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAC,iBAAiB,EAAoB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"generateSwift.d.ts","sourceRoot":"","sources":["../../src/contract/generateSwift.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAC,iBAAiB,EAAoB,MAAM,YAAY,CAAC;AAoJrE,6EAA6E;AAC7E,wBAAgB,aAAa,CAC3B,SAAS,EAAE,SAAS,iBAAiB,EAAE,GACtC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMxB"}
|
|
@@ -16,10 +16,6 @@ const APPLE_PLATFORMS = [
|
|
|
16
16
|
const camel = (text) => text.replace(/[-_ ]+([a-zA-Z0-9])/g, (_, ch) => ch.toUpperCase());
|
|
17
17
|
const pascal = (text) => camel(text).replace(/^./, ch => ch.toUpperCase());
|
|
18
18
|
const ident = (name) => SWIFT_KEYWORDS.has(name) ? `\`${name}\`` : name;
|
|
19
|
-
/** `1` → `v1`, `0.5` → `v0_5`, `-1` → `vMinus1`. */
|
|
20
|
-
function numberCaseName(value) {
|
|
21
|
-
return `v${String(value).replace('-', 'Minus').replace('.', '_')}`;
|
|
22
|
-
}
|
|
23
19
|
function caseName(value) {
|
|
24
20
|
const name = camel(value.replace(/[^A-Za-z0-9_-]/g, '-'));
|
|
25
21
|
return ident(/^[0-9]/.test(name) ? `v${name}` : name || 'empty');
|
|
@@ -41,13 +37,6 @@ class SwiftWriter {
|
|
|
41
37
|
this.decls.push(`public enum ${name}: String, CaseIterable, Sendable {\n${cases.join('\n')}\n}`);
|
|
42
38
|
return name;
|
|
43
39
|
}
|
|
44
|
-
case 'numberEnum': {
|
|
45
|
-
const name = `${owner}${pascal(prop)}`;
|
|
46
|
-
const raw = type.values.every(Number.isInteger) ? 'Int' : 'Double';
|
|
47
|
-
const cases = type.values.map(value => ` case ${numberCaseName(value)} = ${value}`);
|
|
48
|
-
this.decls.push(`public enum ${name}: ${raw}, CaseIterable, Sendable {\n${cases.join('\n')}\n}`);
|
|
49
|
-
return name;
|
|
50
|
-
}
|
|
51
40
|
case 'array':
|
|
52
41
|
return `[${this.type(owner, prop, type.of)}]`;
|
|
53
42
|
case 'record': {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generateTypeScript.d.ts","sourceRoot":"","sources":["../../src/contract/generateTypeScript.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAC,iBAAiB,EAAoB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"generateTypeScript.d.ts","sourceRoot":"","sources":["../../src/contract/generateTypeScript.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAC,iBAAiB,EAAoB,MAAM,YAAY,CAAC;AA2FrE,0EAA0E;AAC1E,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,SAAS,iBAAiB,EAAE,GACtC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASxB"}
|
|
@@ -16,8 +16,6 @@ function tsType(type) {
|
|
|
16
16
|
return type.kind;
|
|
17
17
|
case 'enum':
|
|
18
18
|
return type.values.map(value => `'${value}'`).join(' | ');
|
|
19
|
-
case 'numberEnum':
|
|
20
|
-
return type.values.join(' | ');
|
|
21
19
|
case 'array': {
|
|
22
20
|
const inner = tsType(type.of);
|
|
23
21
|
return `ReadonlyArray<${inner}>`;
|
package/dist/contract/index.d.ts
CHANGED
|
@@ -2,19 +2,15 @@ import { type ComponentDocLike } from './fromComponentDoc.js';
|
|
|
2
2
|
import { type ContractIssue } from './rules.js';
|
|
3
3
|
import type { ComponentContract } from './types.js';
|
|
4
4
|
export { fromComponentDoc } from './fromComponentDoc.js';
|
|
5
|
-
export type { ComponentDocLike, NativeAdditions
|
|
5
|
+
export type { ComponentDocLike, NativeAdditions } from './fromComponentDoc.js';
|
|
6
6
|
export { generateSwift } from './generateSwift.js';
|
|
7
7
|
export { generateTypeScript } from './generateTypeScript.js';
|
|
8
8
|
export { parseDocType } from './parseDocType.js';
|
|
9
9
|
export { checkContract, coreNameProblem } from './rules.js';
|
|
10
10
|
export type { ContractIssue } from './rules.js';
|
|
11
11
|
export type * from './types.js';
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
* `<Name>/<Name>.doc.mjs`, or a path such as `Text/Heading` relative to core's
|
|
15
|
-
* `src` without `.doc.mjs`.
|
|
16
|
-
*/
|
|
17
|
-
export type ComponentDocLoader = (file: string) => Promise<ComponentDocLike>;
|
|
12
|
+
/** Loads one core component's documentation by component name. */
|
|
13
|
+
export type ComponentDocLoader = (name: string) => Promise<ComponentDocLike>;
|
|
18
14
|
export interface BuiltContracts {
|
|
19
15
|
readonly contracts: readonly ComponentContract[];
|
|
20
16
|
readonly warnings: readonly ContractIssue[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/contract/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAmB,KAAK,gBAAgB,EAAC,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAgB,KAAK,aAAa,EAAC,MAAM,YAAY,CAAC;AAC7D,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,YAAY,CAAC;AAElD,OAAO,EAAC,gBAAgB,EAAC,MAAM,uBAAuB,CAAC;AACvD,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/contract/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAmB,KAAK,gBAAgB,EAAC,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAgB,KAAK,aAAa,EAAC,MAAM,YAAY,CAAC;AAC7D,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,YAAY,CAAC;AAElD,OAAO,EAAC,gBAAgB,EAAC,MAAM,uBAAuB,CAAC;AACvD,YAAY,EAAC,gBAAgB,EAAE,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAC7E,OAAO,EAAC,aAAa,EAAC,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAC,kBAAkB,EAAC,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAC,YAAY,EAAC,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAC,aAAa,EAAE,eAAe,EAAC,MAAM,YAAY,CAAC;AAC1D,YAAY,EAAC,aAAa,EAAC,MAAM,YAAY,CAAC;AAC9C,mBAAmB,YAAY,CAAC;AAEhC,kEAAkE;AAClE,MAAM,MAAM,kBAAkB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;AAE7E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;CAC7C;AAED,8EAA8E;AAC9E,wBAAsB,cAAc,CAClC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,cAAc,CAAC,CAiBzB"}
|
package/dist/contract/index.js
CHANGED
|
@@ -20,8 +20,7 @@ export async function buildContracts(loadDoc) {
|
|
|
20
20
|
const warnings = [];
|
|
21
21
|
const errors = [];
|
|
22
22
|
for (const additions of NATIVE_COMPONENTS) {
|
|
23
|
-
const
|
|
24
|
-
const contract = fromComponentDoc(doc, additions);
|
|
23
|
+
const contract = fromComponentDoc(await loadDoc(additions.name), additions);
|
|
25
24
|
for (const issue of checkContract(contract)) {
|
|
26
25
|
(issue.level === 'error' ? errors : warnings).push(issue);
|
|
27
26
|
}
|
|
@@ -11,14 +11,11 @@ export type ParsedType = {
|
|
|
11
11
|
} | {
|
|
12
12
|
readonly ok: false;
|
|
13
13
|
readonly reason: string;
|
|
14
|
-
/** True for a callback whose only arguments are DOM events. */
|
|
15
|
-
readonly eventOnly?: boolean;
|
|
16
14
|
};
|
|
17
15
|
/**
|
|
18
16
|
* Parses a documented type string. Supported portable forms: `string`,
|
|
19
|
-
* `number`, `boolean`, closed string
|
|
20
|
-
*
|
|
21
|
-
* is the web's part), and named types. A union of different
|
|
17
|
+
* `number`, `boolean`, closed string unions, arrays, plain object literals,
|
|
18
|
+
* callbacks with portable arguments, and named types. A union of different
|
|
22
19
|
* kinds has no Swift or Kotlin form.
|
|
23
20
|
*/
|
|
24
21
|
export declare function parseDocType(raw: string): ParsedType;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parseDocType.d.ts","sourceRoot":"","sources":["../../src/contract/parseDocType.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,OAAO,KAAK,EAAW,QAAQ,EAAC,MAAM,YAAY,CAAC;AAEnD,MAAM,MAAM,UAAU,GAClB;IAAC,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAC,GAC5C;
|
|
1
|
+
{"version":3,"file":"parseDocType.d.ts","sourceRoot":"","sources":["../../src/contract/parseDocType.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,OAAO,KAAK,EAAW,QAAQ,EAAC,MAAM,YAAY,CAAC;AAEnD,MAAM,MAAM,UAAU,GAClB;IAAC,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAC,GAC5C;IAAC,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAC,CAAC;AAuIlD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAwDpD"}
|
|
@@ -19,15 +19,7 @@ const WEB_ONLY_TYPES = {
|
|
|
19
19
|
ChangeEvent: 'DOM event',
|
|
20
20
|
DragEvent: 'DOM event',
|
|
21
21
|
ClipboardEvent: 'DOM event',
|
|
22
|
-
FormEvent: 'DOM event',
|
|
23
|
-
TouchEvent: 'DOM event',
|
|
24
|
-
SyntheticEvent: 'DOM event',
|
|
25
22
|
};
|
|
26
|
-
/** True for a DOM event type, with or without its element parameter. */
|
|
27
|
-
function isDomEvent(text) {
|
|
28
|
-
const name = /^(?:React\.)?([A-Z]\w*)(?:<[\s\S]*>)?$/.exec(text.trim())?.[1];
|
|
29
|
-
return name != null && WEB_ONLY_TYPES[name] === 'DOM event';
|
|
30
|
-
}
|
|
31
23
|
const ok = (type) => ({ ok: true, type });
|
|
32
24
|
const no = (reason) => ({ ok: false, reason });
|
|
33
25
|
/** Splits on a separator at bracket depth zero, ignoring quoted text. */
|
|
@@ -123,24 +115,10 @@ function parseFunction(text) {
|
|
|
123
115
|
: 'callback returns a value; only void callbacks are portable today');
|
|
124
116
|
}
|
|
125
117
|
const params = arrow[0].slice(1, -1).trim();
|
|
126
|
-
const types = (params ? splitTop(params, ',') : []).map(param => {
|
|
127
|
-
const match = /^([A-Za-z_$][\w$]*)\??\s*:\s*([\s\S]+)$/.exec(param);
|
|
128
|
-
return match ? match[2] : param;
|
|
129
|
-
});
|
|
130
|
-
// A value callback ends with the DOM event that caused it, as in
|
|
131
|
-
// `(checked: boolean, e: ChangeEvent) => void`. The values are the portable
|
|
132
|
-
// callback; the trailing event is the web's part. A callback that carries
|
|
133
|
-
// only an event stays on the web.
|
|
134
|
-
let end = types.length;
|
|
135
|
-
while (end > 0 && isDomEvent(types[end - 1])) {
|
|
136
|
-
end--;
|
|
137
|
-
}
|
|
138
|
-
if (end === 0 && types.length > 0) {
|
|
139
|
-
return { ok: false, reason: 'callback argument: DOM event', eventOnly: true };
|
|
140
|
-
}
|
|
141
118
|
const args = [];
|
|
142
|
-
for (const
|
|
143
|
-
const
|
|
119
|
+
for (const param of params ? splitTop(params, ',') : []) {
|
|
120
|
+
const match = /^([A-Za-z_$][\w$]*)\??\s*:\s*([\s\S]+)$/.exec(param);
|
|
121
|
+
const parsed = parseDocType(match ? match[2] : param);
|
|
144
122
|
if (!parsed.ok) {
|
|
145
123
|
return no(`callback argument: ${parsed.reason}`);
|
|
146
124
|
}
|
|
@@ -150,9 +128,8 @@ function parseFunction(text) {
|
|
|
150
128
|
}
|
|
151
129
|
/**
|
|
152
130
|
* Parses a documented type string. Supported portable forms: `string`,
|
|
153
|
-
* `number`, `boolean`, closed string
|
|
154
|
-
*
|
|
155
|
-
* is the web's part), and named types. A union of different
|
|
131
|
+
* `number`, `boolean`, closed string unions, arrays, plain object literals,
|
|
132
|
+
* callbacks with portable arguments, and named types. A union of different
|
|
156
133
|
* kinds has no Swift or Kotlin form.
|
|
157
134
|
*/
|
|
158
135
|
export function parseDocType(raw) {
|
|
@@ -169,9 +146,6 @@ export function parseDocType(raw) {
|
|
|
169
146
|
if (members.every(m => /^'[^']*'$/.test(m))) {
|
|
170
147
|
return ok({ kind: 'enum', values: members.map(m => m.slice(1, -1)) });
|
|
171
148
|
}
|
|
172
|
-
if (members.every(m => /^-?\d+(?:\.\d+)?$/.test(m))) {
|
|
173
|
-
return ok({ kind: 'numberEnum', values: members.map(Number) });
|
|
174
|
-
}
|
|
175
149
|
const webOnly = members.find(m => WEB_ONLY_TYPES[m] != null);
|
|
176
150
|
if (webOnly) {
|
|
177
151
|
return no(WEB_ONLY_TYPES[webOnly]);
|
|
@@ -188,9 +162,6 @@ export function parseDocType(raw) {
|
|
|
188
162
|
if (WEB_ONLY_TYPES[t] != null) {
|
|
189
163
|
return no(WEB_ONLY_TYPES[t]);
|
|
190
164
|
}
|
|
191
|
-
if (isDomEvent(t)) {
|
|
192
|
-
return no('DOM event');
|
|
193
|
-
}
|
|
194
165
|
const generic = /^(?:Readonly)?Array<([\s\S]+)>$/.exec(t);
|
|
195
166
|
if (t.endsWith('[]') || generic) {
|
|
196
167
|
const inner = parseDocType(generic ? generic[1] : t.slice(0, -2));
|
package/dist/contract/rules.d.ts
CHANGED
|
@@ -4,10 +4,7 @@ export interface ContractIssue {
|
|
|
4
4
|
readonly rule: 'core-is-portable' | 'name-keyword' | 'name-toolkit' | 'platform-shadows-core' | 'same-name-different-meaning' | 'unknown-type';
|
|
5
5
|
readonly message: string;
|
|
6
6
|
}
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
* allowed when the component declares it has the same meaning there.
|
|
10
|
-
*/
|
|
11
|
-
export declare function coreNameProblem(name: string, sameMeaning?: boolean): string | null;
|
|
7
|
+
/** Why a core prop name is not allowed, or null when it is. */
|
|
8
|
+
export declare function coreNameProblem(name: string): string | null;
|
|
12
9
|
export declare function checkContract(contract: ComponentContract): ContractIssue[];
|
|
13
10
|
//# sourceMappingURL=rules.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/contract/rules.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAC,iBAAiB,EAAoB,MAAM,YAAY,CAAC;AAErE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,IAAI,EACT,kBAAkB,GAClB,cAAc,GACd,cAAc,GACd,uBAAuB,GACvB,6BAA6B,GAC7B,cAAc,CAAC;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/contract/rules.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAC,iBAAiB,EAAoB,MAAM,YAAY,CAAC;AAErE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,IAAI,EACT,kBAAkB,GAClB,cAAc,GACd,cAAc,GACd,uBAAuB,GACvB,6BAA6B,GAC7B,cAAc,CAAC;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,+DAA+D;AAC/D,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgB3D;AAeD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,iBAAiB,GAAG,aAAa,EAAE,CAoE1E"}
|
package/dist/contract/rules.js
CHANGED
|
@@ -8,18 +8,15 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { NATIVE_PLATFORMS } from '../platforms.js';
|
|
10
10
|
import { KOTLIN_KEYWORDS, RECLAIMED, SWIFT_KEYWORDS, TOOLKIT_NAMES, } from './names.js';
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
* allowed when the component declares it has the same meaning there.
|
|
14
|
-
*/
|
|
15
|
-
export function coreNameProblem(name, sameMeaning = false) {
|
|
11
|
+
/** Why a core prop name is not allowed, or null when it is. */
|
|
12
|
+
export function coreNameProblem(name) {
|
|
16
13
|
if (SWIFT_KEYWORDS.has(name)) {
|
|
17
14
|
return `"${name}" is a Swift keyword`;
|
|
18
15
|
}
|
|
19
16
|
if (KOTLIN_KEYWORDS.has(name)) {
|
|
20
17
|
return `"${name}" is a Kotlin keyword`;
|
|
21
18
|
}
|
|
22
|
-
if (RECLAIMED.has(name)
|
|
19
|
+
if (RECLAIMED.has(name)) {
|
|
23
20
|
return null;
|
|
24
21
|
}
|
|
25
22
|
for (const [toolkit, names] of Object.entries(TOOLKIT_NAMES)) {
|
|
@@ -49,9 +46,8 @@ export function checkContract(contract) {
|
|
|
49
46
|
const add = (level, rule, message) => issues.push({ level, rule, message: `${contract.name}: ${message}` });
|
|
50
47
|
const referenced = new Set();
|
|
51
48
|
const collect = (props) => Object.values(props).forEach(spec => namedTypesIn(spec.type, referenced));
|
|
52
|
-
const sameMeaning = new Set(contract.sameMeaning ?? []);
|
|
53
49
|
for (const name of Object.keys(contract.core)) {
|
|
54
|
-
const problem = coreNameProblem(name
|
|
50
|
+
const problem = coreNameProblem(name);
|
|
55
51
|
if (problem) {
|
|
56
52
|
add('error', SWIFT_KEYWORDS.has(name) || KOTLIN_KEYWORDS.has(name)
|
|
57
53
|
? 'name-keyword'
|
package/dist/contract/types.d.ts
CHANGED
|
@@ -16,11 +16,6 @@ export type PropType = {
|
|
|
16
16
|
} | {
|
|
17
17
|
kind: 'enum';
|
|
18
18
|
values: readonly string[];
|
|
19
|
-
}
|
|
20
|
-
/** A closed set of numbers, such as a heading level or a spacing step. */
|
|
21
|
-
| {
|
|
22
|
-
kind: 'numberEnum';
|
|
23
|
-
values: readonly number[];
|
|
24
19
|
} | {
|
|
25
20
|
kind: 'array';
|
|
26
21
|
of: PropType;
|
|
@@ -58,7 +53,5 @@ export interface ComponentContract {
|
|
|
58
53
|
readonly platforms: Readonly<Partial<Record<NativePlatform, PropMap>>>;
|
|
59
54
|
/** Core-doc props that stay in the web contract. */
|
|
60
55
|
readonly webOnly: readonly WebOnlyProp[];
|
|
61
|
-
/** Core props that share a toolkit name with the same meaning (FR8). */
|
|
62
|
-
readonly sameMeaning?: readonly string[];
|
|
63
56
|
}
|
|
64
57
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/contract/types.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,iBAAiB,CAAC;AAEpD,+EAA+E;AAC/E,MAAM,MAAM,QAAQ,GAChB;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAC,GAChB;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAC,GAChB;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,GACjB;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAAC
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/contract/types.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,iBAAiB,CAAC;AAEpD,+EAA+E;AAC/E,MAAM,MAAM,QAAQ,GAChB;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAC,GAChB;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAC,GAChB;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,GACjB;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAAC,GACzC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,EAAE,EAAE,QAAQ,CAAA;CAAC,GAC7B;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAA;CAAC,GAC5D;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,SAAS,QAAQ,EAAE,CAAA;CAAC,GACvC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,CAAC;AAElC,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;AAEzD,sEAAsE;AACtE,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,CAAC;AAEtE,iEAAiE;AACjE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAClD,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,uEAAuE;IACvE,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACvE,oDAAoD;IACpD,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;CAC1C"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/native",
|
|
3
|
-
"version": "0.7.5-canary.
|
|
3
|
+
"version": "0.7.5-canary.f2dafed",
|
|
4
4
|
"description": "Experimental native platforms for Astryx (iOS, Android, macOS, Windows) — published to npm only under the @canary dist-tag; never released as a stable (latest) version.",
|
|
5
5
|
"author": "Meta Open Source",
|
|
6
6
|
"license": "MIT",
|
|
@@ -64,10 +64,10 @@
|
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
|
-
"@astryxdesign/core": "0.7.5-canary.
|
|
67
|
+
"@astryxdesign/core": "0.7.5-canary.f2dafed"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@astryxdesign/core": "0.7.5-canary.
|
|
70
|
+
"@astryxdesign/core": "0.7.5-canary.f2dafed"
|
|
71
71
|
},
|
|
72
72
|
"scripts": {
|
|
73
73
|
"build": "node ../../scripts/clean-dist.mjs && tsc --project tsconfig.build.json && node scripts/generate.mjs --types",
|
|
@@ -17,62 +17,25 @@ import type {
|
|
|
17
17
|
ComponentContract,
|
|
18
18
|
PropMap,
|
|
19
19
|
PropSpec,
|
|
20
|
-
PropType,
|
|
21
20
|
WebOnlyProp,
|
|
22
21
|
} from './types.js';
|
|
23
22
|
import type {NativePlatform} from '../platforms.js';
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
readonly name: string;
|
|
27
|
-
readonly type?: unknown;
|
|
28
|
-
readonly required?: boolean;
|
|
29
|
-
readonly description?: unknown;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* The subset of a ComponentDoc this reader needs. A file that documents
|
|
34
|
-
* several components, such as Stack with HStack and VStack, lists them in
|
|
35
|
-
* `components`.
|
|
36
|
-
*/
|
|
24
|
+
/** The subset of a ComponentDoc this reader needs. */
|
|
37
25
|
export interface ComponentDocLike {
|
|
38
26
|
readonly name: string;
|
|
39
|
-
readonly props?: ReadonlyArray<
|
|
40
|
-
readonly components?: ReadonlyArray<{
|
|
27
|
+
readonly props?: ReadonlyArray<{
|
|
41
28
|
readonly name: string;
|
|
42
|
-
readonly
|
|
29
|
+
readonly type?: unknown;
|
|
30
|
+
readonly required?: boolean;
|
|
31
|
+
readonly description?: unknown;
|
|
43
32
|
}>;
|
|
44
33
|
}
|
|
45
34
|
|
|
46
|
-
/**
|
|
47
|
-
* Typed slots (AST-082 FR4) for documented free-form content. `text` is a
|
|
48
|
-
* string in the core contract, as a badge's label or a heading's words.
|
|
49
|
-
*/
|
|
50
|
-
export type SlotKind = 'text';
|
|
51
|
-
|
|
52
35
|
/** What the native package adds to one core component. */
|
|
53
36
|
export interface NativeAdditions {
|
|
54
37
|
readonly name: string;
|
|
55
38
|
readonly baseTarget: BaseTarget;
|
|
56
|
-
/**
|
|
57
|
-
* Where core documents the component when it is not `<Name>/<Name>.doc.mjs`:
|
|
58
|
-
* `file` relative to core's `src` without `.doc.mjs`, and `component` when
|
|
59
|
-
* the file documents several components.
|
|
60
|
-
*/
|
|
61
|
-
readonly doc?: {readonly file: string; readonly component?: string};
|
|
62
|
-
/** Documented free-form content props, given a typed slot in the core. */
|
|
63
|
-
readonly slots?: Readonly<Record<string, SlotKind>>;
|
|
64
|
-
/**
|
|
65
|
-
* Callbacks whose only documented argument is a DOM event but which name a
|
|
66
|
-
* moment every platform has, such as a button's press. Their core form
|
|
67
|
-
* takes no arguments.
|
|
68
|
-
*/
|
|
69
|
-
readonly callbacks?: readonly string[];
|
|
70
|
-
/**
|
|
71
|
-
* Core props whose name a platform toolkit also uses, with the same meaning
|
|
72
|
-
* (AST-082 FR8 forbids only a different meaning), and why: a button's
|
|
73
|
-
* `onClick` is a press in Compose too.
|
|
74
|
-
*/
|
|
75
|
-
readonly sameMeaning?: Readonly<Record<string, string>>;
|
|
76
39
|
/**
|
|
77
40
|
* Portable definitions for named types the documentation references. Fields
|
|
78
41
|
* the web type has that are not portable are left out on purpose.
|
|
@@ -88,28 +51,6 @@ export interface NativeAdditions {
|
|
|
88
51
|
readonly webOnly?: Readonly<Record<string, string>>;
|
|
89
52
|
}
|
|
90
53
|
|
|
91
|
-
/** The documented props of the component the additions name. */
|
|
92
|
-
function documentedProps(
|
|
93
|
-
doc: ComponentDocLike,
|
|
94
|
-
additions: NativeAdditions,
|
|
95
|
-
): ReadonlyArray<DocProp> {
|
|
96
|
-
const wanted = additions.doc?.component;
|
|
97
|
-
if (wanted == null) {
|
|
98
|
-
return doc.props ?? [];
|
|
99
|
-
}
|
|
100
|
-
const entry = doc.components?.find(component => component.name === wanted);
|
|
101
|
-
if (!entry) {
|
|
102
|
-
throw new Error(
|
|
103
|
-
`${additions.name}: core documentation "${doc.name}" does not document "${wanted}"`,
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
return entry.props ?? [];
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const SLOT_TYPES: Readonly<Record<SlotKind, PropType>> = {
|
|
110
|
-
text: {kind: 'string'},
|
|
111
|
-
};
|
|
112
|
-
|
|
113
54
|
export function fromComponentDoc(
|
|
114
55
|
doc: ComponentDocLike,
|
|
115
56
|
additions: NativeAdditions,
|
|
@@ -117,15 +58,8 @@ export function fromComponentDoc(
|
|
|
117
58
|
const core: Record<string, PropSpec> = {};
|
|
118
59
|
const webOnly: WebOnlyProp[] = [];
|
|
119
60
|
const types = additions.types ?? {};
|
|
120
|
-
const describe = (prop: DocProp) =>
|
|
121
|
-
typeof prop.description === 'string' ? {doc: prop.description} : {};
|
|
122
61
|
|
|
123
|
-
for (const prop of
|
|
124
|
-
// `id` and `isHidden` are shared base props on every platform (FR7), so a
|
|
125
|
-
// documented prop of either name is the base prop, not a core prop.
|
|
126
|
-
if (prop.name === 'id' || prop.name === 'isHidden') {
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
62
|
+
for (const prop of doc.props ?? []) {
|
|
129
63
|
const declaredWebOnly = additions.webOnly?.[prop.name];
|
|
130
64
|
if (declaredWebOnly) {
|
|
131
65
|
webOnly.push({name: prop.name, reason: declaredWebOnly});
|
|
@@ -142,39 +76,13 @@ export function fromComponentDoc(
|
|
|
142
76
|
webOnly.push({name: prop.name, reason: 'HTML attribute'});
|
|
143
77
|
continue;
|
|
144
78
|
}
|
|
145
|
-
const
|
|
146
|
-
const slotNameProblem =
|
|
147
|
-
slot &&
|
|
148
|
-
coreNameProblem(prop.name, prop.name in (additions.sameMeaning ?? {}));
|
|
149
|
-
if (slotNameProblem) {
|
|
150
|
-
webOnly.push({name: prop.name, reason: slotNameProblem});
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
if (slot) {
|
|
154
|
-
core[prop.name] = {
|
|
155
|
-
type: SLOT_TYPES[slot],
|
|
156
|
-
optional: prop.required !== true,
|
|
157
|
-
...describe(prop),
|
|
158
|
-
};
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
let parsed = parseDocType(String(prop.type ?? ''));
|
|
162
|
-
if (
|
|
163
|
-
!parsed.ok &&
|
|
164
|
-
parsed.eventOnly === true &&
|
|
165
|
-
additions.callbacks?.includes(prop.name)
|
|
166
|
-
) {
|
|
167
|
-
parsed = {ok: true, type: {kind: 'fn', args: []}};
|
|
168
|
-
}
|
|
79
|
+
const parsed = parseDocType(String(prop.type ?? ''));
|
|
169
80
|
if (!parsed.ok) {
|
|
170
81
|
webOnly.push({name: prop.name, reason: parsed.reason});
|
|
171
82
|
continue;
|
|
172
83
|
}
|
|
173
84
|
// A portable type can still carry a name another platform owns.
|
|
174
|
-
const nameProblem = coreNameProblem(
|
|
175
|
-
prop.name,
|
|
176
|
-
prop.name in (additions.sameMeaning ?? {}),
|
|
177
|
-
);
|
|
85
|
+
const nameProblem = coreNameProblem(prop.name);
|
|
178
86
|
if (nameProblem) {
|
|
179
87
|
webOnly.push({name: prop.name, reason: nameProblem});
|
|
180
88
|
continue;
|
|
@@ -208,7 +116,7 @@ export function fromComponentDoc(
|
|
|
208
116
|
core[prop.name] = {
|
|
209
117
|
type,
|
|
210
118
|
optional: prop.required !== true,
|
|
211
|
-
...
|
|
119
|
+
...(typeof prop.description === 'string' ? {doc: prop.description} : {}),
|
|
212
120
|
};
|
|
213
121
|
}
|
|
214
122
|
|
|
@@ -219,8 +127,5 @@ export function fromComponentDoc(
|
|
|
219
127
|
core,
|
|
220
128
|
platforms: additions.platforms ?? {},
|
|
221
129
|
webOnly,
|
|
222
|
-
sameMeaning: Object.keys(additions.sameMeaning ?? {}).filter(
|
|
223
|
-
name => name in core,
|
|
224
|
-
),
|
|
225
130
|
};
|
|
226
131
|
}
|
|
@@ -25,11 +25,6 @@ const pascal = (text: string) =>
|
|
|
25
25
|
const ident = (name: string) =>
|
|
26
26
|
SWIFT_KEYWORDS.has(name) ? `\`${name}\`` : name;
|
|
27
27
|
|
|
28
|
-
/** `1` → `v1`, `0.5` → `v0_5`, `-1` → `vMinus1`. */
|
|
29
|
-
function numberCaseName(value: number): string {
|
|
30
|
-
return `v${String(value).replace('-', 'Minus').replace('.', '_')}`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
28
|
function caseName(value: string): string {
|
|
34
29
|
const name = camel(value.replace(/[^A-Za-z0-9_-]/g, '-'));
|
|
35
30
|
return ident(/^[0-9]/.test(name) ? `v${name}` : name || 'empty');
|
|
@@ -57,17 +52,6 @@ class SwiftWriter {
|
|
|
57
52
|
);
|
|
58
53
|
return name;
|
|
59
54
|
}
|
|
60
|
-
case 'numberEnum': {
|
|
61
|
-
const name = `${owner}${pascal(prop)}`;
|
|
62
|
-
const raw = type.values.every(Number.isInteger) ? 'Int' : 'Double';
|
|
63
|
-
const cases = type.values.map(
|
|
64
|
-
value => ` case ${numberCaseName(value)} = ${value}`,
|
|
65
|
-
);
|
|
66
|
-
this.decls.push(
|
|
67
|
-
`public enum ${name}: ${raw}, CaseIterable, Sendable {\n${cases.join('\n')}\n}`,
|
|
68
|
-
);
|
|
69
|
-
return name;
|
|
70
|
-
}
|
|
71
55
|
case 'array':
|
|
72
56
|
return `[${this.type(owner, prop, type.of)}]`;
|
|
73
57
|
case 'record': {
|
|
@@ -21,8 +21,6 @@ function tsType(type: PropType): string {
|
|
|
21
21
|
return type.kind;
|
|
22
22
|
case 'enum':
|
|
23
23
|
return type.values.map(value => `'${value}'`).join(' | ');
|
|
24
|
-
case 'numberEnum':
|
|
25
|
-
return type.values.join(' | ');
|
|
26
24
|
case 'array': {
|
|
27
25
|
const inner = tsType(type.of);
|
|
28
26
|
return `ReadonlyArray<${inner}>`;
|
package/src/contract/index.ts
CHANGED
|
@@ -14,11 +14,7 @@ import {checkContract, type ContractIssue} from './rules.js';
|
|
|
14
14
|
import type {ComponentContract} from './types.js';
|
|
15
15
|
|
|
16
16
|
export {fromComponentDoc} from './fromComponentDoc.js';
|
|
17
|
-
export type {
|
|
18
|
-
ComponentDocLike,
|
|
19
|
-
NativeAdditions,
|
|
20
|
-
SlotKind,
|
|
21
|
-
} from './fromComponentDoc.js';
|
|
17
|
+
export type {ComponentDocLike, NativeAdditions} from './fromComponentDoc.js';
|
|
22
18
|
export {generateSwift} from './generateSwift.js';
|
|
23
19
|
export {generateTypeScript} from './generateTypeScript.js';
|
|
24
20
|
export {parseDocType} from './parseDocType.js';
|
|
@@ -26,12 +22,8 @@ export {checkContract, coreNameProblem} from './rules.js';
|
|
|
26
22
|
export type {ContractIssue} from './rules.js';
|
|
27
23
|
export type * from './types.js';
|
|
28
24
|
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
* `<Name>/<Name>.doc.mjs`, or a path such as `Text/Heading` relative to core's
|
|
32
|
-
* `src` without `.doc.mjs`.
|
|
33
|
-
*/
|
|
34
|
-
export type ComponentDocLoader = (file: string) => Promise<ComponentDocLike>;
|
|
25
|
+
/** Loads one core component's documentation by component name. */
|
|
26
|
+
export type ComponentDocLoader = (name: string) => Promise<ComponentDocLike>;
|
|
35
27
|
|
|
36
28
|
export interface BuiltContracts {
|
|
37
29
|
readonly contracts: readonly ComponentContract[];
|
|
@@ -46,8 +38,7 @@ export async function buildContracts(
|
|
|
46
38
|
const warnings: ContractIssue[] = [];
|
|
47
39
|
const errors: ContractIssue[] = [];
|
|
48
40
|
for (const additions of NATIVE_COMPONENTS) {
|
|
49
|
-
const
|
|
50
|
-
const contract = fromComponentDoc(doc, additions);
|
|
41
|
+
const contract = fromComponentDoc(await loadDoc(additions.name), additions);
|
|
51
42
|
for (const issue of checkContract(contract)) {
|
|
52
43
|
(issue.level === 'error' ? errors : warnings).push(issue);
|
|
53
44
|
}
|
|
@@ -50,48 +50,3 @@ describe('parseDocType', () => {
|
|
|
50
50
|
}
|
|
51
51
|
});
|
|
52
52
|
});
|
|
53
|
-
|
|
54
|
-
describe('parseDocType callbacks and closed number sets', () => {
|
|
55
|
-
it('keeps the values of a value callback and leaves its DOM event on the web', () => {
|
|
56
|
-
expect(
|
|
57
|
-
parseDocType(
|
|
58
|
-
'(checked: boolean, e: ChangeEvent<HTMLInputElement>) => void',
|
|
59
|
-
),
|
|
60
|
-
).toEqual({ok: true, type: {kind: 'fn', args: [{kind: 'boolean'}]}});
|
|
61
|
-
expect(
|
|
62
|
-
parseDocType('(value: string, event: React.ChangeEvent) => void'),
|
|
63
|
-
).toEqual({ok: true, type: {kind: 'fn', args: [{kind: 'string'}]}});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it.each([
|
|
67
|
-
'(e: FocusEvent<HTMLInputElement>) => void',
|
|
68
|
-
'(event: MouseEvent) => void',
|
|
69
|
-
])('keeps the event-only callback %s on the web', type => {
|
|
70
|
-
expect(parseDocType(type)).toEqual({
|
|
71
|
-
ok: false,
|
|
72
|
-
reason: 'callback argument: DOM event',
|
|
73
|
-
eventOnly: true,
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('refuses an event that is not last', () => {
|
|
78
|
-
const parsed = parseDocType('(e: MouseEvent, value: string) => void');
|
|
79
|
-
expect(parsed).toMatchObject({
|
|
80
|
-
ok: false,
|
|
81
|
-
reason: 'callback argument: DOM event',
|
|
82
|
-
});
|
|
83
|
-
expect(parsed).not.toHaveProperty('eventOnly');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('reads a closed set of numbers', () => {
|
|
87
|
-
expect(parseDocType('1 | 2 | 3')).toEqual({
|
|
88
|
-
ok: true,
|
|
89
|
-
type: {kind: 'numberEnum', values: [1, 2, 3]},
|
|
90
|
-
});
|
|
91
|
-
expect(parseDocType('0 | 0.5 | 1')).toEqual({
|
|
92
|
-
ok: true,
|
|
93
|
-
type: {kind: 'numberEnum', values: [0, 0.5, 1]},
|
|
94
|
-
});
|
|
95
|
-
expect(parseDocType("1 | 'auto'").ok).toBe(false);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
@@ -11,12 +11,7 @@ import type {PropSpec, PropType} from './types.js';
|
|
|
11
11
|
|
|
12
12
|
export type ParsedType =
|
|
13
13
|
| {readonly ok: true; readonly type: PropType}
|
|
14
|
-
| {
|
|
15
|
-
readonly ok: false;
|
|
16
|
-
readonly reason: string;
|
|
17
|
-
/** True for a callback whose only arguments are DOM events. */
|
|
18
|
-
readonly eventOnly?: boolean;
|
|
19
|
-
};
|
|
14
|
+
| {readonly ok: false; readonly reason: string};
|
|
20
15
|
|
|
21
16
|
/** Web-only types, keyed by name, with the reason each has no native form. */
|
|
22
17
|
const WEB_ONLY_TYPES: Readonly<Record<string, string>> = {
|
|
@@ -38,17 +33,8 @@ const WEB_ONLY_TYPES: Readonly<Record<string, string>> = {
|
|
|
38
33
|
ChangeEvent: 'DOM event',
|
|
39
34
|
DragEvent: 'DOM event',
|
|
40
35
|
ClipboardEvent: 'DOM event',
|
|
41
|
-
FormEvent: 'DOM event',
|
|
42
|
-
TouchEvent: 'DOM event',
|
|
43
|
-
SyntheticEvent: 'DOM event',
|
|
44
36
|
};
|
|
45
37
|
|
|
46
|
-
/** True for a DOM event type, with or without its element parameter. */
|
|
47
|
-
function isDomEvent(text: string): boolean {
|
|
48
|
-
const name = /^(?:React\.)?([A-Z]\w*)(?:<[\s\S]*>)?$/.exec(text.trim())?.[1];
|
|
49
|
-
return name != null && WEB_ONLY_TYPES[name] === 'DOM event';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
38
|
const ok = (type: PropType): ParsedType => ({ok: true, type});
|
|
53
39
|
const no = (reason: string): ParsedType => ({ok: false, reason});
|
|
54
40
|
|
|
@@ -148,24 +134,10 @@ function parseFunction(text: string): ParsedType | null {
|
|
|
148
134
|
);
|
|
149
135
|
}
|
|
150
136
|
const params = arrow[0].slice(1, -1).trim();
|
|
151
|
-
const types = (params ? splitTop(params, ',') : []).map(param => {
|
|
152
|
-
const match = /^([A-Za-z_$][\w$]*)\??\s*:\s*([\s\S]+)$/.exec(param);
|
|
153
|
-
return match ? match[2] : param;
|
|
154
|
-
});
|
|
155
|
-
// A value callback ends with the DOM event that caused it, as in
|
|
156
|
-
// `(checked: boolean, e: ChangeEvent) => void`. The values are the portable
|
|
157
|
-
// callback; the trailing event is the web's part. A callback that carries
|
|
158
|
-
// only an event stays on the web.
|
|
159
|
-
let end = types.length;
|
|
160
|
-
while (end > 0 && isDomEvent(types[end - 1])) {
|
|
161
|
-
end--;
|
|
162
|
-
}
|
|
163
|
-
if (end === 0 && types.length > 0) {
|
|
164
|
-
return {ok: false, reason: 'callback argument: DOM event', eventOnly: true};
|
|
165
|
-
}
|
|
166
137
|
const args: PropType[] = [];
|
|
167
|
-
for (const
|
|
168
|
-
const
|
|
138
|
+
for (const param of params ? splitTop(params, ',') : []) {
|
|
139
|
+
const match = /^([A-Za-z_$][\w$]*)\??\s*:\s*([\s\S]+)$/.exec(param);
|
|
140
|
+
const parsed = parseDocType(match ? match[2] : param);
|
|
169
141
|
if (!parsed.ok) {
|
|
170
142
|
return no(`callback argument: ${parsed.reason}`);
|
|
171
143
|
}
|
|
@@ -176,9 +148,8 @@ function parseFunction(text: string): ParsedType | null {
|
|
|
176
148
|
|
|
177
149
|
/**
|
|
178
150
|
* Parses a documented type string. Supported portable forms: `string`,
|
|
179
|
-
* `number`, `boolean`, closed string
|
|
180
|
-
*
|
|
181
|
-
* is the web's part), and named types. A union of different
|
|
151
|
+
* `number`, `boolean`, closed string unions, arrays, plain object literals,
|
|
152
|
+
* callbacks with portable arguments, and named types. A union of different
|
|
182
153
|
* kinds has no Swift or Kotlin form.
|
|
183
154
|
*/
|
|
184
155
|
export function parseDocType(raw: string): ParsedType {
|
|
@@ -199,9 +170,6 @@ export function parseDocType(raw: string): ParsedType {
|
|
|
199
170
|
if (members.every(m => /^'[^']*'$/.test(m))) {
|
|
200
171
|
return ok({kind: 'enum', values: members.map(m => m.slice(1, -1))});
|
|
201
172
|
}
|
|
202
|
-
if (members.every(m => /^-?\d+(?:\.\d+)?$/.test(m))) {
|
|
203
|
-
return ok({kind: 'numberEnum', values: members.map(Number)});
|
|
204
|
-
}
|
|
205
173
|
const webOnly = members.find(m => WEB_ONLY_TYPES[m] != null);
|
|
206
174
|
if (webOnly) {
|
|
207
175
|
return no(WEB_ONLY_TYPES[webOnly]);
|
|
@@ -219,9 +187,6 @@ export function parseDocType(raw: string): ParsedType {
|
|
|
219
187
|
if (WEB_ONLY_TYPES[t] != null) {
|
|
220
188
|
return no(WEB_ONLY_TYPES[t]);
|
|
221
189
|
}
|
|
222
|
-
if (isDomEvent(t)) {
|
|
223
|
-
return no('DOM event');
|
|
224
|
-
}
|
|
225
190
|
|
|
226
191
|
const generic = /^(?:Readonly)?Array<([\s\S]+)>$/.exec(t);
|
|
227
192
|
if (t.endsWith('[]') || generic) {
|
package/src/contract/rules.ts
CHANGED
|
@@ -29,21 +29,15 @@ export interface ContractIssue {
|
|
|
29
29
|
readonly message: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
* allowed when the component declares it has the same meaning there.
|
|
35
|
-
*/
|
|
36
|
-
export function coreNameProblem(
|
|
37
|
-
name: string,
|
|
38
|
-
sameMeaning = false,
|
|
39
|
-
): string | null {
|
|
32
|
+
/** Why a core prop name is not allowed, or null when it is. */
|
|
33
|
+
export function coreNameProblem(name: string): string | null {
|
|
40
34
|
if (SWIFT_KEYWORDS.has(name)) {
|
|
41
35
|
return `"${name}" is a Swift keyword`;
|
|
42
36
|
}
|
|
43
37
|
if (KOTLIN_KEYWORDS.has(name)) {
|
|
44
38
|
return `"${name}" is a Kotlin keyword`;
|
|
45
39
|
}
|
|
46
|
-
if (RECLAIMED.has(name)
|
|
40
|
+
if (RECLAIMED.has(name)) {
|
|
47
41
|
return null;
|
|
48
42
|
}
|
|
49
43
|
for (const [toolkit, names] of Object.entries(TOOLKIT_NAMES)) {
|
|
@@ -79,9 +73,8 @@ export function checkContract(contract: ComponentContract): ContractIssue[] {
|
|
|
79
73
|
const collect = (props: PropMap) =>
|
|
80
74
|
Object.values(props).forEach(spec => namedTypesIn(spec.type, referenced));
|
|
81
75
|
|
|
82
|
-
const sameMeaning = new Set(contract.sameMeaning ?? []);
|
|
83
76
|
for (const name of Object.keys(contract.core)) {
|
|
84
|
-
const problem = coreNameProblem(name
|
|
77
|
+
const problem = coreNameProblem(name);
|
|
85
78
|
if (problem) {
|
|
86
79
|
add(
|
|
87
80
|
'error',
|
package/src/contract/types.ts
CHANGED
|
@@ -16,8 +16,6 @@ export type PropType =
|
|
|
16
16
|
| {kind: 'number'}
|
|
17
17
|
| {kind: 'boolean'}
|
|
18
18
|
| {kind: 'enum'; values: readonly string[]}
|
|
19
|
-
/** A closed set of numbers, such as a heading level or a spacing step. */
|
|
20
|
-
| {kind: 'numberEnum'; values: readonly number[]}
|
|
21
19
|
| {kind: 'array'; of: PropType}
|
|
22
20
|
| {kind: 'record'; fields: Readonly<Record<string, PropSpec>>}
|
|
23
21
|
| {kind: 'fn'; args: readonly PropType[]}
|
|
@@ -51,6 +49,4 @@ export interface ComponentContract {
|
|
|
51
49
|
readonly platforms: Readonly<Partial<Record<NativePlatform, PropMap>>>;
|
|
52
50
|
/** Core-doc props that stay in the web contract. */
|
|
53
51
|
readonly webOnly: readonly WebOnlyProp[];
|
|
54
|
-
/** Core props that share a toolkit name with the same meaning (FR8). */
|
|
55
|
-
readonly sameMeaning?: readonly string[];
|
|
56
52
|
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
import {describe, expect, it} from 'vitest';
|
|
4
|
-
import {fromComponentDoc, type ComponentDocLike} from './fromComponentDoc.js';
|
|
5
|
-
import {generateSwift} from './generateSwift.js';
|
|
6
|
-
|
|
7
|
-
const doc: ComponentDocLike = {
|
|
8
|
-
name: 'Toggle',
|
|
9
|
-
props: [
|
|
10
|
-
{name: 'label', type: 'ReactNode', required: true},
|
|
11
|
-
{name: 'icon', type: 'ReactNode'},
|
|
12
|
-
{name: 'level', type: '1 | 2 | 3'},
|
|
13
|
-
{
|
|
14
|
-
name: 'onChange',
|
|
15
|
-
type: '(on: boolean, e: ChangeEvent<HTMLInputElement>) => void',
|
|
16
|
-
},
|
|
17
|
-
{name: 'onClick', type: '(e: MouseEvent) => void'},
|
|
18
|
-
{name: 'onFocus', type: '(e: FocusEvent) => void'},
|
|
19
|
-
{name: 'onPick', type: '(e: MouseEvent, value: string) => void'},
|
|
20
|
-
],
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
describe('fromComponentDoc', () => {
|
|
24
|
-
const contract = fromComponentDoc(doc, {
|
|
25
|
-
name: 'Toggle',
|
|
26
|
-
baseTarget: 'control',
|
|
27
|
-
slots: {label: 'text'},
|
|
28
|
-
callbacks: ['onClick', 'onPick'],
|
|
29
|
-
sameMeaning: {onClick: 'a press in Compose too'},
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('gives declared content a typed text slot and leaves the rest on the web', () => {
|
|
33
|
-
expect(contract.core.label).toEqual({
|
|
34
|
-
type: {kind: 'string'},
|
|
35
|
-
optional: false,
|
|
36
|
-
});
|
|
37
|
-
expect(contract.webOnly.map(prop => prop.name)).toContain('icon');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('keeps a value callback and a declared press, and leaves other event callbacks on the web', () => {
|
|
41
|
-
expect(contract.core.onChange?.type).toEqual({
|
|
42
|
-
kind: 'fn',
|
|
43
|
-
args: [{kind: 'boolean'}],
|
|
44
|
-
});
|
|
45
|
-
expect(contract.core.onClick?.type).toEqual({kind: 'fn', args: []});
|
|
46
|
-
expect(contract.webOnly).toContainEqual({
|
|
47
|
-
name: 'onFocus',
|
|
48
|
-
reason: 'callback argument: DOM event',
|
|
49
|
-
});
|
|
50
|
-
// `callbacks` never drops values: an event before a value stays on the web.
|
|
51
|
-
expect(contract.core).not.toHaveProperty('onPick');
|
|
52
|
-
expect(contract.webOnly.map(prop => prop.name)).toContain('onPick');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('refuses a toolkit name unless the component declares the same meaning', () => {
|
|
56
|
-
const plain = fromComponentDoc(doc, {
|
|
57
|
-
name: 'Toggle',
|
|
58
|
-
baseTarget: 'control',
|
|
59
|
-
callbacks: ['onClick'],
|
|
60
|
-
});
|
|
61
|
-
expect(plain.core).not.toHaveProperty('onClick');
|
|
62
|
-
expect(
|
|
63
|
-
plain.webOnly.find(prop => prop.name === 'onClick')?.reason,
|
|
64
|
-
).toContain('compose');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('writes a closed set of numbers as a Swift enum with raw values', () => {
|
|
68
|
-
const swift = generateSwift([contract])['Toggle.swift'];
|
|
69
|
-
expect(swift).toContain(
|
|
70
|
-
'public enum ToggleLevel: Int, CaseIterable, Sendable {',
|
|
71
|
-
);
|
|
72
|
-
expect(swift).toContain(' case v1 = 1');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('reads one component of a file that documents several', () => {
|
|
76
|
-
const stack: ComponentDocLike = {
|
|
77
|
-
name: 'Stack',
|
|
78
|
-
components: [
|
|
79
|
-
{name: 'Stack', props: [{name: 'gap', type: '0 | 0.5 | 1'}]},
|
|
80
|
-
{name: 'VStack', props: [{name: 'wrap', type: 'boolean'}]},
|
|
81
|
-
],
|
|
82
|
-
};
|
|
83
|
-
const vstack = fromComponentDoc(stack, {
|
|
84
|
-
name: 'VStack',
|
|
85
|
-
baseTarget: 'container',
|
|
86
|
-
doc: {file: 'Stack', component: 'VStack'},
|
|
87
|
-
});
|
|
88
|
-
expect(Object.keys(vstack.core)).toEqual(['wrap']);
|
|
89
|
-
expect(() =>
|
|
90
|
-
fromComponentDoc(stack, {
|
|
91
|
-
name: 'Missing',
|
|
92
|
-
baseTarget: 'root',
|
|
93
|
-
doc: {file: 'Stack', component: 'Missing'},
|
|
94
|
-
}),
|
|
95
|
-
).toThrow('does not document "Missing"');
|
|
96
|
-
});
|
|
97
|
-
});
|