Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

All notable changes to this project will be documented in this file.

## [3.1.4]

### Added
- **`Cocoar.JsEval.Linq` — `linq.d.ts` is now auto-emitted by `TsDefinitionService.GetTsDefinitions()` whenever `AddLinq()` is on the builder.** Previously the `linq` runtime global (`linq.guid('…')`, `linq.decimal('…')`, `linq.today()`, …) had no TypeScript declaration — Monaco flagged every use as `Cannot find name 'linq'` even though it worked at runtime. The hand-written `linq.d.ts` that accompanied `LinqCasts.Register` is now shipped through a new `IJsTsDefinitionContributor` interface: `AddLinq()` registers `LinqTsContributor` as a singleton, and `TsDefinitionService` merges its output into the returned dictionary. Zero configuration on the consumer side — if `AddLinq()` is called, the definition shows up. Any third-party package with its own runtime-only globals can use the same interface. Contributor output is applied last, so hosts can override bundled files if needed.
- **`JsEvalBuilder.AddTypeAlias<T>("ShortName")` and `MapNamespace(prefix, target)`** — register short-name aliases that are *simultaneously*:
- Emitted at **root scope** in the generated `.d.ts` so Monaco's hover popup shows `CustomerView` instead of `TimeToDo.Infrastructure.Persistence.Marten.Projections.Customers.CustomerView`
- Resolvable by `NewObject("CustomerView")` at runtime — same name works on both sides
Cross-references between aliased/mapped types use the short name too, so `class PrincipalDirectory { Person: PersonData }` renders without the long namespace noise. Single source of truth lives on `JsEngineOptions.TypeAliases` / `JsEngineOptions.NamespaceMappings`; `TsDefinitionService` (when DI-resolved) reads the same data so the two layers cannot drift apart.
- **`DefinitionBuilder.AddType(Type, string alias)` and `DefinitionBuilder.MapNamespace(source, target)`** — the standalone-builder equivalent for consumers not going through DI. Same collision rules.
- **`IJsTsDefinitionContributor` and `JsEvalBuilder.AddTsDefinitionContributor<T>()`** — a general extension point for any package that wants to contribute `.d.ts` files without being registered as an `IJsModule`. `Cocoar.JsEval.Linq` is the first internal user.

### Changed
- **Namespace mappings respect `System.*` by default.** `MapNamespace("", "")` or `MapNamespace("TimeToDo", "")` do not re-home System types — they stay fully qualified as `System.Guid`, `System.DateTime`, etc. The FR had flagged this as the safest default; the alternative (flattening BCL types too) would produce noisy name collisions and pointless short `Guid`/`Int32`/`String` aliases that Monaco already understands via `lib.es*`.
- **Collision detection fires only when a rule is involved.** Two distinct types that naturally share a short name (e.g. nested `Span<T>.Enumerator` and `ReadOnlySpan<T>.Enumerator`, both rendered as `Enumerator$1` under `System`) keep the v3.1.3 pre-existing behavior of emitting two matching `interface` declarations which TypeScript merges. Once any alias or `MapNamespace` rule touches one of the colliding types, the resolver throws `InvalidOperationException` at render-time (or `NewObject`-map-build-time) with both source types named and three suggested fixes — so the user gets immediate feedback when shaping the output, but unconfigured consumers don't regress. Collisions between two *explicit* aliases on the same name throw at registration time, not render time.

## [3.1.3]

### Removed (breaking for direct lib.* consumers)
Expand Down
4 changes: 2 additions & 2 deletions GitVersion.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
mode: ContinuousDeployment

branches:
main:
regex: ^main$
develop:
regex: ^develop$
label: beta
is-main-branch: true

Expand Down
21 changes: 20 additions & 1 deletion src/JsEval.Engine/JsEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,29 @@ public JsEngine(IServiceProvider serviceProvider, IJsModuleRegistry moduleRegist
Initialize();
}

