Skip to content

Commit ed159ed

Browse files
committed
feat: add generator plugin for endpoints returning signals
Fixes #2628
1 parent 03f9ed0 commit ed159ed

11 files changed

+308
-174
lines changed

packages/ts/generator-core/src/PluginManager.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export default class PluginManager {
1515
'ModelPlugin',
1616
'PushPlugin',
1717
'SubTypesPlugin',
18+
'SignalsPlugin',
1819
];
1920
const customPlugins = plugins.filter((p) => !standardPlugins.includes(p.name));
2021
if (customPlugins.length > 0) {

packages/ts/generator-plugin-signals/package.json

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@vaadin/hilla-generator-plugin-signals",
3-
"version": "24.5.0-alpha1",
3+
"version": "24.5.0-alpha5",
44
"description": "A Hilla TypeScript Generator plugin to add Shared Signals support",
55
"main": "index.js",
66
"type": "module",
@@ -51,24 +51,24 @@
5151
"access": "public"
5252
},
5353
"peerDependencies": {
54-
"@vaadin/hilla-generator-plugin-client": "24.5.0-alpha1"
54+
"@vaadin/hilla-generator-plugin-client": "24.5.0-alpha5"
5555
},
5656
"dependencies": {
57-
"@vaadin/hilla-generator-core": "24.5.0-alpha1",
58-
"@vaadin/hilla-generator-plugin-backbone": "^24.5.0-alpha1",
59-
"@vaadin/hilla-generator-utils": "24.5.0-alpha1",
57+
"@vaadin/hilla-generator-core": "24.5.0-alpha5",
58+
"@vaadin/hilla-generator-plugin-backbone": "^24.5.0-alpha5",
59+
"@vaadin/hilla-generator-utils": "24.5.0-alpha5",
6060
"fast-deep-equal": "^3.1.3",
6161
"openapi-types": "^12.1.3",
62-
"typescript": "5.3.2"
62+
"typescript": "5.5.2"
6363
},
6464
"devDependencies": {
6565
"@types/chai": "^4.3.6",
6666
"@types/mocha": "^10.0.2",
6767
"@types/node": "^20.7.1",
6868
"@types/sinon": "^10.0.17",
6969
"@types/sinon-chai": "^3.2.10",
70-
"@vaadin/hilla-generator-core": "24.5.0-alpha1",
71-
"@vaadin/hilla-generator-plugin-client": "24.5.0-alpha1",
70+
"@vaadin/hilla-generator-core": "24.5.0-alpha5",
71+
"@vaadin/hilla-generator-plugin-client": "24.5.0-alpha5",
7272
"c8": "^8.0.1",
7373
"chai": "^4.3.10",
7474
"concurrently": "^8.2.1",

packages/ts/generator-plugin-signals/src/SignalProcessor.ts

+45-38
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,20 @@ import { template, transform } from '@vaadin/hilla-generator-utils/ast.js';
33
import createSourceFile from '@vaadin/hilla-generator-utils/createSourceFile.js';
44
import DependencyManager from '@vaadin/hilla-generator-utils/dependencies/DependencyManager.js';
55
import PathManager from '@vaadin/hilla-generator-utils/dependencies/PathManager.js';
6-
import ts, { type CallExpression, type FunctionDeclaration, type ReturnStatement, type SourceFile } from 'typescript';
6+
import ts, { type FunctionDeclaration, type SourceFile } from 'typescript';
77

88
export type MethodInfo = Readonly<{
99
name: string;
1010
signalType: string;
1111
}>;
1212

13-
const ENDPOINT_CALL_EXPRESSION = '$ENDPOINT_CALL_EXPRESSION$';
14-
const NUMBER_SIGNAL_QUEUE = '$NUMBER_SIGNAL_QUEUE$';
15-
const SIGNALS_HANDLER = '$SIGNALS_HANDLER$';
16-
const CONNECT_CLIENT = '$CONNECT_CLIENT$';
1713
const HILLA_REACT_SIGNALS = '@vaadin/hilla-react-signals';
18-
const ENDPOINTS = 'Frontend/generated/endpoints.js';
14+
15+
const NUMBER_SIGNAL_CHANNEL = '$NUMBER_SIGNAL_CHANNEL$';
16+
const CONNECT_CLIENT = '$CONNECT_CLIENT$';
17+
const ENDPOINT_AND_METHOD_NAME = '$ENDPOINT_AND_METHOD_NAME$';
18+
19+
const signalImportPaths = ['com/vaadin/hilla/signals/NumberSignal'];
1920

2021
export default class SignalProcessor {
2122
readonly #dependencyManager: DependencyManager;
@@ -36,55 +37,59 @@ export default class SignalProcessor {
3637
process(): SourceFile {
3738
this.#owner.logger.debug(`Processing signals: ${this.#service}`);
3839
const { imports } = this.#dependencyManager;
39-
const numberSignalQueueId = imports.named.add(HILLA_REACT_SIGNALS, 'NumberSignalQueue');
40-
const signalHandlerId = imports.named.add(ENDPOINTS, 'SignalsHandler');
40+
const numberSignalChannelId = imports.named.add(HILLA_REACT_SIGNALS, 'NumberSignalChannel');
4141

4242
const [_p, _isType, connectClientId] = imports.default.find((p) => p.includes('connect-client'))!;
4343

44-
this.#processNumberSignalImport('com/vaadin/hilla/signals/NumberSignal');
44+
this.#processSignalImports(signalImportPaths);
4545

4646
const [file] = ts.transform<SourceFile>(this.#sourceFile, [
4747
...this.#methods.map((method) =>
48-
transform<SourceFile>((node) => {
49-
if (ts.isFunctionDeclaration(node) && node.name?.text === method.name) {
50-
const callExpression = (node.body?.statements[0] as ReturnStatement).expression as CallExpression;
48+
transform<SourceFile>((tsNode) => {
49+
if (ts.isFunctionDeclaration(tsNode) && tsNode.name?.text === method.name) {
50+
const endpointAndMethodName = ts.factory.createStringLiteral(`${this.#service}.${method.name}`);
5151
const body = template(
5252
`
5353
function dummy() {
54-
const sharedSignal = await ${ENDPOINT_CALL_EXPRESSION};
55-
const queueDescriptor = {
56-
id: sharedSignal.id,
57-
subscribe: ${SIGNALS_HANDLER}.subscribe,
58-
publish: ${SIGNALS_HANDLER}.update,
59-
};
60-
const valueLog = new ${NUMBER_SIGNAL_QUEUE}(queueDescriptor, ${CONNECT_CLIENT});
61-
return valueLog.getRoot();
54+
return new ${NUMBER_SIGNAL_CHANNEL}(${ENDPOINT_AND_METHOD_NAME}, ${CONNECT_CLIENT}).signal;
6255
}`,
6356
(statements) => (statements[0] as FunctionDeclaration).body?.statements,
6457
[
6558
transform((node) =>
66-
ts.isIdentifier(node) && node.text === ENDPOINT_CALL_EXPRESSION ? callExpression : node,
59+
ts.isIdentifier(node) && node.text === ENDPOINT_AND_METHOD_NAME ? endpointAndMethodName : node,
6760
),
6861
transform((node) =>
69-
ts.isIdentifier(node) && node.text === NUMBER_SIGNAL_QUEUE ? numberSignalQueueId : node,
62+
ts.isIdentifier(node) && node.text === NUMBER_SIGNAL_CHANNEL ? numberSignalChannelId : node,
7063
),
71-
transform((node) => (ts.isIdentifier(node) && node.text === SIGNALS_HANDLER ? signalHandlerId : node)),
7264
transform((node) => (ts.isIdentifier(node) && node.text === CONNECT_CLIENT ? connectClientId : node)),
7365
],
7466
);
7567

68+
let returnType = tsNode.type;
69+
if (
70+
returnType &&
71+
ts.isTypeReferenceNode(returnType) &&
72+
'text' in returnType.typeName &&
73+
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
74+
returnType.typeName.escapedText === 'Promise'
75+
) {
76+
if (returnType.typeArguments && returnType.typeArguments.length > 0) {
77+
returnType = returnType.typeArguments[0];
78+
}
79+
}
80+
7681
return ts.factory.createFunctionDeclaration(
77-
node.modifiers,
78-
node.asteriskToken,
79-
node.name,
80-
node.typeParameters,
81-
node.parameters,
82-
node.type,
83-
ts.factory.createBlock(body ?? [], true),
82+
tsNode.modifiers?.filter((modifier) => modifier.kind !== ts.SyntaxKind.AsyncKeyword),
83+
tsNode.asteriskToken,
84+
tsNode.name,
85+
tsNode.typeParameters,
86+
[],
87+
returnType,
88+
ts.factory.createBlock(body ?? [], false),
8489
);
8590
}
8691

87-
return node;
92+
return tsNode;
8893
}),
8994
),
9095
]).transformed;
@@ -98,15 +103,17 @@ function dummy() {
98103
);
99104
}
100105

101-
#processNumberSignalImport(path: string) {
106+
#processSignalImports(signalImports: string[]) {
102107
const { imports } = this.#dependencyManager;
103108

104-
const result = imports.default.find((p) => p.includes(path));
109+
signalImports.forEach((signalImport) => {
110+
const result = imports.default.find((p) => p.includes(signalImport));
105111

106-
if (result) {
107-
const [path, _, id] = result;
108-
imports.default.remove(path);
109-
imports.named.add(HILLA_REACT_SIGNALS, id.text, true, id);
110-
}
112+
if (result) {
113+
const [path, _, id] = result;
114+
imports.default.remove(path);
115+
imports.named.add(HILLA_REACT_SIGNALS, id.text, true, id);
116+
}
117+
});
111118
}
112119
}

packages/ts/generator-plugin-signals/src/index.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import type SharedStorage from '@vaadin/hilla-generator-core/SharedStorage.js';
21
import Plugin from '@vaadin/hilla-generator-core/Plugin.js';
2+
import type SharedStorage from '@vaadin/hilla-generator-core/SharedStorage.js';
33
import SignalProcessor, { type MethodInfo } from './SignalProcessor.js';
44

55
export type PathSignalType = Readonly<{
66
path: string;
77
signalType: string;
88
}>;
99

10+
const SIGNAL_CLASSES = ['#/components/schemas/com.vaadin.hilla.signals.NumberSignal'];
11+
1012
function extractEndpointMethodsWithSignalsAsReturnType(storage: SharedStorage): PathSignalType[] {
1113
const pathSignalTypes: PathSignalType[] = [];
1214
Object.entries(storage.api.paths).forEach(([path, pathObject]) => {
@@ -16,8 +18,9 @@ function extractEndpointMethodsWithSignalsAsReturnType(storage: SharedStorage):
1618
const responseSchema = response200.content?.['application/json'].schema;
1719
if (responseSchema && 'anyOf' in responseSchema) {
1820
// OpenAPIV3.SchemaObject
21+
// eslint-disable-next-line array-callback-return
1922
responseSchema.anyOf?.some((c) => {
20-
const isSignal = '$ref' in c && c.$ref && SignalsPlugin.SIGNAL_CLASSES.includes(c.$ref);
23+
const isSignal = '$ref' in c && c.$ref && SIGNAL_CLASSES.includes(c.$ref);
2124
if (isSignal) {
2225
pathSignalTypes.push({ path, signalType: c.$ref });
2326
}
@@ -48,13 +51,12 @@ function groupByService(signals: PathSignalType[]): Map<string, MethodInfo[]> {
4851
}
4952

5053
export default class SignalsPlugin extends Plugin {
51-
static readonly SIGNAL_CLASSES = ['#/components/schemas/com.vaadin.hilla.signals.NumberSignal'];
52-
54+
// eslint-disable-next-line @typescript-eslint/require-await
5355
override async execute(sharedStorage: SharedStorage): Promise<void> {
5456
const methodsWithSignals = extractEndpointMethodsWithSignalsAsReturnType(sharedStorage);
5557
const services = groupByService(methodsWithSignals);
5658
services.forEach((methods, service) => {
57-
let index = sharedStorage.sources.findIndex((source) => source.fileName === `${service}.ts`);
59+
const index = sharedStorage.sources.findIndex((source) => source.fileName === `${service}.ts`);
5860
if (index >= 0) {
5961
sharedStorage.sources[index] = new SignalProcessor(
6062
service,
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,52 @@
11
import { readFile } from 'node:fs/promises';
22
import Generator from '@vaadin/hilla-generator-core/Generator.js';
3+
import BackbonePlugin from '@vaadin/hilla-generator-plugin-backbone';
34
import LoggerFactory from '@vaadin/hilla-generator-utils/LoggerFactory.js';
45
import snapshotMatcher from '@vaadin/hilla-generator-utils/testing/snapshotMatcher.js';
56
import { expect, use } from 'chai';
67
import sinonChai from 'sinon-chai';
78
import SignalsPlugin from '../src/index.js';
8-
import BackbonePlugin from '@vaadin/hilla-generator-plugin-backbone';
99

1010
use(sinonChai);
1111
use(snapshotMatcher);
1212

1313
describe('SignalsPlugin', () => {
1414
context('Endpoint methods with Signals as return type', () => {
15-
it('correctly generates service wrapper', async () => {
15+
it('correctly generates service with mixture of normal and signal returning methods', async () => {
16+
const generator = new Generator([BackbonePlugin, SignalsPlugin], {
17+
logger: new LoggerFactory({ name: 'model-plugin-test', verbose: true }),
18+
});
19+
const input = await readFile(new URL('./hilla-openapi-mix.json', import.meta.url), 'utf8');
20+
const files = await generator.process(input);
21+
22+
const generatedNumberSignalService = files.find((f) => f.name === 'NumberSignalService.ts')!;
23+
await expect(await generatedNumberSignalService.text()).toMatchSnapshot(
24+
`NumberSignalServiceMix`,
25+
import.meta.url,
26+
);
27+
28+
// Non-signal returning services should remain unchanged as before:
29+
const generatedHelloWorldService = files.find((f) => f.name === 'HelloWorldService.ts')!;
30+
await expect(await generatedHelloWorldService.text()).toMatchSnapshot(`HelloWorldService`, import.meta.url);
31+
});
32+
33+
it('removes init import if not needed', async () => {
1634
const generator = new Generator([BackbonePlugin, SignalsPlugin], {
1735
logger: new LoggerFactory({ name: 'model-plugin-test', verbose: true }),
1836
});
1937
const input = await readFile(new URL('./hilla-openapi.json', import.meta.url), 'utf8');
2038
const files = await generator.process(input);
2139

22-
let i = 0;
23-
for (const file of files) {
24-
await expect(await file.text()).toMatchSnapshot(`number-signal-${i}`, import.meta.url);
25-
i++;
26-
}
40+
// Signal-only returning services should have the init import removed:
41+
const generatedNumberSignalService = files.find((f) => f.name === 'NumberSignalService.ts')!;
42+
await expect(await generatedNumberSignalService.text()).toMatchSnapshot(
43+
`NumberSignalServiceSignalOnly`,
44+
import.meta.url,
45+
);
46+
47+
// Non-signal returning services should remain unchanged as before:
48+
const generatedHelloWorldService = files.find((f) => f.name === 'HelloWorldService.ts')!;
49+
await expect(await generatedHelloWorldService.text()).toMatchSnapshot(`HelloWorldService`, import.meta.url);
2750
});
2851
});
2952
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { EndpointRequestInit as EndpointRequestInit_1 } from "@vaadin/hilla-frontend";
2+
import client_1 from "./connect-client.default.js";
3+
async function sayHello_1(name: string, init?: EndpointRequestInit_1): Promise<string> { return client_1.call("HelloWorldService", "sayHello", { name }, init); }
4+
export { sayHello_1 as sayHello };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { EndpointRequestInit as EndpointRequestInit_1 } from "@vaadin/hilla-frontend";
2+
import { type NumberSignal as NumberSignal_1, NumberSignalChannel as NumberSignalChannel_1 } from "@vaadin/hilla-react-signals";
3+
import client_1 from "./connect-client.default.js";
4+
function counter_1(): NumberSignal_1 { return new NumberSignalChannel_1("NumberSignalService.counter", client_1).signal; }
5+
async function sayHello_1(name: string, init?: EndpointRequestInit_1): Promise<string> { return client_1.call("NumberSignalService", "sayHello", { name }, init); }
6+
function sharedValue_1(): NumberSignal_1 { return new NumberSignalChannel_1("NumberSignalService.sharedValue", client_1).signal; }
7+
export { counter_1 as counter, sayHello_1 as sayHello, sharedValue_1 as sharedValue };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { EndpointRequestInit as EndpointRequestInit_1 } from "@vaadin/hilla-frontend";
2+
import { type NumberSignal as NumberSignal_1, NumberSignalChannel as NumberSignalChannel_1 } from "@vaadin/hilla-react-signals";
3+
import client_1 from "./connect-client.default.js";
4+
function counter_1(): NumberSignal_1 { return new NumberSignalChannel_1("NumberSignalService.counter", client_1).signal; }
5+
function sharedValue_1(): NumberSignal_1 { return new NumberSignalChannel_1("NumberSignalService.sharedValue", client_1).signal; }
6+
export { counter_1 as counter, sharedValue_1 as sharedValue };

0 commit comments

Comments
 (0)