Skip to content

fix: add --src-root for subdirectory-aware package.json paths (#50)#52

Merged
danfma merged 2 commits intomainfrom
fix/src-root-prefix
Apr 13, 2026
Merged

fix: add --src-root for subdirectory-aware package.json paths (#50)#52
danfma merged 2 commits intomainfrom
fix/src-root-prefix

Conversation

@danfma
Copy link
Copy Markdown
Owner

@danfma danfma commented Apr 13, 2026

Summary

  • Add --src-root CLI parameter (MetanoSrcRoot MSBuild property) to specify the TypeScript source root relative to the package root
  • When output targets a subdirectory (e.g., src/domain/), the output prefix (domain) is applied to dist paths and export subpaths
  • Imports are merged preserving user-defined entries (e.g., #custom/*), with stale managed keys (#, #/*) cleaned up automatically
  • Exports are fully replaced on each transpilation (stale barrel entries removed)
  • Validate --src-root is an ancestor of the output path — emit MS0007 warning on mismatch
  • Exclude samples from release build via Metano-packages.slnx to avoid file-lock conflicts
  • Update FR-030 spec and feature matrix

Example

With MetanoOutputDir = src/domain/:

Before (wrong):

"exports": { ".": { "types": "./dist/index.d.ts" } }

After (correct):

"exports": { "./domain": { "types": "./dist/domain/index.d.ts" } }

Test plan

  • 7 new tests in EmitPackageTests (subdirectory prefix, merge, stale cleanup, explicit srcRoot)
  • 352 .NET tests pass
  • sample-todo: 18 JS tests pass
  • sample-issue-tracker: 65 JS tests pass
  • sample-todo-service: 19 JS tests pass
  • Existing sample package.json exports unchanged (backward compatible)
  • Metano-packages.slnx builds successfully

Closes #50

Copilot AI review requested due to automatic review settings April 13, 2026 23:49
Create Metano-packages.slnx with only publishable src/ projects and
point dotnet-releaser at it. Samples have AfterBuild targets that run
the compiler via `dotnet run`, causing file-lock conflicts under
parallel builds.

Also specify Metano.slnx explicitly in the CI build step since two
.slnx files now exist in the root.

Part of #50
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a --src-root / MetanoSrcRoot option so package.json imports/exports generation becomes aware of TypeScript source roots when the transpiler output targets a subdirectory (e.g., src/domain/), and updates release/build plumbing plus tests/spec-related artifacts.

Changes:

  • Add --src-root plumbing (CLI + MSBuild) and compute an output prefix to correctly prefix dist paths and export subpaths for subdirectory outputs.
  • Update PackageJsonWriter behavior to (a) merge imports while preserving user-defined entries and cleaning stale managed keys, and (b) fully replace exports on regeneration.
  • Add new tests covering subdirectory prefixing and merge/stale-cleanup behavior; adjust release build to use a packages-only solution.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/Metano.Tests/EmitPackageTests.cs Adds tests for imports merge/stale cleanup and subdirectory-aware exports/imports prefix behavior.
src/Metano.Compiler.TypeScript/PackageJsonWriter.cs Implements srcRoot/prefix computation and updates merge/replace strategy for imports/exports.
src/Metano.Compiler.TypeScript/Commands.cs Adds --src-root CLI parameter and wires it into PackageJsonWriter.
src/Metano.Build/buildTransitive/Metano.Build.targets Adds MetanoSrcRoot property passthrough to CLI args.
src/Metano.Build/build/Metano.Build.targets Adds MetanoSrcRoot property passthrough to CLI args.
js/sample-todo-service/package.json Updates generated imports to remove the # alias entry.
dotnet-releaser.toml Switches release build to Metano-packages.slnx to avoid building samples.
Metano-packages.slnx Introduces packages-only solution file for release builds.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Metano.Compiler.TypeScript/PackageJsonWriter.cs
Comment thread src/Metano.Compiler.TypeScript/PackageJsonWriter.cs
@danfma danfma force-pushed the fix/src-root-prefix branch from e0ca805 to fc39364 Compare April 13, 2026 23:53
Normalize srcRoot by trimming trailing slashes so "src/" behaves like
"src". Handle "--src-root ." as package root (full srcRelative becomes
the output prefix). Avoid using NormalizePath on srcRoot since it
strips leading ".." segments.
@danfma danfma merged commit 86c4966 into main Apr 13, 2026
@danfma danfma deleted the fix/src-root-prefix branch April 13, 2026 23:59
danfma added a commit that referenced this pull request Apr 15, 2026
)

Closes task #51 — the first of three gaps that block
UseIrBodiesWhenCovered from going default-on.

C#'s `decimal` surfaces as the decimal.js Decimal class at runtime,
which needs method-call dispatch (`a.plus(b)`, `.times`, `.div`,
`.mod`, `.eq`, `.lt`, …) instead of the raw TS `+` / `*` / `/`
operators. The legacy OperatorHandler detects both operands as
System_Decimal via the SemanticModel and rewrites the call accordingly
(TryMapDecimalBinary). The IR path used to emit raw IrBinary-
Expressions and the TS bridge had no way to recover the type info to
rewrite at lowering time.

IrExpressionExtractor.ExtractBinary now runs the same detection:
when both sides are Decimal and the operator maps to a decimal.js
method, it normalizes the expression to an IrCallExpression
(`IrMemberAccess(left, method)`, [right]). Inequality (`!=`) wraps
the call in `IrUnaryExpression(LogicalNot, …)` to preserve the
`!eq` convention. The rest of the binary-op path stays untouched
so non-Decimal operands continue to use IrBinaryExpression — no
behavior change for any other type.

Regenerating SampleOperatorOverloading under
UseIrBodiesWhenCovered = true confirms the fix: `this.cents / 100m`
now lowers through `this.cents.div(100)` instead of raw `/`, and
`amount * 100m` through `amount.times(100)`.

Three decimal-adjacent gaps remain and block the flag flip:
1. Decimal literals (`100m`) don't wrap as `new Decimal("100")`.
2. `decimal → BigInt` conversions skip the
   `.round().toFixed(0)` chain legacy emits.
3. `Math.Round(decimal)` doesn't dispatch to `.round()`.

Each is a separate extractor-side rewrite captured as task #52.
Flag goes back to false at the end of this commit so production
samples keep their canonical output.

522/522 .NET tests; all bun suites still green; every sample
(regenerated with the flag off) shows zero diff.

Part of #56
danfma added a commit that referenced this pull request Apr 15, 2026
…goes default-on (#56)

Closes task #52 and flips UseIrBodiesWhenCovered default to true
with zero diff across every regenerated sample (SampleTodo,
SampleIssueTracker, SampleTodo.Service, SampleOperatorOverloading,
and SampleCounter).

Three extractor-side rewrites, all in IrExpressionExtractor, each
mirroring a piece of the legacy ExpressionTransformer / LiteralHandler
/ InvocationHandler that used to run only via Roslyn symbols:

1. Numeric literals in a Decimal / BigInteger context. A new
   ClassifyNumericInContext consults SemanticModel.GetTypeInfo
   (lit).ConvertedType. When the inferred type is `decimal`, the
   literal is emitted as IrLiteral(text, Decimal) so the bridge
   wraps it as `new Decimal("…")`; for BigInteger contexts
   IrLiteral(text, BigInteger) produces the native `n` suffix
   (`150n`). Covers both explicit `100m` literals and implicit
   conversions in mixed-type arithmetic.

2. Numeric casts that change representation. ExtractCast takes
   over CastExpressionSyntax handling and produces the four shapes
   the legacy emits: `(decimal)bigInt` → `new Decimal(val.toString
   ())`, `(BigInteger)decimal` → `BigInt(val.toFixed(0))`,
   `(int|long|...)decimal` → `val.toNumber()`, and `(BigInteger)
   int|long|decimal` → `BigInt(val)`. Uses IrTypeReference("BigInt")
   for the global builtin so the TS bridge's camelCase pass doesn't
   rewrite it to `bigInt`.

3. `Math.Round/Floor/Ceiling/Abs` on a decimal argument. A new
   TryRewriteMathDecimalCall detects `System.Math.Round(decimal)`
   style calls and rewrites them to the receiver's instance method
   (`amount.round()`), matching decimal.js semantics. Multi-argument
   overloads fall through to the normal BCL mapping path.

Bridge support:

- IrToTsExpressionBridge.MapLiteral now emits `new Decimal("val")`
  for Decimal kind and `valn` for BigInteger kind; IntN literals
  keep the plain number form.
- MapNewTarget special-cases IrPrimitiveTypeRef(Decimal) so
  synthesized `new Decimal(...)` expressions (produced by ExtractCast)
  reach the decimal.js constructor even though the type mapper
  flattens decimal to `number` for signatures.

Validation: every TS sample plus SampleCounter.dart regenerates
byte-identical to the legacy output with the flag on. 522/522 .NET
tests; bun suites all green (18 + 65 + 19).

Follow-up: the flip to true is local to this commit — the refactor's
wrap-up will decide whether to land default-on as part of the
legacy-retirement series or keep the seam as an opt-in for another
round.

Part of #56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Subdirectory-aware package.json imports/exports (--src-root)

2 participants