Skip to content

Commit 25c986b

Browse files
committed
feat(compiler): add ReplexicaAttributeScope
1 parent f91bfba commit 25c986b

1 file changed

Lines changed: 171 additions & 0 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import * as t from '@babel/types';
2+
import { NodePath } from '@babel/core';
3+
import { IReplexicaScope } from "./types";
4+
import { ReplexicaChunk } from './chunk';
5+
import { generateScopeId } from '../../utils/id';
6+
import { findJsxParentForTheAttribute, getImportName, getJsxAttributeName, getJsxElementName, getStringAttributeValue, injectImport, parseMemberExpressionFromJsxMemberExpression } from '../../utils/ast';
7+
import { ReplexicaScopeData, ReplexicaScopeHint } from '../types';
8+
import { ReplexicaBaseScope } from './base';
9+
10+
export class ReplexicaAttributeScope extends ReplexicaBaseScope implements IReplexicaScope {
11+
public static fromNode(path: NodePath<t.Node>): IReplexicaScope[] {
12+
const result: IReplexicaScope[] = [];
13+
if (!path.isJSXElement()) { return result; }
14+
15+
const supportedAttributeNames = ['title', 'alt', 'placeholder'];
16+
const openingElement = path.get('openingElement');
17+
const attributesPaths = openingElement.get('attributes');
18+
for (const attributePath of attributesPaths) {
19+
if (!attributePath.isJSXAttribute()) { continue; }
20+
21+
const attributeName = attributePath.get('name');
22+
if (!attributeName.isJSXIdentifier()) { continue; }
23+
24+
const attributeNameValue = attributeName.node.name;
25+
if (!supportedAttributeNames.includes(attributeNameValue)) { continue; }
26+
27+
result.push(new ReplexicaAttributeScope(attributePath));
28+
}
29+
30+
return result;
31+
}
32+
33+
public constructor(
34+
private readonly path: NodePath<t.JSXAttribute>,
35+
) {
36+
super();
37+
38+
const valuePath = this.path.get("value") as NodePath<t.Node>;
39+
if (valuePath.isStringLiteral()) {
40+
this._chunk = ReplexicaChunk.fromStringLiteral(valuePath);
41+
} else if (valuePath.isJSXExpressionContainer()) {
42+
const expressionPath = valuePath.get("expression") as NodePath<t.Node>;
43+
if (expressionPath.isStringLiteral()) {
44+
this._chunk = ReplexicaChunk.fromJsxExpressionContainer(valuePath);
45+
}
46+
}
47+
48+
const chunkIds = this._chunk ? [this._chunk.id] : [];
49+
const result = generateScopeId(chunkIds, 0);
50+
this._id = result;
51+
}
52+
53+
private _chunk: ReplexicaChunk | null = null;
54+
private _id: string;
55+
56+
public get id(): string {
57+
return this._id;
58+
}
59+
60+
public injectIntl(fileId: string, isServer: boolean): ReplexicaScopeData {
61+
const result: ReplexicaScopeData = {};
62+
if (!this._chunk) { return result; }
63+
64+
const programNode = this.path.findParent((p) => p.isProgram()) as NodePath<t.Program> | null;
65+
if (!programNode) { throw new Error(`Couldn't find file node`); }
66+
67+
const jsxElement = findJsxParentForTheAttribute(this.path);
68+
if (!jsxElement) {
69+
throw new Error(`Couldn't find JSX element for attribute ${this.path.get("name").toString()}`);
70+
}
71+
72+
const packageName = isServer ? '@replexica/react/server' : '@replexica/react/client';
73+
const localHelperName = isServer ? 'ReplexicaServerProxy' : 'ReplexicaClientProxy';
74+
75+
let helperName = getImportName(programNode, packageName, localHelperName);
76+
if (!helperName) {
77+
helperName = injectImport(programNode, packageName, localHelperName);
78+
}
79+
80+
const isProxyAlreadyApplied = t.isJSXIdentifier(jsxElement.node.openingElement.name) && jsxElement.node.openingElement.name.name === helperName;
81+
if (!isProxyAlreadyApplied) {
82+
// 1. add __ReplexicaComponent attribute to the element and set it to the original component name / identifier / member expression
83+
if (t.isJSXIdentifier(jsxElement.node.openingElement.name)) {
84+
// if it's an identifier, it's a native element or a component
85+
if (/^[a-z]/.test(jsxElement.node.openingElement.name.name)) {
86+
// if it's lowercased identifier, it's a native element
87+
jsxElement.node.openingElement.attributes.push(
88+
t.jSXAttribute(
89+
t.jSXIdentifier('__ReplexicaComponent'),
90+
t.stringLiteral(jsxElement.node.openingElement.name.name),
91+
),
92+
);
93+
} else {
94+
// if it's uppercased identifier, it's a component
95+
jsxElement.node.openingElement.attributes.push(
96+
t.jSXAttribute(
97+
t.jSXIdentifier('__ReplexicaComponent'),
98+
t.jSXExpressionContainer(t.identifier(jsxElement.node.openingElement.name.name)),
99+
),
100+
);
101+
}
102+
} else if (t.isJSXMemberExpression(jsxElement.node.openingElement.name)) {
103+
const memberExpression = parseMemberExpressionFromJsxMemberExpression(jsxElement.node.openingElement.name);
104+
jsxElement.node.openingElement.attributes.push(
105+
t.jSXAttribute(
106+
t.jSXIdentifier('__ReplexicaComponent'),
107+
t.jSXExpressionContainer(memberExpression),
108+
),
109+
);
110+
} else {
111+
throw new Error(`Unsupported JSXElement name type.`);
112+
}
113+
// 2. replace the element name with the name of the proxy component
114+
jsxElement.node.openingElement.name = t.jSXIdentifier(helperName);
115+
if (jsxElement.node.closingElement) {
116+
jsxElement.node.closingElement.name = t.jSXIdentifier(helperName);
117+
}
118+
}
119+
120+
// add the current attribute to the proxy component's __ReplexicaAttributes attribute
121+
// like so: <ProxyComponent __ReplexicaAttributes={{ [attributeName]: { fileId, scopeId, chunkId } }} />
122+
// create __ReplexicaAttributes attribute entry only if it doesn't exist yet
123+
let systemReplexicaAttribute = jsxElement.node.openingElement.attributes.find((attr) => {
124+
return t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === '__ReplexicaAttributes';
125+
}) as t.JSXAttribute | undefined;
126+
if (!systemReplexicaAttribute) {
127+
systemReplexicaAttribute = t.jSXAttribute(
128+
t.jSXIdentifier('__ReplexicaAttributes'),
129+
t.jSXExpressionContainer(t.objectExpression([])),
130+
);
131+
jsxElement.node.openingElement.attributes.push(systemReplexicaAttribute);
132+
}
133+
134+
// create the current attribute entry
135+
const attributeName = this.path.get("name").toString();
136+
137+
const selectorProperty = t.objectProperty(
138+
t.stringLiteral(attributeName),
139+
t.objectExpression([
140+
t.objectProperty(t.stringLiteral('fileId'), t.stringLiteral(fileId)),
141+
t.objectProperty(t.stringLiteral('scopeId'), t.stringLiteral(this.id)),
142+
t.objectProperty(t.stringLiteral('chunkId'), t.stringLiteral(this._chunk.id)),
143+
]),
144+
);
145+
// add the current attribute entry to the __ReplexicaAttributes attribute
146+
if (t.isJSXExpressionContainer(systemReplexicaAttribute.value) && t.isObjectExpression(systemReplexicaAttribute.value.expression)) {
147+
systemReplexicaAttribute.value.expression.properties.push(selectorProperty);
148+
result[this._chunk.id] = this._chunk.text;
149+
}
150+
151+
return result;
152+
}
153+
154+
public extractHints(): ReplexicaScopeHint[] {
155+
const jsxElement = findJsxParentForTheAttribute(this.path);
156+
if (!jsxElement) {
157+
throw new Error(`Couldn't find JSX element for attribute ${this.path.get("name").toString()}`);
158+
}
159+
160+
const attributeName = getJsxAttributeName(this.path);
161+
const attributeHint: ReplexicaScopeHint = {
162+
type: 'attribute',
163+
name: attributeName,
164+
};
165+
166+
const baseHints = this._extractBaseHints(jsxElement);
167+
168+
const result = [...baseHints, attributeHint];
169+
return result;
170+
}
171+
}

0 commit comments

Comments
 (0)