Serenity 6.3.0 Release Notes (2022-11-06)

Ported All Common Features Projects to ES Modules

The common features are projects that are shared between Serene and StartSharp:

  • Serenity.Extensions: Contains extension classes shared by Serenity apps like Excel / PDF export functionality
  • Serenity.Demo.Northwind: Northwind demo project
  • Serenity.Demo.BasicSamples: Basic samples demo project, referencing mostly Northwind modules

These are hosted in their separate repository at:

All the TypeScript code in these projects are now converted to ES modules.

We have not updated Serene template yet to ES modules, as we'll need to update movie tutorial and other sample code in our documentation to ES modules first, but we'll eventually do that.

Also even though these projects are converted to ES modules, we still produce a namespaces bundle of them for compatibility reasons as there may be still customer projects using their namespace versions, or customer projects that are using a mix of ES modules and Namespaces.

So currently, even though StartSharp is completely converted to ES Modules, we still use the namespace style output generated by esbuild at runtime.

Thus, you don't need to do any changes in your projects yet for Serenity.Extensions or others.

Ported All Pro Features Projects to ES Modules

The pro features are projects that are only accessible for StartSharp customers. They are hosted in StartSharp repository:

https://github.com/serenity-premium/startsharp/tree/master/pro-features/src

These include, but not limited to:

  • Serenity.Pro.Extensions: Contains extra mixins like drag drop grouping, auto column width, excel style filtering etc.
  • Serenity.Demo.AdvancedSamples: Advanced samples demo project, referencing Northwind and other pro modules
  • Serenity.Pro.Coder: Contains source generators, similar to Sergen but does transforms while editing in Visual Studio
  • Serenity.Pro.DataAuditLog: data audit logging
  • Serenity.Pro.DataExplorer: dynamic sql data explorer
  • Serenity.Pro.UI: React / Serenity integration lib
  • Serenity.Pro.EmailClient: imap email client implementation in React
  • Serenity.Pro.Meeting: meeting management sample module
  • Serenity.Pro.WorkLog: work log / time tracking sample module

All these projects are now converted to ES modules.

We only left out Serenity.Pro.EmailClient and Serenity.Pro.UI as the Email client is currently under a complete rewrite in another branch, and Serenity.Pro.UI will become obsolete as it is only used by the old email client.

Again, even though they are converted to ES modules, we still provide their classic namespaces style output generated by esbuild in parallel, so you won't have to do any changes in your existing projects yet.

We hope these will help our users better understand how to write ES modules style code with Serenity and their own feature libraries. It was also an important milestone for stabilizing Sergen and Serenity.Pro.Coder for ES module style transformations.

Added IFileSystem Abstraction

We were using System.IO.Abstractions library to abstract file system access so that we can write tests without having to create physical temporary folders on disk.

System.IO.Abstractions works great, and is strongly recommended. We are still using its mock file system in our test projects, but its IFileSystem interface covers all possible System.IO classes / methods and is a bit hard to partially implement without their mock file system abstraction.

For example, this caused some issues in our source generator, because of the way package references work in a source generator project.

So we decided to use a simplified file system interface, and implement that interface with System.IO.Abstractions in our test projects only:

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 only contains the most trivial shared methods we use in our code generators and upload system.

There are also IGeneratorFileSystem and IDiskUploadFileSystem interfaces that derive from this abstraction and add just a few specific methods.

The interfaces are also injectable via DI, so for example, you could simply redirect DiskUploadStorage to some other virtual file system like an SQL database table/file mapping table, by implementing the interface methods as you like.

Upload Behavior Refinements

We had ImageUploadBehavior and MultipleImageUploadBehavior behaviors, which were automatically activated for fields with [ImageUploadEditor], [FileUploadEditor], [MultipleImageUploadEditor] or [MultipleFileUploadEditor] attributes.

As those behaviors had hard-wired dependencies to these attribute types, it was not possible to activate them for any other editor types, e.g. some custom upload editor type that you could want to develop.

