@ttsc/unplugin 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.
Files changed (58) hide show
  1. package/README.md +286 -0
  2. package/lib/api.d.ts +1 -0
  3. package/lib/api.js +18 -0
  4. package/lib/api.js.map +1 -0
  5. package/lib/bun.d.ts +15 -0
  6. package/lib/bun.js +34 -0
  7. package/lib/bun.js.map +1 -0
  8. package/lib/core/index.d.ts +8 -0
  9. package/lib/core/index.js +44 -0
  10. package/lib/core/index.js.map +1 -0
  11. package/lib/core/options.d.ts +32 -0
  12. package/lib/core/options.js +16 -0
  13. package/lib/core/options.js.map +1 -0
  14. package/lib/core/transform.d.ts +11 -0
  15. package/lib/core/transform.js +307 -0
  16. package/lib/core/transform.js.map +1 -0
  17. package/lib/esbuild.d.ts +3 -0
  18. package/lib/esbuild.js +9 -0
  19. package/lib/esbuild.js.map +1 -0
  20. package/lib/farm.d.ts +3 -0
  21. package/lib/farm.js +9 -0
  22. package/lib/farm.js.map +1 -0
  23. package/lib/index.d.ts +3 -0
  24. package/lib/index.js +8 -0
  25. package/lib/index.js.map +1 -0
  26. package/lib/next.d.ts +8 -0
  27. package/lib/next.js +21 -0
  28. package/lib/next.js.map +1 -0
  29. package/lib/rolldown.d.ts +3 -0
  30. package/lib/rolldown.js +9 -0
  31. package/lib/rolldown.js.map +1 -0
  32. package/lib/rollup.d.ts +3 -0
  33. package/lib/rollup.js +9 -0
  34. package/lib/rollup.js.map +1 -0
  35. package/lib/rspack.d.ts +3 -0
  36. package/lib/rspack.js +9 -0
  37. package/lib/rspack.js.map +1 -0
  38. package/lib/vite.d.ts +3 -0
  39. package/lib/vite.js +9 -0
  40. package/lib/vite.js.map +1 -0
  41. package/lib/webpack.d.ts +3 -0
  42. package/lib/webpack.js +9 -0
  43. package/lib/webpack.js.map +1 -0
  44. package/package.json +106 -0
  45. package/src/api.ts +1 -0
  46. package/src/bun.ts +47 -0
  47. package/src/core/index.ts +61 -0
  48. package/src/core/options.ts +52 -0
  49. package/src/core/transform.ts +357 -0
  50. package/src/esbuild.ts +5 -0
  51. package/src/farm.ts +5 -0
  52. package/src/index.ts +5 -0
  53. package/src/next.ts +27 -0
  54. package/src/rolldown.ts +5 -0
  55. package/src/rollup.ts +5 -0
  56. package/src/rspack.ts +5 -0
  57. package/src/vite.ts +5 -0
  58. package/src/webpack.ts +5 -0
