Serenity 6.3.0 Release Notes (2022-11-06)

Ported All Common Features Projects to ES Modules

In this release, we've migrated all common features projects to ES modules. These shared projects are used in both Serene and StartSharp and include the following:

  • Serenity.Extensions: Contains extension classes shared by Serenity apps, offering functionalities like Excel and PDF export.
  • Serenity.Demo.Northwind: The Northwind demo project.
  • Serenity.Demo.BasicSamples: A basic samples demo project that primarily references Northwind modules.

These projects are now hosted in their separate repository at:

The TypeScript code in these projects has been converted to ES modules. Although the Serene template has not yet been updated to use ES modules, we are planning to make this transition. This update will require adjustments to our movie tutorials and sample code in the documentation.

It's important to note that despite the conversion to ES modules, we still produce a "namespaces" bundle for compatibility. Some customer projects may still use the namespace versions or employ a mixture of ES modules and namespaces. Thus, there's no need to make immediate changes in your projects for Serenity.Extensions or others.

Ported All Pro Features Projects to ES Modules

We've also migrated the pro features projects to ES modules. These projects are exclusively available for StartSharp customers and are hosted in the StartSharp repository:

The pro features projects encompass a range of functionalities, including advanced samples, data audit logging, data exploration, and more. The projects that have been converted to ES modules include:

  • Serenity.Pro.Extensions: Contains additional mixins like drag-drop grouping, auto column width, Excel-style filtering, and more.
  • Serenity.Demo.AdvancedSamples: An advanced samples demo project that references Northwind and other pro modules.
  • Serenity.Pro.Coder: Contains source generators, similar to Sergen, with real-time transformations within Visual Studio.
  • Serenity.Pro.DataAuditLog: A project for data audit logging.
  • Serenity.Pro.DataExplorer: A dynamic SQL data explorer.
  • Serenity.Pro.Meeting: A meeting management sample module.
  • Serenity.Pro.WorkLog: A work log and time tracking sample module.

While most projects have been converted to ES modules, we've excluded Serenity.Pro.EmailClient, which is currently undergoing a complete rewrite, and Serenity.Pro.UI, which is becoming obsolete as it's only used by the old email client.

Similar to the common features, we continue to provide the classic "namespaces style" output generated by esbuild for these projects in parallel. This ensures that you won't need to make any immediate changes to your existing projects.

We believe these changes will better serve our users and provide a clear path for writing ES module-style code with Serenity and their own feature libraries. This transition has also been instrumental in stabilizing Sergen and Serenity.Pro.Coder for ES module-style transformations.

Added IFileSystem Abstraction

We have introduced an "IFileSystem" abstraction to enhance the flexibility of file system access within our projects. Previously, we relied on the System.IO.Abstractions library for this purpose, enabling us to perform tests without the need to create physical temporary folders on disk.

While System.IO.Abstractions serves its purpose well and comes highly recommended, it provides a comprehensive "IFileSystem" interface that covers all possible "System.IO" classes and methods. However, this extensive interface can be challenging to partially implement without their mock file system abstraction. This complexity led to issues in our source generator due to how package references work in a source generator project.

To address this challenge and simplify our approach, we have introduced a streamlined "IFileSystem" interface. In our test projects, we continue to utilize System.IO.Abstractions' mock file system. Still, in our main codebase, we now leverage this simplified interface.

Here's a glimpse of the "IFileSystem" interface:

public interface IFileSystem
{
    System.IO.Stream CreateFile(string path, bool overwrite = true);
    void CreateDirectory(string path);
    void DeleteDirectory(string path, bool recursive = false);
    void DeleteFile(string path);
    bool DirectoryExists(string path);
    bool FileExists(string path);
    string[] GetDirectories(string path, string searchPattern = "*", bool recursive = false);
    string[] GetFiles(string path, string searchPattern = "*", bool recursive = false);
    long GetFileSize(string path);
    string GetFullPath(string path);
    string GetRelativePath(string relativeTo, string path);
    System.IO.Stream OpenRead(string path);
    byte[] ReadAllBytes(string path);
    string ReadAllText(string path, Encoding encoding = null);
    void WriteAllBytes(string path, byte[] content);
    void WriteAllText(string path, string content, Encoding encoding = null);
}

