@ttsc/unplugin 0.12.3 → 0.13.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/lib/index.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}
1
+ {"version":3,"file":"index.mjs","sources":["../src/index.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAAA;;;;;;;;AAQG;;;;"}
package/lib/next.d.ts CHANGED
@@ -1,8 +1,37 @@
1
1
  import type { TtscUnpluginOptions } from "./core/options";
2
+ /**
3
+ * Minimal structural type for a Next.js configuration object.
4
+ *
5
+ * Only the `webpack` field is used by this adapter; all other Next.js options
6
+ * are forwarded as-is through the spread operator.
7
+ */
2
8
  export type NextLikeConfig = Record<string, unknown> & {
9
+ /**
10
+ * Optional existing webpack customisation hook. When the caller has already
11
+ * defined one, `next()` will chain through to it after injecting the ttsc
12
+ * webpack plugin.
13
+ */
3
14
  webpack?: (config: WebpackLikeConfig, options: unknown) => WebpackLikeConfig;
4
15
  };
16
+ /**
17
+ * Minimal structural type for a webpack configuration object as seen by the
18
+ * Next.js `webpack` hook callback.
19
+ */
5
20
  export type WebpackLikeConfig = Record<string, unknown> & {
21
+ /** The webpack plugin array; initialised to `[]` by this adapter if absent. */
6
22
  plugins?: unknown[];
7
23
  };
24
+ /**
25
+ * Wrap a Next.js config object so that the ttsc webpack plugin is injected into
26
+ * every webpack build Next.js performs.
27
+ *
28
+ * The adapter uses `unshift` to ensure the ttsc plugin runs before any other
29
+ * plugins in the array (unplugin `enforce: "pre"` semantics). An existing
30
+ * `nextConfig.webpack` hook is preserved and called after the plugin is
31
+ * injected.
32
+ *
33
+ * @param nextConfig - The caller's existing Next.js config (spread into the
34
+ * returned object unchanged, except for `webpack`).
35
+ * @param options - Ttsc plugin options forwarded to the webpack adapter.
36
+ */
8
37
  export default function next(nextConfig?: NextLikeConfig, options?: TtscUnpluginOptions): NextLikeConfig;
package/lib/next.js CHANGED
@@ -4,11 +4,25 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var webpack = require('./webpack.js');
6
6
 
7
+ /**
8
+ * Wrap a Next.js config object so that the ttsc webpack plugin is injected into
9
+ * every webpack build Next.js performs.
10
+ *
11
+ * The adapter uses `unshift` to ensure the ttsc plugin runs before any other
12
+ * plugins in the array (unplugin `enforce: "pre"` semantics). An existing
13
+ * `nextConfig.webpack` hook is preserved and called after the plugin is
14
+ * injected.
15
+ *
16
+ * @param nextConfig - The caller's existing Next.js config (spread into the
17
+ * returned object unchanged, except for `webpack`).
18
+ * @param options - Ttsc plugin options forwarded to the webpack adapter.
19
+ */
7
20
  function next(nextConfig = {}, options) {
8
21
  return {
9
22
  ...nextConfig,
10
23
  webpack(config, webpackOptions) {
11
24
  config.plugins = Array.isArray(config.plugins) ? config.plugins : [];
25
+ // Prepend so ttsc runs before any user-added plugins.
12
26
  config.plugins.unshift(webpack.default(options));
13
27
  if (typeof nextConfig.webpack === "function") {
14
28
  return nextConfig.webpack(config, webpackOptions);
package/lib/next.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"next.js","sources":["../src/next.ts"],"sourcesContent":[null],"names":["webpack"],"mappings":";;;;;;AAWc,SAAU,IAAI,CAC1B,UAAA,GAA6B,EAAE,EAC/B,OAA6B,EAAA;IAE7B,OAAO;AACL,QAAA,GAAG,UAAU;QACb,OAAO,CAAC,MAAyB,EAAE,cAAuB,EAAA;YACxD,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC,OAAO,GAAG,EAAE;YACpE,MAAM,CAAC,OAAO,CAAC,OAAO,CAACA,eAAO,CAAC,OAAO,CAAC,CAAC;AACxC,YAAA,IAAI,OAAO,UAAU,CAAC,OAAO,KAAK,UAAU,EAAE;gBAC5C,OAAO,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,cAAc,CAAC;YACnD;AACA,YAAA,OAAO,MAAM;QACf,CAAC;KACF;AACH;;;;"}
1
+ {"version":3,"file":"next.js","sources":["../src/next.ts"],"sourcesContent":[null],"names":["webpack"],"mappings":";;;;;;AA2BA;;;;;;;;;;;;AAYG;AACW,SAAU,IAAI,CAC1B,UAAA,GAA6B,EAAE,EAC/B,OAA6B,EAAA;IAE7B,OAAO;AACL,QAAA,GAAG,UAAU;QACb,OAAO,CAAC,MAAyB,EAAE,cAAuB,EAAA;YACxD,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC,OAAO,GAAG,EAAE;;YAEpE,MAAM,CAAC,OAAO,CAAC,OAAO,CAACA,eAAO,CAAC,OAAO,CAAC,CAAC;AACxC,YAAA,IAAI,OAAO,UAAU,CAAC,OAAO,KAAK,UAAU,EAAE;gBAC5C,OAAO,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,cAAc,CAAC;YACnD;AACA,YAAA,OAAO,MAAM;QACf,CAAC;KACF;AACH;;;;"}
package/lib/next.mjs CHANGED
@@ -1,10 +1,24 @@
1
1
  import webpack from './webpack.mjs';
2
2
 
3
+ /**
4
+ * Wrap a Next.js config object so that the ttsc webpack plugin is injected into
5
+ * every webpack build Next.js performs.
6
+ *
7
+ * The adapter uses `unshift` to ensure the ttsc plugin runs before any other
8
+ * plugins in the array (unplugin `enforce: "pre"` semantics). An existing
9
+ * `nextConfig.webpack` hook is preserved and called after the plugin is
10
+ * injected.
11
+ *
12
+ * @param nextConfig - The caller's existing Next.js config (spread into the
13
+ * returned object unchanged, except for `webpack`).
14
+ * @param options - Ttsc plugin options forwarded to the webpack adapter.
15
+ */
3
16
  function next(nextConfig = {}, options) {
4
17
  return {
5
18
  ...nextConfig,
6
19
  webpack(config, webpackOptions) {
7
20
  config.plugins = Array.isArray(config.plugins) ? config.plugins : [];
21
+ // Prepend so ttsc runs before any user-added plugins.
8
22
  config.plugins.unshift(webpack(options));
9
23
  if (typeof nextConfig.webpack === "function") {
10
24
  return nextConfig.webpack(config, webpackOptions);
package/lib/next.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"next.mjs","sources":["../src/next.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAWc,SAAU,IAAI,CAC1B,UAAA,GAA6B,EAAE,EAC/B,OAA6B,EAAA;IAE7B,OAAO;AACL,QAAA,GAAG,UAAU;QACb,OAAO,CAAC,MAAyB,EAAE,cAAuB,EAAA;YACxD,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC,OAAO,GAAG,EAAE;YACpE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;AACxC,YAAA,IAAI,OAAO,UAAU,CAAC,OAAO,KAAK,UAAU,EAAE;gBAC5C,OAAO,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,cAAc,CAAC;YACnD;AACA,YAAA,OAAO,MAAM;QACf,CAAC;KACF;AACH;;;;"}
1
+ {"version":3,"file":"next.mjs","sources":["../src/next.ts"],"sourcesContent":[null],"names":[],"mappings":";;AA2BA;;;;;;;;;;;;AAYG;AACW,SAAU,IAAI,CAC1B,UAAA,GAA6B,EAAE,EAC/B,OAA6B,EAAA;IAE7B,OAAO;AACL,QAAA,GAAG,UAAU;QACb,OAAO,CAAC,MAAyB,EAAE,cAAuB,EAAA;YACxD,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC,OAAO,GAAG,EAAE;;YAEpE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;AACxC,YAAA,IAAI,OAAO,UAAU,CAAC,OAAO,KAAK,UAAU,EAAE;gBAC5C,OAAO,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,cAAc,CAAC;YACnD;AACA,YAAA,OAAO,MAAM;QACf,CAAC;KACF;AACH;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/unplugin",
3
- "version": "0.12.3",
3
+ "version": "0.13.0",
4
4
  "description": "Bundler adapters for ttsc plugins.",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/index.mjs",
@@ -89,7 +89,7 @@
89
89
  "@rollup/plugin-node-resolve": "^16.0.3",
90
90
  "@rollup/plugin-typescript": "^12.3.0",
91
91
  "@types/node": "^25.3.0",
92
- "@typescript/native-preview": "7.0.0-dev.20260518.1",
92
+ "@typescript/native-preview": "7.0.0-dev.20260522.1",
93
93
  "esbuild": "^0.25.12",
94
94
  "rimraf": "^6.1.2",
95
95
  "rollup": "^4.60.3",
@@ -99,7 +99,7 @@
99
99
  "tslib": "^2.8.1",
100
100
  "typescript": "^6.0.3",
101
101
  "vite": "^7.1.12",
102
- "ttsc": "0.12.3"
102
+ "ttsc": "0.13.0"
103
103
  },
104
104
  "repository": {
105
105
  "type": "git",
package/src/api.ts CHANGED
@@ -1 +1,8 @@
1
+ /**
2
+ * Public API surface for `@ttsc/unplugin`.
3
+ *
4
+ * Re-exports everything from `core/index` so consumers can access the unified
5
+ * `unplugin` instance, the transform helpers, and the option types from the
6
+ * `@ttsc/unplugin/api` entry point without importing directly from `core/`.
7
+ */
1
8
  export * from "./core/index";
package/src/bun.ts CHANGED
@@ -2,22 +2,51 @@ import fs from "node:fs/promises";
2
2
  import type { UnpluginContextMeta } from "unplugin";
3
3
 
4
4
  import { unplugin } from "./api";
5
+ import { sourceFilePattern } from "./core/index";
5
6
  import type { TtscUnpluginOptions } from "./core/options";
6
7
 
8
+ /**
9
+ * Minimal subset of the Bun plugin API consumed by this adapter.
10
+ *
11
+ * Bun does not yet ship TypeScript types for its bundler plugin interface, so
12
+ * we define the subset we need here. This keeps the adapter free of a Bun
13
+ * runtime dependency while remaining type-safe.
14
+ */
7
15
  export interface BunLikePlugin {
16
+ /** Plugin identifier shown in Bun bundler output. */
8
17
  name: string;
18
+ /** Called by Bun when the plugin is registered. */
9
19
  setup(build: BunLikeBuild): void | Promise<void>;
10
20
  }
11
21
 
22
+ /**
23
+ * Minimal subset of the Bun `BuildConfig` plugin build object.
24
+ *
25
+ * Only the `onLoad` hook is used; other hooks are not needed for a
26
+ * source-to-source transform.
27
+ */
12
28
  export interface BunLikeBuild {
29
+ /**
30
+ * Register a loader callback for files matching `filter`.
31
+ *
32
+ * The callback receives the absolute file path and must return the
33
+ * transformed file contents.
34
+ */
13
35
  onLoad(
14
36
  options: { filter: RegExp },
15
37
  loader: (args: { path: string }) => Promise<{ contents: string }>,
16
38
  ): void;
17
39
  }
18
40
 
19
- const sourceFilePattern = /\.[cm]?tsx?$/;
20
-
41
+ /**
42
+ * Create a ttsc plugin for Bun's bundler.
43
+ *
44
+ * Bun does not implement the unplugin protocol, so this adapter instantiates
45
+ * the raw unplugin transform and wires it to Bun's `onLoad` hook manually. The
46
+ * adapter reads each matching file from disk and forwards the content to the
47
+ * ttsc transform; if the transform returns no changes the original source is
48
+ * passed through unchanged.
49
+ */
21
50
  export default function bun(options?: TtscUnpluginOptions): BunLikePlugin {
22
51
  return {
23
52
  name: "ttsc-unplugin",
@@ -29,6 +58,7 @@ export default function bun(options?: TtscUnpluginOptions): BunLikePlugin {
29
58
  typeof raw.transform === "function"
30
59
  ? await raw.transform.call({} as never, source, args.path)
31
60
  : undefined;
61
+ // Unpack both shorthand string and object result shapes.
32
62
  if (typeof result === "string") {
33
63
  return { contents: result };
34
64
  }
package/src/core/index.ts CHANGED
@@ -11,10 +11,30 @@ import {
11
11
  } from "./transform";
12
12
 
13
13
  const name = "ttsc-unplugin";
14
- const sourceFilePattern = /\.[cm]?tsx?$/;
14
+ /**
15
+ * Matches any TypeScript or JavaScript source extension (.ts, .tsx, .mts, .cts,
16
+ * etc.). Shared with the Bun adapter (`bun.ts`) so the filter is defined once
17
+ * and both adapters stay in sync.
18
+ */
19
+ export const sourceFilePattern = /\.[cm]?tsx?$/;
20
+ /** Matches any path segment that is a `node_modules` directory (cross-platform). */
15
21
  const nodeModulesPattern = /(?:^|[/\\])node_modules(?:[/\\]|$)/;
22
+ /**
23
+ * Matches virtual module ids — Rollup/Vite use a leading NUL byte (`\0`) as
24
+ * convention.
25
+ */
16
26
  const virtualModulePattern = /\0/;
17
27
 
28
+ /**
29
+ * Unplugin factory that wires the ttsc transform pipeline into any supported
30
+ * bundler (Vite, Rollup, Rolldown, webpack, Rspack, esbuild, Farm).
31
+ *
32
+ * The factory resolves raw options once, creates a per-build transform cache,
33
+ * and captures Vite alias configuration via the `vite.configResolved` hook so
34
+ * that path aliases are forwarded to the generated tsconfig overlay. The cache
35
+ * is cleared on every `buildStart` to avoid stale results across watch-mode
36
+ * rebuilds.
37
+ */
18
38
  const unpluginFactory: UnpluginFactory<
19
39
  TtscUnpluginOptions | undefined,
20
40
  false
@@ -63,6 +83,13 @@ export { createTtscTransformCache, resolveOptions, transformTtsc, unplugin };
63
83
 
64
84
  export default unplugin;
65
85
 
86
+ /**
87
+ * Returns `true` when the module id refers to a real TypeScript/JavaScript
88
+ * source file that should be processed by the ttsc transform.
89
+ *
90
+ * Excluded ids: virtual modules (NUL prefix), `.d.ts` declaration files, and
91
+ * anything inside `node_modules`.
92
+ */
66
93
  function isTransformTarget(id: string): boolean {
67
94
  return (
68
95
  sourceFilePattern.test(id) &&
@@ -1,7 +1,9 @@
1
1
  import type { ITtscProjectPluginConfig } from "ttsc";
2
2
 
3
+ /** Raw compiler-options overlay supplied by the caller as a plain JSON value. */
3
4
  export type TtscUnpluginCompilerOptionsJson = Record<string, unknown>;
4
5
 
6
+ /** Options accepted by the `@ttsc/unplugin` bundler adapter. */
5
7
  export interface TtscUnpluginOptions {
6
8
  /**
7
9
  * Project config used by the bundler adapter.
@@ -29,9 +31,22 @@ export interface TtscUnpluginOptions {
29
31
  plugins?: readonly ITtscProjectPluginConfig[] | false;
30
32
  }
31
33
 
34
+ /**
35
+ * Fully-resolved plugin options with all defaults applied.
36
+ *
37
+ * Produced by {@link resolveOptions}; consumed internally by the transform
38
+ * pipeline. Every field is present and normalised — callers should not
39
+ * construct this type directly.
40
+ */
32
41
  export interface ResolvedTtscUnpluginOptions {
42
+ /** Compiler-options overlay applied on top of the discovered tsconfig. */
33
43
  compilerOptions: TtscUnpluginCompilerOptionsJson;
44
+ /**
45
+ * Resolved plugin list; mirrors the semantics of
46
+ * {@link TtscUnpluginOptions.plugins}.
47
+ */
34
48
  plugins?: readonly ITtscProjectPluginConfig[] | false;
49
+ /** Resolved path to the project tsconfig, or `undefined` to auto-discover. */
35
50
  project?: string;
36
51
  }
37
52
 
@@ -41,6 +56,13 @@ const defaultOptions: ResolvedTtscUnpluginOptions = {
41
56
  project: undefined,
42
57
  };
43
58
 
59
+ /**
60
+ * Normalise raw user-supplied options into {@link ResolvedTtscUnpluginOptions}.
61
+ *
62
+ * Merges provided values with defaults. The `plugins` field uses an explicit
63
+ * `"plugins" in options` presence check rather than a falsy guard so that
64
+ * `plugins: false` (disable all plugins) is preserved as-is.
65
+ */
44
66
  export function resolveOptions(
45
67
  options: TtscUnpluginOptions = {},
46
68
  ): ResolvedTtscUnpluginOptions {
@@ -11,31 +11,86 @@ import type { TransformResult } from "unplugin";
11
11
 
12
12
  import type { ResolvedTtscUnpluginOptions } from "./options";
13
13
 
14
+ /**
15
+ * The normalised transform result type that this module produces.
16
+ *
17
+ * Excludes the shorthand `string`, `null`, and `undefined` variants of
18
+ * unplugin's `TransformResult` so callers always receive an object or
19
+ * `undefined`.
20
+ */
14
21
  export type TtscTransformResult = Exclude<
15
22
  TransformResult,
16
23
  string | null | undefined
17
24
  >;
18
25
 
26
+ /**
27
+ * Normalised alias entry used when building the `paths` overlay for the
28
+ * generated tsconfig. Derived from either a Vite array alias or a webpack/
29
+ * Rspack object alias.
30
+ */
19
31
  export interface TtscTransformAlias {
32
+ /** The alias key (module specifier prefix). */
20
33
  find: string;
34
+ /** Absolute or cwd-relative path that the alias points to. */
21
35
  replacement: string;
22
36
  }
23
37
 
38
+ /**
39
+ * A single entry in the per-build transform cache.
40
+ *
41
+ * Stores the full compiler result together with SHA-256 hashes of every input
42
+ * file. On subsequent transforms the cached entry is validated by comparing
43
+ * fresh hashes against {@link inputHashes}; a mismatch triggers a full
44
+ * re-transform of the project.
45
+ */
24
46
  export interface TtscCachedProjectTransform {
47
+ /**
48
+ * SHA-256 hash of each project-relative input path at the time of the
49
+ * transform.
50
+ */
25
51
  inputHashes: Record<string, string>;
52
+ /** Absolute path to the directory that owns the tsconfig. */
26
53
  projectRoot: string;
54
+ /** Raw compiler output returned by {@link TtscCompiler.transform}. */
27
55
  result: ITtscCompilerTransformation;
28
56
  }
29
57
 
58
+ /**
59
+ * Keyed by a stable JSON string that encodes the tsconfig path, compiler
60
+ * options overlay, plugin list, and alias paths. The value is a `Promise` so
61
+ * concurrent transforms for the same project share a single in-flight
62
+ * compilation rather than spawning multiple `TtscCompiler` instances.
63
+ */
30
64
  export type TtscTransformCache = Map<
31
65
  string,
32
66
  Promise<TtscCachedProjectTransform>
33
67
  >;
34
68
 
69
+ /** Create an empty transform cache for a single build session. */
35
70
  export function createTtscTransformCache(): TtscTransformCache {
36
71
  return new Map();
37
72
  }
38
73
 
74
+ /**
75
+ * Apply the ttsc plugin transform to a single source file.
76
+ *
77
+ * The function is intentionally project-scoped: it compiles the entire tsconfig
78
+ * project in one shot and extracts the result for `id`. Subsequent calls for
79
+ * sibling files in the same project reuse the cached result as long as none of
80
+ * the project's input files have changed (verified by comparing SHA-256
81
+ * hashes).
82
+ *
83
+ * Returns `undefined` when no transform is needed (declaration files, virtual
84
+ * modules, disabled plugins, or source unchanged after transform).
85
+ *
86
+ * @param id - Bundler module id (may carry a query string or virtual prefix).
87
+ * @param source - Current file content supplied by the bundler.
88
+ * @param options - Resolved plugin options.
89
+ * @param aliases - Raw bundler alias configuration (Vite array or webpack
90
+ * object).
91
+ * @param cache - Optional per-build cache; cleared by the caller on
92
+ * `buildStart`.
93
+ */
39
94
  export async function transformTtsc(
40
95
  id: string,
41
96
  source: string,
@@ -100,21 +155,44 @@ export async function transformTtsc(
100
155
  return createTransformResult(source, code);
101
156
  }
102
157
 
158
+ /**
159
+ * Strip a query string or hash fragment from a bundler module id.
160
+ *
161
+ * Vite appends query parameters (e.g. `?raw`, `?url`, `?inline`) to
162
+ * differentiate import variants of the same file. We must strip them before
163
+ * using the id as a file-system path.
164
+ */
103
165
  export function stripQuery(id: string): string {
104
166
  const query = id.search(/[?#]/);
105
167
  return query === -1 ? id : id.slice(0, query);
106
168
  }
107
169
 
170
+ /**
171
+ * Returns `true` for TypeScript declaration files (`.d.ts`, `.d.mts`,
172
+ * `.d.cts`).
173
+ */
108
174
  export function isDeclarationFile(id: string): boolean {
109
175
  return id.endsWith(".d.ts") || id.endsWith(".d.mts") || id.endsWith(".d.cts");
110
176
  }
111
177
 
178
+ /**
179
+ * Returns `true` when the caller has explicitly opted out of all plugins. An
180
+ * empty array is treated as disabled so we don't invoke the compiler for a
181
+ * no-op transform.
182
+ */
112
183
  function pluginsAreDisabled(
113
184
  plugins: ResolvedTtscUnpluginOptions["plugins"],
114
185
  ): boolean {
115
186
  return plugins === false || (Array.isArray(plugins) && plugins.length === 0);
116
187
  }
117
188
 
189
+ /**
190
+ * Build the unplugin transform result, or `undefined` when the transform
191
+ * produced no changes.
192
+ *
193
+ * Returning `undefined` instead of `{ code: source }` lets the bundler skip the
194
+ * unnecessary module update and preserves the original source map.
195
+ */
118
196
  export function createTransformResult(
119
197
  source: string,
120
198
  code: string,
@@ -136,6 +214,16 @@ function matchesCachedSource(
136
214
  return sameHashes(cached.inputHashes, currentHashes);
137
215
  }
138
216
 
217
+ /**
218
+ * Build the complete input-hash snapshot stored alongside a fresh compiler
219
+ * result.
220
+ *
221
+ * Combines filesystem hashes for every file in the project directory with
222
+ * hashes for each emitted TypeScript output key (the compiler may have read
223
+ * files not visible via the directory walk). The in-memory source for the file
224
+ * that triggered the build is overlaid last to capture unsaved editor content
225
+ * correctly.
226
+ */
139
227
  function collectInputHashes(props: {
140
228
  currentFile: string;
141
229
  currentSource: string;
@@ -154,6 +242,7 @@ function collectInputHashes(props: {
154
242
  }
155
243
  }
156
244
  }
245
+ // Overlay the in-memory source so unsaved edits invalidate the cache.
157
246
  hashes[toProjectKey(props.projectRoot, props.currentFile)] = hashText(
158
247
  props.currentSource,
159
248
  );
@@ -175,6 +264,14 @@ function collectProjectInputHashes(
175
264
  return hashes;
176
265
  }
177
266
 
267
+ /**
268
+ * Enumerate every regular file under `root`, skipping well-known output and
269
+ * tooling directories (see {@link isIgnoredProjectDirectory}).
270
+ *
271
+ * Uses an iterative DFS instead of `fs.readdirSync` recursion to avoid
272
+ * unbounded call-stack depth on deep project trees. The result is sorted so
273
+ * that hash comparisons are deterministic across OS-level directory orderings.
274
+ */
178
275
  function listProjectInputFiles(root: string): string[] {
179
276
  const out: string[] = [];
180
277
  const stack = [root];
@@ -311,16 +408,30 @@ function createTransformTsconfig(props: {
311
408
  };
312
409
  }
313
410
 
411
+ /**
412
+ * Resolve all relative paths inside `compilerOptions` against `tsconfigDir`.
413
+ *
414
+ * The generated tsconfig lives in a system temp directory, so any relative path
415
+ * (e.g. `"outDir": "../dist"`) that was meaningful relative to the original
416
+ * tsconfig must be converted to an absolute path before writing the generated
417
+ * file. Otherwise TypeScript-Go resolves it against the temp dir.
418
+ *
419
+ * Also inserts a synthetic `baseUrl` equal to `tsconfigDir` when `paths` is
420
+ * provided but `baseUrl` is absent — TypeScript requires `baseUrl` alongside
421
+ * `paths` when the latter contains non-absolute targets.
422
+ */
314
423
  function normalizeCompilerOptionsForGeneratedTsconfig(
315
424
  compilerOptions: Record<string, unknown>,
316
425
  tsconfigDir: string,
317
426
  ): Record<string, unknown> {
318
427
  const output = { ...compilerOptions };
428
+ // Scalar path fields: resolve each against the original tsconfig directory.
319
429
  for (const key of ["baseUrl", "declarationDir", "outDir", "rootDir"]) {
320
430
  if (typeof output[key] === "string") {
321
431
  output[key] = path.resolve(tsconfigDir, output[key]);
322
432
  }
323
433
  }
434
+ // Array path fields: resolve each element individually.
324
435
  for (const key of ["rootDirs", "typeRoots"]) {
325
436
  if (Array.isArray(output[key])) {
326
437
  output[key] = output[key].map((entry) =>
@@ -473,6 +584,14 @@ function createTransformCacheKey(props: {
473
584
  });
474
585
  }
475
586
 
587
+ /**
588
+ * JSON-serialise `value` with object keys sorted alphabetically.
589
+ *
590
+ * Standard `JSON.stringify` does not guarantee key ordering, so two
591
+ * semantically identical option objects could produce different strings and
592
+ * cause unnecessary cache misses. Sorting keys makes the cache key stable
593
+ * regardless of the order properties were added to the options object.
594
+ */
476
595
  function stableStringify(value: unknown): string {
477
596
  if (Array.isArray(value)) {
478
597
  return `[${value.map(stableStringify).join(",")}]`;
@@ -508,6 +627,15 @@ function isAlias(value: unknown): value is TtscTransformAlias {
508
627
  );
509
628
  }
510
629
 
630
+ /**
631
+ * Extract the transformed source for a single file from the compiler result.
632
+ *
633
+ * Throws on compiler exception or hard failure so the bundler surfaces the
634
+ * error to the user. On success, tries a fast exact-match lookup by
635
+ * project-relative key first, then falls back to a resolve-based scan for the
636
+ * rare case where the key in `result.typescript` uses an absolute or
637
+ * differently-cased path.
638
+ */
511
639
  function selectTransformedSource(props: {
512
640
  file: string;
513
641
  projectRoot: string;
@@ -520,11 +648,13 @@ function selectTransformedSource(props: {
520
648
  throw new Error(formatDiagnostics(props.result.diagnostics));
521
649
  }
522
650
 
651
+ // Fast path: the compiler key matches the normalised project-relative path.
523
652
  const key = toProjectKey(props.projectRoot, props.file);
524
653
  const direct = props.result.typescript[key];
525
654
  if (direct !== undefined) {
526
655
  return direct;
527
656
  }
657
+ // Slow path: resolve each candidate to an absolute path for comparison.
528
658
  for (const [candidate, source] of Object.entries(props.result.typescript)) {
529
659
  if (path.resolve(props.projectRoot, candidate) === props.file) {
530
660
  return source;
@@ -533,6 +663,13 @@ function selectTransformedSource(props: {
533
663
  throw new Error(`ttsc transform did not return output for ${props.file}`);
534
664
  }
535
665
 
666
+ /**
667
+ * Forward non-fatal plugin diagnostics to stderr.
668
+ *
669
+ * A `success` result may still carry warnings or informational messages from
670
+ * plugins. These are surfaced via stderr rather than throwing so the build
671
+ * continues. Failures and exceptions are handled by the caller.
672
+ */
536
673
  function reportSuccessDiagnostics(result: ITtscCompilerTransformation): void {
537
674
  if (result.type !== "success" || result.diagnostics === undefined) {
538
675
  return;
@@ -543,6 +680,14 @@ function reportSuccessDiagnostics(result: ITtscCompilerTransformation): void {
543
680
  }
544
681
  }
545
682
 
683
+ /**
684
+ * Format a compiler diagnostic list into a human-readable string.
685
+ *
686
+ * Produces `"file: line:col: message"` entries joined by newlines, matching the
687
+ * output style of `tsc`. When the list is empty (e.g. a failure with no
688
+ * attached diagnostics) returns a generic fallback message so the thrown
689
+ * `Error` is never empty.
690
+ */
546
691
  function formatDiagnostics(diagnostics: ITtscCompilerDiagnostic[]): string {
547
692
  if (diagnostics.length === 0) {
548
693
  return "ttsc transform failed";
@@ -577,6 +722,16 @@ function formatUnknownError(error: unknown): string {
577
722
  return String(error);
578
723
  }
579
724
 
725
+ /**
726
+ * Locate the tsconfig that should govern the transform for `file`.
727
+ *
728
+ * If `tsconfig` is supplied it is returned as-is (absolute) or resolved from
729
+ * `process.cwd()` (relative). Otherwise the function walks ancestor directories
730
+ * starting at `file`'s directory, returning the first `tsconfig.json` found.
731
+ * Falls back to `<cwd>/tsconfig.json` when no ancestor contains one — the
732
+ * compiler will error if that file does not exist, which is the correct
733
+ * behavior for a mis-configured project.
734
+ */
580
735
  function resolveTsconfig(file: string, tsconfig?: string): string {
581
736
  if (tsconfig !== undefined) {
582
737
  return path.isAbsolute(tsconfig)
@@ -591,6 +746,7 @@ function resolveTsconfig(file: string, tsconfig?: string): string {
591
746
  return candidate;
592
747
  }
593
748
  const parent = path.dirname(current);
749
+ // Reached filesystem root — stop walking.
594
750
  if (parent === current) {
595
751
  break;
596
752
  }
package/src/index.ts CHANGED
@@ -1,3 +1,12 @@
1
+ /**
2
+ * @ttsc/unplugin — bundler-agnostic ttsc plugin adapter.
3
+ *
4
+ * Re-exports the unified `unplugin` instance that carries named bundler
5
+ * adapters (`.vite`, `.rollup`, `.rolldown`, `.webpack`, `.rspack`, `.esbuild`,
6
+ * `.farm`) as well as the raw factory for custom integrations. Per-bundler
7
+ * entry points (`@ttsc/unplugin/vite`, `/webpack`, …) each re-export the
8
+ * matching adapter directly to keep bundler-specific builds lean.
9
+ */
1
10
  import unplugin from "./core/index";
2
11
 
3
12
  export type { TtscUnpluginOptions } from "./core/options";
package/src/next.ts CHANGED
@@ -1,14 +1,43 @@
1
1
  import type { TtscUnpluginOptions } from "./core/options";
2
2
  import webpack from "./webpack";
3
3
 
4
+ /**
5
+ * Minimal structural type for a Next.js configuration object.
6
+ *
7
+ * Only the `webpack` field is used by this adapter; all other Next.js options
8
+ * are forwarded as-is through the spread operator.
9
+ */
4
10
  export type NextLikeConfig = Record<string, unknown> & {
11
+ /**
12
+ * Optional existing webpack customisation hook. When the caller has already
13
+ * defined one, `next()` will chain through to it after injecting the ttsc
14
+ * webpack plugin.
15
+ */
5
16
  webpack?: (config: WebpackLikeConfig, options: unknown) => WebpackLikeConfig;
6
17
  };
7
18
 
19
+ /**
20
+ * Minimal structural type for a webpack configuration object as seen by the
21
+ * Next.js `webpack` hook callback.
22
+ */
8
23
  export type WebpackLikeConfig = Record<string, unknown> & {
24
+ /** The webpack plugin array; initialised to `[]` by this adapter if absent. */
9
25
  plugins?: unknown[];
10
26
  };
11
27
 
28
+ /**
29
+ * Wrap a Next.js config object so that the ttsc webpack plugin is injected into
30
+ * every webpack build Next.js performs.
31
+ *
32
+ * The adapter uses `unshift` to ensure the ttsc plugin runs before any other
33
+ * plugins in the array (unplugin `enforce: "pre"` semantics). An existing
34
+ * `nextConfig.webpack` hook is preserved and called after the plugin is
35
+ * injected.
36
+ *
37
+ * @param nextConfig - The caller's existing Next.js config (spread into the
38
+ * returned object unchanged, except for `webpack`).
39
+ * @param options - Ttsc plugin options forwarded to the webpack adapter.
40
+ */
12
41
  export default function next(
13
42
  nextConfig: NextLikeConfig = {},
14
43
  options?: TtscUnpluginOptions,
@@ -17,6 +46,7 @@ export default function next(
17
46
  ...nextConfig,
18
47
  webpack(config: WebpackLikeConfig, webpackOptions: unknown) {
19
48
  config.plugins = Array.isArray(config.plugins) ? config.plugins : [];
49
+ // Prepend so ttsc runs before any user-added plugins.
20
50
  config.plugins.unshift(webpack(options));
21
51
  if (typeof nextConfig.webpack === "function") {
22
52
  return nextConfig.webpack(config, webpackOptions);