Instead of directly referencing the attributes, we now converted the dependency to separate interfaces like:

  • IUploadEditor
  • IUploadFileConstraints
  • IUploadFileSizeConstraints
  • IUploadImageOptions

The IUploadEditor interface has only one property:

public interface IUploadEditor
{
    bool IsMultiple { get; }
}

So the behaviors are now activated for any attribute implementing this interface, and if it returns true from IsMultiple, the multiple upload behavior is enabled, otherwise single file upload behavior is used.

The ImageUploadBehavior is now renamed to FileUploadBehavior, and MultipleImageUploadBehavior is renamed to MultipleFileUploadBehavior to align with the fact that they are not only used for image uploads.

The old behavior classes are still there for compatibility reasons, but they are abstract and obsolete.

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

We have many options that can be configurable in sergen.json, but you may not see them there as the defaults derived from conventions are usually good enough. And we don't recommend overriding the default unless you have a reason to do so.

The full set of options can be seen in GeneratorConfig.cs file in the Serenity repository:

https://github.com/serenity-is/Serenity/blob/master/src/Serenity.Net.CodeGenerator/Generator/GeneratorConfig.cs

One of these options that control MVC.cs, e.g. View Paths generation is under MVC/SearchViewPaths and MVC/StripViewPaths:

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

MVC generator in the Sergen creates a helper class named MVC in your project which helps typing Razor .cshtml view paths less error-prone and with 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, it searches for views under Modules, Views and Your.ProjectName folders under the project directory. So views under different folders are not located unless the SearchViewPaths option is manually set in the sergen.json file.

The default also includes the Areas/Your.ProjectName folder since 6.3.0

Let's say you have a file located at /Modules/MyModule/MyView.cshtml. You may access its path via the generated helper like MVC.Views.MyModule.MyView. So why not MVC.Views.Modules.MyModule.MyView instead?

That's where the StripViewPaths option comes to play. In Serenity applications, most of our views reside in the Modules folder, so we thought it is a nice idea to omit that. Thus, paths specified in the StripViewPaths option are omitted from the generated code. The default for the StripViewPaths option is the same for the SearchViewPaths option, e.g. Modules, Views and Your.ProjectName folders.

This might have an unwanted side effect of having clashing file names if any of the views under those directories have the same paths when their prefix is omitted, but we assume that does not happen often.

We also have a source generator in StartSharp named Serenity.Pro.Coder which also generates the MVC.cs file. Before this fix, the output it generated could be different from Sergen as it did not read the StripViewPaths option in sergen.json and used the defaults.

As a side note, the source generator still ignores the SearchViewPaths option as the locations of views are sent to it by the Compilation system, unlike Sergen which has to scan directories itself for files with the .cshtml extension. We'll consider filtering views by that option in the future to make it consistent.

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

Source generators in Serenity.Pro.Coder and Sergen can both perform MVC, ClientTypes and ServerTypings transformations. In StartSharp the source generator is enabled by default and Sergen is disabled unless SourceGeneratorTransform is set to false by adding a condition:

  <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>

you could disable source generator transformations and use Sergen instead.

Another way could be to remove the Serenity.Pro.Coder reference and remove the condition, but in that case, you would be also disabling the row-template-based source generator which Sergen does not have any equivalent for.

Anyway, before 6.2.7, the Serenity.Pro.Coder package did not read the SourceGeneratorTransform setting in your project file, so adding that property only meant re-enabling Sergen, and it did not disable the source generator transforms that works in the background.

This is now fixed, and now Source Generator transformations are disabled when your project file has the SourceGeneratorTransform property as false.

Possible to Disable Individual Source Generator Transforms via sergen.json

If for some reason, you only want to disable one of the MVC, ClientTypes or ServerTypings transformations, the SourceGeneratorTransform property in your project file would not be helpful as it would disable all of them in Serenity.Pro.Coder.

