@cloudcare/hvigor-ohos-plugin 0.1.2-alpha.1 → 0.1.2-alpha.2
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 +5 -0
- package/README.md +23 -1
- package/hvigorCompile/ft-cold-launch-transform.ts +75 -6
- package/hvigorCompile/ft-console-log-transform.ts +27 -11
- package/hvigorCompile/ft-session-replay-transform.ts +39 -21
- package/hvigorCompile/ft-source-transform-host.ts +74 -8
- package/hvigorCompile/ft-transform-context.ts +55 -0
- package/hvigorCompile/ft-web-replay-transform.ts +14 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.2-alpha.2] - 2026-10-30
|
|
4
|
+
|
|
5
|
+
- Added build warnings with feature names, source paths, skip reasons and source locations for Console/HiLog, cold-launch lifecycle hooks, Session Replay root registration and ArkWeb binding.
|
|
6
|
+
- Fixed automatic Session Replay page registration being skipped on pages that use UI enum values or `ForEach`/`LazyForEach`/`Repeat` child blocks, which could leave native View/Action events without `session_has_replay`.
|
|
7
|
+
|
|
3
8
|
## [0.1.2-alpha.1] - 2026-10-19
|
|
4
9
|
|
|
5
10
|
- Added automatic Session Replay integration for supported ArkUI `@Entry` pages, ArkWeb components and touch events, without manual page registration or event forwarding. Requires `@guancecloud/ft_session_replay` in the HAP and Replay initialization; touch coordinates require `TouchPrivacy.SHOW`.
|
package/README.md
CHANGED
|
@@ -46,6 +46,28 @@ The plugin automatically captures page touch events without changing existing `o
|
|
|
46
46
|
|
|
47
47
|
Router page changes and explicit `PageTransitionEnter/Exit` slide and opacity animations are captured automatically. Existing transition callbacks are preserved; playback of default system transitions may be approximate.
|
|
48
48
|
|
|
49
|
+
## Build diagnostics
|
|
50
|
+
|
|
51
|
+
Search for `[GuanceBuild]` in DevEco Studio's Build output or `.hvigor/outputs/build-logs/build.log` to find instrumentation warnings. Each warning includes a feature name, source path, one-based line and column, skip reason, and explanation. These build-time warnings do not depend on `FTSDKConfig.setDebug(...)`.
|
|
52
|
+
|
|
53
|
+
| Filter | Feature | Feature-specific skip reasons |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| `[GuanceLog]` | Console/HiLog collection | `unsupported-log-call`: optional calls, missing HiLog arguments, or an unusable compiler source range |
|
|
56
|
+
| `[GuanceColdLaunch]` | AbilityStage/UIAbility/InsightIntent lifecycle and first-frame hooks | `unresolved-ability-base`: a declared module entry's platform base cannot be resolved; `unsupported-lifecycle`: no instrumentable method/function body; `unsupported-lifecycle-parameters`: unsupported launch or WindowStage parameters |
|
|
57
|
+
| `[GuanceReplay]` | Session Replay `@Entry` page root registration | `unsupported-parser`, `missing-build`, `unsupported-root` |
|
|
58
|
+
| `[GuanceWebReplay]` | ArkWeb controller binding | `unsupported-web-call`: a `Web` call is optional, has a spread argument, or does not have exactly one options argument |
|
|
59
|
+
|
|
60
|
+
All four features report `parse-error` when a relevant source file cannot be parsed, including the first compiler diagnostic and the number of additional parse errors. Unexpected transform exceptions report `transform-exception` and retain the original build failure. When an exception has no source position, its location is the file's `1:1`.
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
[GuanceBuild][GuanceReplay] /project/entry/src/main/ets/pages/Example.ets:12:19: feature=session-replay-root; reason=parse-error; TS1109: Expression expected.
|
|
64
|
+
[GuanceBuild][GuanceColdLaunch] /project/entry/src/main/ets/entryability/EntryAbility.ets:18:3: feature=cold-launch; reason=unsupported-lifecycle-parameters; onWindowStageCreate requires a named WindowStage parameter for automatic first-frame observation.
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Warnings do not fail the build by themselves. Unsupported calls/hooks are left unchanged while other supported hooks can still be applied; a parse failure skips that feature's transformation for the file. Application files are never modified on disk. Locations refer to the original source when available; a warning explicitly identifies any location that refers to transformed source instead. Identical warnings are deduplicated per feature and source location within the active compilation.
|
|
68
|
+
|
|
69
|
+
Modules without the required runtime dependencies, files with no matching calls/classes/pages, and locally shadowed names are skipped without warnings. The plugin does not infer that an arbitrary unresolved business class is an Ability. After updating the plugin, rebuild the application so the source transforms execute again. Existing `ArkTS:WARN` compiler diagnostics remain independent of these plugin warnings.
|
|
70
|
+
|
|
49
71
|
## Compatibility
|
|
50
72
|
|
|
51
73
|
The plugin is verified with Node.js 18 or newer, Hvigor and `@ohos/hvigor-ohos-plugin` 6.23.5, and HarmonyOS SDK 6.0.2 (API 22). Console/HiLog and cold-launch instrumentation require `@guancecloud/ft_sdk` `0.1.17` or later. Automatic Session Replay additionally requires `@guancecloud/ft_sdk` `0.1.18` or later and `@guancecloud/ft_session_replay` `0.1.0` or later.
|
|
@@ -59,4 +81,4 @@ Each module that needs automatic collection must declare these SDK packages unde
|
|
|
59
81
|
|
|
60
82
|
The plugin checks SDK dependencies in the `oh-package.json5` of the module that contains each source file (HAP, HAR or HSP). Declaring an SDK only in the project-root `oh-package.json5`, another module, or `devDependencies` does not enable automatic collection for that source file.
|
|
61
83
|
|
|
62
|
-
Optional logging calls such as `console?.info(...)` and `hilog.info?.(...)` are not automatically collected.
|
|
84
|
+
Optional logging calls such as `console?.info(...)` and `hilog.info?.(...)` are not automatically collected and produce `unsupported-log-call` warnings.
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* the compiler's in-memory source. Application files are never modified.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { hasRuntimeDependency
|
|
8
|
+
import { hasRuntimeDependency, objectProperty, SourceDiagnosticReporter, SourceTransformDiagnostic,
|
|
9
|
+
parseApplicationSource, reportSourceDiagnostic, reportSourceParseError } from './ft-transform-context';
|
|
9
10
|
import { createLexicalResolver } from './ft-lexical-scope';
|
|
10
11
|
import { allocateName as selectIdentifierName, collectIdentifierNames } from './ft-generated-names';
|
|
11
12
|
|
|
@@ -42,6 +43,7 @@ interface ColdLaunchInjection {
|
|
|
42
43
|
interface ColdLaunchInjections {
|
|
43
44
|
injections: ColdLaunchInjection[];
|
|
44
45
|
hasUIAbility: boolean;
|
|
46
|
+
hasAbility: boolean;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
const COLD_LAUNCH_BRIDGE_IMPORT = '@guancecloud/ft_sdk/Index';
|
|
@@ -129,6 +131,37 @@ function needsUIAbilityModuleFallback(ts: TypeScriptApi, id: string, readSource?
|
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
133
|
|
|
134
|
+
/** An unresolved business class is not necessarily an Ability; trust explicit module entries. */
|
|
135
|
+
function isDeclaredAbilityEntry(ts: TypeScriptApi, id: string, readSource?: SourceReader): boolean {
|
|
136
|
+
const normalized = id.replace(/\\/g, '/');
|
|
137
|
+
const sourceIndex = normalized.lastIndexOf('/src/main/ets/');
|
|
138
|
+
if (sourceIndex < 0) return false;
|
|
139
|
+
const nodeRequire = require.main && typeof require.main.require === 'function'
|
|
140
|
+
? require.main.require.bind(require.main) : require;
|
|
141
|
+
const path = nodeRequire('path');
|
|
142
|
+
const directory = normalized.slice(0, sourceIndex) + '/src/main';
|
|
143
|
+
try {
|
|
144
|
+
const manifest = directory + '/module.json5';
|
|
145
|
+
const code = readSource ? readSource(manifest) : nodeRequire('fs').readFileSync(manifest, 'utf8');
|
|
146
|
+
if (typeof code !== 'string') return false;
|
|
147
|
+
const source = ts.createSourceFile(manifest, `const __manifest = ${code}`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
148
|
+
if (source.parseDiagnostics.length > 0 || source.statements.length !== 1 ||
|
|
149
|
+
!ts.isVariableStatement(source.statements[0])) return false;
|
|
150
|
+
const root = source.statements[0].declarationList.declarations[0].initializer;
|
|
151
|
+
const module = objectProperty(ts, root, 'module');
|
|
152
|
+
const entries = [objectProperty(ts, module, 'srcEntry')];
|
|
153
|
+
for (const key of ['abilities', 'extensionAbilities']) {
|
|
154
|
+
const list = objectProperty(ts, module, key);
|
|
155
|
+
if (list && ts.isArrayLiteralExpression(list)) {
|
|
156
|
+
list.elements.forEach((item: any) => entries.push(objectProperty(ts, item, 'srcEntry')));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return entries.some(entry => entry && ts.isStringLiteral(entry) && path.resolve(directory, entry.text) === path.resolve(id));
|
|
160
|
+
} catch (_) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
132
165
|
function classifyAbilityBaseName(name: string): AbilityClassKind | null {
|
|
133
166
|
if (name === 'InsightIntentEntryExecutor') {
|
|
134
167
|
return 'intent';
|
|
@@ -504,9 +537,11 @@ function createFirstFrameRetryInjections(ts: TypeScriptApi, sourceFile: any, cod
|
|
|
504
537
|
|
|
505
538
|
function createColdLaunchInjections(ts: TypeScriptApi, sourceFile: any, code: string,
|
|
506
539
|
bridgeName: string, pluginVersion: string, identifiers: Set<string>,
|
|
507
|
-
launchTypes: LaunchParameterTypes, readSource?: SourceReader): ColdLaunchInjections {
|
|
540
|
+
launchTypes: LaunchParameterTypes, readSource?: SourceReader, reportDiagnostic?: SourceDiagnosticReporter): ColdLaunchInjections {
|
|
508
541
|
const injections: ColdLaunchInjection[] = [];
|
|
509
542
|
let hasUIAbility = false;
|
|
543
|
+
let hasAbility = false;
|
|
544
|
+
const declaredEntry = isDeclaredAbilityEntry(ts, sourceFile.fileName, readSource);
|
|
510
545
|
const resolve = createLexicalResolver(ts, sourceFile);
|
|
511
546
|
const resolveClass = createAbilityClassResolver(ts, sourceFile, readSource);
|
|
512
547
|
const pluginVersionArgument = pluginVersion.length > 0 ? `, ${JSON.stringify(pluginVersion)}` : '';
|
|
@@ -555,8 +590,14 @@ function createColdLaunchInjections(ts: TypeScriptApi, sourceFile: any, code: st
|
|
|
555
590
|
const classification = resolveClass(node);
|
|
556
591
|
const kind = classification.kind;
|
|
557
592
|
if (!kind) {
|
|
593
|
+
if (declaredEntry && (node.modifiers || []).some((modifier: any) => modifier.kind === ts.SyntaxKind.DefaultKeyword)) {
|
|
594
|
+
reportSourceDiagnostic(sourceFile, node.getStart(sourceFile), 'unresolved-ability-base',
|
|
595
|
+
'This module entry has no resolvable platform Ability/AbilityStage/InsightIntent base; automatic lifecycle instrumentation was skipped.',
|
|
596
|
+
reportDiagnostic);
|
|
597
|
+
}
|
|
558
598
|
return;
|
|
559
599
|
}
|
|
600
|
+
hasAbility = true;
|
|
560
601
|
if (kind === 'ui') {
|
|
561
602
|
hasUIAbility = true;
|
|
562
603
|
}
|
|
@@ -572,7 +613,30 @@ function createColdLaunchInjections(ts: TypeScriptApi, sourceFile: any, code: st
|
|
|
572
613
|
if (name === 'onBackground') hasOnBackground = true;
|
|
573
614
|
if (name === 'onNewWant') hasOnNewWant = true;
|
|
574
615
|
const member = callableMember(ts, declaration);
|
|
575
|
-
if (!
|
|
616
|
+
if (!name) return;
|
|
617
|
+
const expected = kind === 'intent' ? name === 'onExecute' : kind === 'legacyIntent'
|
|
618
|
+
? ['onExecuteInUIAbilityForegroundMode', 'onExecuteInUIAbilityBackgroundMode'].includes(name)
|
|
619
|
+
: name === 'onCreate' || (kind === 'ui' &&
|
|
620
|
+
['onForeground', 'onBackground', 'onNewWant', 'onWindowStageCreate'].includes(name));
|
|
621
|
+
if (!member) {
|
|
622
|
+
if (expected && !node.members.some((other: any) => memberName(ts, other) === name && callableMember(ts, other))) {
|
|
623
|
+
reportSourceDiagnostic(sourceFile, declaration.getStart(sourceFile), 'unsupported-lifecycle',
|
|
624
|
+
`${name} is not a method or function property with an instrumentable body; its automatic hook was skipped.`, reportDiagnostic);
|
|
625
|
+
}
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (kind === 'ui' && name === 'onCreate' && member.parameters.slice(0, 2).some((parameter: any) =>
|
|
629
|
+
!ts.isIdentifier(parameter.name) || parameter.dotDotDotToken)) {
|
|
630
|
+
reportSourceDiagnostic(sourceFile, declaration.getStart(sourceFile), 'unsupported-lifecycle-parameters',
|
|
631
|
+
'onCreate uses destructured or rest parameters; automatic launch-reason capture was skipped.', reportDiagnostic);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (kind === 'ui' && name === 'onWindowStageCreate' && (member.parameters.length === 0 ||
|
|
635
|
+
!ts.isIdentifier(member.parameters[0].name) || member.parameters[0].dotDotDotToken)) {
|
|
636
|
+
reportSourceDiagnostic(sourceFile, declaration.getStart(sourceFile), 'unsupported-lifecycle-parameters',
|
|
637
|
+
'onWindowStageCreate requires a named WindowStage parameter for automatic first-frame observation.', reportDiagnostic);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
576
640
|
const indentation = methodIndentation(code, declaration);
|
|
577
641
|
const block = ts.isBlock(member.body);
|
|
578
642
|
const position = member.body.getStart(sourceFile) + (block ? 1 : 0);
|
|
@@ -721,7 +785,7 @@ function createColdLaunchInjections(ts: TypeScriptApi, sourceFile: any, code: st
|
|
|
721
785
|
}
|
|
722
786
|
}
|
|
723
787
|
});
|
|
724
|
-
return { injections, hasUIAbility };
|
|
788
|
+
return { injections, hasUIAbility, hasAbility };
|
|
725
789
|
}
|
|
726
790
|
|
|
727
791
|
function applyColdLaunchInjections(code: string, injections: ColdLaunchInjection[]): string {
|
|
@@ -743,7 +807,7 @@ function applyColdLaunchInjections(code: string, injections: ColdLaunchInjection
|
|
|
743
807
|
}
|
|
744
808
|
|
|
745
809
|
export function transformColdLaunchSource(ts: TypeScriptApi, id: string, code: string,
|
|
746
|
-
readSource?: SourceReader): string {
|
|
810
|
+
readSource?: SourceReader, reportDiagnostic?: SourceDiagnosticReporter): string {
|
|
747
811
|
if (!isApplicationArkTsSource(id) || code.includes(COLD_LAUNCH_TRANSFORM_MARKER) ||
|
|
748
812
|
!hasRuntimeDependency(ts, id, '@guancecloud/ft_sdk', readSource)) {
|
|
749
813
|
return code;
|
|
@@ -757,8 +821,13 @@ export function transformColdLaunchSource(ts: TypeScriptApi, id: string, code: s
|
|
|
757
821
|
needed: false
|
|
758
822
|
};
|
|
759
823
|
const version = getPluginVersion();
|
|
824
|
+
const diagnostics: SourceTransformDiagnostic[] = [];
|
|
760
825
|
const result = createColdLaunchInjections(ts, sourceFile, code, bridgeName, version, identifiers,
|
|
761
|
-
launchTypes, readSource);
|
|
826
|
+
launchTypes, readSource, diagnostic => diagnostics.push(diagnostic));
|
|
827
|
+
if (result.hasAbility || diagnostics.length > 0) {
|
|
828
|
+
if (reportSourceParseError(ts, parseApplicationSource(ts, id, code), reportDiagnostic)) return code;
|
|
829
|
+
diagnostics.forEach(diagnostic => reportDiagnostic?.(diagnostic));
|
|
830
|
+
}
|
|
762
831
|
const injections = result.injections;
|
|
763
832
|
const moduleFallback = result.hasUIAbility && needsUIAbilityModuleFallback(ts, id, readSource);
|
|
764
833
|
if (injections.length === 0 && !moduleFallback) {
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* never changed.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { hasRuntimeDependency, SourceReader
|
|
10
|
+
import { hasRuntimeDependency, SourceReader, SourceDiagnosticReporter, parseApplicationSource,
|
|
11
|
+
reportSourceDiagnostic, reportSourceParseError } from './ft-transform-context';
|
|
11
12
|
import { createLexicalResolver } from './ft-lexical-scope';
|
|
12
13
|
import { allocateName, collectIdentifierNames } from './ft-generated-names';
|
|
13
14
|
|
|
@@ -94,16 +95,14 @@ function needsStatementSeparator(ts: TypeScriptApi, node: any): boolean {
|
|
|
94
95
|
ts.isCaseClause(container) || ts.isDefaultClause(container);
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
function collectCandidates(ts: TypeScriptApi, sourceFile: any, code: string
|
|
98
|
+
function collectCandidates(ts: TypeScriptApi, sourceFile: any, code: string,
|
|
99
|
+
unsupported: (node: any, message: string) => void): TransformCandidate[] {
|
|
98
100
|
const candidates: TransformCandidate[] = [];
|
|
99
101
|
const hiLogBindings = collectHiLogBindings(ts, sourceFile);
|
|
100
102
|
const resolve = createLexicalResolver(ts, sourceFile);
|
|
101
103
|
|
|
102
104
|
visit(ts, sourceFile, (node: any) => {
|
|
103
|
-
|
|
104
|
-
// controls argument evaluation. Nested ordinary calls remain eligible.
|
|
105
|
-
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) ||
|
|
106
|
-
(ts.isOptionalChain && ts.isOptionalChain(node))) {
|
|
105
|
+
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) {
|
|
107
106
|
return;
|
|
108
107
|
}
|
|
109
108
|
const delegate = node.expression.expression;
|
|
@@ -112,10 +111,20 @@ function collectCandidates(ts: TypeScriptApi, sourceFile: any, code: string): Tr
|
|
|
112
111
|
}
|
|
113
112
|
const delegateName = delegate.text;
|
|
114
113
|
const level = node.expression.name.text;
|
|
114
|
+
const isConsole = delegateName === 'console' && !resolve(delegate, delegateName) && CONSOLE_LEVELS.includes(level);
|
|
115
|
+
const isHiLog = hiLogBindings.has(delegateName) &&
|
|
116
|
+
resolve(delegate, delegateName) === hiLogBindings.get(delegateName) && HILOG_LEVELS.includes(level);
|
|
117
|
+
if (!isConsole && !isHiLog) return;
|
|
118
|
+
// Preserve short-circuit semantics instead of transforming optional calls.
|
|
119
|
+
if (ts.isOptionalChain && ts.isOptionalChain(node)) {
|
|
120
|
+
unsupported(node, 'Optional Console/HiLog calls are left unchanged and are not automatically collected.');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
115
123
|
const start = node.getStart(sourceFile);
|
|
116
124
|
const end = node.end;
|
|
117
125
|
const calleeEnd = node.expression.end;
|
|
118
126
|
if (start < 0 || end > code.length || calleeEnd < start || calleeEnd > end) {
|
|
127
|
+
unsupported(node, 'The compiler returned an invalid source range for this logging call.');
|
|
119
128
|
return;
|
|
120
129
|
}
|
|
121
130
|
|
|
@@ -125,15 +134,16 @@ function collectCandidates(ts: TypeScriptApi, sourceFile: any, code: string): Tr
|
|
|
125
134
|
spread: ts.isSpreadElement(argument)
|
|
126
135
|
}));
|
|
127
136
|
const statementSeparator = needsStatementSeparator(ts, node);
|
|
128
|
-
if (
|
|
137
|
+
if (isConsole) {
|
|
129
138
|
candidates.push({ start, end, calleeEnd, delegate: delegateName, level, source: 'console', callArguments,
|
|
130
139
|
needsStatementSeparator: statementSeparator });
|
|
131
140
|
return;
|
|
132
141
|
}
|
|
133
|
-
if (
|
|
134
|
-
HILOG_LEVELS.includes(level) && (callArguments.length >= 3 || callArguments.some(argument => argument.spread))) {
|
|
142
|
+
if (callArguments.length >= 3 || callArguments.some(argument => argument.spread)) {
|
|
135
143
|
candidates.push({ start, end, calleeEnd, delegate: delegateName, level, source: 'hilog', callArguments,
|
|
136
144
|
needsStatementSeparator: statementSeparator });
|
|
145
|
+
} else {
|
|
146
|
+
unsupported(node, 'HiLog collection requires domain, tag and format arguments, or a spread argument supplying them.');
|
|
137
147
|
}
|
|
138
148
|
});
|
|
139
149
|
return candidates;
|
|
@@ -210,13 +220,19 @@ function findImportInsertionOffset(ts: TypeScriptApi, sourceFile: any): number {
|
|
|
210
220
|
return offset;
|
|
211
221
|
}
|
|
212
222
|
|
|
213
|
-
export function transformConsoleLogSource(ts: TypeScriptApi, id: string, code: string, readSource?: SourceReader
|
|
223
|
+
export function transformConsoleLogSource(ts: TypeScriptApi, id: string, code: string, readSource?: SourceReader,
|
|
224
|
+
reportDiagnostic?: SourceDiagnosticReporter): string {
|
|
214
225
|
if (!isApplicationArkTsSource(id) || code.includes(TRANSFORM_MARKER) ||
|
|
215
226
|
!hasRuntimeDependency(ts, id, '@guancecloud/ft_sdk', readSource)) {
|
|
216
227
|
return code;
|
|
217
228
|
}
|
|
218
229
|
const sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
219
|
-
const
|
|
230
|
+
const unsupported: Array<{ node: any; message: string }> = [];
|
|
231
|
+
const candidates = collectCandidates(ts, sourceFile, code, (node, message) => unsupported.push({ node, message }));
|
|
232
|
+
if (candidates.length === 0 && unsupported.length === 0) return code;
|
|
233
|
+
if (reportSourceParseError(ts, parseApplicationSource(ts, id, code), reportDiagnostic)) return code;
|
|
234
|
+
unsupported.forEach(item => reportSourceDiagnostic(sourceFile, item.node.getStart(sourceFile),
|
|
235
|
+
'unsupported-log-call', item.message, reportDiagnostic));
|
|
220
236
|
if (candidates.length === 0) {
|
|
221
237
|
return code;
|
|
222
238
|
}
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { isApplicationArkTsSource } from './ft-console-log-transform';
|
|
11
|
-
import { hasRuntimeDependency, SourceReader
|
|
11
|
+
import { hasRuntimeDependency, SourceReader, SourceDiagnosticReporter, parseApplicationSource,
|
|
12
|
+
reportSourceDiagnostic, reportSourceParseError } from './ft-transform-context';
|
|
12
13
|
import { allocateName as selectUniqueName } from './ft-generated-names';
|
|
13
14
|
|
|
14
15
|
type TypeScriptApi = any;
|
|
@@ -35,6 +36,7 @@ interface RootExpression {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
interface EntryStruct {
|
|
39
|
+
node: any;
|
|
38
40
|
bodyEnd: number;
|
|
39
41
|
methods: Map<string, MethodRange>;
|
|
40
42
|
}
|
|
@@ -86,6 +88,19 @@ function findImportInsertionOffset(ts: TypeScriptApi, id: string, code: string):
|
|
|
86
88
|
return offset;
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
function findEntryDecorator(ts: TypeScriptApi, source: any): any {
|
|
92
|
+
let entry: any;
|
|
93
|
+
const visit = (node: any): void => {
|
|
94
|
+
if (ts.isDecorator(node)) {
|
|
95
|
+
const expression = ts.isCallExpression(node.expression) ? node.expression.expression : node.expression;
|
|
96
|
+
if (ts.isIdentifier(expression) && expression.text === 'Entry') entry ??= node;
|
|
97
|
+
}
|
|
98
|
+
ts.forEachChild(node, visit);
|
|
99
|
+
};
|
|
100
|
+
visit(source);
|
|
101
|
+
return entry;
|
|
102
|
+
}
|
|
103
|
+
|
|
89
104
|
/** Structs, decorators and members must come from the ArkTS parser, never text matches. */
|
|
90
105
|
function collectEntryStructs(ts: TypeScriptApi, source: any): EntryStruct[] {
|
|
91
106
|
const entries: EntryStruct[] = [];
|
|
@@ -106,7 +121,7 @@ function collectEntryStructs(ts: TypeScriptApi, source: any): EntryStruct[] {
|
|
|
106
121
|
bodyStart: member.body.getStart(source), bodyEnd: member.body.end - 1 });
|
|
107
122
|
}
|
|
108
123
|
});
|
|
109
|
-
entries.push({ bodyEnd: node.end - 1, methods });
|
|
124
|
+
entries.push({ node, bodyEnd: node.end - 1, methods });
|
|
110
125
|
});
|
|
111
126
|
return entries;
|
|
112
127
|
}
|
|
@@ -250,33 +265,31 @@ function addLifecycleInjection(
|
|
|
250
265
|
* Adds an opaque root ID and page lifecycle registration to every @Entry page.
|
|
251
266
|
* ArkTS nodes locate real decorators, struct members and component expressions.
|
|
252
267
|
*/
|
|
253
|
-
export function transformSessionReplaySource(ts: TypeScriptApi, id: string, code: string, readSource?: SourceReader
|
|
268
|
+
export function transformSessionReplaySource(ts: TypeScriptApi, id: string, code: string, readSource?: SourceReader,
|
|
269
|
+
reportDiagnostic?: SourceDiagnosticReporter): string {
|
|
254
270
|
if (!isApplicationArkTsSource(id) || code.includes(REPLAY_ROOT_TRANSFORM_MARKER) ||
|
|
255
271
|
!hasRuntimeDependency(ts, id, '@guancecloud/ft_sdk', readSource) ||
|
|
256
272
|
!hasRuntimeDependency(ts, id, REPLAY_BRIDGE_IMPORT, readSource)) {
|
|
257
273
|
return code;
|
|
258
274
|
}
|
|
259
275
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
concurrent: { decorator: 'Concurrent' },
|
|
272
|
-
customComponent: 'CustomComponent',
|
|
273
|
-
syntaxComponents: { paramsUICallback: ['ForEach', 'LazyForEach'], attrUICallback: [{ name: 'Repeat', attributes: ['each', 'template'] }] }
|
|
274
|
-
} });
|
|
275
|
-
if (source.parseDiagnostics.length > 0) return code;
|
|
276
|
-
const entries = collectEntryStructs(ts, source);
|
|
277
|
-
if (entries.length === 0) {
|
|
276
|
+
const preliminarySource = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true,
|
|
277
|
+
ts.ScriptKind.ETS ?? ts.ScriptKind.TS);
|
|
278
|
+
const report = (source: any, position: number, reason: string, message: string): void => {
|
|
279
|
+
reportSourceDiagnostic(source, position, reason, message, reportDiagnostic);
|
|
280
|
+
};
|
|
281
|
+
if (typeof ts.isStructDeclaration !== 'function' || typeof ts.isEtsComponentExpression !== 'function') {
|
|
282
|
+
// Even a standard TypeScript parser retains real decorator nodes. Do not
|
|
283
|
+
// mistake @Entry text inside comments or strings for a page needing Replay.
|
|
284
|
+
const entryDecorator = findEntryDecorator(ts, preliminarySource);
|
|
285
|
+
if (entryDecorator) report(preliminarySource, entryDecorator.getStart(preliminarySource), 'unsupported-parser',
|
|
286
|
+
'The installed compiler does not expose the ArkTS struct/component parser required for automatic Replay roots.');
|
|
278
287
|
return code;
|
|
279
288
|
}
|
|
289
|
+
const source = parseApplicationSource(ts, id, code, preliminarySource);
|
|
290
|
+
const entries = collectEntryStructs(ts, source);
|
|
291
|
+
if (entries.length === 0 && !findEntryDecorator(ts, source)) return code;
|
|
292
|
+
if (reportSourceParseError(ts, source, reportDiagnostic)) return code;
|
|
280
293
|
|
|
281
294
|
const identifiers = collectIdentifiers(code);
|
|
282
295
|
const bridgeName = selectUniqueName(identifiers, '__ftSessionReplayAutoRootBridge');
|
|
@@ -287,10 +300,15 @@ export function transformSessionReplaySource(ts: TypeScriptApi, id: string, code
|
|
|
287
300
|
entries.forEach((entry: EntryStruct): void => {
|
|
288
301
|
const buildMethod = entry.methods.get('build');
|
|
289
302
|
if (!buildMethod) {
|
|
303
|
+
report(source, entry.node.name.getStart(source), 'missing-build',
|
|
304
|
+
'The @Entry page has no build() method with a body to instrument.');
|
|
290
305
|
return;
|
|
291
306
|
}
|
|
292
307
|
const root = findRootExpression(ts, source, code, buildMethod);
|
|
293
308
|
if (root === null) {
|
|
309
|
+
const first = buildMethod.node.body.statements[0];
|
|
310
|
+
report(source, first ? first.getStart(source) : buildMethod.bodyStart + 1, 'unsupported-root',
|
|
311
|
+
'build() must start with a supported ArkUI root component with a child block for automatic Replay registration.');
|
|
294
312
|
return;
|
|
295
313
|
}
|
|
296
314
|
const indentation = lineIndentation(code, buildMethod.nameStart);
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { transformColdLaunchSource } from './ft-cold-launch-transform';
|
|
15
15
|
import { transformSessionReplaySource } from './ft-session-replay-transform';
|
|
16
16
|
import { transformWebReplaySource } from './ft-web-replay-transform';
|
|
17
|
+
import { SourceDiagnosticReporter, SourceTransformDiagnostic } from './ft-transform-context';
|
|
17
18
|
|
|
18
19
|
declare const require: {
|
|
19
20
|
main: {
|
|
@@ -23,7 +24,8 @@ declare const require: {
|
|
|
23
24
|
|
|
24
25
|
type TypeScriptApi = any;
|
|
25
26
|
type SourceReader = (fileName: string) => string | undefined;
|
|
26
|
-
type SourceTransform = (ts: TypeScriptApi, fileName: string, code: string, readSource?: SourceReader
|
|
27
|
+
type SourceTransform = (ts: TypeScriptApi, fileName: string, code: string, readSource?: SourceReader,
|
|
28
|
+
reportDiagnostic?: SourceDiagnosticReporter) => string;
|
|
27
29
|
|
|
28
30
|
interface CompilePluginContext {
|
|
29
31
|
share: {
|
|
@@ -35,6 +37,8 @@ interface CompilePluginContext {
|
|
|
35
37
|
|
|
36
38
|
interface SourceTransformDefinition {
|
|
37
39
|
id: string;
|
|
40
|
+
prefix: string;
|
|
41
|
+
feature: string;
|
|
38
42
|
transform: SourceTransform;
|
|
39
43
|
}
|
|
40
44
|
|
|
@@ -43,14 +47,15 @@ interface SharedSourceTransformRegistry {
|
|
|
43
47
|
buffer: any;
|
|
44
48
|
transforms: Map<string, (fileName: string, code: string) => string>;
|
|
45
49
|
owners: Map<object, Map<string, (fileName: string, code: string) => string>>;
|
|
50
|
+
reportedDiagnostics: Set<string>;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
const SOURCE_TRANSFORM_REGISTRY_KEY = '__guanceHvigorSourceTransformRegistry';
|
|
49
54
|
const SOURCE_TRANSFORMS: SourceTransformDefinition[] = [
|
|
50
|
-
{ id: 'guance-console-log-transform', transform: transformConsoleLogSource },
|
|
51
|
-
{ id: 'guance-cold-launch-transform', transform: transformColdLaunchSource },
|
|
52
|
-
{ id: 'guance-session-replay-root-transform', transform: transformSessionReplaySource },
|
|
53
|
-
{ id: 'guance-web-replay-transform', transform: transformWebReplaySource }
|
|
55
|
+
{ id: 'guance-console-log-transform', prefix: 'GuanceLog', feature: 'console-log', transform: transformConsoleLogSource },
|
|
56
|
+
{ id: 'guance-cold-launch-transform', prefix: 'GuanceColdLaunch', feature: 'cold-launch', transform: transformColdLaunchSource },
|
|
57
|
+
{ id: 'guance-session-replay-root-transform', prefix: 'GuanceReplay', feature: 'session-replay-root', transform: transformSessionReplaySource },
|
|
58
|
+
{ id: 'guance-web-replay-transform', prefix: 'GuanceWebReplay', feature: 'web-replay', transform: transformWebReplaySource }
|
|
54
59
|
];
|
|
55
60
|
|
|
56
61
|
const registrations: Array<{ fileSystem: any; owner: object; context: CompilePluginContext }> = [];
|
|
@@ -60,6 +65,44 @@ function refreshTransforms(registry: SharedSourceTransformRegistry): void {
|
|
|
60
65
|
registry.owners.forEach(transforms => transforms.forEach((transform, id) => registry.transforms.set(id, transform)));
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
function createDiagnosticReporter(registry: SharedSourceTransformRegistry, ts: TypeScriptApi,
|
|
69
|
+
readSource: SourceReader, definition: SourceTransformDefinition):
|
|
70
|
+
(diagnostic: SourceTransformDiagnostic, inputFile: string, inputCode: string, occurrence?: number) => void {
|
|
71
|
+
let logger: { warn(message: string): void } | undefined;
|
|
72
|
+
return (diagnostic: SourceTransformDiagnostic, inputFile: string, inputCode: string, occurrence = 0): void => {
|
|
73
|
+
// Earlier transforms can insert imports or wrappers. Recheck only failed
|
|
74
|
+
// pages against the on-disk source so clickable locations refer to app code.
|
|
75
|
+
const originalCode = readSource(diagnostic.fileName);
|
|
76
|
+
if (diagnostic.reason !== 'transform-exception' && diagnostic.fileName === inputFile &&
|
|
77
|
+
originalCode !== undefined && originalCode !== inputCode) {
|
|
78
|
+
const originalDiagnostics: SourceTransformDiagnostic[] = [];
|
|
79
|
+
try {
|
|
80
|
+
definition.transform(ts, diagnostic.fileName, originalCode, readSource, candidate => {
|
|
81
|
+
if (candidate.reason === diagnostic.reason &&
|
|
82
|
+
(candidate.message === diagnostic.message || candidate.reason === 'parse-error')) originalDiagnostics.push(candidate);
|
|
83
|
+
});
|
|
84
|
+
} catch (_) {
|
|
85
|
+
// Location mapping is diagnostic-only and must not change build behavior.
|
|
86
|
+
}
|
|
87
|
+
diagnostic = originalDiagnostics[occurrence] ?? { ...diagnostic,
|
|
88
|
+
message: `${diagnostic.message} (Location refers to in-memory transformed source.)` };
|
|
89
|
+
}
|
|
90
|
+
const key = `${definition.id}:${JSON.stringify(diagnostic)}`;
|
|
91
|
+
if (registry.reportedDiagnostics.has(key)) return;
|
|
92
|
+
registry.reportedDiagnostics.add(key);
|
|
93
|
+
const message = `[GuanceBuild][${definition.prefix}] ${diagnostic.fileName}:${diagnostic.line}:${diagnostic.column}: ` +
|
|
94
|
+
`feature=${definition.feature}; reason=${diagnostic.reason}; ${diagnostic.message}`;
|
|
95
|
+
if (!logger) {
|
|
96
|
+
try {
|
|
97
|
+
logger = require.main.require('@ohos/hvigor').HvigorLogger.getLogger(definition.prefix);
|
|
98
|
+
} catch (_) {
|
|
99
|
+
logger = { warn: (value: string): void => console.warn(value) };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
logger!.warn(message);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
63
106
|
function installSourceReader(context: CompilePluginContext): void {
|
|
64
107
|
const typeScriptPath = `${context.share.projectConfig.etsLoaderPath}/node_modules/typescript`;
|
|
65
108
|
const ts = require.main.require(typeScriptPath);
|
|
@@ -70,7 +113,8 @@ function installSourceReader(context: CompilePluginContext): void {
|
|
|
70
113
|
originalReadFile: fileSystem.readFileSync as (fileName: string, options?: any) => any,
|
|
71
114
|
buffer: require.main.require('buffer'),
|
|
72
115
|
transforms: new Map<string, (fileName: string, code: string) => string>(),
|
|
73
|
-
owners: new Map()
|
|
116
|
+
owners: new Map(),
|
|
117
|
+
reportedDiagnostics: new Set()
|
|
74
118
|
};
|
|
75
119
|
fileSystem[SOURCE_TRANSFORM_REGISTRY_KEY] = registry;
|
|
76
120
|
fileSystem.readFileSync = (fileName: string, options?: any): any => {
|
|
@@ -93,11 +137,33 @@ function installSourceReader(context: CompilePluginContext): void {
|
|
|
93
137
|
return undefined;
|
|
94
138
|
}
|
|
95
139
|
};
|
|
140
|
+
// A daemon may retain a registry installed by an earlier plugin version.
|
|
141
|
+
registry.reportedDiagnostics ??= new Set();
|
|
96
142
|
const owner = {};
|
|
97
143
|
const transforms = new Map<string, (fileName: string, code: string) => string>();
|
|
98
144
|
SOURCE_TRANSFORMS.forEach((definition: SourceTransformDefinition) => {
|
|
99
|
-
|
|
100
|
-
|
|
145
|
+
const reportDiagnostic = createDiagnosticReporter(registry!, ts, readSource, definition);
|
|
146
|
+
transforms.set(definition.id, (fileName: string, code: string) => {
|
|
147
|
+
const occurrences = new Map<string, number>();
|
|
148
|
+
try {
|
|
149
|
+
return definition.transform(ts, fileName, code, readSource,
|
|
150
|
+
diagnostic => {
|
|
151
|
+
const key = `${diagnostic.reason}:${diagnostic.message}`;
|
|
152
|
+
const occurrence = occurrences.get(key) ?? 0;
|
|
153
|
+
occurrences.set(key, occurrence + 1);
|
|
154
|
+
reportDiagnostic(diagnostic, fileName, code, occurrence);
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
try {
|
|
158
|
+
reportDiagnostic({ fileName, line: 1, column: 1, reason: 'transform-exception',
|
|
159
|
+
message: `Source transform failed: ${error instanceof Error ? error.message : String(error)}` }, fileName, code);
|
|
160
|
+
} finally {
|
|
161
|
+
// Preserve the original failure: diagnostics must never turn a broken
|
|
162
|
+
// transform into a successful build or replace its exception.
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
101
167
|
});
|
|
102
168
|
registry.owners.set(owner, transforms);
|
|
103
169
|
refreshTransforms(registry);
|
|
@@ -3,6 +3,61 @@ declare const require: any;
|
|
|
3
3
|
|
|
4
4
|
export type SourceReader = (fileName: string) => string | undefined;
|
|
5
5
|
|
|
6
|
+
export interface SourceTransformDiagnostic {
|
|
7
|
+
fileName: string;
|
|
8
|
+
line: number;
|
|
9
|
+
column: number;
|
|
10
|
+
reason: string;
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type SourceDiagnosticReporter = (diagnostic: SourceTransformDiagnostic) => void;
|
|
15
|
+
|
|
16
|
+
export function reportSourceDiagnostic(source: any, position: number, reason: string, message: string,
|
|
17
|
+
reporter?: SourceDiagnosticReporter): void {
|
|
18
|
+
if (!reporter) return;
|
|
19
|
+
const location = source.getLineAndCharacterOfPosition(position);
|
|
20
|
+
reporter({ fileName: source.fileName, line: location.line + 1, column: location.character + 1, reason, message });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Parse declarative child blocks without treating enum members as components. */
|
|
24
|
+
export function parseApplicationSource(ts: any, id: string, code: string, preliminary?: any): any {
|
|
25
|
+
const source = preliminary ?? ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true,
|
|
26
|
+
ts.ScriptKind.ETS ?? ts.ScriptKind.TS);
|
|
27
|
+
if (!id.endsWith('.ets') || typeof ts.isStructDeclaration !== 'function' ||
|
|
28
|
+
typeof ts.isEtsComponentExpression !== 'function') return source;
|
|
29
|
+
const names = new Set<string>();
|
|
30
|
+
const visit = (node: any): void => {
|
|
31
|
+
if ((ts.isCallExpression(node) || ts.isEtsComponentExpression(node)) && ts.isIdentifier(node.expression)) {
|
|
32
|
+
names.add(node.expression.text);
|
|
33
|
+
}
|
|
34
|
+
ts.forEachChild(node, visit);
|
|
35
|
+
};
|
|
36
|
+
visit(source);
|
|
37
|
+
const components = Array.from(names).filter(name => !['ForEach', 'LazyForEach', 'Repeat'].includes(name));
|
|
38
|
+
return ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.ETS, { ets: {
|
|
39
|
+
components,
|
|
40
|
+
render: { method: ['build', 'pageTransition'], decorator: ['Builder', 'LocalBuilder'] },
|
|
41
|
+
extend: { decorator: ['Extend', 'AnimatableExtend'], components: components.map(name =>
|
|
42
|
+
({ name, type: `${name}Attribute`, instance: `${name}Instance` })) },
|
|
43
|
+
styles: { decorator: 'Styles', component: { name: 'Common', type: 'T', instance: 'CommonInstance' }, property: 'stateStyles' },
|
|
44
|
+
concurrent: { decorator: 'Concurrent' },
|
|
45
|
+
customComponent: 'CustomComponent',
|
|
46
|
+
syntaxComponents: { paramsUICallback: ['ForEach', 'LazyForEach'],
|
|
47
|
+
attrUICallback: [{ name: 'Repeat', attributes: ['each', 'template'] }] }
|
|
48
|
+
} });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function reportSourceParseError(ts: any, source: any, reporter?: SourceDiagnosticReporter): boolean {
|
|
52
|
+
if (source.parseDiagnostics.length === 0) return false;
|
|
53
|
+
const diagnostic = source.parseDiagnostics[0];
|
|
54
|
+
const remaining = source.parseDiagnostics.length - 1;
|
|
55
|
+
reportSourceDiagnostic(source, diagnostic.start ?? 0, 'parse-error',
|
|
56
|
+
`TS${diagnostic.code}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, ' ')}${
|
|
57
|
+
remaining > 0 ? ` (${remaining} additional parse errors)` : ''}`, reporter);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
6
61
|
export function objectProperty(ts: any, object: any, name: string): any | undefined {
|
|
7
62
|
if (!object || !ts.isObjectLiteralExpression(object)) return undefined;
|
|
8
63
|
const matches = object.properties.filter((item: any) => ts.isPropertyAssignment(item) &&
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Associate every native Web with its controller without replacing application IDs or callbacks. */
|
|
2
2
|
import { isApplicationArkTsSource } from './ft-console-log-transform';
|
|
3
|
-
import { hasRuntimeDependency, SourceReader
|
|
3
|
+
import { hasRuntimeDependency, SourceReader, SourceDiagnosticReporter, parseApplicationSource,
|
|
4
|
+
reportSourceDiagnostic, reportSourceParseError } from './ft-transform-context';
|
|
4
5
|
import { createLexicalResolver } from './ft-lexical-scope';
|
|
5
6
|
import { allocateName, collectIdentifierNames } from './ft-generated-names';
|
|
6
7
|
|
|
@@ -10,7 +11,8 @@ interface Injection { position: number; content: string; }
|
|
|
10
11
|
const MARKER = '/* guance-web-replay-transform:instrumented */';
|
|
11
12
|
const SDK_IMPORT = '@guancecloud/ft_sdk/Index';
|
|
12
13
|
|
|
13
|
-
export function transformWebReplaySource(ts: TypeScriptApi, id: string, code: string, readSource?: SourceReader
|
|
14
|
+
export function transformWebReplaySource(ts: TypeScriptApi, id: string, code: string, readSource?: SourceReader,
|
|
15
|
+
reportDiagnostic?: SourceDiagnosticReporter): string {
|
|
14
16
|
if (!isApplicationArkTsSource(id) || code.includes(MARKER) ||
|
|
15
17
|
!hasRuntimeDependency(ts, id, '@guancecloud/ft_sdk', readSource) ||
|
|
16
18
|
!hasRuntimeDependency(ts, id, '@guancecloud/ft_session_replay', readSource)) return code;
|
|
@@ -18,16 +20,24 @@ export function transformWebReplaySource(ts: TypeScriptApi, id: string, code: st
|
|
|
18
20
|
const source = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.ETS ?? ts.ScriptKind.TS);
|
|
19
21
|
const identifiers = collectIdentifierNames(ts, source);
|
|
20
22
|
const constructors: any[] = [];
|
|
23
|
+
const unsupported: any[] = [];
|
|
21
24
|
const resolve = createLexicalResolver(ts, source);
|
|
22
25
|
let importEnd = 0;
|
|
23
26
|
const visit = (node: any): void => {
|
|
24
27
|
if (ts.isImportDeclaration(node)) importEnd = Math.max(importEnd, node.end);
|
|
25
28
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'Web' &&
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
!resolve(node.expression, 'Web')) {
|
|
30
|
+
if (node.arguments.length === 1 && !ts.isSpreadElement(node.arguments[0]) &&
|
|
31
|
+
!(ts.isOptionalChain && ts.isOptionalChain(node))) constructors.push(node);
|
|
32
|
+
else unsupported.push(node);
|
|
33
|
+
}
|
|
28
34
|
ts.forEachChild(node, visit);
|
|
29
35
|
};
|
|
30
36
|
visit(source);
|
|
37
|
+
if (constructors.length === 0 && unsupported.length === 0) return code;
|
|
38
|
+
if (reportSourceParseError(ts, parseApplicationSource(ts, id, code), reportDiagnostic)) return code;
|
|
39
|
+
unsupported.forEach(node => reportSourceDiagnostic(source, node.getStart(source), 'unsupported-web-call',
|
|
40
|
+
'Automatic ArkWeb binding requires a non-optional Web(options) call with exactly one non-spread argument.', reportDiagnostic));
|
|
31
41
|
if (constructors.length === 0) return code;
|
|
32
42
|
|
|
33
43
|
const bridge = allocateName(identifiers, '__ftWebReplayBridge');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudcare/hvigor-ohos-plugin",
|
|
3
|
-
"version": "0.1.2-alpha.
|
|
3
|
+
"version": "0.1.2-alpha.2",
|
|
4
4
|
"description": "Guance HarmonyOS Hvigor plugin for console, cold-launch, Session Replay page roots, and Web controller binding.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|