This interface encapsulates the most essential shared methods used in our code generators and upload system. Additionally, we've introduced "IGeneratorFileSystem" and "IDiskUploadFileSystem" interfaces that build upon this abstraction, adding specific methods to suit their respective purposes.

These interfaces are injectable via Dependency Injection (DI), providing the flexibility to redirect file system operations to alternative sources. For instance, you can easily reroute "DiskUploadStorage" to utilize a virtual file system, such as an SQL database table/file mapping table, by implementing the interface methods according to your requirements.

Upload Behavior Refinements

We have made refinements to the "ImageUploadBehavior" and "MultipleImageUploadBehavior" behaviors, which were previously automatically activated for fields with [ImageUploadEditor], [FileUploadEditor], [MultipleImageUploadEditor], or [MultipleFileUploadEditor] attributes.

These behaviors were inherently tied to these specific attribute types, limiting their applicability to other custom upload editor types that users might develop. To enhance flexibility, we have removed the direct dependencies on these attributes and introduced separate interfaces, such as:

  • IUploadEditor
  • IUploadFileConstraints
  • IUploadFileSizeConstraints
  • IUploadImageOptions

The new "IUploadEditor" interface features a single property:

public interface IUploadEditor
{
    bool IsMultiple { get; }
}

The behaviors are now activated for any attribute that implements this interface. If an attribute returns true for IsMultiple, the multiple upload behavior is enabled; otherwise, the single-file upload behavior is used.

To align with this change, we have renamed "ImageUploadBehavior" to "FileUploadBehavior" and "MultipleImageUploadBehavior" to "MultipleFileUploadBehavior" to reflect that they are not limited to image uploads. The older behavior classes, while retained for compatibility, are now marked as abstract and obsolete.

Source Generator Reads MVC/StripViewPaths Options from Sergen.json If Specified (since 6.2.7)

In Serenity, we offer numerous configuration options that are typically derived from conventions and serve their purpose well. It's not recommended to override the defaults unless you have a specific need to do so.

For a comprehensive list of these options, please refer to the "GeneratorConfig.cs" file in the Serenity repository:

Link to GeneratorConfig.cs

One of the options pertains to the "MVC.cs" generation, specifically the generation of "View Paths." This option is defined under MVC/SearchViewPaths and MVC/StripViewPaths:

{
    //...
    "MVC": {
        "SearchViewPaths": {
            "Folder1",
            "Folder2",
            "Folder3/SubFolder4"
        },
        "StripViewPaths": {
            "Folder1",
            "Folder2"
        }
    }
}

Sergen's MVC generator creates a helper class named "MVC" in your project. This helper class makes working with Razor .cshtml view paths less error-prone and provides Visual Studio intelli-sense support.

Yes, we like and make use of the intelli-sense and compile time checking as much as possible in Serenity By default, the generator searches for views within specific folders under the project directory, including "Modules," "Views," and "Your.ProjectName." If your views are located in different directories, you need to manually set the SearchViewPaths option in the "sergen.json" file. The default also includes the Areas/Your.ProjectName folder since 6.3.0 In Serenity applications, many views are stored in the "Modules" folder. To streamline the generated code and omit common path prefixes, we introduced the "StripViewPaths" option. The default values for the "StripViewPaths" option are the same as those for the "SearchViewPaths" option, covering "Modules," "Views," and "Your.ProjectName" folders.

This change helps reduce redundancy in file paths while maintaining code clarity. However, keep in mind that using "StripViewPaths" might lead to clashing file names if multiple views under those directories share the same paths when their prefixes are omitted.

