Skip to content

[canton-dev-tools] Extract shared DAML JS bindings tooling - #6

Merged
HardlyDifficult merged 12 commits into
mainfrom
cursor/daml-js-codegen-tooling-0d51
Aug 12, 2026
Merged

[canton-dev-tools] Extract shared DAML JS bindings tooling#6
HardlyDifficult merged 12 commits into
mainfrom
cursor/daml-js-codegen-tooling-0d51

Conversation

@HardlyDifficult

@HardlyDifficult HardlyDifficult commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Extract generic DAML→npm JS bindings tooling into @fairmint/canton-dev-tools so public consumer repos (esp. canton-assets) stay thin.

Companion: canton-assets#6.

Standard

Owned by @fairmint/canton-dev-tools Stays in consumer
LocalNet + DAR lifecycle npm identity / license / exports
codegen-js, prepare-release, collapse-manifest daml-js-bundle.json config (pins, namespaces, template constants)
bundle-dependencies, create-root-index, fix-splice-refs Import/runtime assertion lists + examples
Splice/stdlib presets (no product names) Product DAML sources

New CLI (Phase 1 + 2)

canton-dev-tools codegen-js [--root] [--skip-dpm]
canton-dev-tools bundle-dependencies [--root] [--config]
canton-dev-tools create-root-index [--root] [--config]
canton-dev-tools fix-splice-refs [--target lib]
canton-dev-tools prepare-release [--root] [--changelog-repo]
canton-dev-tools collapse-manifest

Config: daml-js-bundle.json (presets + pins + rootIndex). Zero WrappedAssets/OCP/NFT names in CDT code.

Out of scope (still consumer / later)

  • daml NFT merge / CapTableBatchError hooks
  • Migrating Fairmint/daml + open-captable-protocol-daml off local forks (same config pattern)

Merge order

  1. Merge this PR → OIDC publishes
  2. Point canton-assets at published version
  3. Merge canton-assets#6

Verify

npm test (98) · build · pack:check · lint

Open in Web Open in Cursor 

cursoragent and others added 3 commits August 12, 2026 00:08
Move shared codegen/publish helpers into @fairmint/canton-dev-tools so
consumer repos (canton-assets) can stay thin. Adds codegen-js and
prepare-release CLI commands; leaves bundle-dependencies and
create-root-index in consumers for Phase 2.