We now made it possible to disable them individually in sergen.json by setting the SourceGenerator property to false:

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

Going Back to Non-Incremental Source Generator for ClientTypes Transform

Source generators in .NET are normally designed to generate only C# code. This is done generally by looking again at your C# syntax trees and in some cases other types of files in your project by registering them as AdditionalFiles.

In Serenity.Pro.Coder transformations, we are doing some cheating as we are generating C# code from your TypeScript files, and we are generating TypeScript code from your C# files.

For ClientTypes transformation we have to read your TypeScript sources to discover the Editor/Formatter types and generate corresponding C# attributes for them.

Unfortunately, when we tried to register .ts files as AdditionalFiles, the syntax highlighting in Visual Studio in TypeScript files are partially lost. This is probably a bug with the TypeScript language service in Visual Studio itself.

Source generators V2 in .NET 6 came with an incremental generator feature so that the performance effect for source generators in Visual Studio while editing files could be theoretically improved as they could process only the changes that occurred:

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

Because of the .ts files as the AdditionalFiles problem mentioned before, we had to use some tricks to write an incremental generator for ClientTypes transformation. Unfortunately, it seems like that caused some other problems like new editor types or renames of existing types not being discovered during editing especially when ES modules are used, so we decided to go back to the classic source generator system for ClientTypes for now.

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

As stated in Serenity 6.2.6 release notes, there are some issues with ES modules and referenced types like the in-place add dialog types due to the runtime differences between global namespaces and ES modules.

A similar problem also applies to the Enum types.

Let's start with a sample enum to explain the issue:

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

When ServerTypings transform runs, a corresponding file (MyModule.MyColors.ts) for this enum is generated in your project:

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

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

This way the enum type is available in the global script context (window) via the path window.MyProject.MyModule.MyColors and also registered as MyProject.MyModule.MyColors in the Serenity type system.

Let's say we have a form in another module that uses this enum type:

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

And the form helper (AnotherModule.MyForm.ts) generated in TypeScript when ServerTypings is run:

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

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

A common misconception is considering this class as the actual form definition that is used to generate the form client side.

This is just a helper for you to access your form inputs by their prefixed IDs with the intellisense support. It is not used to create your form.

As you might have noticed, it does not contain even the enum type. How could it populate the enum editor with values of the MyModule.MyColors enum?

The forms defined in server-side code, are inspected via the .NET reflection by Serenity and are converted to JSON objects. PropertyGrid class renders them client-side by looking at this JSON metadata and creating corresponding editor objects.

Here is 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 specifies the form should have one field named Color, with an editor type of Enum, e.g. Serenity.EnumEditor. The enumeration type MyProject.MyModule.MyColors is passed to the editor as a constructor option.

Now, the enum editor only knows a string (MyProject.MyModule.MyColors) about the enum that it should list values for.

It asks the Serenity type system to locate this enumeration type and get the list of its values.

The Serenity type system checks if it has an enumeration type registered with that key. If it doesn't, it may try to find it in the global context (window) by its namespace and name.

In our sample, the enum generated by ServerTypings was already registered via Serenity.Decorators.registerEnumType call. And the script block containing this call is already loaded as it is compiled to YourProject.Web.js script along with any other TypeScript code in your project.

The situation is similar with grids and their set of columns. The columns are also sent to client-side as JSON objects containing similar information about the enumeration type, like the formatter type (Enum) and its enum key.

Problems start to occur when we switch to ES modules.

Let's look at 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');

You should notice that, unlike the namespaces, the enum won't be available in the global context (window) as this is a module and can only be accessed via an import statement.

But we still have a Decorators.registerEnumType and the enum type will be registered in the Serenity type system, right?

That's only partially correct. Yes, it will be registered, but ONLY IF this module is explicitly IMPORTED on the current page.

To understand the difference, let's start by looking at MyPage.ts entry-point module containing a reference to the MyGrid:

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

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

OK, so this imports 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; }
    //...
}