In the "StartSharp" source generator "Serenity.Pro.Coder," a similar "MVC.cs" file is generated. Before this update, it did not read the "StripViewPaths" option from "sergen.json" and relied on defaults.

As a side note, the source generator currently doesn't take the "SearchViewPaths" option into account, as the Compilation system provides view locations. We are considering adding support for this option in the future to ensure consistency.

Source Generator Transforms Are Disabled When SourceGeneratorTransform is False in the Project File

Source generators in Serenity.Pro.Coder and Sergen can perform MVC, ClientTypes, and ServerTypings transformations. By default, the source generator is enabled in StartSharp, while Sergen is disabled, unless you explicitly set SourceGeneratorTransform to false in your project file. You can achieve this by adding the following condition in your project file:

  <Target Name="TransformMvcClientTypes" BeforeTargets="BeforeBuild" 
    Condition="'$(SourceGeneratorTransform)' == 'false'" >
    <Exec Command="dotnet tool restore" ContinueOnError="true" />
    <Exec Command="$(DotNetSergen) restore" ContinueOnError="true" />
    <Exec Command="$(DotNetSergen) mvct" ContinueOnError="true" />
  </Target>
  <Target Name="TransformServerTypings" AfterTargets="AfterBuild">
    <Exec Command="$(DotNetSergen) servertypings" ContinueOnError="true" 
    Condition="'$(SourceGeneratorTransform)' == 'false'" />
  </Target>

We left Sergen-related targets there in case you wanted to go back to using classic transformations for some reason like having an issue with the way source generators perform.

So by adding this property to your project file:

 <PropertyGroup>
    <SourceGeneratorTransform>false</SourceGeneratorTransform>
 </PropertyGroup>

This change allows you to disable source generator transformations and use Sergen instead. Keep in mind that removing the reference to Serenity.Pro.Coder and the related condition in your project file would also disable the row-template-based source generator, which Sergen does not have an equivalent for.

Before version 6.2.7, the Serenity.Pro.Coder package did not read the SourceGeneratorTransform setting in your project file. Thus, adding that property only re-enabled Sergen, but it did not disable the source generator transforms working in the background. This has now been fixed, and Source Generator transformations are indeed disabled when your project file includes the SourceGeneratorTransform property set to false.

Possible to Disable Individual Source Generator Transforms via sergen.json

If, for some specific reason, you wish to disable only one of the MVC, ClientTypes, or ServerTypings transformations, you can now achieve this individually by setting the SourceGenerator property to false in the "sergen.json" configuration file:

{
    "MVC": {
        "SourceGenerator": false
    },
    "ClientTypes": {
        "SourceGenerator": false
    },
    "ServerTypings": {
        "SourceGenerator": false
    }
}

This feature offers fine-grained control over which transformations are active or inactive.

Going Back to Non-Incremental Source Generator for ClientTypes Transform

Typically, source generators in .NET are designed to generate C# code by analyzing C# syntax trees or other files registered as AdditionalFiles. In the case of Serenity's transformations, there's a unique challenge: generating C# code from TypeScript files and vice versa.

For the ClientTypes transformation, Serenity needs to read your TypeScript sources to discover Editor/Formatter types and generate corresponding C# attributes. However, when registering .ts files as AdditionalFiles, Visual Studio's syntax highlighting for TypeScript files can be partially lost, possibly due to a bug with the TypeScript language service in Visual Studio itself.

Source generators in .NET 6, specifically the V2 version, introduced an incremental generator feature. This enhancement aimed to potentially improve the performance of source generators in Visual Studio while editing files. The key idea behind this feature was to process only the changes that occurred in the codebase. You can read more about this feature here:

https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md

However, due to an issue related to .ts files, specifically with the AdditionalFiles, we had to employ some workarounds to implement an incremental generator for the ClientTypes transformation. Unfortunately, it appears that these workarounds resulted in some unexpected problems. Notably, they caused issues with the detection of new editor types and the recognition of renamed types, especially when working with ES modules. As a result, we've made the decision to revert to the classic source generator system for ClientTypes, at least for the time being.