package/src/bun.ts ADDED
@@ -0,0 +1,47 @@
1
+ import fs from "node:fs/promises";
2
+ import type { UnpluginContextMeta } from "unplugin";
3
+
4
+ import { unplugin } from "./api";
5
+ import type { TtscUnpluginOptions } from "./core/options";
6
+
7
+ export interface BunLikePlugin {
8
+ name: string;
9
+ setup(build: BunLikeBuild): void | Promise<void>;
10
+ }
11
+
12
+ export interface BunLikeBuild {
13
+ onLoad(
14
+ options: { filter: RegExp },
15
+ loader: (args: { path: string }) => Promise<{ contents: string }>,
16
+ ): void;
17
+ }
18
+
19
+ const sourceFilePattern = /\.[cm]?[jt]sx?$/;
20
+
21
+ export default function bun(options?: TtscUnpluginOptions): BunLikePlugin {
22
+ return {
23
+ name: "ttsc-unplugin",
24
+ setup(build) {
25
+ const raw = unplugin.raw(options, {} as UnpluginContextMeta);
26
+ build.onLoad({ filter: sourceFilePattern }, async (args) => {
27
+ const source = await fs.readFile(args.path, "utf8");
28
+ const result =
29
+ typeof raw.transform === "function"
30
+ ? await raw.transform.call({} as never, source, args.path)
31
+ : undefined;
32
+ if (typeof result === "string") {
33
+ return { contents: result };
34
+ }
35
+ if (
36
+ typeof result === "object" &&
37
+ result !== null &&
38
+ "code" in result &&
39
+ typeof result.code === "string"
40
+ ) {
41
+ return { contents: result.code };
42
+ }
43
+ return { contents: source };
44
+ });
45
+ },
46
+ };
47
+ }
@@ -0,0 +1,61 @@
1
+ import type { UnpluginFactory, UnpluginInstance } from "unplugin";
2
+ import { createUnplugin } from "unplugin";
3
+
4
+ import type { TtscUnpluginOptions } from "./options";
5
+ import { resolveOptions } from "./options";
6
+ import { isDeclarationFile, stripQuery, transformTtsc } from "./transform";
7
+
8
+ const name = "ttsc-unplugin";
9
+ const sourceFilePattern = /\.[cm]?[jt]sx?$/;
10
+ const nodeModulesPattern = /(?:^|[/\\])node_modules(?:[/\\]|$)/;
11
+
12
+ const unpluginFactory: UnpluginFactory<
13
+ TtscUnpluginOptions | undefined,
14
+ false
15
+ > = (rawOptions = {}) => {
16
+ const options = resolveOptions(rawOptions);
17
+ let aliases: unknown;
18
+
19
+ return {
20
+ name,
21
+ enforce: "pre",
22
+
23
+ vite: {
24
+ configResolved(config) {
25
+ aliases = config.resolve.alias;
26
+ },
27
+ },
28
+
29
+ transformInclude(id) {
30
+ const file = stripQuery(id);
31
+ return isTransformTarget(file);
32
+ },
33
+
34
+ async transform(source, id) {
35
+ const file = stripQuery(id);
36
+ if (!isTransformTarget(file)) {
37
+ return undefined;
38
+ }
39
+ return transformTtsc(file, source, options, aliases);
40
+ },
41
+ };
42
+ };
43
+
44
+ const unplugin: UnpluginInstance<TtscUnpluginOptions | undefined, false> =
45
+ createUnplugin(unpluginFactory);
46
+
47
+ export type {
48
+ TtscUnpluginCompilerOptionsJson,
49
+ TtscUnpluginOptions,
50
+ } from "./options";
51
+ export { resolveOptions, transformTtsc, unplugin };
52
+
53
+ export default unplugin;
54
+
55
+ function isTransformTarget(id: string): boolean {
56
+ return (
57
+ sourceFilePattern.test(id) &&
58
+ !isDeclarationFile(id) &&
59
+ !nodeModulesPattern.test(id)
60
+ );
61
+ }
@@ -0,0 +1,52 @@
1
+ import type { ITtscProjectPluginConfig } from "ttsc";
2
+
3
+ export type TtscUnpluginCompilerOptionsJson = Record<string, unknown>;
4
+
5
+ export interface TtscUnpluginOptions {
6
+ /**
7
+ * Project config used by the bundler adapter.
8
+ *
9
+ * Relative paths resolve from `process.cwd()`. When omitted, the nearest
10
+ * `tsconfig.json` is discovered from the transformed file.
11
+ */
12
+ project?: string;
13
+
14
+ /**
15
+ * Compiler options overlaid on top of the selected project config.
16
+ *
17
+ * This can include `plugins`; `plugins` passed at the top level still wins as
18
+ * the explicit plugin override.
19
+ */
20
+ compilerOptions?: TtscUnpluginCompilerOptionsJson;
21
+
22
+ /**
23
+ * `ttsc` plugin entries.
24
+ *
25
+ * `undefined` reads `compilerOptions.plugins` from the project config,
26
+ * `false` disables project plugins, and an array overrides the project
27
+ * plugin list.
28
+ */
29
+ plugins?: readonly ITtscProjectPluginConfig[] | false;
30
+ }
31
+
32
+ export interface ResolvedTtscUnpluginOptions {
33
+ compilerOptions: TtscUnpluginCompilerOptionsJson;
34
+ plugins?: readonly ITtscProjectPluginConfig[] | false;
35
+ project?: string;
36
+ }
37
+
38
+ const defaultOptions: ResolvedTtscUnpluginOptions = {
39
+ compilerOptions: {},
40
+ plugins: undefined,
41
+ project: undefined,
42
+ };
43
+
44
+ export function resolveOptions(
45
+ options: TtscUnpluginOptions = {},
46
+ ): ResolvedTtscUnpluginOptions {
47
+ return {
48
+ compilerOptions: { ...(options.compilerOptions ?? {}) },
49
+ plugins: "plugins" in options ? options.plugins : defaultOptions.plugins,
50
+ project: options.project ?? defaultOptions.project,
51
+ };
52
+ }
@@ -0,0 +1,357 @@
1
+ import * as Diff from "diff-match-patch-es";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import MagicString from "magic-string";
5
+ import type {
6
+ ITtscCompilerDiagnostic,
7
+ ITtscCompilerTransformation,
8
+ } from "ttsc";
9
+ import { TtscCompiler } from "ttsc";
10
+ import type { TransformResult } from "unplugin";
11
+
12
+ import type { ResolvedTtscUnpluginOptions } from "./options";
13
+
14
+ export type TtscTransformResult = Exclude<
15
+ TransformResult,
16
+ string | null | undefined
17
+ >;
18
+
19
+ export interface TtscTransformAlias {
20
+ find: string | RegExp;
21
+ replacement: string;
22
+ }
23
+
24
+ export async function transformTtsc(
25
+ id: string,
26
+ source: string,
27
+ options: ResolvedTtscUnpluginOptions,
28
+ aliases?: unknown,
29
+ ): Promise<TtscTransformResult | undefined> {
30
+ const file = path.resolve(stripQuery(id));
31
+ if (isDeclarationFile(file)) {
32
+ return undefined;
33
+ }
34
+
35
+ const tsconfig = resolveTsconfig(file, options.project);
36
+ const tsconfigDir = path.dirname(tsconfig);
37
+ const baseUrl = resolveBaseUrl(tsconfigDir, options.compilerOptions);
38
+ const aliasPaths = createAliasPaths(baseUrl, aliases);
39
+ const configured = createTransformTsconfig({
40
+ aliasPaths,
41
+ baseUrl,
42
+ compilerOptions: options.compilerOptions,
43
+ tsconfig,
44
+ });
45
+ try {
46
+ const effectiveTsconfig = configured.path;
47
+ const result = new TtscCompiler({
48
+ cwd: path.dirname(effectiveTsconfig),
49
+ plugins: options.plugins,
50
+ tsconfig: effectiveTsconfig,
51
+ }).transform();
52
+ const code = selectTransformedSource({
53
+ file,
54
+ projectRoot: path.dirname(effectiveTsconfig),
55
+ result,
56
+ });
57
+ return createTransformResult(source, code, file);
58
+ } finally {
59
+ configured.dispose();
60
+ }
61
+ }
62
+
63
+ export function stripQuery(id: string): string {
64
+ const query = id.search(/[?#]/);
65
+ const clean = query === -1 ? id : id.slice(0, query);
66
+ return clean.endsWith("?__rslib_entry__")
67
+ ? clean.slice(0, -"?__rslib_entry__".length)
68
+ : clean;
69
+ }
70
+
71
+ export function isDeclarationFile(id: string): boolean {
72
+ return id.endsWith(".d.ts") || id.endsWith(".d.mts") || id.endsWith(".d.cts");
73
+ }
74
+
75
+ export function createTransformResult(
76
+ source: string,
77
+ code: string,
78
+ id: string,
79
+ ): TtscTransformResult | undefined {
80
+ if (source === code) {
81
+ return undefined;
82
+ }
83
+
84
+ const magic = new MagicString(source);
85
+ const diff = Diff.diff(source, code);
86
+ Diff.diffCleanupSemantic(diff);
87
+
88
+ let offset = 0;
89
+ for (let index = 0; index < diff.length; index += 1) {
90
+ const [type, text] = diff[index]!;
91
+ if (type === 0) {
92
+ offset += text.length;
93
+ continue;
94
+ }
95
+ if (type === 1) {
96
+ magic.prependLeft(offset, text);
97
+ continue;
98
+ }
99
+
100
+ const next = diff[index + 1];
101
+ if (next?.[0] === 1) {
102
+ magic.update(offset, offset + text.length, next[1]);
103
+ index += 1;
104
+ } else {
105
+ magic.remove(offset, offset + text.length);
106
+ }
107
+ offset += text.length;
108
+ }
109
+
110
+ if (!magic.hasChanged()) {
111
+ return undefined;
112
+ }
113
+ return {
114
+ code: magic.toString(),
115
+ map: magic.generateMap({
116
+ file: `${id}.map`,
117
+ includeContent: true,
118
+ source: id,
119
+ }),
120
+ };
121
+ }
122
+
123
+ function createTransformTsconfig(props: {
124
+ aliasPaths: Record<string, string[]>;
125
+ baseUrl: string;
126
+ compilerOptions: Record<string, unknown>;
127
+ tsconfig: string;
128
+ }): { path: string; dispose: () => void } {
129
+ const compilerOptions = {
130
+ ...props.compilerOptions,
131
+ ...createAliasCompilerOptions(props),
132
+ };
133
+ if (Object.keys(compilerOptions).length === 0) {
134
+ return {
135
+ path: props.tsconfig,
136
+ dispose: () => undefined,
137
+ };
138
+ }
139
+
140
+ const directory = path.dirname(props.tsconfig);
141
+ const file = path.join(
142
+ directory,
143
+ `.ttsc-unplugin-${process.pid}-${Date.now()}-${Math.random()
144
+ .toString(36)
145
+ .slice(2)}.json`,
146
+ );
147
+ fs.writeFileSync(
148
+ file,
149
+ JSON.stringify(
150
+ {
151
+ extends: `./${path.basename(props.tsconfig)}`,
152
+ compilerOptions,
153
+ },
154
+ null,
155
+ 2,
156
+ ),
157
+ "utf8",
158
+ );
159
+ return {
160
+ path: file,
161
+ dispose: () => fs.rmSync(file, { force: true }),
162
+ };
163
+ }
164
+
165
+ function createAliasCompilerOptions(props: {
166
+ aliasPaths: Record<string, string[]>;
167
+ baseUrl: string;
168
+ compilerOptions: Record<string, unknown>;
169
+ }): Record<string, unknown> {
170
+ if (Object.keys(props.aliasPaths).length === 0) {
171
+ return {};
172
+ }
173
+ return {
174
+ baseUrl: toCompilerPath(props.baseUrl, props.compilerOptions),
175
+ paths: {
176
+ ...readPaths(props.compilerOptions.paths),
177
+ ...props.aliasPaths,
178
+ },
179
+ };
180
+ }
181
+
182
+ function readPaths(value: unknown): Record<string, string[]> {
183
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
184
+ return {};
185
+ }
186
+ const output: Record<string, string[]> = {};
187
+ for (const [key, paths] of Object.entries(value)) {
188
+ if (!Array.isArray(paths)) {
189
+ continue;
190
+ }
191
+ const filtered = paths.filter(
192
+ (entry): entry is string => typeof entry === "string",
193
+ );
194
+ if (filtered.length !== 0) {
195
+ output[key] = filtered;
196
+ }
197
+ }
198
+ return output;
199
+ }
200
+
201
+ function resolveBaseUrl(
202
+ tsconfigDir: string,
203
+ compilerOptions: Record<string, unknown>,
204
+ ): string {
205
+ return typeof compilerOptions.baseUrl === "string"
206
+ ? path.resolve(tsconfigDir, compilerOptions.baseUrl)
207
+ : tsconfigDir;
208
+ }
209
+
210
+ function toCompilerPath(
211
+ absoluteBaseUrl: string,
212
+ compilerOptions: Record<string, unknown>,
213
+ ): string {
214
+ return typeof compilerOptions.baseUrl === "string"
215
+ ? compilerOptions.baseUrl
216
+ : absoluteBaseUrl;
217
+ }
218
+
219
+ function createAliasPaths(
220
+ baseUrl: string,
221
+ aliases: unknown,
222
+ ): Record<string, string[]> {
223
+ const paths: Record<string, string[]> = {};
224
+ for (const alias of normalizeAliases(aliases)) {
225
+ if (typeof alias.find !== "string" || alias.find.length === 0) {
226
+ continue;
227
+ }
228
+ if (alias.find.includes("*")) {
229
+ continue;
230
+ }
231
+ const key = alias.find.replace(/\/+$/, "");
232
+ if (key.length === 0) {
233
+ continue;
234
+ }
235
+ const replacement = path.isAbsolute(alias.replacement)
236
+ ? alias.replacement
237
+ : path.resolve(process.cwd(), alias.replacement);
238
+ const target = normalizePath(path.relative(baseUrl, replacement) || ".");
239
+ paths[key] = [target];
240
+ paths[`${key}/*`] = [`${target}/*`];
241
+ }
242
+ return paths;
243
+ }
244
+
245
+ function normalizeAliases(aliases: unknown): TtscTransformAlias[] {
246
+ if (Array.isArray(aliases)) {
247
+ return aliases.filter(isAlias);
248
+ }
249
+ if (typeof aliases === "object" && aliases !== null) {
250
+ return Object.entries(aliases)
251
+ .filter(
252
+ (entry): entry is [string, string] => typeof entry[1] === "string",
253
+ )
254
+ .map(([find, replacement]) => ({ find, replacement }));
255
+ }
256
+ return [];
257
+ }
258
+
259
+ function isAlias(value: unknown): value is TtscTransformAlias {
260
+ return (
261
+ typeof value === "object" &&
262
+ value !== null &&
263
+ "find" in value &&
264
+ "replacement" in value &&
265
+ (typeof value.find === "string" || value.find instanceof RegExp) &&
266
+ typeof value.replacement === "string"
267
+ );
268
+ }
269
+
270
+ function selectTransformedSource(props: {
271
+ file: string;
272
+ projectRoot: string;
273
+ result: ITtscCompilerTransformation;
274
+ }): string {
275
+ if (props.result.type === "exception") {
276
+ throw new Error(formatUnknownError(props.result.error));
277
+ }
278
+ if (props.result.type === "failure") {
279
+ throw new Error(formatDiagnostics(props.result.diagnostics));
280
+ }
281
+
282
+ const key = toProjectKey(props.projectRoot, props.file);
283
+ const direct = props.result.typescript[key];
284
+ if (direct !== undefined) {
285
+ return direct;
286
+ }
287
+ for (const [candidate, source] of Object.entries(props.result.typescript)) {
288
+ if (path.resolve(props.projectRoot, candidate) === props.file) {
289
+ return source;
290
+ }
291
+ }
292
+ throw new Error(`ttsc transform did not return output for ${props.file}`);
293
+ }
294
+
295
+ function formatDiagnostics(diagnostics: ITtscCompilerDiagnostic[]): string {
296
+ if (diagnostics.length === 0) {
297
+ return "ttsc transform failed";
298
+ }
299
+ return diagnostics
300
+ .map((diag) =>
301
+ [
302
+ diag.file ?? "ttsc",
303
+ diag.line === undefined
304
+ ? undefined
305
+ : `${diag.line}:${diag.character ?? 1}`,
306
+ diag.messageText,
307
+ ]
308
+ .filter((part) => part !== undefined && part !== "")
309
+ .join(": "),
310
+ )
311
+ .join("\n");
312
+ }
313
+
314
+ function formatUnknownError(error: unknown): string {
315
+ if (error instanceof Error) {
316
+ return error.message;
317
+ }
318
+ if (
319
+ typeof error === "object" &&
320
+ error !== null &&
321
+ "message" in error &&
322
+ typeof error.message === "string"
323
+ ) {
324
+ return error.message;
325
+ }
326
+ return String(error);
327
+ }
328
+
329
+ function resolveTsconfig(file: string, tsconfig?: string): string {
330
+ if (tsconfig !== undefined) {
331
+ return path.isAbsolute(tsconfig)
332
+ ? tsconfig
333
+ : path.resolve(process.cwd(), tsconfig);
334
+ }
335
+
336
+ let current = path.dirname(file);
337
+ while (true) {
338
+ const candidate = path.join(current, "tsconfig.json");
339
+ if (fs.existsSync(candidate)) {
340
+ return candidate;
341
+ }
342
+ const parent = path.dirname(current);
343
+ if (parent === current) {
344
+ break;
345
+ }
346
+ current = parent;
347
+ }
348
+ return path.resolve(process.cwd(), "tsconfig.json");
349
+ }
350
+
351
+ function toProjectKey(root: string, file: string): string {
352
+ return normalizePath(path.relative(root, file));
353
+ }
354
+
355
+ function normalizePath(file: string): string {
356
+ return file.replace(/\\/g, "/");
357
+ }
package/src/esbuild.ts ADDED
@@ -0,0 +1,5 @@
1
+ import unplugin from "./core/index";
2
+
3
+ const esbuild: typeof unplugin.esbuild = unplugin.esbuild;
4
+
5
+ export default esbuild;
package/src/farm.ts ADDED
@@ -0,0 +1,5 @@
1
+ import unplugin from "./core/index";
2
+
3
+ const farm: typeof unplugin.farm = unplugin.farm;
4
+
5
+ export default farm;
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import unplugin from "./core/index";
2
+
3
+ export type { TtscUnpluginOptions } from "./core/options";
4
+
5
+ export default unplugin;
package/src/next.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { TtscUnpluginOptions } from "./core/options";
2
+ import webpack from "./webpack";
3
+
4
+ export type NextLikeConfig = Record<string, unknown> & {
5
+ webpack?: (config: WebpackLikeConfig, options: unknown) => WebpackLikeConfig;
6
+ };
7
+
8
+ export type WebpackLikeConfig = Record<string, unknown> & {
9
+ plugins?: unknown[];
10
+ };
11
+
12
+ export default function next(
13
+ nextConfig: NextLikeConfig = {},
14
+ options?: TtscUnpluginOptions,
15
+ ): NextLikeConfig {
16
+ return {
17
+ ...nextConfig,
18
+ webpack(config: WebpackLikeConfig, webpackOptions: unknown) {
19
+ config.plugins = Array.isArray(config.plugins) ? config.plugins : [];
20
+ config.plugins.unshift(webpack(options));
21
+ if (typeof nextConfig.webpack === "function") {
22
+ return nextConfig.webpack(config, webpackOptions);
23
+ }
24
+ return config;
25
+ },
26
+ };
27
+ }
@@ -0,0 +1,5 @@
1
+ import unplugin from "./core/index";
2
+
3
+ const rolldown: typeof unplugin.rolldown = unplugin.rolldown;
4
+
5
+ export default rolldown;
package/src/rollup.ts ADDED
@@ -0,0 +1,5 @@
1
+ import unplugin from "./core/index";
2
+
3
+ const rollup: typeof unplugin.rollup = unplugin.rollup;
4
+
5
+ export default rollup;
package/src/rspack.ts ADDED
@@ -0,0 +1,5 @@
1
+ import unplugin from "./core/index";
2
+
3
+ const rspack: typeof unplugin.rspack = unplugin.rspack;
4
+
5
+ export default rspack;
package/src/vite.ts ADDED
@@ -0,0 +1,5 @@
1
+ import unplugin from "./core/index";
2
+
3
+ const vite: typeof unplugin.vite = unplugin.vite;
4
+
5
+ export default vite;
package/src/webpack.ts ADDED
@@ -0,0 +1,5 @@
1
+ import unplugin from "./core/index";
2
+
3
+ const webpack: typeof unplugin.webpack = unplugin.webpack;
4
+
5
+ export default webpack;