Skip to content

Conversation

@jsantell
Copy link
Collaborator

@jsantell jsantell commented Oct 9, 2025

As a part of the "pretransform" step, we replace the CTS enable directive with a wildcard import for "commontools" module that is used when injecting new functionality via transformations.


Summary by cubic

Introduced a pretransform that replaces the CTS directive with import * as __ctHelpers from "commontools", and added CTHelpers so transformers access helpers reliably without custom import management. This simplifies helper resolution and runs before TypeScript binding to avoid symbol issues.

  • Refactors

    • Added CTHelpers to find and access helpers like derive, ifElse, toSchema, and JSONSchema.
    • Replaced imports.ts and cts-directive with ct-helpers; transformers now use ctHelpers instead of ImportRequirements.
    • Updated runner to pretransform programs (inject helper import and prefix files) via pretransformProgram.
    • Schema transformers detect both toSchema() and __ctHelpers.toSchema(), and use JSONSchema via a qualified name (no import edits).
    • Tests pretransform sources with transformCtDirective.
  • Migration

    • If using ts-transformers outside the runner, run transformCtDirective on sources (or add the __ctHelpers import) before compiling.
    • Do not use the reserved identifier __ctHelpers in user code.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 17 files

Prompt for AI agents (all 1 issues)

Understand the root cause of the following 1 issues and fix them.


<file name="packages/ts-transformers/src/core/ct-helpers.ts">

<violation number="1" location="packages/ts-transformers/src/core/ct-helpers.ts:102">
`getCTHelpersIdentifier` should compare the string literal’s text, not its quoted source, so helpers added with single quotes are still detected.</violation>
</file>

React with 👍 or 👎 to teach cubic. Mention @cubic-dev-ai to give feedback, ask questions, or re-run the review.

): string {
// Throw when this symbol name is already in use
// for some reason.
if (source.indexOf(CT_HELPERS_IDENTIFIER) !== -1) {
Copy link
Contributor

Choose a reason for hiding this comment

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

i think this is reasonable... just noticing it'll reject even on something like // TODO: include __ctHelpers or // note: we omit __ctHelpers or whatever. i can't really think of an important use case where this is a problem so i'm good with this as-is, mentioning for your consideration

Copy link
Contributor

Choose a reason for hiding this comment

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

i guess this also means it's not valid for users to manually write import * as __ctHelpers from commontools right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Right, we could use ts API to create a unique name, or use the existence of any * import for commontools rather than a new one, though that works less well with the sourceline preservation I think, could try that too

name: string,
): ts.PropertyAccessExpression {
if (!this.sourceHasHelpers()) {
throw new Error("Source file does not contain helpers.");
Copy link
Contributor

Choose a reason for hiding this comment

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

should we consider making this error message more suggestive, eg "Source file does not contain helpers. Did you mean to include /// <cts-enable />?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This occurs if the transformers filtering have failed, so it's on our end, not an author's

name: string,
): ts.QualifiedName {
if (!this.sourceHasHelpers()) {
throw new Error("Source file does not contain helpers.");
Copy link
Contributor

Choose a reason for hiding this comment

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

same q here re error message potentially being more explicit

return `/${id}${filename}`;
}

function isCTSEnabled(line: string) {
Copy link
Contributor

Choose a reason for hiding this comment

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

unused (i think you moved it elsewhere)

@@ -1,31 +1,5 @@
import ts from "typescript";
import {
Copy link
Contributor

Choose a reason for hiding this comment

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

very nice simplifications here and throughout!


// Helper type extending CallExpression with
// truthy typeArguments.
interface ToSchemaNode extends ts.CallExpression {
Copy link
Contributor

Choose a reason for hiding this comment

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

cool thank you this is nicer

const jsonSchemaIdentifier = context.imports.getIdentifier(
context,
{ module: "commontools", name: "JSONSchema" },
const jsonSchemaName = context.ctHelpers.getHelperQualified(
Copy link
Contributor

Choose a reason for hiding this comment

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

i had to ask claude for some help understanding the reason for getHelperQualified here. ultimately it suggests that something like the following comment might be helpful, slash, is this in fact the correct understanding?

"// Use getHelperQualified() for type positions (satisfies, type references)
// vs getHelperExpr() for value positions (function calls, property access)"

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Mostly due to that's what the SatisfiesExpression needs, as its a const statement, and when we use the expression form, it's for identifiers which could represent values or functions etc. -- TBD on what other Node forms we'll need to offers

detectCallKind,
isEventHandlerJsxAttribute,
} from "../ast/mod.ts";
import { OpaqueRefHelperName, rewriteExpression } from "./opaque-ref/mod.ts";
Copy link
Contributor

Choose a reason for hiding this comment

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

hm was this previously an unused import? i'd thought typescript would have complained about that

@@ -1,6 +1,6 @@
import ts from "typescript";

import type { Emitter, OpaqueRefHelperName } from "../types.ts";
Copy link
Contributor

Choose a reason for hiding this comment

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

same q here, previously unused?

@mathpirate
Copy link
Contributor

one general DX question... since the runner is where we do the pretransform, is there any nice way to warn users if they seem to be trying to use the transformers outside of the runner? eg if a user has /// in their source file but they try to run the transformers pipeline without sending it through the runner, is there any nice way we can indicate to them that that's probably their issue?

the only idea i had would be to check in each transformer's filter() function, which is pretty ugly and requires us, transformer authors, to remember to do that for each new transformer sub-class, so i don't love it.

otherwise i guess they'll just get typescript compiler errors about missing the injected functions?

@mathpirate
Copy link
Contributor

one other general DX question... if a user manually imports derive from commontools, and then uses it - will there be two differently qualified versions of derive in the JS that the AMD loader generates? it's probably fine right?

@mathpirate
Copy link
Contributor

this looks really great! thank you so much for figuring this out and continuing to make everything in this part of the codebase so much nicer.

@jsantell
Copy link
Collaborator Author

jsantell commented Oct 9, 2025

one other general DX question... if a user manually imports derive from commontools, and then uses it - will there be two differently qualified versions of derive in the JS that the AMD loader generates? it's probably fine right?

Yes, exactly!

@jsantell
Copy link
Collaborator Author

jsantell commented Oct 9, 2025

one general DX question... since the runner is where we do the pretransform, is there any nice way to warn users if they seem to be trying to use the transformers outside of the runner? eg if a user has /// in their source file but they try to run the transformers pipeline without sending it through the runner, is there any nice way we can indicate to them that that's probably their issue?

the only idea i had would be to check in each transformer's filter() function, which is pretty ugly and requires us, transformer authors, to remember to do that for each new transformer sub-class, so i don't love it.

otherwise i guess they'll just get typescript compiler errors about missing the injected functions?

Yeah it's tricky, ts-transformers is a part of the compilation step which also must have been preprocessed with runner-specific information. Two-pass compiling makes this nicer, at the cost of 50% slower compiles, but definite room for improvement here

@jsantell jsantell force-pushed the ct-helpers-refactor branch 2 times, most recently from e79728e to d93c319 Compare October 10, 2025 16:34
As a part of the "pretransform" step, we replace the CTS enable
directive with a wildcard import for "commontools" module that is
used when injecting new functionality via transformations.

Remove unused imports from various patterns that are now injected
by the transformers.
Copy link
Contributor

@mathpirate mathpirate left a comment

Choose a reason for hiding this comment

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

yet another big improvement thank you

@jsantell jsantell merged commit c097738 into main Oct 10, 2025
8 checks passed
@jsantell jsantell deleted the ct-helpers-refactor branch October 10, 2025 17:29
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