Auto Import Enum Types to Form.ts and Columns.ts Files

As mentioned in the Serenity 6.2.6 release notes, there have been issues related to ES modules and referenced types, such as the in-place add dialog types, due to runtime differences between global namespaces and ES modules. A similar challenge arises with Enum types.

To illustrate the issue, let's consider a sample enum:

namespace MyProject.MyModule
{
    public enum MyColors
    {
        Red,
        Blue
    }
}

When the ServerTypings transformation runs, it generates a TypeScript file (MyModule.MyColors.ts) for this enum in your project:

namespace MyProject.MyModule {
    export enum MyColors {
        Red = 1,
        Blue = 2
    }

    Serenity.Decorators.registerEnumType(MyColors, 'MyProject.MyModule.MyColors');
}

This allows the enum type to be accessible both in the global script context (via window.MyProject.MyModule.MyColors) and registered as MyProject.MyModule.MyColors in the Serenity type system.

Now, let's assume you have a form in another module that uses this enum type:

namespace MyProject.AnotherModule
{
    [FormScript]
    public class MyForm 
    {
        MyModule.MyColors Color { get; set; }
    }
}

The generated form helper (AnotherModule.MyForm.ts) in TypeScript, which is created when ServerTypings runs:

namespace MyProject.AnotherModule {
    export interface MyForm {
        Color: EnumEditor;
    }

    export class MyForm extends Serenity.PrefixedContext {
        constructor(prefix: string) {
            // ...
        }
    }
}

It's essential to clarify that this class serves as a helper for accessing form inputs using IntelliSense support. It's not used to create your form. As observed, it doesn't include the enum type. So, how can it populate the enum editor with values from the MyModule.MyColors enum?

Forms defined in server-side code are inspected through .NET reflection by Serenity and converted into JSON objects. The PropertyGrid class renders these forms on the client side by looking at this JSON metadata and creating corresponding editor objects.

Here's a simplified version of the JSON object generated for the MyForm definition:

{
    "items": [
        {
            "name": "Color",
            "editorType": "Enum",
            "editorParams": {
                "enumKey": "MyProject.MyModule.MyColors"
            },
            "formatterType": "Enum",
            "formatterParams": {
                "enumKey": "MyProject.MyModule.MyColors"
            },
            "width": 80,
            //...
        }
    ],
    "additionalItems": []
}

This JSON specifies that the form should have a field named Color with an editor type of Enum, specifically Serenity.EnumEditor. The enumeration type, MyProject.MyModule.MyColors, is passed to the editor as a constructor option.

Now, the enum editor only knows the string identifier (MyProject.MyModule.MyColors) of the enum it should list values for. It relies on the Serenity type system to locate this enumeration type and retrieve the list of its values.

The Serenity type system checks if it has an enumeration type registered with that key. If not, it may attempt to find it in the global context (window) based on its namespace and name.

In the provided sample, the enum generated by ServerTypings is already registered via the Serenity.Decorators.registerEnumType call. Additionally, the script block containing this call is loaded as it is compiled into the YourProject.Web.js script, alongside other TypeScript code in your project.

However, complications arise when transitioning to ES modules.

Let's explore the client-side enum definition for the ES module-style enum:

import { Decorators } from "@serenity-is/corelib";

export enum MyColors {
    Red = 1,
    Blue = 2
}

Decorators.registerEnumType(MyColors, 'MyProject.MyModule.MyColors');

It's worth noting that, unlike namespaces, this enum won't be available in the global context (window) since it's an ES module. It can only be accessed through an import statement.

In summary, this section emphasizes the issues related to enum types when working with ES modules and their impact on form rendering and the Serenity type system.

Importing Enum Types and Their Implications

We still employ Decorators.registerEnumType, and the enum type will indeed be registered in the Serenity type system. However, there is an important caveat to consider: this registration only occurs if the module is explicitly IMPORTED on the current page.

