Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make default value optional for ValueSignal #3160

Merged
merged 16 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 168 additions & 27 deletions packages/ts/generator-plugin-signals/src/SignalProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@
const genericSignals = ['ValueSignal', 'ListSignal'];
const collectionSignals = ['ListSignal'];

const primitiveModels = Object.freeze(
new Map<ts.SyntaxKind, string>([
[ts.SyntaxKind.StringKeyword, 'StringModel'],
[ts.SyntaxKind.NumberKeyword, 'NumberModel'],
[ts.SyntaxKind.BooleanKeyword, 'BooleanModel'],
[ts.SyntaxKind.ArrayType, 'ArrayModel'],
]),
);

export default class SignalProcessor {
readonly #dependencyManager: DependencyManager;
readonly #owner: Plugin;
Expand Down Expand Up @@ -48,27 +57,21 @@
transform((tsNode) => {
if (ts.isFunctionDeclaration(tsNode) && tsNode.name && this.#methods.has(tsNode.name.text)) {
const signalId = this.#replaceSignalImport(tsNode);
let initialValue: ts.Expression = signalId.text.startsWith('NumberSignal')
? ts.factory.createNumericLiteral('0')
: ts.factory.createIdentifier('undefined');
const filteredParams = tsNode.parameters.filter(
(p) => !p.type || !ts.isTypeReferenceNode(p.type) || p.type.typeName !== initTypeId,
);
// `filteredParams` can be altered after, need to store the param names now
const paramNames = filteredParams.map((p) => (p.name as ts.Identifier).text).join(', ');
const isCollectionSignal = collectionSignals.includes(signalId.text);
let genericReturnType;
if (genericSignals.includes(signalId.text)) {
genericReturnType = (tsNode.type as ts.TypeReferenceNode).typeArguments![0];
if (!isCollectionSignal) {
const defaultValueType = SignalProcessor.#getDefaultValueType(genericReturnType);
if (defaultValueType) {
const { alias, param } = SignalProcessor.#createDefaultValueParameter(defaultValueType);
initialValue = alias;
filteredParams.push(param);
}
}

const { defaultValueExpression, defaultValueParam, genericReturnType } = this.#createDefaultValue(
signalId,
tsNode,
);
if (defaultValueParam) {
filteredParams.push(defaultValueParam);
}

const returnType = genericReturnType ?? signalId;
if (filteredParams.length > 0) {
functionParams.set(tsNode.name.text, filteredParams);
Expand All @@ -83,7 +86,9 @@
transform((node) => (ts.isIdentifier(node) && node.text === SIGNAL ? signalId : node)),
transform((node) => (ts.isIdentifier(node) && node.text === RETURN_TYPE ? returnType : node)),
transform((node) => (ts.isIdentifier(node) && node.text === CONNECT_CLIENT ? connectClientId : node)),
transform((node) => (ts.isIdentifier(node) && node.text === INITIAL_VALUE ? initialValue : node)),
transform((node) =>
ts.isIdentifier(node) && node.text === INITIAL_VALUE ? defaultValueExpression : node,
),
],
);
}
Expand Down Expand Up @@ -135,6 +140,50 @@
);
}

#createDefaultValue(signalId: ts.Identifier, functionDeclaration: FunctionDeclaration) {
const defaultValue: {
defaultValueExpression: ts.Expression | ts.Identifier;
defaultValueParam: ts.ParameterDeclaration | undefined;
genericReturnType: ts.TypeNode | undefined;
} = {
defaultValueExpression: signalId.text.startsWith('NumberSignal')
? ts.factory.createNumericLiteral('0')
: ts.factory.createIdentifier('undefined'),
defaultValueParam: undefined,
genericReturnType: undefined,
};

if (!genericSignals.includes(signalId.text)) {
return defaultValue;
}

defaultValue.genericReturnType = (functionDeclaration.type as ts.TypeReferenceNode).typeArguments![0];

if (collectionSignals.includes(signalId.text)) {
return defaultValue;
}

const defaultValueType = SignalProcessor.#getDefaultValueType(defaultValue.genericReturnType);
if (!defaultValueType) {
return defaultValue;
}

Check warning on line 169 in packages/ts/generator-plugin-signals/src/SignalProcessor.ts

View check run for this annotation

Codecov / codecov/patch

packages/ts/generator-plugin-signals/src/SignalProcessor.ts#L168-L169

Added lines #L168 - L169 were not covered by tests

defaultValue.defaultValueParam = SignalProcessor.#createDefaultValueParameter(defaultValueType);
const emptyValueExpression = this.#createEmptyValueExpression(defaultValueType);

