@cloudcare/hvigor-ohos-plugin 0.1.1 → 0.1.2-alpha.1
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/CHANGELOG.md +10 -1
- package/README.md +31 -6
- package/hvigorCompile/ft-cold-launch-transform.ts +599 -121
- package/hvigorCompile/ft-console-log-transform.ts +73 -95
- package/hvigorCompile/ft-generated-names.ts +19 -0
- package/hvigorCompile/ft-lexical-scope.ts +65 -0
- package/hvigorCompile/ft-session-replay-transform.ts +363 -0
- package/hvigorCompile/ft-source-transform-host.ts +45 -22
- package/hvigorCompile/ft-transform-context.ts +36 -0
- package/hvigorCompile/ft-web-replay-transform.ts +51 -0
- package/index.js +5 -3
- package/package.json +12 -3
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
* never changed.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { hasRuntimeDependency, SourceReader } from './ft-transform-context';
|
|
11
|
+
import { createLexicalResolver } from './ft-lexical-scope';
|
|
12
|
+
import { allocateName, collectIdentifierNames } from './ft-generated-names';
|
|
13
|
+
|
|
10
14
|
type TypeScriptApi = any;
|
|
11
15
|
|
|
12
16
|
interface TransformCandidate {
|
|
@@ -17,11 +21,14 @@ interface TransformCandidate {
|
|
|
17
21
|
level: string;
|
|
18
22
|
source: 'console' | 'hilog';
|
|
19
23
|
callArguments: SourceRange[];
|
|
24
|
+
needsStatementSeparator: boolean;
|
|
25
|
+
generatedNames?: string[];
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
interface SourceRange {
|
|
23
29
|
start: number;
|
|
24
30
|
end: number;
|
|
31
|
+
spread: boolean;
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
const HILOG_MODULES: string[] = ['@ohos.hilog', '@kit.PerformanceAnalysisKit'];
|
|
@@ -45,77 +52,58 @@ function visit(ts: TypeScriptApi, node: any, visitor: (current: any) => void): v
|
|
|
45
52
|
ts.forEachChild(node, (child: any) => visit(ts, child, visitor));
|
|
46
53
|
}
|
|
47
54
|
|
|
48
|
-
function
|
|
49
|
-
const
|
|
50
|
-
visit(ts, sourceFile, (node: any) => {
|
|
51
|
-
if (ts.isIdentifier(node)) {
|
|
52
|
-
identifiers.add(node.text);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
return identifiers;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function isNameDeclaredOutsideImports(ts: TypeScriptApi, sourceFile: any, name: string): boolean {
|
|
59
|
-
let declared = false;
|
|
60
|
-
visit(ts, sourceFile, (node: any) => {
|
|
61
|
-
if (declared || ts.isImportDeclaration(node)) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
if (ts.isVariableDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) ||
|
|
65
|
-
ts.isParameter(node) || ts.isEnumDeclaration(node) || ts.isInterfaceDeclaration(node) ||
|
|
66
|
-
ts.isTypeAliasDeclaration(node)) {
|
|
67
|
-
const declarationName = node.name;
|
|
68
|
-
if (declarationName && ts.isIdentifier(declarationName) && declarationName.text === name) {
|
|
69
|
-
declared = true;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
return declared;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function collectHiLogBindings(ts: TypeScriptApi, sourceFile: any): Set<string> {
|
|
77
|
-
const bindings = new Set<string>();
|
|
55
|
+
function collectHiLogBindings(ts: TypeScriptApi, sourceFile: any): Map<string, any> {
|
|
56
|
+
const bindings = new Map<string, any>();
|
|
78
57
|
sourceFile.statements.forEach((statement: any) => {
|
|
79
58
|
if (!ts.isImportDeclaration(statement) || !statement.moduleSpecifier ||
|
|
80
59
|
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
81
|
-
!HILOG_MODULES.includes(statement.moduleSpecifier.text) || !statement.importClause) {
|
|
60
|
+
!HILOG_MODULES.includes(statement.moduleSpecifier.text) || !statement.importClause || statement.importClause.isTypeOnly) {
|
|
82
61
|
return;
|
|
83
62
|
}
|
|
84
63
|
|
|
85
64
|
const importClause = statement.importClause;
|
|
86
65
|
if (importClause.name) {
|
|
87
|
-
bindings.
|
|
66
|
+
bindings.set(importClause.name.text, importClause);
|
|
88
67
|
}
|
|
89
68
|
if (!importClause.namedBindings) {
|
|
90
69
|
return;
|
|
91
70
|
}
|
|
92
71
|
if (ts.isNamespaceImport(importClause.namedBindings)) {
|
|
93
|
-
bindings.
|
|
72
|
+
bindings.set(importClause.namedBindings.name.text, importClause.namedBindings);
|
|
94
73
|
return;
|
|
95
74
|
}
|
|
96
75
|
importClause.namedBindings.elements.forEach((element: any) => {
|
|
97
76
|
const importedName = element.propertyName ? element.propertyName.text : element.name.text;
|
|
98
|
-
if (importedName === 'hilog') {
|
|
99
|
-
bindings.
|
|
77
|
+
if (importedName === 'hilog' && !element.isTypeOnly) {
|
|
78
|
+
bindings.set(element.name.text, element);
|
|
100
79
|
}
|
|
101
80
|
});
|
|
102
81
|
});
|
|
103
82
|
return bindings;
|
|
104
83
|
}
|
|
105
84
|
|
|
85
|
+
function needsStatementSeparator(ts: TypeScriptApi, node: any): boolean {
|
|
86
|
+
const statement = node.parent;
|
|
87
|
+
if (!ts.isExpressionStatement(statement)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const container = statement.parent;
|
|
91
|
+
// Only statement lists accept a leading empty statement. In an unbraced
|
|
92
|
+
// if/loop body it would replace the body; inside an expression it is invalid.
|
|
93
|
+
return ts.isSourceFile(container) || ts.isBlock(container) || ts.isModuleBlock(container) ||
|
|
94
|
+
ts.isCaseClause(container) || ts.isDefaultClause(container);
|
|
95
|
+
}
|
|
96
|
+
|
|
106
97
|
function collectCandidates(ts: TypeScriptApi, sourceFile: any, code: string): TransformCandidate[] {
|
|
107
98
|
const candidates: TransformCandidate[] = [];
|
|
108
99
|
const hiLogBindings = collectHiLogBindings(ts, sourceFile);
|
|
109
|
-
const
|
|
110
|
-
const shadowedHiLogBindings = new Set<string>();
|
|
111
|
-
hiLogBindings.forEach((binding: string) => {
|
|
112
|
-
if (isNameDeclaredOutsideImports(ts, sourceFile, binding)) {
|
|
113
|
-
shadowedHiLogBindings.add(binding);
|
|
114
|
-
}
|
|
115
|
-
});
|
|
100
|
+
const resolve = createLexicalResolver(ts, sourceFile);
|
|
116
101
|
|
|
117
102
|
visit(ts, sourceFile, (node: any) => {
|
|
118
|
-
|
|
103
|
+
// Retain optional calls verbatim so their receiver/method short-circuit also
|
|
104
|
+
// controls argument evaluation. Nested ordinary calls remain eligible.
|
|
105
|
+
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) ||
|
|
106
|
+
(ts.isOptionalChain && ts.isOptionalChain(node))) {
|
|
119
107
|
return;
|
|
120
108
|
}
|
|
121
109
|
const delegate = node.expression.expression;
|
|
@@ -133,29 +121,27 @@ function collectCandidates(ts: TypeScriptApi, sourceFile: any, code: string): Tr
|
|
|
133
121
|
|
|
134
122
|
const callArguments: SourceRange[] = node.arguments.map((argument: any) => ({
|
|
135
123
|
start: argument.getStart(sourceFile),
|
|
136
|
-
end: argument.end
|
|
124
|
+
end: argument.end,
|
|
125
|
+
spread: ts.isSpreadElement(argument)
|
|
137
126
|
}));
|
|
138
|
-
|
|
139
|
-
|
|
127
|
+
const statementSeparator = needsStatementSeparator(ts, node);
|
|
128
|
+
if (delegateName === 'console' && !resolve(delegate, delegateName) && CONSOLE_LEVELS.includes(level)) {
|
|
129
|
+
candidates.push({ start, end, calleeEnd, delegate: delegateName, level, source: 'console', callArguments,
|
|
130
|
+
needsStatementSeparator: statementSeparator });
|
|
140
131
|
return;
|
|
141
132
|
}
|
|
142
|
-
if (hiLogBindings.has(delegateName) &&
|
|
143
|
-
HILOG_LEVELS.includes(level) && callArguments.length >= 3) {
|
|
144
|
-
candidates.push({ start, end, calleeEnd, delegate: delegateName, level, source: 'hilog', callArguments
|
|
133
|
+
if (hiLogBindings.has(delegateName) && resolve(delegate, delegateName) === hiLogBindings.get(delegateName) &&
|
|
134
|
+
HILOG_LEVELS.includes(level) && (callArguments.length >= 3 || callArguments.some(argument => argument.spread))) {
|
|
135
|
+
candidates.push({ start, end, calleeEnd, delegate: delegateName, level, source: 'hilog', callArguments,
|
|
136
|
+
needsStatementSeparator: statementSeparator });
|
|
145
137
|
}
|
|
146
138
|
});
|
|
147
139
|
return candidates;
|
|
148
140
|
}
|
|
149
141
|
|
|
150
|
-
function
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
let suffix = 1;
|
|
154
|
-
while (identifiers.has(bridgeName)) {
|
|
155
|
-
bridgeName = `${baseName}${suffix}`;
|
|
156
|
-
suffix++;
|
|
157
|
-
}
|
|
158
|
-
return bridgeName;
|
|
142
|
+
function allocateCandidateNames(candidate: TransformCandidate, identifiers: Set<string>): string[] {
|
|
143
|
+
const prefix = candidate.source === 'console' ? '__ftConsole' : '__ftHiLog';
|
|
144
|
+
return ['Call', 'Args', 'Values'].map(suffix => allocateName(identifiers, prefix + suffix));
|
|
159
145
|
}
|
|
160
146
|
|
|
161
147
|
function isNestedIn(candidate: TransformCandidate, parent: TransformCandidate): boolean {
|
|
@@ -187,42 +173,31 @@ function renderCandidate(code: string, candidates: TransformCandidate[], bridgeN
|
|
|
187
173
|
candidate: TransformCandidate): string {
|
|
188
174
|
const renderedArguments = candidate.callArguments.map((argument: SourceRange) =>
|
|
189
175
|
renderCandidates(code, candidates, bridgeName, argument.start, argument.end));
|
|
176
|
+
const statementSeparator = candidate.needsStatementSeparator ? ';' : '';
|
|
177
|
+
const [original, args, values] = candidate.generatedNames!;
|
|
178
|
+
const valueType = '(Object | null | undefined)';
|
|
179
|
+
const originalType = candidate.source === 'console' ? `(...${args}: ${valueType}[]) => void` :
|
|
180
|
+
`(domain: number, tag: string, format: string, ...${values}: ${valueType}[]) => void`;
|
|
181
|
+
let body: string;
|
|
190
182
|
if (candidate.source === 'console') {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
183
|
+
body = `${bridgeName}.console('${candidate.level}', (): void => ${original}(...${args}), ` +
|
|
184
|
+
`...(${args} as Object[]))`;
|
|
185
|
+
} else {
|
|
186
|
+
const header = `${args}[0] as number, ${args}[1] as string, ${args}[2] as string`;
|
|
187
|
+
body = `{ const ${values}: ${valueType}[] = ${args}.slice(3); ` +
|
|
188
|
+
`${bridgeName}.hilog('${candidate.level}', ${header}, (): void => ${original}(${header}, ...${values}), ` +
|
|
189
|
+
`...(${values} as Object[])); }`;
|
|
198
190
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
return `__ftHiLogArg${index - 3}`;
|
|
211
|
-
});
|
|
212
|
-
const parameterDeclarations = argumentNames.map((name: string, index: number) => {
|
|
213
|
-
if (index === 0) {
|
|
214
|
-
return `${name}: number`;
|
|
215
|
-
}
|
|
216
|
-
if (index === 1 || index === 2) {
|
|
217
|
-
return `${name}: string`;
|
|
218
|
-
}
|
|
219
|
-
return `${name}: Object`;
|
|
220
|
-
}).join(', ');
|
|
221
|
-
const originalCall = `${candidate.delegate}.${candidate.level}(${argumentNames.join(', ')})`;
|
|
222
|
-
const captureArguments = argumentNames.length > 3 ? `, ${argumentNames.slice(3).join(', ')}` : '';
|
|
223
|
-
return `((${parameterDeclarations}): void => ${bridgeName}.hilog('${candidate.level}', ` +
|
|
224
|
-
`${argumentNames[0]}, ${argumentNames[1]}, ${argumentNames[2]}, (): void => ${originalCall}` +
|
|
225
|
-
`${captureArguments}))(${renderedArguments.join(', ')})`;
|
|
191
|
+
// Resolve and bind the original method before evaluating its arguments.
|
|
192
|
+
// Casting only the capture array keeps compatibility with older runtime HARs;
|
|
193
|
+
// null/undefined values themselves are forwarded unchanged.
|
|
194
|
+
// HiLog is a platform namespace, not an ArkTS object receiver. Its exported
|
|
195
|
+
// functions are static; console methods retain their receiver through bind.
|
|
196
|
+
const callee = `${candidate.delegate}.${candidate.level}`;
|
|
197
|
+
const captured = candidate.source === 'console' ? `${callee}.bind(${candidate.delegate})` : callee;
|
|
198
|
+
const argumentsText = renderedArguments.length > 0 ? ', ' + renderedArguments.join(', ') : '';
|
|
199
|
+
return `${statementSeparator}((${original}: ${originalType}, ...${args}: ${valueType}[]): void => ${body})` +
|
|
200
|
+
`(${captured} as ${originalType}${argumentsText})`;
|
|
226
201
|
}
|
|
227
202
|
|
|
228
203
|
function findImportInsertionOffset(ts: TypeScriptApi, sourceFile: any): number {
|
|
@@ -235,8 +210,9 @@ function findImportInsertionOffset(ts: TypeScriptApi, sourceFile: any): number {
|
|
|
235
210
|
return offset;
|
|
236
211
|
}
|
|
237
212
|
|
|
238
|
-
export function transformConsoleLogSource(ts: TypeScriptApi, id: string, code: string): string {
|
|
239
|
-
if (!isApplicationArkTsSource(id) || code.includes(TRANSFORM_MARKER)
|
|
213
|
+
export function transformConsoleLogSource(ts: TypeScriptApi, id: string, code: string, readSource?: SourceReader): string {
|
|
214
|
+
if (!isApplicationArkTsSource(id) || code.includes(TRANSFORM_MARKER) ||
|
|
215
|
+
!hasRuntimeDependency(ts, id, '@guancecloud/ft_sdk', readSource)) {
|
|
240
216
|
return code;
|
|
241
217
|
}
|
|
242
218
|
const sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
@@ -245,7 +221,9 @@ export function transformConsoleLogSource(ts: TypeScriptApi, id: string, code: s
|
|
|
245
221
|
return code;
|
|
246
222
|
}
|
|
247
223
|
|
|
248
|
-
const
|
|
224
|
+
const identifiers = collectIdentifierNames(ts, sourceFile);
|
|
225
|
+
const bridgeName = allocateName(identifiers, '__ftConsoleLogBridge');
|
|
226
|
+
candidates.forEach(candidate => { candidate.generatedNames = allocateCandidateNames(candidate, identifiers); });
|
|
249
227
|
const transformedCode = renderCandidates(code, candidates, bridgeName, 0, code.length);
|
|
250
228
|
const insertionOffset = findImportInsertionOffset(ts, sourceFile);
|
|
251
229
|
const transformedInsertionOffset = renderCandidates(code, candidates, bridgeName, 0, insertionOffset).length;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Reserve generated identifiers across all scopes and every nested rewrite. */
|
|
2
|
+
export function allocateName(identifiers: Set<string>, base: string): string {
|
|
3
|
+
let candidate = base;
|
|
4
|
+
let suffix = 1;
|
|
5
|
+
while (identifiers.has(candidate)) candidate = `${base}${suffix++}`;
|
|
6
|
+
identifiers.add(candidate);
|
|
7
|
+
return candidate;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function collectIdentifierNames(ts: any, source: any): Set<string> {
|
|
11
|
+
const identifiers = new Set<string>();
|
|
12
|
+
const visit = (node: any): void => {
|
|
13
|
+
if (ts.isIdentifier(node)) identifiers.add(node.text);
|
|
14
|
+
if (node.name && ts.isStringLiteral(node.name)) identifiers.add(node.name.text);
|
|
15
|
+
ts.forEachChild(node, visit);
|
|
16
|
+
};
|
|
17
|
+
visit(source);
|
|
18
|
+
return identifiers;
|
|
19
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Value bindings visible at a call site, including declarations later in the same scope. */
|
|
2
|
+
interface Scope {
|
|
3
|
+
parent?: Scope;
|
|
4
|
+
functionScope: boolean;
|
|
5
|
+
bindings: Map<string, any>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createLexicalResolver(ts: any, source: any): (location: any, name: string) => any | undefined {
|
|
9
|
+
const scopes = new WeakMap<object, Scope>();
|
|
10
|
+
const root: Scope = { functionScope: true, bindings: new Map() };
|
|
11
|
+
const bind = (scope: Scope, name: any, declaration: any): void => {
|
|
12
|
+
if (!name) return;
|
|
13
|
+
if (ts.isIdentifier(name)) scope.bindings.set(name.text, declaration);
|
|
14
|
+
else if (name.elements) name.elements.forEach((element: any) => {
|
|
15
|
+
if (ts.isBindingElement(element)) bind(scope, element.name, declaration);
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
const walk = (node: any, outer: Scope): void => {
|
|
19
|
+
const struct = typeof ts.isStructDeclaration === 'function' && ts.isStructDeclaration(node);
|
|
20
|
+
const classLike = ts.isClassDeclaration(node) || ts.isClassExpression(node) || struct;
|
|
21
|
+
if ((ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || struct || ts.isEnumDeclaration(node) ||
|
|
22
|
+
ts.isModuleDeclaration(node)) && node.name) {
|
|
23
|
+
bind(outer, node.name, node);
|
|
24
|
+
}
|
|
25
|
+
let scope = outer;
|
|
26
|
+
const functionLike = ts.isFunctionLike(node);
|
|
27
|
+
if (functionLike || ts.isBlock(node) || ts.isCaseBlock(node) || ts.isCatchClause(node) ||
|
|
28
|
+
ts.isForStatement(node) || ts.isForInStatement(node) || ts.isForOfStatement(node) ||
|
|
29
|
+
ts.isModuleBlock(node) || classLike) {
|
|
30
|
+
scope = { parent: outer, functionScope: functionLike || ts.isModuleBlock(node), bindings: new Map() };
|
|
31
|
+
}
|
|
32
|
+
scopes.set(node, scope);
|
|
33
|
+
if (functionLike) {
|
|
34
|
+
if (ts.isFunctionExpression(node) && node.name) bind(scope, node.name, node);
|
|
35
|
+
node.parameters.forEach((parameter: any) => bind(scope, parameter.name, parameter));
|
|
36
|
+
}
|
|
37
|
+
if (classLike && node.name) bind(scope, node.name, node);
|
|
38
|
+
if (ts.isVariableDeclaration(node)) {
|
|
39
|
+
const list = node.parent;
|
|
40
|
+
let target = scope;
|
|
41
|
+
if (ts.isVariableDeclarationList(list) && !(list.flags & ts.NodeFlags.BlockScoped)) {
|
|
42
|
+
while (!target.functionScope && target.parent) target = target.parent;
|
|
43
|
+
}
|
|
44
|
+
bind(target, node.name, node);
|
|
45
|
+
}
|
|
46
|
+
if (ts.isImportDeclaration(node) && node.importClause && !node.importClause.isTypeOnly) {
|
|
47
|
+
const clause = node.importClause;
|
|
48
|
+
if (clause.name) bind(scope, clause.name, clause);
|
|
49
|
+
const bindings = clause.namedBindings;
|
|
50
|
+
if (bindings && ts.isNamespaceImport(bindings)) bind(scope, bindings.name, bindings);
|
|
51
|
+
else if (bindings) bindings.elements.forEach((element: any) => {
|
|
52
|
+
if (!element.isTypeOnly) bind(scope, element.name, element);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (ts.isImportEqualsDeclaration(node) && !node.isTypeOnly) bind(scope, node.name, node);
|
|
56
|
+
ts.forEachChild(node, (child: any) => walk(child, scope));
|
|
57
|
+
};
|
|
58
|
+
walk(source, root);
|
|
59
|
+
return (location: any, name: string): any | undefined => {
|
|
60
|
+
for (let scope = scopes.get(location); scope; scope = scope.parent) {
|
|
61
|
+
if (scope.bindings.has(name)) return scope.bindings.get(name);
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
};
|
|
65
|
+
}
|