To grasp the distinction, let's start by examining the entry-point module MyPage.ts, which contains a reference to MyGrid:

import { initFullHeightGridPage } from "@serenity-is/corelib/q";
import { MyGrid } from "./MyGrid";

$(function () {
    initFullHeightGridPage(new MyGrid($('#GridDiv')).element);
});

This import statement brings in the MyGrid class, which in turn imports MyDialog:

import { EntityGrid } from "@serenity-is/corelib";
import { MyColumns, MyRow, MyTypes } from "../ServerTypes/AnotherModule";
import { MyDialog } from "./MyDialog";

export class MyGrid extends EntityGrid<MyRow, any> {
    protected getDialogType() { return <any>MyDialog; }
    //...
}

Next, let's examine the MyDialog module, which imports MyForm:

import { EntityDialog } from "@serenity-is/corelib";
import { MyForm, MyRow, MyService } from "../ServerTypes/AnotherModule";

export class MyDialog is EntityDialog<MyRow, any> {
    protected getFormKey() { return MyForm.formKey; }
    //...
    protected form = new MyForm(this.idPrefix);
}

Lastly, we look at MyForm.ts:

import { EnumEditor, PrefixedContext } from "@serenity-is/corelib";
import { initFormType } from "@serenity-is/corelib/q";

export interface MyForm {
    Color: EnumEditor;
}

export class MyForm extends Serenity.PrefixedContext {
    constructor(prefix: string) {
        // ...
    }
}

Have you noticed an import statement for MyModule.MyColors anywhere in the code thus far? The answer is no. Therefore, when we attempt to open the dialog for MyForm, an error message similar to the following may appear:

Can't find the "MyProject.MyModule.MyColors" enum type! If you defined this enum type....

The ESbuild tool generates separate script bundles for each entry point module in your project, containing only the references to the parts of your application that are actually used. It lacks information about our usage of the MyModule.MyColors enumeration type in MyForm. This information is only available in our JSON metadata, which is generated at runtime.

Consequently, the module that contains our enum type is not included in the script bundle, and the Serenity type system has no means to locate it by its key, as it remains unregistered and unavailable in the global namespace.

In summary, this section clarifies how enum types are imported and the implications for your project when working with ES modules.

A Manual Solution: Importing the Enum Type

As a temporary solution, we can manually import the enum type into our dialog module (MyDialog.ts):

import { EntityDialog } from "@serenity-is/corelib";
import { MyForm, MyRow, MyService } from "../ServerTypes/AnotherModule";
import { MyColors } from "../ServerTypes/MyModule";

[MyColors] // Please note the dummy usage here; otherwise, the unused import will be removed during the build process.

export class MyDialog extends EntityDialog<MyRow, any> {
    protected getFormKey() { return MyForm.formKey; }
    //...
    protected form = new MyForm(this.idPrefix);
}

By employing this workaround, we can ensure that there are no errors. ESBuild will include the enum in the script bundle.

Should you encounter an error in your grid, you can apply a similar workaround to MyGrid.ts:

import { EntityGrid } from "@serenity-is/corelib";
import { MyColumns, MyRow, MyTypes } from "../ServerTypes/AnotherModule";
import { MyDialog } from "./MyDialog";
import { MyColors } from "../ServerTypes/MyModule";

[MyColors] // Please note the dummy usage here; otherwise, the unused import will be removed during the build process.

export class MyGrid extends EntityGrid<MyRow, any> {
    protected getDialogType() { return <any>MyDialog; }
    //...
}

Handling Enums in the Same Namespace

What if the enum is located in the same namespace as MyForm, for example, in the AnotherModule?

namespace MyProject.AnotherModule {
    export enum MyColors {
        Red = 1,
        Blue = 2
    }

    Serenity.Decorators.registerEnumType(MyColors, 'MyProject.AnotherModule.MyColors');
}

Surprisingly, you won't encounter the same error, and the workaround won't be necessary. But why is that?