defaultValue.defaultValueExpression = ts.factory.createBinaryExpression(
ts.factory.createPropertyAccessChain(
ts.factory.createIdentifier('options'),
ts.factory.createToken(ts.SyntaxKind.QuestionDotToken),
ts.factory.createIdentifier('defaultValue'),
),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
emptyValueExpression,
);

return defaultValue;
}

static #getDefaultValueType(node: ts.Node) {
if (
ts.isUnionTypeNode(node) &&
Expand All @@ -145,23 +194,115 @@
) {
return node.types[0].typeArguments[0];
}

return undefined;
}

static #createDefaultValueParameter(returnType: ts.TypeNode) {
const alias = createFullyUniqueIdentifier('defaultValue');
const bindingPattern = ts.factory.createObjectBindingPattern([
ts.factory.createBindingElement(undefined, ts.factory.createIdentifier('defaultValue'), alias, undefined),
]);
static #createDefaultValueParameter(defaultValueType: ts.TypeNode) {
const paramType = ts.factory.createTypeLiteralNode([
ts.factory.createPropertySignature(undefined, ts.factory.createIdentifier('defaultValue'), undefined, returnType),
ts.factory.createPropertySignature(
undefined,
ts.factory.createIdentifier('defaultValue'),
undefined,
defaultValueType,
),
]);
// Return both the alias and the full parameter
return {
alias,
param: ts.factory.createParameterDeclaration(undefined, undefined, bindingPattern, undefined, paramType),
};

return ts.factory.createParameterDeclaration(
undefined,
undefined,
'options',
ts.factory.createToken(ts.SyntaxKind.QuestionToken),
paramType,
);
}

static #isDefaultValueTypeNullable(defaultValueType: ts.TypeNode) {
return (
ts.isUnionTypeNode(defaultValueType) &&
defaultValueType.types.length &&
defaultValueType.types.length > 1 &&
defaultValueType.types.map((t) => t.kind).includes(ts.SyntaxKind.UndefinedKeyword)
);
}

#createEmptyValueExpression(defaultValueType: ts.UnionTypeNode) {
if (SignalProcessor.#isDefaultValueTypeNullable(defaultValueType)) {
return ts.factory.createIdentifier('undefined');
}
const importedModelUniqueId = this.#determineModelImportUniqueIdentifier(defaultValueType);
return ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(importedModelUniqueId, 'createEmptyValue'),
undefined,
[],
);
}

#determineModelImportUniqueIdentifier(returnTypeNode: ts.UnionTypeNode) {
let modelName = primitiveModels.get(returnTypeNode.types[0].kind);
let entityName;
if (modelName === undefined) {
const { entityName: e, modelName: m } = SignalProcessor.#extractModelNameFromTypeNode(returnTypeNode);
modelName = m;
entityName = e;
}
const modelImportUniqueId =
this.#getExistingEntityModelUniqueIdentifier(modelName) ?? createFullyUniqueIdentifier(modelName);

this.#addModelImport(entityName, modelName, modelImportUniqueId);
return modelImportUniqueId;
}

static #extractModelNameFromTypeNode(returnTypeNode: ts.UnionTypeNode) {
if (ts.isTypeReferenceNode(returnTypeNode.types[0])) {
const typeIdentifier = returnTypeNode.types[0].typeName;
if (ts.isIdentifier(typeIdentifier)) {
const entityName = typeIdentifier.text;
const modelName = `${entityName}Model`;
return { entityName, modelName };
}
}
throw new Error('Unsupported type reference node');
}

#getExistingEntityModelUniqueIdentifier(modelName: string) {
const { imports } = this.#dependencyManager;
return (
imports.named.getIdentifier('@vaadin/hilla-lit-form', modelName) ??
imports.default.iter().find(([path]) => path.endsWith(`/${modelName}.js`))?.[1]
);
}

#addModelImport(
entityName: string | undefined,
modelName: string | undefined,
modelNameUniqueId: ts.Identifier | undefined,
) {
if (modelName) {
if (primitiveModels.values().find((primitiveModel) => primitiveModel === modelName)) {
const { imports } = this.#dependencyManager;
const importedModel = imports.named.getIdentifier('@vaadin/hilla-lit-form', modelName);
if (importedModel === undefined) {
imports.named.add('@vaadin/hilla-lit-form', modelName, false, modelNameUniqueId);
}
} else {
this.#addObjectModelImport(entityName!, modelName, modelNameUniqueId!);
}
}
}

#addObjectModelImport(entityName: string, modelName: string, modelNameUniqueId: ts.Identifier) {
const { imports } = this.#dependencyManager;
const entityImport = imports.default
.iter()
.map(([path]) => path)
.find((path) => path.startsWith('./') && path.endsWith(`/${entityName}.js`));
if (entityImport) {
const entityModelImportPath = entityImport.replace(`/${entityName}.js`, `/${modelName}.js`);
const importedModel = imports.default.paths().find((path) => path === entityModelImportPath);
if (importedModel === undefined) {
imports.default.add(entityModelImportPath, modelName, false, modelNameUniqueId);
}
}
}