/// <summary>
/// Resolve a NewObject(name, ...) call. First consults
/// <see cref="JsEngineOptions.TypeAliases"/> for user-registered short names;
/// if not found, falls back to the legacy <see cref="TypeHelper.CreateObject"/>
/// path (which still handles TypeScript-specific aliases like "date" →
/// System.DateTime and assembly-qualified lookups).
/// </summary>
private object? ResolveAndCreate(string typeName, object[] parameters)
{
if (!string.IsNullOrEmpty(typeName) && Options.TypeAliases.TryGetValue(typeName, out var aliasedType))
{
parameters ??= Array.Empty<object>();
return parameters.Length > 0
? Activator.CreateInstance(aliasedType, parameters)
: Activator.CreateInstance(aliasedType);
}
return TypeHelper.CreateObject(typeName, parameters);
}

private void Initialize()
{
_engine.SetValue("exit", new Action(Stop));
_engine.SetValue("NewObject", new Func<string, object[], object?>(TypeHelper.CreateObject));
_engine.SetValue("NewObject", new Func<string, object[], object?>(ResolveAndCreate));
_engine.SetValue("require", new Func<string, JsValue>(Require));

RegisterConsole();
Expand Down
65 changes: 65 additions & 0 deletions src/JsEval.Engine/JsEngineOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,69 @@ public JsEngineOptions RegisterEngineConfigurator(Action<Jint.Engine> configurat
EngineConfigurators.Add(configurator);
return this;
}

/// <summary>
/// Short-name aliases for types. Single source of truth consumed by:
/// <list type="bullet">
/// <item><description><c>JsEngine</c>'s <c>NewObject(...)</c> resolver — <c>NewObject("CustomerView")</c> works</description></item>
/// <item><description><c>TsDefinitionService</c> — <c>.d.ts</c> renders the short name so Monaco hovers show the alias instead of the full namespace path</description></item>
/// </list>
/// Populated by <c>JsEvalBuilder.AddTypeAlias</c> and <c>JsEvalBuilder.MapNamespace</c>.
/// </summary>
public Dictionary<string, Type> TypeAliases { get; } = new(StringComparer.Ordinal);

/// <summary>
/// Ordered list of (sourcePrefix, targetPrefix) namespace mappings. Applied
/// in insertion order. When a matched type lands on an already-taken alias
/// derived from another type, collision detection throws with both types
/// listed.
/// </summary>
public List<(string Source, string Target)> NamespaceMappings { get; } = new();

/// <summary>
/// Register an explicit short-name alias for a type. Used by both
/// <c>NewObject(...)</c> at runtime and the <c>.d.ts</c> renderer at save time.
/// Throws if the alias is already assigned to a different type.
/// </summary>
public JsEngineOptions AddTypeAlias(Type type, string alias)
{
ArgumentNullException.ThrowIfNull(type);
if (string.IsNullOrWhiteSpace(alias))
throw new ArgumentException("Alias must be a non-empty identifier.", nameof(alias));

var resolved = type.IsGenericType && !type.IsGenericTypeDefinition
? type.GetGenericTypeDefinition()
: type;

if (TypeAliases.TryGetValue(alias, out var existing) && existing != resolved)
{
throw new InvalidOperationException(
$"Type alias '{alias}' is already assigned to '{existing.FullName}'. " +
$"Cannot reassign it to '{resolved.FullName}'. " +
$"Pick a different alias to disambiguate.");
}
TypeAliases[alias] = resolved;
return this;
}

/// <summary>
/// Map a source namespace prefix to a target namespace prefix. Any type whose
/// namespace starts with <paramref name="sourcePrefix"/> has that prefix
/// replaced by <paramref name="targetPrefix"/> when rendering the <c>.d.ts</c>
/// and when resolving <c>NewObject(name)</c>. Empty <paramref name="targetPrefix"/>
/// flattens matched types to the root scope (short names).
/// <para>
/// <c>System.*</c> types are excluded by default. Collisions where two distinct
/// types would end up with the same resolved name surface as
/// <see cref="InvalidOperationException"/> at resolution time (render or
/// NewObject map build) — not silently overridden.
/// </para>
/// </summary>
public JsEngineOptions MapNamespace(string sourcePrefix, string targetPrefix)
{
ArgumentNullException.ThrowIfNull(sourcePrefix);
ArgumentNullException.ThrowIfNull(targetPrefix);
NamespaceMappings.Add((sourcePrefix, targetPrefix));
return this;
}
}
53 changes: 53 additions & 0 deletions src/JsEval.Engine/JsEvalBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Cocoar.JsEval;
using Microsoft.Extensions.DependencyInjection;

