ttsc 0.8.0-dev.20260505 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/shim/ast/shim.go CHANGED
@@ -92,6 +92,8 @@ type ParameterDeclaration = innerast.ParameterDeclaration
92
92
  type ParenthesizedTypeNode = innerast.ParenthesizedTypeNode
93
93
 
94
94
  const (
95
+ KindEndOfFile = innerast.KindEndOfFile
96
+ KindSingleLineCommentTrivia = innerast.KindSingleLineCommentTrivia
95
97
  KindCallExpression = innerast.KindCallExpression
96
98
  KindIdentifier = innerast.KindIdentifier
97
99
  KindMultiLineCommentTrivia = innerast.KindMultiLineCommentTrivia
@@ -167,12 +169,12 @@ const (
167
169
  ModifierFlagsReadonly = innerast.ModifierFlagsReadonly
168
170
  )
169
171
 
170
- func NewNodeFactory(hooks NodeFactoryHooks) *NodeFactory {
171
- return innerast.NewNodeFactory(hooks)
172
+ func NewNodeFactory(options NodeFactoryHooks) *NodeFactory {
173
+ return innerast.NewNodeFactory(options)
172
174
  }
173
175
 
174
- func NewNodeVisitor(visit func(node *Node) *Node, factory *NodeFactory, hooks NodeVisitorHooks) *NodeVisitor {
175
- return innerast.NewNodeVisitor(visit, factory, hooks)
176
+ func NewNodeVisitor(visit func(node *Node) *Node, factory *NodeFactory, options NodeVisitorHooks) *NodeVisitor {
177
+ return innerast.NewNodeVisitor(visit, factory, options)
176
178
  }
177
179
 
178
180
  func IsFunctionLike(node *Node) bool {
@@ -323,7 +323,6 @@ function serializeNativePlugins(
323
323
  return JSON.stringify(
324
324
  plugins.map((plugin) => ({
325
325
  config: plugin.config,
326
- hooks: plugin.hooks,
327
326
  name: plugin.name,
328
327
  stage: plugin.stage,
329
328
  })),
@@ -411,7 +410,7 @@ function assertCompilerHostCompatibility(
411
410
  }
412
411
  throw new Error(
413
412
  "ttsc: multiple compiler native backends cannot share one emit pass; " +
414
- "compose transform hook libraries through one aggregate host",
413
+ "compose transform libraries through one aggregate native host",
415
414
  );
416
415
  }
417
416
 
@@ -218,7 +218,6 @@ function serializeNativePlugins(
218
218
  return JSON.stringify(
219
219
  plugins.map((plugin) => ({
220
220
  config: plugin.config,
221
- hooks: plugin.hooks,
222
221
  name: plugin.name,
223
222
  stage: plugin.stage,
224
223
  })),
@@ -237,7 +236,7 @@ function assertTransformHostCompatibility(
237
236
  }
238
237
  throw new Error(
239
238
  "ttsc: multiple transform native backends cannot share one source-to-source pass; " +
240
- "compose transform hook libraries through one aggregate host",
239
+ "compose transform libraries through one aggregate native host",
241
240
  );
242
241
  }
243
242
 
@@ -369,7 +369,7 @@ function printHelp(): void {
369
369
  "Plugin contract:",
370
370
  " ttsc reads compilerOptions.plugins from tsconfig.json.",
371
371
  " Plugin modules are descriptors for ordered native transformer backends.",
372
- " JS transformOutput/transformSource hooks are not part of the public contract.",
372
+ " JS transformOutput/transformSource functions are not part of the public contract.",
373
373
  "",
374
374
  "Compatibility aliases:",
375
375
  " ttsc build [options] Same project build lane as `ttsc [options]`.",
@@ -60,7 +60,6 @@ export function loadProjectPlugins(options: {
60
60
  plugins.forEach((plugin, index) => {
61
61
  const stage = resolvePluginStage(plugin);
62
62
  validatePluginSource(plugin);
63
- validatePluginHooks(plugin, stage);
64
63
  const binary = buildSourcePlugin({
65
64
  baseDir: context.projectRoot,
66
65
  cacheDir: options.cacheDir,
@@ -72,7 +71,6 @@ export function loadProjectPlugins(options: {
72
71
  nativePlugins.push({
73
72
  binary,
74
73
  config: entries[index]!,
75
- hooks: plugin.hooks,
76
74
  name: plugin.name,
77
75
  source: plugin.source,
78
76
  stage,
@@ -92,7 +90,6 @@ function loadPluginEntry(
92
90
  if (typeof specifier !== "string" || specifier.length === 0) {
93
91
  throw new Error(`ttsc: plugin entry is missing a string "transform" field`);
94
92
  }
95
- rejectUserPhaseOptions(specifier, entry);
96
93
 
97
94
  const request = resolvePluginRequest(specifier, context.projectRoot);
98
95
  const mod = require(request) as {
@@ -111,11 +108,11 @@ function loadPluginEntry(
111
108
  `ttsc: plugin "${specifier}" does not export a valid ttsc plugin`,
112
109
  );
113
110
  }
114
- rejectJsTransformHooks(specifier, plugin);
111
+ rejectJsTransformFunctions(specifier, plugin);
115
112
  return plugin;
116
113
  }
117
114
  if (isTtscPlugin(candidate)) {
118
- rejectJsTransformHooks(specifier, candidate);
115
+ rejectJsTransformFunctions(specifier, candidate);
119
116
  return candidate;
120
117
  }
121
118
  throw new Error(
@@ -131,35 +128,18 @@ function isTtscPlugin(value: unknown): value is ITtscPlugin {
131
128
  );
132
129
  }
133
130
 
134
- function rejectJsTransformHooks(specifier: string, candidate: object): void {
131
+ function rejectJsTransformFunctions(
132
+ specifier: string,
133
+ candidate: object,
134
+ ): void {
135
135
  if ("transformSource" in candidate || "transformOutput" in candidate) {
136
136
  throw new Error(
137
- `ttsc: plugin "${specifier}" declares unsupported JS transform hooks; ` +
137
+ `ttsc: plugin "${specifier}" declares unsupported JS transform functions; ` +
138
138
  "declare a native backend instead",
139
139
  );
140
140
  }
141
141
  }
142
142
 
143
- function rejectUserPhaseOptions(
144
- specifier: string,
145
- entry: ITtscProjectPluginConfig,
146
- ): void {
147
- for (const key of [
148
- "after",
149
- "afterDeclarations",
150
- "before",
151
- "phase",
152
- "source:after",
153
- ]) {
154
- if (key in entry) {
155
- throw new Error(
156
- `ttsc: plugin entry for "${specifier}" uses unsupported ts-patch option ${JSON.stringify(key)}; ` +
157
- "ttsc plugins declare source/declaration hooks in the package descriptor, not in user tsconfig",
158
- );
159
- }
160
- }
161
- }
162
-
163
143
  function resolvePluginStage(plugin: ITtscPlugin): TtscPluginStage {
164
144
  if (plugin.stage === undefined) {
165
145
  return "transform";
@@ -168,7 +148,7 @@ function resolvePluginStage(plugin: ITtscPlugin): TtscPluginStage {
168
148
  if (plugin.stage === "output") {
169
149
  throw new Error(
170
150
  `ttsc: plugin "${plugin.name}" requested removed stage "output"; ` +
171
- "upgrade the plugin to a transform hook descriptor compatible with this ttsc version",
151
+ "upgrade the plugin to a transform-stage descriptor compatible with this ttsc version",
172
152
  );
173
153
  }
174
154
  throw new Error(
@@ -184,54 +164,6 @@ function validatePluginSource(plugin: ITtscPlugin): void {
184
164
  }
185
165
  }
186
166
 
187
- function validatePluginHooks(
188
- plugin: ITtscPlugin,
189
- stage: TtscPluginStage,
190
- ): void {
191
- const hooks = plugin.hooks;
192
- if (hooks !== undefined && typeof hooks !== "object") {
193
- throw new Error(`ttsc: plugin "${plugin.name}" hooks must be an object`);
194
- }
195
- if (stage === "check") {
196
- if (hooks !== undefined && Object.keys(hooks).length !== 0) {
197
- throw new Error(
198
- `ttsc: check plugin "${plugin.name}" must not declare transform hooks`,
199
- );
200
- }
201
- return;
202
- }
203
- if (hooks === undefined) {
204
- throw new Error(
205
- `ttsc: transform plugin "${plugin.name}" must declare source/declaration hooks in its package descriptor`,
206
- );
207
- }
208
- for (const key of Object.keys(hooks)) {
209
- if (key !== "source" && key !== "declaration") {
210
- throw new Error(
211
- `ttsc: plugin "${plugin.name}" requested unsupported hook ${JSON.stringify(key)}`,
212
- );
213
- }
214
- }
215
- if (hooks.source !== undefined && typeof hooks.source !== "boolean") {
216
- throw new Error(
217
- `ttsc: plugin "${plugin.name}" hooks.source must be boolean`,
218
- );
219
- }
220
- if (
221
- hooks.declaration !== undefined &&
222
- typeof hooks.declaration !== "boolean"
223
- ) {
224
- throw new Error(
225
- `ttsc: plugin "${plugin.name}" hooks.declaration must be boolean`,
226
- );
227
- }
228
- if (hooks.source !== true && hooks.declaration !== true) {
229
- throw new Error(
230
- `ttsc: transform plugin "${plugin.name}" must enable at least one source/declaration hook`,
231
- );
232
- }
233
- }
234
-
235
167
  function isPluginStage(value: string): value is TtscPluginStage {
236
168
  return value === "transform" || value === "check";
237
169
  }
@@ -1,20 +1,5 @@
1
1
  import type { TtscPluginStage } from "./TtscPluginStage";
2
2
 
3
- export interface ITtscPluginHooks {
4
- /**
5
- * Runs against TypeScript SourceFile AST before TypeScript-Go's built-in emit
6
- * transforms. The Program/checker are created from the original source before
7
- * this hook participates in emit.
8
- */
9
- source?: boolean;
10
-
11
- /**
12
- * Runs against declaration AST before declaration printing. This is a
13
- * plugin-package capability, never a user-facing tsconfig phase option.
14
- */
15
- declaration?: boolean;
16
- }
17
-
18
3
  /**
19
4
  * Runtime descriptor returned by a ttsc plugin module.
20
5
  *
@@ -69,9 +54,6 @@ export interface ITtscPlugin {
69
54
  * @default "transform"
70
55
  */
71
56
  stage?: TtscPluginStage;
72
-
73
- /** Native transform hook capabilities implemented by the plugin package. */
74
- hooks?: ITtscPluginHooks;
75
57
  }
76
58
 
77
59
  export function defineTtscPlugin<T extends ITtscPlugin>(plugin: T): T;
@@ -2,9 +2,7 @@
2
2
  * Pipeline stage where a plugin's lazily built Go sidecar participates.
3
3
  *
4
4
  * - `"transform"`: participates in the TypeScript-Go transform path. Transform
5
- * plugins declare their source/declaration hook capabilities in the plugin
6
- * package descriptor and do not receive emitted JavaScript or emitted file
7
- * text.
5
+ * plugins do not receive emitted JavaScript or emitted file text.
8
6
  * - `"check"`: runs before emit and reports diagnostics only. Use this for
9
7
  * lint or validation plugins that should fail the compile before JavaScript
10
8
  * or declaration output is generated.
@@ -1,5 +1,4 @@
1
1
  import type { ITtscProjectPluginConfig } from "../ITtscProjectPluginConfig";
2
- import type { ITtscPluginHooks } from "../ITtscPlugin";
3
2
  import type { TtscPluginStage } from "../TtscPluginStage";
4
3
 
5
4
  /** Native sidecar selected and built from one plugin descriptor. */
@@ -14,6 +13,4 @@ export interface ITtscLoadedNativePlugin {
14
13
  source: string;
15
14
  /** Pipeline stage where the sidecar participates. */
16
15
  stage: TtscPluginStage;
17
- /** Native transform hook capabilities declared by the plugin package. */
18
- hooks?: ITtscPluginHooks;
19
16
  }
@@ -274,7 +274,7 @@ function createProject(options = {}) {
274
274
  function writeSourcePlugin(root) {
275
275
  fs.writeFileSync(
276
276
  path.join(root, "plugin.cjs"),
277
- 'module.exports = { name: "prepare-fixture", source: "./plugin-go", hooks: { source: true } };\n',
277
+ 'module.exports = { name: "prepare-fixture", source: "./plugin-go" };\n',
278
278
  "utf8",
279
279
  );
280
280
  fs.mkdirSync(path.join(root, "plugin-go"), { recursive: true });
@@ -293,7 +293,7 @@ function writeSourcePlugin(root) {
293
293
  function writeBrokenTransformPlugin(root) {
294
294
  fs.writeFileSync(
295
295
  path.join(root, "plugin.cjs"),
296
- 'module.exports = { name: "broken-transform-fixture", source: "./plugin-go", hooks: { source: true } };\n',
296
+ 'module.exports = { name: "broken-transform-fixture", source: "./plugin-go" };\n',
297
297
  "utf8",
298
298
  );
299
299
  fs.mkdirSync(path.join(root, "plugin-go"), { recursive: true });
@@ -327,7 +327,7 @@ function writeBrokenTransformPlugin(root) {
327
327
  function writeCompilerPlugin(root) {
328
328
  fs.writeFileSync(
329
329
  path.join(root, "plugin.cjs"),
330
- 'module.exports = { name: "compile-fixture", source: "./plugin-go", hooks: { source: true } };\n',
330
+ 'module.exports = { name: "compile-fixture", source: "./plugin-go" };\n',
331
331
  "utf8",
332
332
  );
333
333
  fs.mkdirSync(path.join(root, "plugin-go"), { recursive: true });
package/utility/host.go CHANGED
@@ -18,7 +18,6 @@ import (
18
18
 
19
19
  type pluginEntry struct {
20
20
  Config map[string]any `json:"config"`
21
- Hooks map[string]any `json:"hooks"`
22
21
  Name string `json:"name"`
23
22
  Stage string `json:"stage"`
24
23
  }
@@ -35,9 +34,8 @@ type hostOptions struct {
35
34
  }
36
35
 
37
36
  type transformState struct {
38
- banner string
39
- paths *pathsRewriter
40
- strip *stripRewriter
37
+ paths *pathsRewriter
38
+ strip *stripRewriter
41
39
  }
42
40
 
43
41
  type transformResult struct {
@@ -51,7 +49,7 @@ func RunBuild(args []string) int {
51
49
  if !ok {
52
50
  return 2
53
51
  }
54
- prog, entries, state, ok := loadUtilityProgram(opts)
52
+ prog, entries, _, sourcePreamble, ok := loadUtilityProgram(opts)
55
53
  if !ok {
56
54
  return 2
57
55
  }
@@ -70,19 +68,7 @@ func RunBuild(args []string) int {
70
68
  eDiags []driver.Diagnostic
71
69
  err error
72
70
  )
73
- if state.banner != "" {
74
- hooks := shimcompiler.EmitHooks{
75
- BeforePrintJS: func(context *shimcompiler.EmitContext, file *shimast.SourceFile) *shimast.SourceFile {
76
- return applyBannerJSDoc(context, file, state.banner)
77
- },
78
- BeforePrintDeclaration: func(context *shimcompiler.EmitContext, file *shimast.SourceFile) *shimast.SourceFile {
79
- return applyBannerJSDoc(context, file, state.banner)
80
- },
81
- }
82
- res, eDiags, err = prog.EmitAllRawWithHooks(nil, hooks)
83
- } else {
84
- res, eDiags, err = prog.EmitAllRaw(nil)
85
- }
71
+ res, eDiags, err = prog.EmitAllRaw(makeSourcePreambleWriteFile(prog, sourcePreamble))
86
72
  if err != nil {
87
73
  fmt.Fprintf(os.Stderr, "ttsc utility: emit failed: %v\n", err)
88
74
  return 3
@@ -96,13 +82,13 @@ func RunBuild(args []string) int {
96
82
  return 0
97
83
  }
98
84
 
99
- // RunTransform returns the project TypeScript text after source-hook mutations.
85
+ // RunTransform returns the project TypeScript text after source mutations.
100
86
  func RunTransform(args []string) int {
101
87
  opts, ok := parseHostOptions("transform", args)
102
88
  if !ok {
103
89
  return 2
104
90
  }
105
- prog, _, _, ok := loadUtilityProgram(opts)
91
+ prog, _, _, _, ok := loadUtilityProgram(opts)
106
92
  if !ok {
107
93
  return 2
108
94
  }
@@ -163,39 +149,45 @@ func parseHostOptions(command string, args []string) (hostOptions, bool) {
163
149
  }, true
164
150
  }
165
151
 
166
- func loadUtilityProgram(opts hostOptions) (*driver.Program, []pluginEntry, transformState, bool) {
152
+ func loadUtilityProgram(opts hostOptions) (*driver.Program, []pluginEntry, transformState, string, bool) {
167
153
  entries, err := parsePluginEntries(opts.pluginsJSON)
168
154
  if err != nil {
169
155
  fmt.Fprintln(os.Stderr, err)
170
- return nil, nil, transformState{}, false
156
+ return nil, nil, transformState{}, "", false
157
+ }
158
+ sourcePreamble, err := prepareSourcePreamble(entries)
159
+ if err != nil {
160
+ fmt.Fprintln(os.Stderr, err)
161
+ return nil, nil, transformState{}, "", false
171
162
  }
172
163
  prog, diags, err := driver.LoadProgram(opts.cwd, opts.tsconfig, driver.LoadProgramOptions{
173
- ForceEmit: opts.emit,
174
- ForceNoEmit: opts.noEmit,
175
- OutDir: opts.outDir,
164
+ ForceEmit: opts.emit,
165
+ ForceNoEmit: opts.noEmit,
166
+ OutDir: opts.outDir,
167
+ SourcePreamble: sourcePreamble,
176
168
  })
177
169
  if err != nil {
178
170
  fmt.Fprintf(os.Stderr, "ttsc utility: %v\n", err)
179
- return nil, nil, transformState{}, false
171
+ return nil, nil, transformState{}, "", false
180
172
  }
181
173
  if len(diags) > 0 {
182
174
  driver.WritePrettyDiagnostics(os.Stderr, diags, opts.cwd)
183
- return nil, nil, transformState{}, false
175
+ return nil, nil, transformState{}, "", false
184
176
  }
185
177
  if diags := prog.Diagnostics(); len(diags) > 0 {
186
178
  driver.WritePrettyDiagnostics(os.Stderr, diags, opts.cwd)
187
- return nil, nil, transformState{}, false
179
+ return nil, nil, transformState{}, "", false
188
180
  }
189
181
  state, err := prepareTransforms(prog, entries)
190
182
  if err != nil {
191
183
  fmt.Fprintln(os.Stderr, err)
192
- return nil, nil, transformState{}, false
184
+ return nil, nil, transformState{}, "", false
193
185
  }
194
186
  if err := applySourceTransforms(prog, state); err != nil {
195
187
  fmt.Fprintln(os.Stderr, err)
196
- return nil, nil, transformState{}, false
188
+ return nil, nil, transformState{}, "", false
197
189
  }
198
- return prog, entries, state, true
190
+ return prog, entries, state, sourcePreamble, true
199
191
  }
200
192
 
201
193
  func parsePluginEntries(input string) ([]pluginEntry, error) {
@@ -213,12 +205,6 @@ func prepareTransforms(prog *driver.Program, entries []pluginEntry) (transformSt
213
205
  state := transformState{}
214
206
  for _, entry := range entries {
215
207
  switch entry.Name {
216
- case "@ttsc/banner":
217
- banner, err := parseBanner(entry.Config)
218
- if err != nil {
219
- return state, err
220
- }
221
- state.banner = banner
222
208
  case "@ttsc/paths":
223
209
  state.paths = newPathsRewriter(prog)
224
210
  case "@ttsc/strip":
@@ -232,6 +218,54 @@ func prepareTransforms(prog *driver.Program, entries []pluginEntry) (transformSt
232
218
  return state, nil
233
219
  }
234
220
 
221
+ func prepareSourcePreamble(entries []pluginEntry) (string, error) {
222
+ var preamble strings.Builder
223
+ for _, entry := range entries {
224
+ if entry.Name != "@ttsc/banner" {
225
+ continue
226
+ }
227
+ banner, err := parseBanner(entry.Config)
228
+ if err != nil {
229
+ return "", err
230
+ }
231
+ preamble.WriteString(banner)
232
+ }
233
+ return preamble.String(), nil
234
+ }
235
+
236
+ func makeSourcePreambleWriteFile(prog *driver.Program, sourcePreamble string) shimcompiler.WriteFile {
237
+ if sourcePreamble == "" || shouldRemoveComments(prog) {
238
+ return nil
239
+ }
240
+ return func(fileName, text string, _ *shimcompiler.WriteFileData) error {
241
+ if shouldEnsureSourcePreamble(fileName, text, sourcePreamble) {
242
+ text = sourcePreamble + text
243
+ }
244
+ return driver.DefaultWriteFile(fileName, text)
245
+ }
246
+ }
247
+
248
+ func shouldRemoveComments(prog *driver.Program) bool {
249
+ if prog == nil || prog.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig.CompilerOptions == nil {
250
+ return false
251
+ }
252
+ return prog.ParsedConfig.ParsedConfig.CompilerOptions.RemoveComments.IsTrue()
253
+ }
254
+
255
+ func shouldEnsureSourcePreamble(fileName, text, sourcePreamble string) bool {
256
+ return isSourcePreambleOutputTarget(fileName) && !strings.Contains(text, sourcePreamble)
257
+ }
258
+
259
+ func isSourcePreambleOutputTarget(fileName string) bool {
260
+ lower := strings.ToLower(filepath.ToSlash(fileName))
261
+ for _, suffix := range []string{".d.ts", ".d.mts", ".d.cts", ".js", ".jsx", ".mjs", ".cjs"} {
262
+ if strings.HasSuffix(lower, suffix) {
263
+ return true
264
+ }
265
+ }
266
+ return false
267
+ }
268
+
235
269
  func applySourceTransforms(prog *driver.Program, state transformState) error {
236
270
  for _, file := range prog.SourceFiles() {
237
271
  if state.paths != nil {
@@ -267,7 +301,8 @@ func parseBanner(config map[string]any) (string, error) {
267
301
  }
268
302
  var b strings.Builder
269
303
  sep := strings.Repeat("-", 64)
270
- b.WriteString("*\n * ")
304
+ b.WriteString("/**\n")
305
+ b.WriteString(" * ")
271
306
  b.WriteString(sep)
272
307
  b.WriteByte('\n')
273
308
  for _, line := range lines {
@@ -277,6 +312,7 @@ func parseBanner(config map[string]any) (string, error) {
277
312
  }
278
313
  b.WriteString(" *\n")
279
314
  b.WriteString(" * @packageDocumentation\n ")
315
+ b.WriteString("*/\n")
280
316
  return b.String(), nil
281
317
  }
282
318
 
@@ -284,14 +320,6 @@ func sanitizeJSDocLine(line string) string {
284
320
  return strings.ReplaceAll(line, "*/", "* /")
285
321
  }
286
322
 
287
- func applyBannerJSDoc(context *shimcompiler.EmitContext, file *shimast.SourceFile, banner string) *shimast.SourceFile {
288
- if context == nil || file == nil || banner == "" || file.Statements == nil || len(file.Statements.Nodes) == 0 {
289
- return file
290
- }
291
- context.AddSyntheticLeadingComment(file.Statements.Nodes[0], shimast.KindMultiLineCommentTrivia, banner, true)
292
- return file
293
- }
294
-
295
323
  type pathsRewriter struct {
296
324
  basePath string
297
325
  outDir string