Co-authored-by: HardlyDifficult <hardlydiff@gmail.com>
Co-authored-by: HardlyDifficult <hardlydiff@gmail.com>
Git branch consumers (npm install github:…#branch) need a prepare
lifecycle so dist/cli.js exists; prepack alone only runs for npm pack/publish.

Co-authored-by: hardlydiff <hardlydiff@gmail.com>
@HardlyDifficult
HardlyDifficult marked this pull request as ready for review August 12, 2026 00:14
@HardlyDifficult
HardlyDifficult requested a balanced review from Copilot August 12, 2026 11:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Extracts reusable DAML-to-npm tooling for consumer repositories.

Changes:

  • Adds codegen discovery, rewriting, verification, and package-generation helpers.
  • Adds codegen-js, prepare-release, and collapse-manifest commands.
  • Centralizes release preparation and documents consumer integration.

Reviewed changes

Copilot reviewed 20 out of 21 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
test/unit/scripts/canton-dev-tools.test.ts Tests CLI help and dispatch.
test/unit/daml/codegen.test.ts Tests new codegen and release helpers.
src/prepare-release.ts Implements shared release preparation.
src/daml/types.ts Adds package metadata types.
src/daml/index.ts Exports codegen APIs.
src/daml/codegen/verify-package-imports.ts Detects unresolved imports.
src/daml/codegen/update-generated-package.ts Updates generated package metadata.
src/daml/codegen/install-generated-deps.ts Installs generated dependencies.
src/daml/codegen/index.ts Exports codegen helpers.
src/daml/codegen/generated-package-index.ts Generates package entry points.
src/daml/codegen/generated-output-helpers.ts Provides generated-file rewriting utilities.
src/daml/codegen/fix-splice-refs.ts Rewrites Splice references and imports.
src/daml/codegen/discover-codegen-packages.ts Discovers codegen-enabled packages.
src/daml/codegen/create-package-index.ts Creates package indexes.
src/daml/codegen/collapse-manifest.ts Collapses package manifest entries.
src/daml/codegen/codegen-js.ts Orchestrates DAML JS generation.
src/cli.ts Registers new commands.
scripts/prepare-release.ts Delegates to shared release logic.
README.md Documents the new tooling.
package.json Builds branch installs automatically.
bin/canton-dev-tools Dispatches new CLI commands.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +58 to +66
const escapedImportPath = escapeRegExp(importPath);
if (isDts) {
return source.replace(new RegExp(`from '${escapedImportPath}';`, 'g'), `from '${relativePath}';`);
}

return source.replace(
new RegExp(`require\\('${escapedImportPath}'\\)`, 'g'),
`require('${relativePath}')`
);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 175c600 — import rewrite regex now handles both single- and double-quoted require/from paths.

Comment on lines +24 to +30
for (const file of filesToKeep) {
if (file.endsWith('.d.ts') || file.endsWith('.js')) {
collapsedFiles.add(file.replace(/\.(d\.ts|js)$/, ''));
} else {
collapsedFiles.add(file);
}
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 175c600 — manifest collapse only merges entries when both .js and .d.ts counterparts exist.

Comment thread src/daml/codegen/codegen-js.ts Outdated
Comment on lines +94 to +104
const updateTargets = packages
.filter((pkg) => fs.existsSync(path.join(pkg.absoluteGeneratedJsDir, 'package.json')))
.map((pkg) => ({
dir: pkg.absoluteGeneratedJsDir,
publishedPackageName: resolvePublishedPackageName({
rootPackageName: rootPackage.name!,
pkg,
suffixes,
codegenPackageCount: packages.length,
}),
}));

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 175c600 — codegen-js throws when a discovered package lacks generated package.json instead of silently skipping.

Comment thread src/prepare-release.ts Outdated
Comment on lines +214 to +215
if (!latestNpmVersion && npmVersions.size === 0) {
const registryMetadata = getNpmMetadataFromRegistry(packageName);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 175c600 — npm/registry lookup fails closed on transient errors; 404 alone means unpublished.

Comment thread src/prepare-release.ts Outdated
encoding: 'utf8',
}).trim();
console.log(`Last tag: ${lastTag}`);
commits = execSync(`git log --oneline --format="%s" ${lastTag}..HEAD`, {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 175c600 — git log range uses execFileSync argv instead of shell-interpolated lastTag.

Comment thread src/prepare-release.ts Outdated
let commits: string;
let lastTag: string | null = null;
try {
lastTag = execSync('git describe --tags --abbrev=0 2>/dev/null', {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 175c600 — git describe --match v[0-9].[0-9].[0-9]* so only release semver tags are used.

Comment thread src/prepare-release.ts Outdated
Comment on lines +180 to +181
const match = url.match(/github\.com[/:]([^/]+\/[^/.]+)(?:\.git)?/i);
return match?.[1];

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 175c600 — anchored changelog-repo regex allows dots in repository names.

Comment thread src/prepare-release.ts Outdated
Comment on lines +107 to +116
function parseVersion(version: string): ParsedVersion | null {
const parts = version.split('.').map(Number);
if (parts.length !== 3 || parts.some(isNaN)) {
return null;
}
if (!parts.every((part) => Number.isInteger(part) && part >= 0)) {
return null;
}
return { major: parts[0]!, minor: parts[1]!, patch: parts[2]! };
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 175c600 — strict x.y.z semver validation rejects invalid segments before numeric compare.

…Phase 2)

Add general-purpose DAML→JS bundling engines driven by daml-js-bundle.json
with stdlib/Splice presets only (no product package names). Expose CLI
commands and library exports; cover with unit fixtures.

Co-authored-by: hardlydiff <hardlydiff@gmail.com>
Comment thread src/daml/codegen/daml-js-bundle-config.ts Outdated
Comment thread src/daml/codegen/create-root-index.ts
@HardlyDifficult

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

Parent consumer PR: Fairmint/canton-assets#6 (branch cursor/wrapped-assets-daml-js-0d51) depends on this branch via git dep. canton-assets CI fails at npm install with --min-release-age cannot be provided when using --before during git dep preparation — likely .npmrc in this repo. Please review Phase 2 bundle + create-root-index exports.

cursoragent and others added 2 commits August 12, 2026 12:34
- Rewrite both single- and double-quoted generated imports
- Collapse manifest entries only when .js/.d.ts pairs both exist
- Fail codegen-js when a discovered package lacks generated output
- Strict x.y.z validation, dotted repo URL parsing, fail-closed npm lookup,
  v-semver git describe, and execFileSync for git log ranges
- Validate rootIndex outputDir/copy with assertSafeRelativePath and require
  basename lib for bundle rewrite layout

Co-authored-by: hardlydiff <hardlydiff@gmail.com>
Co-authored-by: hardlydiff <hardlydiff@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 27 out of 28 changed files in this pull request and generated 3 comments.

Suppressed comments (5)

src/daml/codegen/generated-output-helpers.ts:65

  • Import discovery and verification accept both quote styles, but this rewrite only matches single-quoted specifiers. A generated file using require("...") or from "..." is detected for bundling, then left unresolved while its dependency is removed. Match and back-reference either quote style.
  // Preserve whichever quote style the generated import used.
  if (isDts) {
    return source.replace(
      new RegExp(`from (['"])${escapedImportPath}\\1;`, 'g'),
      `from $1${relativePath}$1;`
    );
  }

src/prepare-release.ts:115

  • Converting components with Number accepts non-semver forms such as 1e2.0.0, 1..0, and unsafe integers, despite the function promising x.y.z. These values can then be selected and written back as release versions. Parse the canonical numeric form explicitly (consistent with the repository's existing strict semver parser).
  } catch (error) {
    return { kind: 'error', message: getErrorMessage(error) };
  }
}

function isNpmNotFoundMessage(message: string): boolean {
  return /\b404\b|E404|Not Found|not found/i.test(message);
}

src/prepare-release.ts:252

  • lastTag comes from repository ref data and is interpolated into a shell command. Git ref names can contain shell metacharacters, so a crafted fetched tag can execute commands when release preparation runs. Invoke git with an argument array (for example, execFileSync('git', ['log', '--oneline', '--format=%s', ${lastTag}..HEAD], ...)) instead of constructing a shell string.
  const baseline =
    npmParsed && compareVersions(npmParsed, manifestParsed) > 0 ? npmParsed : manifestParsed;
  return findNextAvailableVersion(isVersionTaken, baseline.major, baseline.minor, baseline.patch);
}

src/daml/codegen/create-root-index.ts:235

  • A safe outputDir that does not end in lib is still broken. For example, with outputDir: "dist", this chooses dist as the package root, so post-bundle presets and namespace indexes are written under dist/lib, while the root index and copied trees remain directly under dist; import patching also resolves wrappers under the repository's lib. Either restrict outputDir to paths ending in lib or make these helpers operate on the configured library directory directly.
  if (path.basename(outputRel) !== 'lib') {
    throw new Error(
      `rootIndex.outputDir must resolve to a directory named "lib" (got ${JSON.stringify(outputRel)}). ` +

src/daml/codegen/create-root-index.ts:227

  • copyDirectory silently returns when a configured source entry is missing, so a typo in rootIndex.copy still reports a successful combined library and leaves generated index imports pointing at absent namespaces. Fail before copying unless each configured source exists and is a directory.
  if (!fs.existsSync(pkgLib)) {
    throw new Error(

Comment on lines +216 to +219
const outputDir = raw['outputDir'];
if (outputDir !== undefined && (typeof outputDir !== 'string' || outputDir.length === 0)) {
throw new Error(`Invalid ${label}.outputDir`);
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 175c600 — rootIndex.outputDir validated with assertSafeRelativePath and resolveContainedPath.

Comment on lines +203 to +206
const copyRaw = raw['copy'];
if (!Array.isArray(copyRaw) || copyRaw.length === 0 || !copyRaw.every((v) => typeof v === 'string')) {
throw new Error(`Invalid ${label}.copy (expected non-empty string[])`);
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 175c600 — rootIndex.copy entries validated with assertSafeRelativePath and resolveContainedPath.

Comment thread src/daml/codegen/bundle-dependencies.ts Outdated
Comment on lines +273 to +281
preset.apply({
targetDir,
generatedJsDir: options.generatedJsDir,
pins: options.pins,
willBundleAmulet,
});
applied.push(preset.id);
rewriteRules.push(...preset.rewriteRules(targetDir, options.pins));
packageJsonDeps.push(...preset.importSpecs(options.pins));

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 175c600 — preset apply records rewriteRules/deps only after successful apply (missing sources skip rewrite).

Comment thread package.json Outdated
Comment thread src/daml/codegen/bundle-dependencies.ts Outdated
@HardlyDifficult

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

Addressed Copilot + Bugbot findings in 175c600. Please re-review.

cursoragent and others added 2 commits August 12, 2026 12:52
Align with canton-assets / canton-privy-sdk: do not run prepare-release on
autofix or [skip publish] commits (or github-actions[bot] pushes). Manual
workflow_dispatch still publishes.

Co-authored-by: hardlydiff <hardlydiff@gmail.com>
Bugbot: prepare now invokes tsc directly (no nested npm run build), and
failed/missing preset materialization skips import rewrite and package.json
dep removal so packages are not left pointing at missing __bundled__ paths.

Co-authored-by: hardlydiff <hardlydiff@gmail.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 920d4a3. Configure here.

Comment thread src/daml/codegen/create-root-index.ts
Comment thread src/daml/codegen/bundle-presets.ts Outdated
Comment thread src/prepare-release.ts
cursoragent and others added 2 commits August 12, 2026 13:11
- createRootIndex only rewrites presets that materialized (apply success
  + detection targets present), matching bundleDependenciesForTarget
- splice-token-v1 wrappers/rewrites/dep cleanup only for packages that
  actually copied; zero copies still returns false
- npm-view latest fallback uses semver compare instead of string sort

Co-authored-by: hardlydiff <hardlydiff@gmail.com>
Co-authored-by: hardlydiff <hardlydiff@gmail.com>
Comment thread .github/workflows/publish.yml Outdated
cursoragent and others added 2 commits August 12, 2026 13:35
Remove publish.yml autofix/[skip publish] guards. Run npm run fix in CI
and fail when the working tree is dirty so contributors commit fixes
locally.

Co-authored-by: hardlydiff <hardlydiff@gmail.com>
Run npm run fix locally so CI no longer fails on uncommitted
formatting after the fail-on-dirty-tree workflow change.

Co-authored-by: hardlydiff <hardlydiff@gmail.com>
@HardlyDifficult
HardlyDifficult merged commit b144740 into main Aug 12, 2026
3 checks passed
@HardlyDifficult
HardlyDifficult deleted the cursor/daml-js-codegen-tooling-0d51 branch August 12, 2026 13:59
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.

3 participants