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:
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();