Let's look at the MyDialog module that imports MyForm:

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

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

And finally 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 seen an import for MyModule.MyColors anywhere so far? No, we don't. So when we try to open the dialog for MyForm we'll be greeted with an error message like the following:

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

ESbuild generates a separate script bundle for every entry point module you have in your project that only contains references to the parts of your application that are used. It has no information about our usage of MyModule.MyColors enumeration type in MyForm as that information is only available in our JSON metadata that is generated at runtime.

So, the module containing our enum type is not included here, and the Serenity type system has no way to find it by its key as it is not registered and is also not available in the global namespace.

As a workaround, we could manually import the enum type to our dialog module (MyDialog.ts):

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

[MyColors] // notice the dummy usage here. otherwise the unused import will be erased on build!

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

This time we'll get no errors, as ESBuild will include the enum in the bundle.

If you got the error in your grid, you could 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] // notice the dummy usage here. otherwise the unused import will be erased on build!

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

What if the enum was in the same namespace as MyForm? E.g. in the AnotherModule:

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

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

Somehow, you would not have the same error, and would not need the workaround. But how is it possible?

If we remember that we are importing some types from ServerTypes/AnotherModule:

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

And if we look at this ServerTypes/AnotherModule.ts:

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

As you might have noticed, this file re-exports all the other types from the AnotherModule namespace, including MyColors. Thus, even if you did not import MyColors anywhere, as you imported something from the AnotherModule, you also imported MyColors, without being aware of that.

This is an esbuild detail so we recommend not relying on this. You should not assume it will work every time, as in the future esbuild might use tree-shaking and erase that import during bundling.

Ok, so, do we have to fake-import every enum we use in our dialogs/grids? Not really.

We added an automatic workaround to the ServerTypings transform so that it does this automatically for you. But instead of importing them to your Dialog.ts and Grid.ts, it fake imports these types into Form.ts and Columns.ts files it generates:

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

export interface MyForm {
    //...

[MyColors]; // referenced types

This should solve a big portion of such issues as long as you use these Form/Columns classes somewhere. They are by default used in Dialog.ts/Grid.ts files generated by sergen. In rare cases this does not work for you, you may still use the fallback workaround.

Try to Improve Multi Project ES-Modules Type References

There is a type discovery problem when referencing types like editors, formatters, enums that are defined in another project in an ES module.

Let's say you have multi project application like our feature packages, e.g. Serenity.Demo.Northwind and Serenity.Demo.BasicSamples. BasicSamples project references some types from Northwind project in TypeScript / C# code.

For our sample, let's assume project MyProject.Common contains shared editors, formatters etc:

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

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

}

Notice that this editor class is defined in project MyProject.Common and it is registering the editor class as MyProject.General.MyEditor.

The npm package name defined for MyProject.Common in package.json is myproject.common as it should be (e.g. lowercase version of the project name):

{
    "name": "myproject.common"
}

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

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

In Form.ts file generated for this class:

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

export class SomeForm {
    SomeProperty: Widget;
}

What happened? Why ServerTypings could not locate MyEditor type and used Widget base type instead?

The problem is code generator has to work like TypeScript works. It can't scan your C# project references. It can only read .ts or .d.ts files to discover types.

MyProject.Common project when built, created a dist/index.d.ts file containing type definitions in that project:

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

export class MyEditor extends Widget<any> {

Unfortunately, TypeScript erases the decorators from .d.ts output, so the key you specified for your editor type is not available here.

This file is referenced by MyProject.Web, not the source .ts files under MyProject.Common/Modules. Thus the type lister can only parse node_modules/myproject.common/dist/index.d.ts file to discover types.

And it does not know anything about the key you assigned to the MyEditor class, which was MyProject.General.MyEditor. For the parser, the editor is MyEditor in the module myproject.common.

To overcome this problem, we are applying a workaround but it only works if the namespace/key you assign to your types matches the project name, e.g. you should assign your editor key like:

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

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

}