namespace Cocoar.JsEval.Engine;

Expand All @@ -12,6 +14,27 @@ public sealed class JsEvalBuilder
internal JsEngineOptions Options { get; } = new();
internal JsModuleRegistry ModuleRegistry { get; } = new();

/// <summary>
/// Deferred DI-side registrations applied by <c>ServiceCollectionExtensions.AddJsEval</c>
/// after the builder callback runs. Used by add-on packages (e.g. <c>AddLinq</c>)
/// that need to register services (like <see cref="IJsTsDefinitionContributor"/>s)
/// alongside their engine-configurator hook.
/// </summary>
internal List<Action<IServiceCollection>> DeferredRegistrations { get; } = new();

/// <summary>
/// Registers a type that implements <see cref="IJsTsDefinitionContributor"/> as
/// a singleton when the host calls <c>services.AddJsEval(b =&gt; ...)</c>.
/// Used by add-on packages to contribute <c>.d.ts</c> files to
/// <c>TsDefinitionService.GetTsDefinitions()</c> without being registered as
/// a <see cref="IJsModule"/>.
/// </summary>
public JsEvalBuilder AddTsDefinitionContributor<T>() where T : class, IJsTsDefinitionContributor
{
DeferredRegistrations.Add(sc => sc.AddSingleton<IJsTsDefinitionContributor, T>());
return this;
}

public JsEvalBuilder EnableFetch()
{
Options.EnableFetch();
Expand Down Expand Up @@ -74,4 +97,34 @@ public JsEvalBuilder AllowCurrentDomainAssemblies()
Options.AllowCurrentDomainAssemblies();
return this;
}

/// <summary>
/// Register an explicit short-name alias for a type. Usable immediately in
/// scripts as <c>NewObject("Alias")</c>, and emitted as the short name in
/// <c>.d.ts</c> output so Monaco hovers show the alias rather than the full
/// namespace path.
/// Throws if the alias is already assigned to a different type.
/// </summary>
public JsEvalBuilder AddTypeAlias<T>(string alias) => AddTypeAlias(typeof(T), alias);

/// <inheritdoc cref="AddTypeAlias{T}(string)"/>
public JsEvalBuilder AddTypeAlias<T>() => AddTypeAlias(typeof(T), typeof(T).Name);

/// <inheritdoc cref="AddTypeAlias{T}(string)"/>
public JsEvalBuilder AddTypeAlias(Type type, string alias)
{
Options.AddTypeAlias(type, alias);
return this;
}

/// <summary>
/// Map a source namespace prefix to a target namespace prefix for both
/// <c>.d.ts</c> emission and <c>NewObject(...)</c> resolution. See
/// <see cref="JsEngineOptions.MapNamespace(string, string)"/> for details.
/// </summary>
public JsEvalBuilder MapNamespace(string sourcePrefix, string targetPrefix)
{
Options.MapNamespace(sourcePrefix, targetPrefix);
return this;
}
}
6 changes: 6 additions & 0 deletions src/JsEval.Engine/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ public static IServiceCollection AddJsEval(this IServiceCollection services, Act

services.TryAddSingleton(builder.Options);
services.TryAddSingleton<IJsModuleRegistry>(builder.ModuleRegistry);

// Apply any deferred DI registrations (e.g. IJsTsDefinitionContributor
// singletons queued by AddLinq/AddTsDefinitionContributor on the builder).
foreach (var register in builder.DeferredRegistrations)
register(services);

// Scoped, not Transient: Jint engines are not thread-safe, and multiple
// services resolving JsEngine in the same request should share one engine
// so globals set via SetValue are visible across them. Consumers that
Expand Down
Loading
Loading