remaxjs / remax
1 1
import { declare } from '@babel/helper-plugin-utils';
2
import { NodePath } from '@babel/traverse';
3
import { ConfigAPI } from '@babel/core';
4 1
import * as t from '@babel/types';
5 1
import { kebabCase } from 'lodash';
6
import type { HostComponent, Platform } from '@remax/types';
7 1
import Store from '@remax/build-store';
8 1
import { slash } from '@remax/shared';
9

10
interface Options {
11
  target: Platform;
12
  hostComponents: Map<string, any>;
13
  skipHostComponents: string[];
14
  skipProps: string[];
15
  includeProps: string[];
16
}
17

18 1
export default function hostComponent(options: Options) {
19 1
  return declare((api: ConfigAPI) => {
20 1
    api.assertVersion(7);
21

22 1
    function shouldRegisterProp(propName: string, isNative: boolean, hostComponent?: HostComponent) {
23
      // key 属性
24 1
      if (propName === 'key') {
25 1
        return true;
26
      }
27

28 1
      if (options.includeProps.includes(propName)) {
29 1
        return true;
30
      }
31

32
      // 原生组件的属性都要注册
33 1
      if (isNative) {
34 1
        return true;
35
      }
36

37
      // host component 上的标准属性
38 1
      if (hostComponent?.alias?.[propName]) {
39 1
        return true;
40
      }
41

42 1
      const prefix = `${options.target}-`;
43

44
      // 平台特定属性
45 1
      if (propName.startsWith(prefix)) {
46 1
        return true;
47
      }
48

49
      // data 属性
50 1
      if (propName.startsWith('data-')) {
51 1
        return true;
52
      }
53

54 1
      return false;
55
    }
56

57 1
    function aliasProp(propName: string, hostComponent?: HostComponent) {
58 1
      const prefix = `${options.target}-`;
59

60 1
      if (propName.startsWith(prefix)) {
61 1
        return propName.replace(new RegExp(`^${prefix}`), '');
62
      }
63

64 1
      return hostComponent?.alias?.[propName] || propName;
65
    }
66

67 1
    function registerSlotViewProps(node: t.JSXElement) {
68 1
      let props: string[] = [];
69 1
      node.openingElement.attributes.forEach(attr => {
70 1
        if (t.isJSXSpreadAttribute(attr)) {
71 1
          props = [...props, ...(options.hostComponents.get('view')?.props || [])];
72 1
          return;
73
        }
74

75 1
        const prop = attr.name;
76 1
        let propName = '';
77

78 1
        if (t.isJSXIdentifier(prop)) {
79 1
          propName = prop.name;
80
        }
81

82 1
        if (t.isJSXNamespacedName(prop)) {
83 1
          return;
84
        }
85

86 1
        props.push(propName);
87
      });
88

89 1
      return (
90
        props
91
          // 无需收集 slot 字段
92 1
          .filter(p => p !== 'slot')
93 1
          .map(prop => aliasProp(prop, options.hostComponents.get('view')))
94
          .sort()
95
      );
96
    }
97

98 1
    function isSlotView(componentName: string, node?: t.JSXElement) {
99 1
      if (!node || componentName !== 'view') {
100 1
        return false;
101
      }
102

103 1
      if (node.openingElement.attributes.find(attr => t.isJSXAttribute(attr) && attr.name.name === 'slot')) {
104 1
        return true;
105
      }
106

107 1
      return false;
108
    }
109

110 1
    function getProps(id: string, node?: t.JSXElement, isNative?: boolean) {
111 1
      const hostComponent = options.hostComponents.get(id);
112

113 1
      if (!isNative && !hostComponent) {
114 1
        return;
115
      }
116

117 1
      const props: string[] = hostComponent ? hostComponent.props.slice() : [];
118

119 1
      if (node) {
120 1
        node.openingElement.attributes.forEach(attr => {
121 1
          if (t.isJSXSpreadAttribute(attr)) {
122 1
            return;
123
          }
124

125 1
          const prop = attr.name;
126 1
          let propName = '';
127

128 1
          if (t.isJSXIdentifier(prop)) {
129 1
            propName = prop.name;
130
          }
131

132 1
          if (t.isJSXNamespacedName(prop)) {
133 1
            propName = prop.namespace.name + ':' + prop.name.name;
134
          }
135

136
          /**
137
           * React 运行时读不到 key
138
           * 所以在这里如果发现组件上设置了 key
139
           * 就再设置一个别名 __key
140
           * 然后在模板里写死 key="{{item.props.__key}}"
141
           */
142 1
          if (propName === 'key') {
143 1
            node.openingElement.attributes.push(t.jsxAttribute(t.jsxIdentifier('__key'), attr.value));
144
          }
145

146 1
          if (!shouldRegisterProp(propName, !!isNative, hostComponent)) {
147 1
            return;
148
          }
149

150 1
          props.push(propName);
151
        });
152
      }
153

154 1
      if (isNative) {
155 1
        return Array.from(
156
          new Set(
157
            props
158
              // 剔除 ref,在 axml 特殊处理
159 1
              .filter(p => p !== 'ref')
160
              .filter(Boolean)
161 1
              .map(prop => prop.replace('className', 'class'))
162
          )
163
        ).sort();
164
      }
165

166 1
      return Array.from(
167
        new Set(
168
          props
169
            // 静态编译辅助字段
170 1
            .filter(p => !options.skipProps.includes(p))
171
            .filter(Boolean)
172 1
            .map(prop => aliasProp(prop, hostComponent))
173
        )
174
      ).sort();
175
    }
176

177 1
    function getHostComponentName(path: NodePath<t.JSXElement>) {
178 1
      const node = path.node;
179 1
      const openingElement = node.openingElement;
180

181 1
      if (!t.isJSXIdentifier(openingElement.name)) {
182 1
        return;
183
      }
184

185 1
      const name = openingElement.name.name;
186 1
      const binding = path.scope.getBinding(name);
187

188 1
      if (!binding) {
189 1
        return;
190
      }
191

192 1
      const bindingPath = binding.path;
193

194
      // binding
195 1
      if (!bindingPath || !t.isImportSpecifier(bindingPath.node)) {
196 1
        return;
197
      }
198

199 1
      const importPath = bindingPath.parentPath;
200

201 1
      if (t.isImportDeclaration(importPath) && t.isIdentifier(bindingPath.node.imported)) {
202 1
        return kebabCase(bindingPath.node.imported.name);
203
      }
204

205 0
      return;
206
    }
207

208 1
    function registerHostComponentManifest(id: string, node?: t.JSXElement) {
209 1
      if (options.skipHostComponents.includes(id)) {
210 1
        return;
211
      }
212

213 1
      let props: string[] | undefined = [];
214

215 1
      if (isSlotView(id, node)) {
216
        // isSlotView 确保了 node 一定存在
217 1
        props = registerSlotViewProps(node!);
218

219 1
        Store.slotView.props = Array.from(new Set([...Store.slotView.props, ...props]));
220

221 1
        return;
222
      } else {
223 1
        props = getProps(id, node);
224
      }
225

226 1
      if (!props) {
227 1
        return;
228
      }
229

230 1
      const component = {
231
        id,
232
        props,
233
      };
234

235 1
      const registeredComponent = Store.collectedComponents.get(id);
236

237 1
      if (registeredComponent) {
238 1
        component.props = Array.from(new Set([...props, ...registeredComponent.props])).sort();
239
      }
240

241 1
      Store.collectedComponents.set(id, component);
242
    }
243

244 1
    function collectCompositionComponents(path: NodePath<t.JSXElement>, importer: string) {
245 1
      const node = path.node;
246 1
      const openingElement = node.openingElement;
247

248 1
      if (!t.isJSXIdentifier(openingElement.name)) {
249 1
        return false;
250
      }
251

252 1
      const name = openingElement.name.name;
253 1
      const binding = path.scope.getBinding(name);
254

255 1
      if (!binding) {
256 1
        return false;
257
      }
258

259 1
      const bindingPath = binding.path;
260

261
      // binding
262 1
      if (!bindingPath) {
263 0
        return false;
264
      }
265

266 1
      const importPath = bindingPath.parentPath;
267

268 1
      if (t.isImportDeclaration(importPath)) {
269 1
        const importNode = importPath.node as t.ImportDeclaration;
270 1
        const source = importNode.source.value;
271 1
        const props = getProps('', node, true) || [];
272

273 1
        const modules = Store.compositionComponents.get(importer) || new Map();
274 1
        const component: { import: string; props: Set<string> } = modules.get(source) || {
275
          import: source,
276
          props: new Set(props),
277
        };
278 1
        modules.set(source, {
279
          import: source,
280
          props: new Set([...component.props, ...props]),
281
        });
282 1
        Store.compositionComponents.set(importer, modules);
283
      }
284

285 1
      if (
286 1
        t.isVariableDeclarator(bindingPath.node) &&
287 1
        t.isCallExpression(bindingPath.node.init) &&
288 1
        t.isIdentifier(bindingPath.node.init.callee) &&
289 1
        (bindingPath.node.init.callee as any).name === 'createNativeComponent'
290
      ) {
291 1
        const arg0 = bindingPath.node.init.arguments[0];
292 1
        if (t.isStringLiteral(arg0)) {
293 1
          const id = arg0.value;
294
          // macro 先执行,肯定注册过了
295 1
          const component = Array.from(Store.pluginComponents.values()).find(c => c.id === id)!;
296 1
          const props = getProps('', node, true) || [];
297 1
          props.forEach(component.props.add, component.props);
298
        }
299
      }
300
    }
301

302 1
    return {
303
      visitor: {
304 1
        JSXElement: (path: NodePath<t.JSXElement>, state: any) => {
305 1
          const hostComponentName = getHostComponentName(path);
306

307 1
          if (hostComponentName) {
308 1
            registerHostComponentManifest(hostComponentName, path.node);
309 1
            return;
310
          }
311

312 1
          collectCompositionComponents(path, slash(state.file.opts.filename));
313
        },
314
        CallExpression: {
315 1
          enter: (path: NodePath<t.CallExpression>, state: any) => {
316 1
            const importer = slash(state.file.opts.filename);
317

318 1
            if (!t.isMemberExpression(path?.node?.callee)) return;
319 1
            const memberExpression: t.MemberExpression = path?.node?.callee;
320 1
            if (!t.isIdentifier(memberExpression?.property)) return;
321

322 1
            const name = memberExpression?.property.name;
323 1
            if (name !== 'createElement') return;
324

325 1
            const elementName = path.node.arguments[0];
326 1
            if (!t.isIdentifier(elementName)) return;
327 1
            const binding = path.scope.getBinding(elementName.name);
328

329 1
            if (!binding) {
330 0
              return false;
331
            }
332

333 1
            const bindingPath = binding.path;
334

335
            // binding
336 1
            if (!bindingPath) {
337 0
              return false;
338
            }
339

340 1
            const importPath = bindingPath.parentPath;
341

342 1
            if (t.isImportDeclaration(importPath)) {
343 1
              const importNode = importPath.node as t.ImportDeclaration;
344 1
              const source = importNode.source.value;
345

346 1
              if (source === 'remax') return;
347

348 1
              const propsObject = path.node.arguments[1];
349 1
              let props: any[] = [];
350 1
              if (t.isObjectExpression(propsObject)) {
351 1
                props = propsObject.properties
352 1
                  .map(it => {
353 1
                    if (!t.isObjectProperty(it)) return;
354 1
                    if (t.isIdentifier(it.key)) return it.key.name;
355 1
                    if (t.isStringLiteral(it.key)) return it.key.value;
356
                  })
357 1
                  .filter(p => p !== 'ref')
358
                  .filter(Boolean);
359
              }
360 1
              const modules = Store.compositionComponents.get(importer) || new Map();
361 1
              const component: { import: string; props: Set<string> } = modules.get(source) || {
362
                import: source,
363
                props: new Set(props),
364
              };
365 1
              modules.set(source, {
366
                import: source,
367
                props: new Set([...component.props, ...props]),
368
              });
369 1
              Store.compositionComponents.set(importer, modules);
370
            }
371
          },
372
        },
373
      },
374
    };
375
  });
376
}

Read our documentation on viewing source code .

Loading