@astryxdesign/native 0.0.0-stage → 0.7.4-canary.720bb10
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/LICENSE +21 -0
- package/README.md +120 -2
- package/apple/Package.swift +18 -0
- package/apple/Sources/AstryxNative/Generated/AstryxBase.swift +12 -0
- package/apple/Sources/AstryxNative/Generated/Selector.swift +159 -0
- package/apple/Tests/AstryxNativeTests/SelectorPropsTests.swift +17 -0
- package/dist/components/Selector.d.ts +10 -0
- package/dist/components/Selector.d.ts.map +1 -0
- package/dist/components/Selector.js +38 -0
- package/dist/components/index.d.ts +10 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +3 -0
- package/dist/contract/fromComponentDoc.d.ts +32 -0
- package/dist/contract/fromComponentDoc.d.ts.map +1 -0
- package/dist/contract/fromComponentDoc.js +81 -0
- package/dist/contract/generateSwift.d.ts +4 -0
- package/dist/contract/generateSwift.d.ts.map +1 -0
- package/dist/contract/generateSwift.js +133 -0
- package/dist/contract/generateTypeScript.d.ts +4 -0
- package/dist/contract/generateTypeScript.d.ts.map +1 -0
- package/dist/contract/generateTypeScript.js +87 -0
- package/dist/contract/index.d.ts +20 -0
- package/dist/contract/index.d.ts.map +1 -0
- package/dist/contract/index.js +33 -0
- package/dist/contract/names.d.ts +20 -0
- package/dist/contract/names.d.ts.map +1 -0
- package/dist/contract/names.js +90 -0
- package/dist/contract/parseDocType.d.ts +22 -0
- package/dist/contract/parseDocType.d.ts.map +1 -0
- package/dist/contract/parseDocType.js +183 -0
- package/dist/contract/rules.d.ts +10 -0
- package/dist/contract/rules.d.ts.map +1 -0
- package/dist/contract/rules.js +87 -0
- package/dist/contract/types.d.ts +57 -0
- package/dist/contract/types.d.ts.map +1 -0
- package/dist/contract/types.js +2 -0
- package/dist/icons/defaults.d.ts +25 -0
- package/dist/icons/defaults.d.ts.map +1 -0
- package/dist/icons/defaults.js +111 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/platforms.d.ts +13 -0
- package/dist/platforms.d.ts.map +1 -0
- package/dist/platforms.js +14 -0
- package/dist/theme/defineNativeTheme.d.ts +75 -0
- package/dist/theme/defineNativeTheme.d.ts.map +1 -0
- package/dist/theme/defineNativeTheme.js +126 -0
- package/dist/types/android.d.ts +8 -0
- package/dist/types/core.d.ts +35 -0
- package/dist/types/ios.d.ts +8 -0
- package/dist/types/macos.d.ts +8 -0
- package/dist/types/shared.d.ts +6 -0
- package/dist/types/windows.d.ts +6 -0
- package/package.json +75 -3
- package/src/components/Selector.ts +50 -0
- package/src/components/index.ts +14 -0
- package/src/contract/contracts.test.ts +150 -0
- package/src/contract/fromComponentDoc.ts +131 -0
- package/src/contract/generateSwift.ts +170 -0
- package/src/contract/generateTypeScript.ts +115 -0
- package/src/contract/index.ts +53 -0
- package/src/contract/names.ts +103 -0
- package/src/contract/parseDocType.test.ts +52 -0
- package/src/contract/parseDocType.ts +211 -0
- package/src/contract/rules.ts +132 -0
- package/src/contract/types.ts +52 -0
- package/src/icons/defaults.test.ts +47 -0
- package/src/icons/defaults.ts +136 -0
- package/src/index.ts +35 -0
- package/src/platforms.ts +20 -0
- package/src/theme/defineNativeTheme.test.ts +88 -0
- package/src/theme/defineNativeTheme.ts +224 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
/**
|
|
3
|
+
* @file contract/generateSwift.ts
|
|
4
|
+
* @input Component contracts
|
|
5
|
+
* @output Swift sources for the Apple platforms: shared base props and one file
|
|
6
|
+
* per component, with the iOS and macOS contracts behind `#if os(...)`
|
|
7
|
+
* @position Produces the Swift binding (AST-082 FR5). Generated files are
|
|
8
|
+
* committed under apple/ so a Swift package can pin a commit
|
|
9
|
+
*/
|
|
10
|
+
import { SWIFT_KEYWORDS } from './names.js';
|
|
11
|
+
const HEADER = '// Generated by @astryxdesign/native. Do not edit.\n';
|
|
12
|
+
const APPLE_PLATFORMS = [
|
|
13
|
+
{ platform: 'ios', condition: 'os(iOS)' },
|
|
14
|
+
{ platform: 'macos', condition: 'os(macOS)' },
|
|
15
|
+
];
|
|
16
|
+
const camel = (text) => text.replace(/[-_ ]+([a-zA-Z0-9])/g, (_, ch) => ch.toUpperCase());
|
|
17
|
+
const pascal = (text) => camel(text).replace(/^./, ch => ch.toUpperCase());
|
|
18
|
+
const ident = (name) => SWIFT_KEYWORDS.has(name) ? `\`${name}\`` : name;
|
|
19
|
+
function caseName(value) {
|
|
20
|
+
const name = camel(value.replace(/[^A-Za-z0-9_-]/g, '-'));
|
|
21
|
+
return ident(/^[0-9]/.test(name) ? `v${name}` : name || 'empty');
|
|
22
|
+
}
|
|
23
|
+
/** Collects nested declarations (enums, structs) while rendering types. */
|
|
24
|
+
class SwiftWriter {
|
|
25
|
+
decls = [];
|
|
26
|
+
type(owner, prop, type) {
|
|
27
|
+
switch (type.kind) {
|
|
28
|
+
case 'string':
|
|
29
|
+
return 'String';
|
|
30
|
+
case 'number':
|
|
31
|
+
return 'Double';
|
|
32
|
+
case 'boolean':
|
|
33
|
+
return 'Bool';
|
|
34
|
+
case 'enum': {
|
|
35
|
+
const name = `${owner}${pascal(prop)}`;
|
|
36
|
+
const cases = type.values.map(value => ` case ${caseName(value)} = "${value}"`);
|
|
37
|
+
this.decls.push(`public enum ${name}: String, CaseIterable, Sendable {\n${cases.join('\n')}\n}`);
|
|
38
|
+
return name;
|
|
39
|
+
}
|
|
40
|
+
case 'array':
|
|
41
|
+
return `[${this.type(owner, prop, type.of)}]`;
|
|
42
|
+
case 'record': {
|
|
43
|
+
const name = `${owner}${pascal(prop)}`;
|
|
44
|
+
this.decls.push(this.struct(name, type.fields, 'Equatable, Sendable'));
|
|
45
|
+
return name;
|
|
46
|
+
}
|
|
47
|
+
case 'fn':
|
|
48
|
+
return `(${type.args.map((arg, i) => this.type(owner, `${prop}Arg${i}`, arg)).join(', ')}) -> Void`;
|
|
49
|
+
case 'named':
|
|
50
|
+
return type.name;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
fields(owner, props) {
|
|
54
|
+
return Object.entries(props).map(([name, spec]) => {
|
|
55
|
+
const base = this.type(owner, name, spec.type);
|
|
56
|
+
const optional = spec.type.kind === 'fn' ? `(${base})?` : `${base}?`;
|
|
57
|
+
const swiftType = spec.optional ? optional : base;
|
|
58
|
+
return {
|
|
59
|
+
name,
|
|
60
|
+
decl: ` public var ${ident(name)}: ${swiftType}`,
|
|
61
|
+
param: `${name}: ${spec.type.kind === 'fn' && !spec.optional ? `@escaping ${swiftType}` : swiftType}${spec.optional ? ' = nil' : ''}`,
|
|
62
|
+
assign: ` self.${name} = ${ident(name)}`,
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
struct(name, props, conformances, extra = []) {
|
|
67
|
+
const fields = this.fields(name, props);
|
|
68
|
+
const head = conformances
|
|
69
|
+
? `public struct ${name}: ${conformances} {`
|
|
70
|
+
: `public struct ${name} {`;
|
|
71
|
+
return [
|
|
72
|
+
head,
|
|
73
|
+
...extra,
|
|
74
|
+
...fields.map(field => field.decl),
|
|
75
|
+
` public init(${fields.map(field => field.param).join(', ')}) {`,
|
|
76
|
+
...fields.map(field => field.assign),
|
|
77
|
+
' }',
|
|
78
|
+
'}',
|
|
79
|
+
].join('\n');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function componentFile(contract) {
|
|
83
|
+
const shared = new SwiftWriter();
|
|
84
|
+
const typeDecls = Object.entries(contract.types).map(([name, fields]) => shared.struct(name, fields, 'Equatable, Sendable'));
|
|
85
|
+
// Core enums and records are declared once, outside the platform blocks.
|
|
86
|
+
shared.fields(`${contract.name}`, contract.core);
|
|
87
|
+
const variants = APPLE_PLATFORMS.map(({ platform, condition }) => {
|
|
88
|
+
const writer = new SwiftWriter();
|
|
89
|
+
const extra = contract.platforms[platform] ?? {};
|
|
90
|
+
const fields = writer.fields(contract.name, { ...contract.core, ...extra });
|
|
91
|
+
// Core-prop declarations already live outside the platform blocks; keep
|
|
92
|
+
// only the declarations this platform's own props need.
|
|
93
|
+
writer.decls.length = 0;
|
|
94
|
+
writer.fields(contract.name, extra);
|
|
95
|
+
const struct = [
|
|
96
|
+
`public struct ${contract.name}Props {`,
|
|
97
|
+
` /// Base props attach to this part on every platform.`,
|
|
98
|
+
` public static let baseTarget: AstryxBaseTarget = .${contract.baseTarget}`,
|
|
99
|
+
' public var base: AstryxBaseProps',
|
|
100
|
+
...fields.map(field => field.decl),
|
|
101
|
+
` public init(${[...fields.map(field => field.param), 'base: AstryxBaseProps = AstryxBaseProps()'].join(', ')}) {`,
|
|
102
|
+
...fields.map(field => field.assign),
|
|
103
|
+
' self.base = base',
|
|
104
|
+
' }',
|
|
105
|
+
'}',
|
|
106
|
+
].join('\n');
|
|
107
|
+
return `#if ${condition}\n${[...writer.decls, struct].join('\n')}\n#endif`;
|
|
108
|
+
});
|
|
109
|
+
return (HEADER + [...typeDecls, ...shared.decls, ...variants].join('\n') + '\n');
|
|
110
|
+
}
|
|
111
|
+
const BASE_FILE = HEADER +
|
|
112
|
+
[
|
|
113
|
+
'public enum AstryxBaseTarget: String, Sendable { case root, trigger, control, container }',
|
|
114
|
+
'',
|
|
115
|
+
'/// Base props shared by every platform (AST-082 FR7).',
|
|
116
|
+
'public struct AstryxBaseProps: Equatable, Sendable {',
|
|
117
|
+
' public var id: String?',
|
|
118
|
+
' public var isHidden: Bool?',
|
|
119
|
+
' public init(id: String? = nil, isHidden: Bool? = nil) {',
|
|
120
|
+
' self.id = id',
|
|
121
|
+
' self.isHidden = isHidden',
|
|
122
|
+
' }',
|
|
123
|
+
'}',
|
|
124
|
+
].join('\n') +
|
|
125
|
+
'\n';
|
|
126
|
+
/** Returns file name to contents, relative to the generated Swift folder. */
|
|
127
|
+
export function generateSwift(contracts) {
|
|
128
|
+
const files = { 'AstryxBase.swift': BASE_FILE };
|
|
129
|
+
for (const contract of contracts) {
|
|
130
|
+
files[`${contract.name}.swift`] = componentFile(contract);
|
|
131
|
+
}
|
|
132
|
+
return files;
|
|
133
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ComponentContract } from './types.js';
|
|
2
|
+
/** Returns file name to contents, relative to the types output folder. */
|
|
3
|
+
export declare function generateTypeScript(contracts: readonly ComponentContract[]): Record<string, string>;
|
|
4
|
+
//# sourceMappingURL=generateTypeScript.d.ts.map
|
|
@@ -0,0 +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;AA2FrE,0EAA0E;AAC1E,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,SAAS,iBAAiB,EAAE,GACtC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASxB"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
/**
|
|
3
|
+
* @file contract/generateTypeScript.ts
|
|
4
|
+
* @input Component contracts
|
|
5
|
+
* @output Declaration files: core.d.ts, one per native platform, and shared.d.ts
|
|
6
|
+
* @position Produces the platform-scoped types (AST-082 FR5, FR6). The package
|
|
7
|
+
* exports map selects one file per type-resolution condition
|
|
8
|
+
*/
|
|
9
|
+
import { NATIVE_PLATFORMS } from '../platforms.js';
|
|
10
|
+
const HEADER = '// Generated by @astryxdesign/native. Do not edit.\n';
|
|
11
|
+
function tsType(type) {
|
|
12
|
+
switch (type.kind) {
|
|
13
|
+
case 'string':
|
|
14
|
+
case 'number':
|
|
15
|
+
case 'boolean':
|
|
16
|
+
return type.kind;
|
|
17
|
+
case 'enum':
|
|
18
|
+
return type.values.map(value => `'${value}'`).join(' | ');
|
|
19
|
+
case 'array': {
|
|
20
|
+
const inner = tsType(type.of);
|
|
21
|
+
return `ReadonlyArray<${inner}>`;
|
|
22
|
+
}
|
|
23
|
+
case 'record':
|
|
24
|
+
return `{${members(type.fields, ' ')}}`;
|
|
25
|
+
case 'fn':
|
|
26
|
+
return `(${type.args.map((arg, i) => `arg${i}: ${tsType(arg)}`).join(', ')}) => void`;
|
|
27
|
+
case 'named':
|
|
28
|
+
return type.name;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function key(name) {
|
|
32
|
+
return /^[A-Za-z_$][\w$]*$/.test(name) ? name : `'${name}'`;
|
|
33
|
+
}
|
|
34
|
+
function members(props, separator = '\n ') {
|
|
35
|
+
return Object.entries(props)
|
|
36
|
+
.map(([name, spec]) => `${key(name)}${spec.optional ? '?' : ''}: ${tsType(spec.type)};`)
|
|
37
|
+
.join(separator);
|
|
38
|
+
}
|
|
39
|
+
function coreFile(contracts) {
|
|
40
|
+
const types = new Map();
|
|
41
|
+
for (const contract of contracts) {
|
|
42
|
+
for (const [name, fields] of Object.entries(contract.types)) {
|
|
43
|
+
types.set(name, fields);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const typeDecls = [...types].map(([name, fields]) => `export interface ${name} {\n ${members(fields)}\n}`);
|
|
47
|
+
const props = contracts.map(contract => `/** Core contract. Base props attach to the ${contract.baseTarget}. */\n` +
|
|
48
|
+
`export interface ${contract.name}CoreProps extends AstryxSharedBaseProps {\n ${members(contract.core)}\n}`);
|
|
49
|
+
return (HEADER +
|
|
50
|
+
'/** Base props shared by every platform (AST-082 FR7). */\n' +
|
|
51
|
+
'export interface AstryxSharedBaseProps {\n id?: string;\n isHidden?: boolean;\n}\n' +
|
|
52
|
+
[...typeDecls, ...props].join('\n') +
|
|
53
|
+
'\n');
|
|
54
|
+
}
|
|
55
|
+
function reExports(contracts) {
|
|
56
|
+
const names = new Set(['AstryxSharedBaseProps']);
|
|
57
|
+
for (const contract of contracts) {
|
|
58
|
+
Object.keys(contract.types).forEach(name => names.add(name));
|
|
59
|
+
names.add(`${contract.name}CoreProps`);
|
|
60
|
+
}
|
|
61
|
+
return `export type {${[...names].join(', ')}} from './core.js';\n`;
|
|
62
|
+
}
|
|
63
|
+
function platformFile(contracts, platform) {
|
|
64
|
+
const imports = `import type {${contracts.map(c => `${c.name}CoreProps`).join(', ')}} from './core.js';\n`;
|
|
65
|
+
const components = contracts.map(contract => {
|
|
66
|
+
const extra = platform ? (contract.platforms[platform] ?? {}) : {};
|
|
67
|
+
const body = Object.keys(extra).length > 0 ? `\n ${members(extra)}\n` : '';
|
|
68
|
+
const scope = platform
|
|
69
|
+
? `core contract plus the ${platform} contract`
|
|
70
|
+
: 'core contract only';
|
|
71
|
+
return (`/** ${contract.name} props: ${scope}. */\n` +
|
|
72
|
+
`export interface ${contract.name}Props extends ${contract.name}CoreProps {${body}}\n` +
|
|
73
|
+
`export declare function ${contract.name}(props: ${contract.name}Props): unknown;`);
|
|
74
|
+
});
|
|
75
|
+
return HEADER + imports + reExports(contracts) + components.join('\n') + '\n';
|
|
76
|
+
}
|
|
77
|
+
/** Returns file name to contents, relative to the types output folder. */
|
|
78
|
+
export function generateTypeScript(contracts) {
|
|
79
|
+
const files = {
|
|
80
|
+
'core.d.ts': coreFile(contracts),
|
|
81
|
+
'shared.d.ts': platformFile(contracts, null),
|
|
82
|
+
};
|
|
83
|
+
for (const platform of NATIVE_PLATFORMS) {
|
|
84
|
+
files[`${platform}.d.ts`] = platformFile(contracts, platform);
|
|
85
|
+
}
|
|
86
|
+
return files;
|
|
87
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ComponentDocLike } from './fromComponentDoc.js';
|
|
2
|
+
import { type ContractIssue } from './rules.js';
|
|
3
|
+
import type { ComponentContract } from './types.js';
|
|
4
|
+
export { fromComponentDoc } from './fromComponentDoc.js';
|
|
5
|
+
export type { ComponentDocLike, NativeAdditions } from './fromComponentDoc.js';
|
|
6
|
+
export { generateSwift } from './generateSwift.js';
|
|
7
|
+
export { generateTypeScript } from './generateTypeScript.js';
|
|
8
|
+
export { parseDocType } from './parseDocType.js';
|
|
9
|
+
export { checkContract, coreNameProblem } from './rules.js';
|
|
10
|
+
export type { ContractIssue } from './rules.js';
|
|
11
|
+
export type * from './types.js';
|
|
12
|
+
/** Loads one core component's documentation by component name. */
|
|
13
|
+
export type ComponentDocLoader = (name: string) => Promise<ComponentDocLike>;
|
|
14
|
+
export interface BuiltContracts {
|
|
15
|
+
readonly contracts: readonly ComponentContract[];
|
|
16
|
+
readonly warnings: readonly ContractIssue[];
|
|
17
|
+
}
|
|
18
|
+
/** Builds and checks every native component contract; throws on any error. */
|
|
19
|
+
export declare function buildContracts(loadDoc: ComponentDocLoader): Promise<BuiltContracts>;
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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,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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
/**
|
|
3
|
+
* @file contract/index.ts
|
|
4
|
+
* @input Core component documentation, supplied by the caller
|
|
5
|
+
* @output Contracts for every native component, checked, plus the generators
|
|
6
|
+
* @position Build-time entry (`@astryxdesign/native/contract`). Reads core
|
|
7
|
+
* documentation as data; nothing here runs in an app
|
|
8
|
+
*/
|
|
9
|
+
import { NATIVE_COMPONENTS } from '../components/index.js';
|
|
10
|
+
import { fromComponentDoc } from './fromComponentDoc.js';
|
|
11
|
+
import { checkContract } from './rules.js';
|
|
12
|
+
export { fromComponentDoc } from './fromComponentDoc.js';
|
|
13
|
+
export { generateSwift } from './generateSwift.js';
|
|
14
|
+
export { generateTypeScript } from './generateTypeScript.js';
|
|
15
|
+
export { parseDocType } from './parseDocType.js';
|
|
16
|
+
export { checkContract, coreNameProblem } from './rules.js';
|
|
17
|
+
/** Builds and checks every native component contract; throws on any error. */
|
|
18
|
+
export async function buildContracts(loadDoc) {
|
|
19
|
+
const contracts = [];
|
|
20
|
+
const warnings = [];
|
|
21
|
+
const errors = [];
|
|
22
|
+
for (const additions of NATIVE_COMPONENTS) {
|
|
23
|
+
const contract = fromComponentDoc(await loadDoc(additions.name), additions);
|
|
24
|
+
for (const issue of checkContract(contract)) {
|
|
25
|
+
(issue.level === 'error' ? errors : warnings).push(issue);
|
|
26
|
+
}
|
|
27
|
+
contracts.push(contract);
|
|
28
|
+
}
|
|
29
|
+
if (errors.length > 0) {
|
|
30
|
+
throw new Error(`Native contracts are invalid:\n${errors.map(e => ` ${e.message}`).join('\n')}`);
|
|
31
|
+
}
|
|
32
|
+
return { contracts, warnings };
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file contract/names.ts
|
|
3
|
+
* @input None
|
|
4
|
+
* @output Name lists the AST-082 FR8 rules check against
|
|
5
|
+
* @position Data for contract/rules.ts and contract/fromComponentDoc.ts
|
|
6
|
+
*/
|
|
7
|
+
export declare const SWIFT_KEYWORDS: ReadonlySet<string>;
|
|
8
|
+
export declare const KOTLIN_KEYWORDS: ReadonlySet<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Names each platform toolkit already uses with its own meaning. A core prop
|
|
11
|
+
* must not take one of them unless Astryx reclaims it (RECLAIMED).
|
|
12
|
+
*/
|
|
13
|
+
export declare const TOOLKIT_NAMES: Readonly<Record<string, ReadonlySet<string>>>;
|
|
14
|
+
/**
|
|
15
|
+
* Web names Astryx deliberately takes over for a component-owned concept
|
|
16
|
+
* (AST-082 FR8). The platform's own meaning is removed from that component's
|
|
17
|
+
* base props.
|
|
18
|
+
*/
|
|
19
|
+
export declare const RECLAIMED: ReadonlySet<string>;
|
|
20
|
+
//# sourceMappingURL=names.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"names.d.ts","sourceRoot":"","sources":["../../src/contract/names.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,eAAO,MAAM,cAAc,EAAE,WAAW,CAAC,MAAM,CAQ9C,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAK/C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CA4DvE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,SAAS,EAAE,WAAW,CAAC,MAAM,CAKxC,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
/**
|
|
3
|
+
* @file contract/names.ts
|
|
4
|
+
* @input None
|
|
5
|
+
* @output Name lists the AST-082 FR8 rules check against
|
|
6
|
+
* @position Data for contract/rules.ts and contract/fromComponentDoc.ts
|
|
7
|
+
*/
|
|
8
|
+
export const SWIFT_KEYWORDS = new Set(('associatedtype class deinit enum extension fileprivate func import init inout ' +
|
|
9
|
+
'internal let open operator private precedencegroup protocol public rethrows ' +
|
|
10
|
+
'static struct subscript typealias var break case catch continue default defer ' +
|
|
11
|
+
'do else fallthrough for guard if in repeat return throw switch where while Any ' +
|
|
12
|
+
'as await false is nil self Self super throws true try').split(' '));
|
|
13
|
+
export const KOTLIN_KEYWORDS = new Set(('as break class continue do else false for fun if in interface is null object ' +
|
|
14
|
+
'package return super this throw true try typealias typeof val var when while').split(' '));
|
|
15
|
+
/**
|
|
16
|
+
* Names each platform toolkit already uses with its own meaning. A core prop
|
|
17
|
+
* must not take one of them unless Astryx reclaims it (RECLAIMED).
|
|
18
|
+
*/
|
|
19
|
+
export const TOOLKIT_NAMES = {
|
|
20
|
+
web: new Set([
|
|
21
|
+
'children',
|
|
22
|
+
'className',
|
|
23
|
+
'style',
|
|
24
|
+
'title',
|
|
25
|
+
'hidden',
|
|
26
|
+
'role',
|
|
27
|
+
'tabIndex',
|
|
28
|
+
'dir',
|
|
29
|
+
'slot',
|
|
30
|
+
'is',
|
|
31
|
+
'key',
|
|
32
|
+
'ref',
|
|
33
|
+
'onChange',
|
|
34
|
+
'color',
|
|
35
|
+
'content',
|
|
36
|
+
]),
|
|
37
|
+
reactNative: new Set([
|
|
38
|
+
'style',
|
|
39
|
+
'testID',
|
|
40
|
+
'nativeID',
|
|
41
|
+
'onPress',
|
|
42
|
+
'onLongPress',
|
|
43
|
+
'onLayout',
|
|
44
|
+
'accessibilityLabel',
|
|
45
|
+
'accessibilityHint',
|
|
46
|
+
'accessibilityRole',
|
|
47
|
+
'accessibilityState',
|
|
48
|
+
'accessible',
|
|
49
|
+
'hitSlop',
|
|
50
|
+
'pointerEvents',
|
|
51
|
+
'disabled',
|
|
52
|
+
'editable',
|
|
53
|
+
'numberOfLines',
|
|
54
|
+
'onChangeText',
|
|
55
|
+
'selectable',
|
|
56
|
+
]),
|
|
57
|
+
swiftUI: new Set([
|
|
58
|
+
'body',
|
|
59
|
+
'disabled',
|
|
60
|
+
'hidden',
|
|
61
|
+
'opacity',
|
|
62
|
+
'frame',
|
|
63
|
+
'tint',
|
|
64
|
+
'font',
|
|
65
|
+
'background',
|
|
66
|
+
'overlay',
|
|
67
|
+
'accessibilityLabel',
|
|
68
|
+
'accessibilityHint',
|
|
69
|
+
'accessibilityIdentifier',
|
|
70
|
+
]),
|
|
71
|
+
compose: new Set([
|
|
72
|
+
'modifier',
|
|
73
|
+
'enabled',
|
|
74
|
+
'onClick',
|
|
75
|
+
'colors',
|
|
76
|
+
'testTag',
|
|
77
|
+
'contentDescription',
|
|
78
|
+
]),
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Web names Astryx deliberately takes over for a component-owned concept
|
|
82
|
+
* (AST-082 FR8). The platform's own meaning is removed from that component's
|
|
83
|
+
* base props.
|
|
84
|
+
*/
|
|
85
|
+
export const RECLAIMED = new Set([
|
|
86
|
+
'onChange',
|
|
87
|
+
'title',
|
|
88
|
+
'color',
|
|
89
|
+
'content',
|
|
90
|
+
]);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file contract/parseDocType.ts
|
|
3
|
+
* @input A prop's type string as a component's .doc.mjs documents it
|
|
4
|
+
* @output A portable PropType, or the reason the type has no portable form
|
|
5
|
+
* @position Reads core documentation into the contract model (AST-082 FR4)
|
|
6
|
+
*/
|
|
7
|
+
import type { PropType } from './types.js';
|
|
8
|
+
export type ParsedType = {
|
|
9
|
+
readonly ok: true;
|
|
10
|
+
readonly type: PropType;
|
|
11
|
+
} | {
|
|
12
|
+
readonly ok: false;
|
|
13
|
+
readonly reason: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Parses a documented type string. Supported portable forms: `string`,
|
|
17
|
+
* `number`, `boolean`, closed string unions, arrays, plain object literals,
|
|
18
|
+
* callbacks with portable arguments, and named types. A union of different
|
|
19
|
+
* kinds has no Swift or Kotlin form.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseDocType(raw: string): ParsedType;
|
|
22
|
+
//# sourceMappingURL=parseDocType.d.ts.map
|
|
@@ -0,0 +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;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"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
/** Web-only types, keyed by name, with the reason each has no native form. */
|
|
3
|
+
const WEB_ONLY_TYPES = {
|
|
4
|
+
ReactNode: 'free-form content; use a typed slot in the core',
|
|
5
|
+
ReactElement: 'free-form content; use a typed slot in the core',
|
|
6
|
+
'JSX.Element': 'free-form content; use a typed slot in the core',
|
|
7
|
+
StyleXStyles: 'web styling input',
|
|
8
|
+
CSSProperties: 'web styling input',
|
|
9
|
+
SizeValue: 'CSS length',
|
|
10
|
+
IconType: 'web icon component; native icons use shared icon names',
|
|
11
|
+
ElementType: 'web element choice',
|
|
12
|
+
ComponentType: 'web component reference',
|
|
13
|
+
LinkComponentType: 'web link component',
|
|
14
|
+
AriaRole: 'web accessibility attribute',
|
|
15
|
+
MouseEvent: 'DOM event',
|
|
16
|
+
FocusEvent: 'DOM event',
|
|
17
|
+
KeyboardEvent: 'DOM event',
|
|
18
|
+
PointerEvent: 'DOM event',
|
|
19
|
+
ChangeEvent: 'DOM event',
|
|
20
|
+
DragEvent: 'DOM event',
|
|
21
|
+
ClipboardEvent: 'DOM event',
|
|
22
|
+
};
|
|
23
|
+
const ok = (type) => ({ ok: true, type });
|
|
24
|
+
const no = (reason) => ({ ok: false, reason });
|
|
25
|
+
/** Splits on a separator at bracket depth zero, ignoring quoted text. */
|
|
26
|
+
function splitTop(text, separator) {
|
|
27
|
+
const parts = [];
|
|
28
|
+
let depth = 0;
|
|
29
|
+
let quote = null;
|
|
30
|
+
let start = 0;
|
|
31
|
+
for (let i = 0; i < text.length; i++) {
|
|
32
|
+
const ch = text[i];
|
|
33
|
+
if (quote) {
|
|
34
|
+
if (ch === quote) {
|
|
35
|
+
quote = null;
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (ch === "'" || ch === '"') {
|
|
40
|
+
quote = ch;
|
|
41
|
+
}
|
|
42
|
+
else if (text.startsWith('=>', i)) {
|
|
43
|
+
// An arrow is not a closing angle bracket.
|
|
44
|
+
if (depth === 0 && separator === '=>') {
|
|
45
|
+
parts.push(text.slice(start, i));
|
|
46
|
+
start = i + 2;
|
|
47
|
+
}
|
|
48
|
+
i += 1;
|
|
49
|
+
}
|
|
50
|
+
else if ('([{<'.includes(ch)) {
|
|
51
|
+
depth++;
|
|
52
|
+
}
|
|
53
|
+
else if (')]}>'.includes(ch)) {
|
|
54
|
+
depth--;
|
|
55
|
+
}
|
|
56
|
+
else if (depth === 0 && text.startsWith(separator, i)) {
|
|
57
|
+
parts.push(text.slice(start, i));
|
|
58
|
+
start = i + separator.length;
|
|
59
|
+
i += separator.length - 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
parts.push(text.slice(start));
|
|
63
|
+
return parts.map(part => part.trim()).filter(part => part.length > 0);
|
|
64
|
+
}
|
|
65
|
+
/** True when the first `(` closes at the last character, as in `(A | B)`. */
|
|
66
|
+
function isWrappedInParens(text) {
|
|
67
|
+
if (!text.startsWith('(') || !text.endsWith(')')) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
let depth = 0;
|
|
71
|
+
for (let i = 0; i < text.length; i++) {
|
|
72
|
+
if (text[i] === '(') {
|
|
73
|
+
depth++;
|
|
74
|
+
}
|
|
75
|
+
else if (text[i] === ')') {
|
|
76
|
+
depth--;
|
|
77
|
+
}
|
|
78
|
+
if (depth === 0 && i < text.length - 1) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
function stripOuterParens(text) {
|
|
85
|
+
let t = text.trim();
|
|
86
|
+
while (isWrappedInParens(t)) {
|
|
87
|
+
t = t.slice(1, -1).trim();
|
|
88
|
+
}
|
|
89
|
+
return t;
|
|
90
|
+
}
|
|
91
|
+
function parseRecord(body) {
|
|
92
|
+
const fields = {};
|
|
93
|
+
for (const member of splitTop(body, ',').flatMap(part => splitTop(part, ';'))) {
|
|
94
|
+
const match = /^([A-Za-z_$][\w$]*)(\?)?\s*:\s*([\s\S]+)$/.exec(member);
|
|
95
|
+
if (!match) {
|
|
96
|
+
return no(`record member "${member}" is not a plain field`);
|
|
97
|
+
}
|
|
98
|
+
const parsed = parseDocType(match[3]);
|
|
99
|
+
if (!parsed.ok) {
|
|
100
|
+
return no(`field "${match[1]}": ${parsed.reason}`);
|
|
101
|
+
}
|
|
102
|
+
fields[match[1]] = { type: parsed.type, optional: match[2] === '?' };
|
|
103
|
+
}
|
|
104
|
+
return ok({ kind: 'record', fields });
|
|
105
|
+
}
|
|
106
|
+
function parseFunction(text) {
|
|
107
|
+
const arrow = splitTop(text, '=>');
|
|
108
|
+
if (arrow.length !== 2 || !arrow[0].startsWith('(')) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const returns = arrow[1].trim();
|
|
112
|
+
if (returns !== 'void') {
|
|
113
|
+
return no(/React|ReactNode|JSX/.test(returns)
|
|
114
|
+
? 'render function; it returns web UI'
|
|
115
|
+
: 'callback returns a value; only void callbacks are portable today');
|
|
116
|
+
}
|
|
117
|
+
const params = arrow[0].slice(1, -1).trim();
|
|
118
|
+
const args = [];
|
|
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);
|
|
122
|
+
if (!parsed.ok) {
|
|
123
|
+
return no(`callback argument: ${parsed.reason}`);
|
|
124
|
+
}
|
|
125
|
+
args.push(parsed.type);
|
|
126
|
+
}
|
|
127
|
+
return ok({ kind: 'fn', args });
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Parses a documented type string. Supported portable forms: `string`,
|
|
131
|
+
* `number`, `boolean`, closed string unions, arrays, plain object literals,
|
|
132
|
+
* callbacks with portable arguments, and named types. A union of different
|
|
133
|
+
* kinds has no Swift or Kotlin form.
|
|
134
|
+
*/
|
|
135
|
+
export function parseDocType(raw) {
|
|
136
|
+
const text = stripOuterParens(raw.replace(/\s+/g, ' ').trim());
|
|
137
|
+
if (text === '') {
|
|
138
|
+
return no('no documented type');
|
|
139
|
+
}
|
|
140
|
+
const fn = parseFunction(text);
|
|
141
|
+
if (fn) {
|
|
142
|
+
return fn;
|
|
143
|
+
}
|
|
144
|
+
const members = splitTop(text, '|').filter(m => m !== 'undefined' && m !== 'null');
|
|
145
|
+
if (members.length > 1) {
|
|
146
|
+
if (members.every(m => /^'[^']*'$/.test(m))) {
|
|
147
|
+
return ok({ kind: 'enum', values: members.map(m => m.slice(1, -1)) });
|
|
148
|
+
}
|
|
149
|
+
const webOnly = members.find(m => WEB_ONLY_TYPES[m] != null);
|
|
150
|
+
if (webOnly) {
|
|
151
|
+
return no(WEB_ONLY_TYPES[webOnly]);
|
|
152
|
+
}
|
|
153
|
+
return no('a union of different kinds has no Swift or Kotlin form');
|
|
154
|
+
}
|
|
155
|
+
const t = members[0] ?? '';
|
|
156
|
+
if (t === 'string' || t === 'number' || t === 'boolean') {
|
|
157
|
+
return ok({ kind: t });
|
|
158
|
+
}
|
|
159
|
+
if (/^'[^']*'$/.test(t)) {
|
|
160
|
+
return ok({ kind: 'enum', values: [t.slice(1, -1)] });
|
|
161
|
+
}
|
|
162
|
+
if (WEB_ONLY_TYPES[t] != null) {
|
|
163
|
+
return no(WEB_ONLY_TYPES[t]);
|
|
164
|
+
}
|
|
165
|
+
const generic = /^(?:Readonly)?Array<([\s\S]+)>$/.exec(t);
|
|
166
|
+
if (t.endsWith('[]') || generic) {
|
|
167
|
+
const inner = parseDocType(generic ? generic[1] : t.slice(0, -2));
|
|
168
|
+
return inner.ok
|
|
169
|
+
? ok({ kind: 'array', of: inner.type })
|
|
170
|
+
: no(`array item: ${inner.reason}`);
|
|
171
|
+
}
|
|
172
|
+
if (t.startsWith('{') && t.endsWith('}')) {
|
|
173
|
+
return parseRecord(t.slice(1, -1));
|
|
174
|
+
}
|
|
175
|
+
if (/^(?:React\.)?(?:Ref|RefObject|MutableRefObject)</.test(t) ||
|
|
176
|
+
/^HTML\w*Element$/.test(t)) {
|
|
177
|
+
return no('web element reference');
|
|
178
|
+
}
|
|
179
|
+
if (/^[A-Z][\w$]*$/.test(t)) {
|
|
180
|
+
return ok({ kind: 'named', name: t });
|
|
181
|
+
}
|
|
182
|
+
return no(`unsupported type "${t}"`);
|
|
183
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ComponentContract } from './types.js';
|
|
2
|
+
export interface ContractIssue {
|
|
3
|
+
readonly level: 'error' | 'warning';
|
|
4
|
+
readonly rule: 'core-is-portable' | 'name-keyword' | 'name-toolkit' | 'platform-shadows-core' | 'same-name-different-meaning' | 'unknown-type';
|
|
5
|
+
readonly message: string;
|
|
6
|
+
}
|
|
7
|
+
/** Why a core prop name is not allowed, or null when it is. */
|
|
8
|
+
export declare function coreNameProblem(name: string): string | null;
|
|
9
|
+
export declare function checkContract(contract: ComponentContract): ContractIssue[];
|
|
10
|
+
//# sourceMappingURL=rules.d.ts.map
|
|
@@ -0,0 +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,+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"}
|