This time, even if TypeScript erases the decorator from .d.ts file, we'll locate it in the module myproject.common which matches the project name.

We are looking for other methods to resolve such issues, but for now please set your registration keys for all inter-project shared types as ProjectName.ClassName

Editor / Formatter Options Defined in ES Modules Was Not Generated for Client Type Attributes

When you define a custom Editor or Formatter, they may accept options via their constructor or via properties decorated with @option attribute:

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> {
    MyEditor(el: JQuery, opt: MyEditorOptions) {

    }

    @Decorators.option
    optionC: string;

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

    set optionD(): number {
    }
}

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;}
}

These options could not be generated for editors declared in ES modules. This is fixed in 6.2.8.

Omit Generation of Row Types and Properties By Using ScriptSkip Attribute

Normally it is possible to prevent the transformation of .NET types to TypeScript during ServerTypings by adding a [ScriptSkip] attribute on them:

[ScriptSkip]
public class MyRow
{
}

Theoretically, you could prevent the generation of MyRow.ts client side but it didn't always work that way, especially when the row type is referenced somewhere else like a service endpoint. Omitting the row type would result in TypeScript errors as the generated Service.ts file would have 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 any type instead.

It is now also possible to stop the generation of some row fields selectively, by placing the [ScriptSkip] attribute on them.

Sergen Uses Export Modifier on ServerTypings Nested Permission Key Namespaces

Sergen transforms permission key classes with [NestedPermissionKeys] attribute to client-side counterparts like:

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 intelli-sense support while using those permission keys client-side.

Nested classes generated, for example, the Customer namespace in the sample above did not have export modifiers, which rendered them private, e.g. 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.

Q.DeepClone Did Not Handle Some Types Properly

We have a deepClone method in Q helper, which is something similar to jQuery's extend method with true parameter, e.g. takes a deep clone of an object recursively, though it does not merge arrays unlike jQuery.

A check in this method did not correctly handle special types like Date objects, so the cloned object was invalid for such properties.

We now modified it according to https://github.com/angus-c/just#just-clone helper.

It should now handle more cases, including Date, Map, Set object etc, though functions and other special types can still not be cloned.

Changes in TSBuild

To compile ES modules code, we are using esbuild via our tsbuild npm package. It is called from tsbuild.js in the project folder.

Even though defaults should be enough for most projects, we provide some options in tsbuild. Some of them are passed directly to esbuild, while some are preprocessed as they are specific to tsbuild only.

We don't yet provide an autocompletion option (e.g. d.ts file) for tsbuild, but you have a look at its source code to understand the options:

https://github.com/serenity-is/Serenity/blob/master/src/Serenity.Scripts/tsbuild/dist/index.js

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

The default for plugins array originally included cleanPlugin and importAsGlobalsPlugin.

The cleanPlugin removes old files from wwwroot/esm/ folder (or the output folder) after esbuild runs.

ESBuild might create a different set of output files and chunks when set of entry points and input files changes on every build. And it doesn't have an option to clean obsolete chunks / output files. So we developed this plugin to prevent your wwwroot/esm/ output folder keep growing and become a garbage bin.

While porting our feature packages, we noticed that cleanPlugin might have unwanted side effects like deleting unrelated files if the output folder is not specified correctly (it deleted .ts files by mistake in our case), so we now added a check to it to prevent deleting any file other than .js and .js.map extensions.

Another thing to note is that, cleanPlugin is only enabled when splitting esbuild option is true (by default it is true).

So if you want to still enable cleanPlugin when splitting is false, you need to set clean option to true:

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

importAsGlobalsPlugin allows us to replace module imports with variables in global context (window), e.g. mocking modules with classic namespace objects. It gets a map of module names and their corresponding global variables, which is by default is this one:

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"
}

You may add some other mappings if desired like below:

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

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

This would replace an import like following:

import { SomeObject } from "my-library";

SomeObject.toString();

with:

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