#replaceSignalImport(method: FunctionDeclaration): Identifier {
Expand Down
14 changes: 14 additions & 0 deletions packages/ts/generator-plugin-signals/test/SignalsEndpoints.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,19 @@ describe('SignalsPlugin', () => {
import.meta.url,
);
});

it('correctly generates service with automatic default values for value signals of primitive types', async () => {
const generator = new Generator([BackbonePlugin, SignalsPlugin], {
logger: new LoggerFactory({ name: 'signals-plugin-test', verbose: true }),
});
const input = await readFile(new URL('./hilla-openapi-default-value.json', import.meta.url), 'utf8');
const files = await generator.process(input);

const generatedValueSignalService = files.find((f) => f.name === 'PrimitiveTypeValueSignalService.ts')!;
await expect(await generatedValueSignalService.text()).toMatchSnapshot(
`PrimitiveTypeValueSignalService.snap.ts`,
import.meta.url,
);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ArrayModel as ArrayModel_1, BooleanModel as BooleanModel_1, NumberModel as NumberModel_1, StringModel as StringModel_1 } from "@vaadin/hilla-lit-form";
import { ValueSignal as ValueSignal_1 } from "@vaadin/hilla-react-signals";
import client_1 from "./connect-client.default.js";
function anotherStringValueSignal_1(options?: {
defaultValue: string;
}): ValueSignal_1<string> {
return new ValueSignal_1(options?.defaultValue ?? StringModel_1.createEmptyValue(), { client: client_1, endpoint: "PrimitiveTypeValueSignalService", method: "anotherStringValueSignal" });
}
function booleanValueSignal_1(options?: {
defaultValue: boolean;
}): ValueSignal_1<boolean> {
return new ValueSignal_1(options?.defaultValue ?? BooleanModel_1.createEmptyValue(), { client: client_1, endpoint: "PrimitiveTypeValueSignalService", method: "booleanValueSignal" });
}
function booleanValueSignalNullable_1(options?: {
defaultValue: boolean | undefined;
}): ValueSignal_1<boolean | undefined> {
return new ValueSignal_1(options?.defaultValue ?? undefined, { client: client_1, endpoint: "PrimitiveTypeValueSignalService", method: "booleanValueSignalNullable" });
}
function doubleValueSignal_1(options?: {
defaultValue: number;
}): ValueSignal_1<number> {
return new ValueSignal_1(options?.defaultValue ?? NumberModel_1.createEmptyValue(), { client: client_1, endpoint: "PrimitiveTypeValueSignalService", method: "doubleValueSignal" });
}
function doubleValueSignalNullable_1(options?: {
defaultValue: number | undefined;
}): ValueSignal_1<number | undefined> {
return new ValueSignal_1(options?.defaultValue ?? undefined, { client: client_1, endpoint: "PrimitiveTypeValueSignalService", method: "doubleValueSignalNullable" });
}
function stringArrayValueSignal_1(options?: {
defaultValue: Array<string>;
}): ValueSignal_1<Array<string>> {
return new ValueSignal_1(options?.defaultValue ?? ArrayModel_1.createEmptyValue(), { client: client_1, endpoint: "PrimitiveTypeValueSignalService", method: "stringArrayValueSignal" });
}
function stringArrayValueSignalNullable_1(options?: {
defaultValue: Array<string | undefined>;
}): ValueSignal_1<Array<string | undefined>> {
return new ValueSignal_1(options?.defaultValue ?? ArrayModel_1.createEmptyValue(), { client: client_1, endpoint: "PrimitiveTypeValueSignalService", method: "stringArrayValueSignalNullable" });
}
function stringValueSignal_1(options?: {
defaultValue: string;
}): ValueSignal_1<string> {
return new ValueSignal_1(options?.defaultValue ?? StringModel_1.createEmptyValue(), { client: client_1, endpoint: "PrimitiveTypeValueSignalService", method: "stringValueSignal" });
}
function stringValueSignalNullable_1(options?: {
defaultValue: string | undefined;
}): ValueSignal_1<string | undefined> {
return new ValueSignal_1(options?.defaultValue ?? undefined, { client: client_1, endpoint: "PrimitiveTypeValueSignalService", method: "stringValueSignalNullable" });
}
export { anotherStringValueSignal_1 as anotherStringValueSignal, booleanValueSignal_1 as booleanValueSignal, booleanValueSignalNullable_1 as booleanValueSignalNullable, doubleValueSignal_1 as doubleValueSignal, doubleValueSignalNullable_1 as doubleValueSignalNullable, stringArrayValueSignal_1 as stringArrayValueSignal, stringArrayValueSignalNullable_1 as stringArrayValueSignalNullable, stringValueSignal_1 as stringValueSignal, stringValueSignalNullable_1 as stringValueSignalNullable };
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
import { EndpointRequestInit as EndpointRequestInit_1 } from "@vaadin/hilla-frontend";
import { ArrayModel as ArrayModel_1 } from "@vaadin/hilla-lit-form";
import { ListSignal as ListSignal_1, ValueSignal as ValueSignal_1 } from "@vaadin/hilla-react-signals";
import type Person_1 from "./com/github/taefi/data/Person.js";
import PersonModel_1 from "./com/github/taefi/data/PersonModel.js";
import client_1 from "./connect-client.default.js";
async function getPerson_1(init?: EndpointRequestInit_1): Promise<Person_1 | undefined> { return client_1.call("PersonService", "getPerson", {}, init); }
function personArraySignal_1(options?: {
defaultValue: Array<Person_1>;
}): ValueSignal_1<Array<Person_1>> {
return new ValueSignal_1(options?.defaultValue ?? ArrayModel_1.createEmptyValue(), { client: client_1, endpoint: "PersonService", method: "personArraySignal" });
}
function personListSignal_1(): ListSignal_1<Person_1> {
return new ListSignal_1({ client: client_1, endpoint: "PersonService", method: "personListSignal" });
}
function personSignal_1({ defaultValue: defaultValue_1 }: {
function personSignalNotNull_1(options?: {
defaultValue: Person_1;
}): ValueSignal_1<Person_1> {
return new ValueSignal_1(options?.defaultValue ?? PersonModel_1.createEmptyValue(), { client: client_1, endpoint: "PersonService", method: "personSignalNotNull" });
}
function personSignalNullable_1(isAdult: boolean, options?: {
defaultValue: Person_1 | undefined;
}): ValueSignal_1<Person_1 | undefined> {
return new ValueSignal_1(defaultValue_1, { client: client_1, endpoint: "PersonService", method: "personSignal" });
return new ValueSignal_1(options?.defaultValue ?? undefined, { client: client_1, endpoint: "PersonService", method: "personSignalNullable", params: { isAdult } });
}
function personSignalWithParams_1(dummyBoolean: boolean, dummyString: string | undefined, { defaultValue: defaultValue_2 }: {
function personSignalWithParams_1(dummyBoolean: boolean, dummyString: string | undefined, options?: {
defaultValue: Person_1 | undefined;
}): ValueSignal_1<Person_1 | undefined> {
return new ValueSignal_1(defaultValue_2, { client: client_1, endpoint: "PersonService", method: "personSignalWithParams", params: { dummyBoolean, dummyString } });
}
function personSignalNonNull_1({ defaultValue: defaultValue_3 }: {
defaultValue: Person_1;
}): ValueSignal_1<Person_1> {
return new ValueSignal_1(defaultValue_3, { client: client_1, endpoint: "PersonService", method: "personSignalNonNull" });
return new ValueSignal_1(options?.defaultValue ?? undefined, { client: client_1, endpoint: "PersonService", method: "personSignalWithParams", params: { dummyBoolean, dummyString } });
}
function personSignalNonNullWithParams_1(dummyBoolean: boolean, dummyString: string | undefined, { defaultValue: defaultValue_4 }: {
function personSignalNonNullWithParams_1(dummyBoolean: boolean, dummyString: string | undefined, options?: {
defaultValue: Person_1;
}): ValueSignal_1<Person_1> {
return new ValueSignal_1(defaultValue_4, { client: client_1, endpoint: "PersonService", method: "personSignalNonNullWithParams", params: { dummyBoolean, dummyString } });
return new ValueSignal_1(options?.defaultValue ?? PersonModel_1.createEmptyValue(), { client: client_1, endpoint: "PersonService", method: "personSignalNonNullWithParams", params: { dummyBoolean, dummyString } });
}
export { getPerson_1 as getPerson, personListSignal_1 as personListSignal, personSignal_1 as personSignal, personSignalNonNull_1 as personSignalNonNull, personSignalNonNullWithParams_1 as personSignalNonNullWithParams, personSignalWithParams_1 as personSignalWithParams };
export { getPerson_1 as getPerson, personArraySignal_1 as personArraySignal, personListSignal_1 as personListSignal, personSignalNonNullWithParams_1 as personSignalNonNullWithParams, personSignalNotNull_1 as personSignalNotNull, personSignalNullable_1 as personSignalNullable, personSignalWithParams_1 as personSignalWithParams };
Loading
Loading