This is due to the fact that we are importing some types from ServerTypes/AnotherModule:

import { MyColumns, MyRow, MyTypes } from "../ServerTypes/AnotherModule";

Now, let's take a look at ServerTypes/AnotherModule.ts:

export * from "./AnotherModule/MyColumns"
export * from "./AnotherModule/MyColors"
export * from "./AnotherModule/MyForm"
export * from "./AnotherModule/MyRow"
//...

As you can see, this file re-exports all other types from the AnotherModule namespace, including MyColors. Consequently, even if you didn't explicitly import MyColors, the act of importing something from the AnotherModule results in the implicit import of `MyColors," unbeknownst to you.

However, it's essential to note that this behavior is a specific detail of esbuild. It's not advisable to rely on this behavior consistently, as future improvements to esbuild may include tree-shaking, which could lead to the removal of such implicit imports during bundling.

The question remains: Do we need to manually "fake-import" every enum we use in our dialogs and grids? The answer is not necessarily.

Automatic Workaround for ServerTypings Transform

We've introduced an automatic workaround within the ServerTypings transform to handle the issue seamlessly. Instead of requiring you to manually import types into your Dialog.ts and Grid.ts files, it now generates fake imports within the Form.ts and Columns.ts files:

import { MyColors } from "../MyModule/MyColors";

export interface MyForm {
    //...

[MyColors]; // referenced types

This automatic process aims to resolve a significant portion of the issues related to type imports, as long as you make use of the Form/Columns classes in your code. By default, these classes are utilized in the Dialog.ts/Grid.ts files generated by sergen. However, in rare cases where this approach doesn't suffice, you still have the option to apply the manual workaround.

Enhancing Multi-Project ES-Modules Type References

A challenge arises when referencing types such as editors, formatters, and enums defined in another project within an ES module structure. This scenario is particularly relevant in multi-project applications like our feature packages, such as Serenity.Demo.Northwind and Serenity.Demo.BasicSamples. The BasicSamples project may reference certain types from the Northwind project in TypeScript and C# code.

For our example, let's assume that the MyProject.Common project contains shared editors, formatters, and so on:

import { Decorators, Widget } from "@serenity-is/corelib";

@Decorators.registerEditor('MyProject.General.MyEditor')
export class MyEditor extends Widget<any> {
}

Note that this MyEditor class is defined in the MyProject.Common project and registers the editor class as MyProject.General.MyEditor.

The npm package name defined for MyProject.Common in package.json is myproject.common, which is the correct practice (e.g., a lowercase version of the project name):

{
    "name": "myproject.common"
}

In a form definition within the MyProject.Web project, we are referencing this editor type:

public class SomeForm
{
    [MyProject.General.MyEditor]
    public string SomeProperty { get; set; }
}

In the Form.ts file generated for this class:

import { Widget } from "@serenity-is/corelib";

export class SomeForm {
    SomeProperty: Widget;
}

However, you may wonder why ServerTypings couldn't locate the MyEditor type and instead used the Widget base type. This situation arises because the code generator operates in a manner similar to TypeScript. It can't scan your C# project references but relies on reading .ts or .d.ts files to discover types.

In this context, the MyProject.Common project, when built, generates a dist/index.d.ts file containing type definitions for that project:

import { Widget } from "@serenity-is/corelib";

export class MyEditor extends Widget<any> {

As a result, the MyEditor type isn't automatically discovered by ServerTypings, causing it to default to the Widget base type.

Handling TypeScript Decorators and Type Discovery

In TypeScript, the issue arises when decorators are removed from .d.ts output, rendering the specified key for your editor type unavailable in the generated declaration file.

This .d.ts file is referenced by MyProject.Web, as opposed to the source .ts files residing under MyProject.Common/Modules. Consequently, the type lister can only parse the node_modules/myproject.common/dist/index.d.ts file to identify types. Unfortunately, it doesn't have knowledge of the key you've assigned to the MyEditor class, such as MyProject.General.MyEditor. For the parser, it merely perceives the editor as MyEditor within the module myproject.common.

To address this issue, we've implemented a workaround. However, it's important to note that this workaround functions optimally when the namespace/key you assign to your types aligns with the project name. In other words, you should set your editor key in the following format:

import { Decorators, Widget } from "@serenity-is/corelib";

@Decorators.registerEditor('MyProject.Common.MyEditor')
export class MyEditor extends Widget<any> {
}

By doing this, even if TypeScript omits the decorator from the .d.ts file, we can locate it within the myproject.common module, which corresponds to the project name.

We are actively exploring alternative methods to resolve such issues, but until then, it's advisable to establish consistent registration keys for all inter-project shared types in the format ProjectName.ClassName.

Generation of Editor / Formatter Options for Client Type Attributes in ES Modules

In cases where you define a custom Editor or Formatter, these components may accept options either through their constructors or via properties decorated with the @option attribute. However, when generating attributes during the ClientTypes transformation, the options associated with these custom types were not being generated for editors declared in ES modules.

Here's an example illustrating how these custom Editor options were defined:

import { Decorators } from "@serenity-is/corelib";

export interface MyEditorOptions {
    optionA: string;
    optionB: number;
}

@Decorators.registerEditor('MyProject.MyModule.MyEditor')
export class MyEditor extends Serenity.Widget<any> {
    constructor(el: JQuery, opt: MyEditorOptions) {
        // Constructor implementation
    }

    @Decorators.option
    optionC: string;

    @Decorators.option
    get optionD(): number {
        // Getter implementation
    }

    @Decorators.option
    set optionD(): number {
        // Setter implementation
    }
}

During ClientTypes transformation, attributes are generated for these custom types, and their options also generate properties in those attributes:

public class MyEditorAttribute : Attribute
{
    public string OptionA { get; set;}
    public string OptionB { get; set;}
    public string OptionC { get; set;}
    public string OptionD { get; set;}
}

To resolve this issue, we have addressed this problem in version 6.2.8.

Here are the improved release notes with grammar fixes:

Omit Generation of Row Types and Properties by Using ScriptSkip Attribute

Typically, you can prevent the transformation of .NET types to TypeScript during ServerTypings by adding a [ScriptSkip] attribute to them:

[ScriptSkip]
public class MyRow
{
}

In theory, this should prevent the generation of the MyRow.ts file on the client-side. However, it didn't always work as expected, especially when the row type was referenced elsewhere, such as in a service endpoint. Omitting the row type would result in TypeScript errors, as the generated Service.ts file would include type references to that row type in its request/response objects.

We now respect the [ScriptSkip] attribute, and wherever a reference to that row type is required, we use the any type instead. It is now also possible to selectively stop the generation of some row fields by placing the [ScriptSkip] attribute on them.

Sergen Uses Export Modifier on ServerTypings Nested Permission Key Namespaces

Sergen now adds export modifiers to permission key classes with the [NestedPermissionKeys] attribute, creating client-side counterparts like this:

export namespace PermissionKeys {
    export const General = "Northwind:General";

    export namespace Customer {
        export const Delete = "Northwind:Customer:Delete";
        export const Modify = "Northwind:Customer:Modify";
        export const View = "Northwind:Customer:View";
    }
}

This way, you can have compile-time checking and IntelliSense support when using those permission keys on the client-side. Previously, nested classes generated (e.g., the Customer namespace in the sample above) lacked export modifiers, rendering them private and inaccessible in TypeScript.

Fixed GlobFilter for Modules/**/* Style Patterns and TSFileLister

We have a GlobFilter class in Serenity.Core that can handle glob matching patterns similar to .gitignore files.

It has many optimizations for special cases, but for more complex cases it tries to convert the glob masks to RegEx objects.

A pattern like Modules/**/* means any file at any depth under a Modules folder at any depth. Any of the following should match:

  • Modules/X
  • Modules/Y/X
  • A/Modules/X

Due to a mistake in RegEx conversion, this was processed as any file at any depth under a root Modules folder.

So, A/Modules/X did not match the pattern while it should.

This also affected our TSFileLister class in Sergen which uses GlobFilter internally. TSFileLister looks at your project's tsconfig file and its Include/Exclude options to determine which files are included in the TypeScript compilation.

The TypeScript include/exclude patterns should be processed differently than .gitignore as TypeScript assumes all patterns start with a leading slash /, e.g. they are matched starting from the location of tsconfig.json file. This means, Modules/**/* should not match A/Modules/X.

Certainly, here are the revised release notes with improvements and grammar fixes:

Q.DeepClone Did Not Handle Some Types Properly

We have a deepClone method in the Q helper, which is similar to jQuery's extend method with the true parameter. This method performs a deep clone of an object recursively, although it does not merge arrays like jQuery.

However, a check in this method did not correctly handle special types such as Date objects. This issue resulted in cloned objects with invalid properties in the case of Date objects.

We have now made modifications according to the https://github.com/angus-c/just#just-clone helper. This update should improve the handling of various cases, including Date, Map, and Set objects. Please note that functions and other special types still cannot be cloned.

Changes in TSBuild

For the compilation of ES modules, we are using esbuild via our tsbuild npm package, which is invoked from tsbuild.js in the project folder.

While the default settings should suffice for most projects, we provide various options within tsbuild. Some of these options are directly passed to esbuild, while others are preprocessed because they are specific to tsbuild only.

It's important to note that we currently do not offer an auto-completion option (e.g., d.ts file) for tsbuild. However, you can review its source code to gain a better understanding of the available options by visiting the following link: tsbuild Source Code.

One of these options is the plugins array, which, when specified, is directly passed to esbuild.

The default plugins array originally included cleanPlugin and importAsGlobalsPlugin.

The cleanPlugin is responsible for removing old files from the wwwroot/esm/ folder (or the specified output folder) after esbuild completes its operation.

ESBuild might generate a different set of output files and chunks when the set of entry points and input files changes with each build. Unfortunately, ESBuild lacks an option to clean up obsolete chunks or output files. To address this issue, we developed the cleanPlugin to prevent your wwwroot/esm/ output folder from continuously growing and becoming cluttered.

While porting our feature packages, we discovered that cleanPlugin might have unintended side effects, such as the inadvertent deletion of unrelated files when the output folder is not correctly specified (for example, it erroneously deleted .ts files in our case). Therefore, we have added a check to prevent the deletion of any files other than those with .js and .js.map extensions.

Another important point to note is that cleanPlugin is only enabled when the splitting esbuild option is true (which is the default setting).

So, if you still wish to enable cleanPlugin when splitting is set to false, you need to configure the clean option as follows:

build({
    splitting: false,
    clean: true
})

The importAsGlobalsPlugin allows us to replace module imports with variables in the global context (window). This is particularly useful for mocking modules with classic namespace objects. The plugin receives a map of module names and their corresponding global variables. The default mapping includes the following:

export const importAsGlobalsMapping = {
    "@serenity-is/corelib": "Serenity",
    "@serenity-is/corelib/q": "Q",
    "@serenity-is/corelib/slick": "Slick",
    "@serenity-is/sleekgrid": "Slick",
    "@serenity-is/extensions": "Serenity.Extensions",
    "@serenity-is/pro.extensions": "Serenity"
}

Certainly, here's a revised version of the release notes with improvements and grammar fixes:


You have the option to add additional mappings as desired, as shown below:

import { build, importAsGlobalsMapping } from "@serenity-is/tsbuild";

build({
    importAsGlobals: Object.extend({}, importAsGlobalsMapping, {
        "my-library": "MyLibrary"
    })
})

By doing so, you can replace an import statement, such as the following:

import { SomeObject } from "my-library";

SomeObject.toString();

With the updated code:

const SomeObject = MyLibrary.SomeObject;
SomeObject.toString();