@ttsc/unplugin 0.8.1 → 0.10.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/README.md +8 -9
- package/lib/api.js +3 -2
- package/lib/api.js.map +1 -1
- package/lib/api.mjs +1 -1
- package/lib/bun.js +1 -1
- package/lib/bun.js.map +1 -1
- package/lib/bun.mjs +1 -1
- package/lib/bun.mjs.map +1 -1
- package/lib/core/index.d.ts +2 -2
- package/lib/core/index.js +7 -2
- package/lib/core/index.js.map +1 -1
- package/lib/core/index.mjs +8 -4
- package/lib/core/index.mjs.map +1 -1
- package/lib/core/options.d.ts +3 -3
- package/lib/core/transform.d.ts +11 -3
- package/lib/core/transform.js +233 -80
- package/lib/core/transform.js.map +1 -1
- package/lib/core/transform.mjs +234 -63
- package/lib/core/transform.mjs.map +1 -1
- package/package.json +4 -7
- package/src/bun.ts +1 -1
- package/src/core/index.ts +14 -4
- package/src/core/options.ts +3 -3
- package/src/core/transform.ts +304 -66
package/src/core/transform.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import * as Diff from "diff-match-patch-es";
|
|
2
1
|
import fs from "node:fs";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
import MagicString from "magic-string";
|
|
5
5
|
import type {
|
|
6
6
|
ITtscCompilerDiagnostic,
|
|
7
7
|
ITtscCompilerTransformation,
|
|
@@ -17,15 +17,31 @@ export type TtscTransformResult = Exclude<
|
|
|
17
17
|
>;
|
|
18
18
|
|
|
19
19
|
export interface TtscTransformAlias {
|
|
20
|
-
find: string
|
|
20
|
+
find: string;
|
|
21
21
|
replacement: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export interface TtscCachedProjectTransform {
|
|
25
|
+
inputHashes: Record<string, string>;
|
|
26
|
+
projectRoot: string;
|
|
27
|
+
result: ITtscCompilerTransformation;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type TtscTransformCache = Map<
|
|
31
|
+
string,
|
|
32
|
+
Promise<TtscCachedProjectTransform>
|
|
33
|
+
>;
|
|
34
|
+
|
|
35
|
+
export function createTtscTransformCache(): TtscTransformCache {
|
|
36
|
+
return new Map();
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
export async function transformTtsc(
|
|
25
40
|
id: string,
|
|
26
41
|
source: string,
|
|
27
42
|
options: ResolvedTtscUnpluginOptions,
|
|
28
43
|
aliases?: unknown,
|
|
44
|
+
cache?: TtscTransformCache,
|
|
29
45
|
): Promise<TtscTransformResult | undefined> {
|
|
30
46
|
const clean = stripQuery(id);
|
|
31
47
|
if (clean.includes("\0")) {
|
|
@@ -40,36 +56,50 @@ export async function transformTtsc(
|
|
|
40
56
|
const tsconfigDir = path.dirname(tsconfig);
|
|
41
57
|
const baseUrl = resolveBaseUrl(tsconfigDir, options.compilerOptions);
|
|
42
58
|
const aliasPaths = createAliasPaths(baseUrl, aliases);
|
|
43
|
-
const
|
|
59
|
+
const key = createTransformCacheKey({
|
|
44
60
|
aliasPaths,
|
|
45
|
-
baseUrl,
|
|
46
61
|
compilerOptions: options.compilerOptions,
|
|
62
|
+
plugins: options.plugins,
|
|
47
63
|
tsconfig,
|
|
48
64
|
});
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
|
|
66
|
+
let transformed = cache?.get(key);
|
|
67
|
+
if (transformed !== undefined) {
|
|
68
|
+
const cached = await transformed;
|
|
69
|
+
if (matchesCachedSource(cached, file, source)) {
|
|
70
|
+
reportSuccessDiagnostics(cached.result);
|
|
71
|
+
const code = selectTransformedSource({
|
|
72
|
+
file,
|
|
73
|
+
projectRoot: cached.projectRoot,
|
|
74
|
+
result: cached.result,
|
|
75
|
+
});
|
|
76
|
+
return createTransformResult(source, code);
|
|
77
|
+
}
|
|
78
|
+
cache?.delete(key);
|
|
79
|
+
transformed = undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (transformed === undefined) {
|
|
83
|
+
transformed = transformProject({
|
|
84
|
+
aliasPaths,
|
|
85
|
+
baseUrl,
|
|
86
|
+
compilerOptions: options.compilerOptions,
|
|
87
|
+
currentFile: file,
|
|
88
|
+
currentSource: source,
|
|
53
89
|
plugins: options.plugins,
|
|
54
|
-
tsconfig
|
|
55
|
-
}).transform();
|
|
56
|
-
const code = selectTransformedSource({
|
|
57
|
-
file,
|
|
58
|
-
projectRoot: path.dirname(effectiveTsconfig),
|
|
59
|
-
result,
|
|
90
|
+
tsconfig,
|
|
60
91
|
});
|
|
61
|
-
|
|
62
|
-
} finally {
|
|
63
|
-
configured.dispose();
|
|
92
|
+
cache?.set(key, transformed);
|
|
64
93
|
}
|
|
94
|
+
const { projectRoot, result } = await transformed;
|
|
95
|
+
reportSuccessDiagnostics(result);
|
|
96
|
+
const code = selectTransformedSource({ file, projectRoot, result });
|
|
97
|
+
return createTransformResult(source, code);
|
|
65
98
|
}
|
|
66
99
|
|
|
67
100
|
export function stripQuery(id: string): string {
|
|
68
101
|
const query = id.search(/[?#]/);
|
|
69
|
-
|
|
70
|
-
return clean.endsWith("?__rslib_entry__")
|
|
71
|
-
? clean.slice(0, -"?__rslib_entry__".length)
|
|
72
|
-
: clean;
|
|
102
|
+
return query === -1 ? id : id.slice(0, query);
|
|
73
103
|
}
|
|
74
104
|
|
|
75
105
|
export function isDeclarationFile(id: string): boolean {
|
|
@@ -79,49 +109,157 @@ export function isDeclarationFile(id: string): boolean {
|
|
|
79
109
|
export function createTransformResult(
|
|
80
110
|
source: string,
|
|
81
111
|
code: string,
|
|
82
|
-
id: string,
|
|
83
112
|
): TtscTransformResult | undefined {
|
|
84
113
|
if (source === code) {
|
|
85
114
|
return undefined;
|
|
86
115
|
}
|
|
116
|
+
return { code };
|
|
117
|
+
}
|
|
87
118
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
119
|
+
function matchesCachedSource(
|
|
120
|
+
cached: TtscCachedProjectTransform,
|
|
121
|
+
file: string,
|
|
122
|
+
source: string,
|
|
123
|
+
): boolean {
|
|
124
|
+
const currentKey = toProjectKey(cached.projectRoot, file);
|
|
125
|
+
const currentHashes = collectProjectInputHashes(cached.projectRoot);
|
|
126
|
+
currentHashes[currentKey] = hashText(source);
|
|
127
|
+
return sameHashes(cached.inputHashes, currentHashes);
|
|
128
|
+
}
|
|
91
129
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
130
|
+
function collectInputHashes(props: {
|
|
131
|
+
currentFile: string;
|
|
132
|
+
currentSource: string;
|
|
133
|
+
projectRoot: string;
|
|
134
|
+
result: ITtscCompilerTransformation;
|
|
135
|
+
}): Record<string, string> {
|
|
136
|
+
const hashes = collectProjectInputHashes(props.projectRoot);
|
|
137
|
+
if (props.result.type !== "exception") {
|
|
138
|
+
for (const key of Object.keys(props.result.typescript)) {
|
|
139
|
+
const file = path.resolve(props.projectRoot, key);
|
|
140
|
+
try {
|
|
141
|
+
hashes[key] = hashText(fs.readFileSync(file, "utf8"));
|
|
142
|
+
} catch {
|
|
143
|
+
// A plugin may synthesize a virtual TypeScript file. It should not
|
|
144
|
+
// decide cache reuse for real source files.
|
|
145
|
+
}
|
|
98
146
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
147
|
+
}
|
|
148
|
+
hashes[toProjectKey(props.projectRoot, props.currentFile)] = hashText(
|
|
149
|
+
props.currentSource,
|
|
150
|
+
);
|
|
151
|
+
return hashes;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function collectProjectInputHashes(
|
|
155
|
+
projectRoot: string,
|
|
156
|
+
): Record<string, string> {
|
|
157
|
+
const hashes: Record<string, string> = {};
|
|
158
|
+
for (const file of listProjectInputFiles(projectRoot)) {
|
|
159
|
+
try {
|
|
160
|
+
hashes[toProjectKey(projectRoot, file)] = hashText(fs.readFileSync(file));
|
|
161
|
+
} catch {
|
|
162
|
+
// File watchers may observe a transform while another process is moving
|
|
163
|
+
// or deleting files. The missing key invalidates older cache entries.
|
|
102
164
|
}
|
|
165
|
+
}
|
|
166
|
+
return hashes;
|
|
167
|
+
}
|
|
103
168
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
169
|
+
function listProjectInputFiles(root: string): string[] {
|
|
170
|
+
const out: string[] = [];
|
|
171
|
+
const stack = [root];
|
|
172
|
+
while (stack.length !== 0) {
|
|
173
|
+
const current = stack.pop()!;
|
|
174
|
+
let entries: fs.Dirent[];
|
|
175
|
+
try {
|
|
176
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
177
|
+
} catch {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
if (isIgnoredProjectDirectory(entry.name)) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const file = path.join(current, entry.name);
|
|
185
|
+
if (entry.isDirectory()) {
|
|
186
|
+
stack.push(file);
|
|
187
|
+
} else if (entry.isFile()) {
|
|
188
|
+
out.push(file);
|
|
189
|
+
}
|
|
110
190
|
}
|
|
111
|
-
offset += text.length;
|
|
112
191
|
}
|
|
192
|
+
out.sort();
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
113
195
|
|
|
114
|
-
|
|
115
|
-
|
|
196
|
+
function isIgnoredProjectDirectory(name: string): boolean {
|
|
197
|
+
return (
|
|
198
|
+
name === ".git" ||
|
|
199
|
+
name === ".ttsc" ||
|
|
200
|
+
name === ".cache" ||
|
|
201
|
+
name === ".next" ||
|
|
202
|
+
name === ".nuxt" ||
|
|
203
|
+
name === ".svelte-kit" ||
|
|
204
|
+
name === ".turbo" ||
|
|
205
|
+
name === ".vite" ||
|
|
206
|
+
name === "build" ||
|
|
207
|
+
name === "coverage" ||
|
|
208
|
+
name === "dist" ||
|
|
209
|
+
name === "node_modules" ||
|
|
210
|
+
name === "out" ||
|
|
211
|
+
name === "temp" ||
|
|
212
|
+
name === "tmp"
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function sameHashes(
|
|
217
|
+
left: Record<string, string>,
|
|
218
|
+
right: Record<string, string>,
|
|
219
|
+
): boolean {
|
|
220
|
+
const leftKeys = Object.keys(left);
|
|
221
|
+
const rightKeys = Object.keys(right);
|
|
222
|
+
if (leftKeys.length !== rightKeys.length) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
return leftKeys.every((key) => right[key] === left[key]);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function hashText(input: string | Buffer): string {
|
|
229
|
+
return crypto.createHash("sha256").update(input).digest("hex");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function transformProject(props: {
|
|
233
|
+
aliasPaths: Record<string, string[]>;
|
|
234
|
+
baseUrl: string;
|
|
235
|
+
compilerOptions: Record<string, unknown>;
|
|
236
|
+
currentFile: string;
|
|
237
|
+
currentSource: string;
|
|
238
|
+
plugins?: ResolvedTtscUnpluginOptions["plugins"];
|
|
239
|
+
tsconfig: string;
|
|
240
|
+
}): Promise<TtscCachedProjectTransform> {
|
|
241
|
+
const configured = createTransformTsconfig(props);
|
|
242
|
+
const projectRoot = path.dirname(props.tsconfig);
|
|
243
|
+
try {
|
|
244
|
+
const result = new TtscCompiler({
|
|
245
|
+
cwd: projectRoot,
|
|
246
|
+
plugins: props.plugins,
|
|
247
|
+
projectRoot,
|
|
248
|
+
tsconfig: configured.path,
|
|
249
|
+
}).transform();
|
|
250
|
+
return {
|
|
251
|
+
inputHashes: collectInputHashes({
|
|
252
|
+
currentFile: props.currentFile,
|
|
253
|
+
currentSource: props.currentSource,
|
|
254
|
+
projectRoot,
|
|
255
|
+
result,
|
|
256
|
+
}),
|
|
257
|
+
projectRoot,
|
|
258
|
+
result,
|
|
259
|
+
};
|
|
260
|
+
} finally {
|
|
261
|
+
configured.dispose();
|
|
116
262
|
}
|
|
117
|
-
return {
|
|
118
|
-
code: magic.toString(),
|
|
119
|
-
map: magic.generateMap({
|
|
120
|
-
file: `${id}.map`,
|
|
121
|
-
includeContent: true,
|
|
122
|
-
source: id,
|
|
123
|
-
}),
|
|
124
|
-
};
|
|
125
263
|
}
|
|
126
264
|
|
|
127
265
|
function createTransformTsconfig(props: {
|
|
@@ -130,10 +268,13 @@ function createTransformTsconfig(props: {
|
|
|
130
268
|
compilerOptions: Record<string, unknown>;
|
|
131
269
|
tsconfig: string;
|
|
132
270
|
}): { path: string; dispose: () => void } {
|
|
133
|
-
const compilerOptions =
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
271
|
+
const compilerOptions = normalizeCompilerOptionsForGeneratedTsconfig(
|
|
272
|
+
{
|
|
273
|
+
...props.compilerOptions,
|
|
274
|
+
...createAliasCompilerOptions(props),
|
|
275
|
+
},
|
|
276
|
+
path.dirname(props.tsconfig),
|
|
277
|
+
);
|
|
137
278
|
if (Object.keys(compilerOptions).length === 0) {
|
|
138
279
|
return {
|
|
139
280
|
path: props.tsconfig,
|
|
@@ -141,18 +282,13 @@ function createTransformTsconfig(props: {
|
|
|
141
282
|
};
|
|
142
283
|
}
|
|
143
284
|
|
|
144
|
-
const directory = path.
|
|
145
|
-
const file = path.join(
|
|
146
|
-
directory,
|
|
147
|
-
`.ttsc-unplugin-${process.pid}-${Date.now()}-${Math.random()
|
|
148
|
-
.toString(36)
|
|
149
|
-
.slice(2)}.json`,
|
|
150
|
-
);
|
|
285
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "ttsc-unplugin-"));
|
|
286
|
+
const file = path.join(directory, "tsconfig.json");
|
|
151
287
|
fs.writeFileSync(
|
|
152
288
|
file,
|
|
153
289
|
JSON.stringify(
|
|
154
290
|
{
|
|
155
|
-
extends:
|
|
291
|
+
extends: normalizePath(props.tsconfig),
|
|
156
292
|
compilerOptions,
|
|
157
293
|
},
|
|
158
294
|
null,
|
|
@@ -162,10 +298,55 @@ function createTransformTsconfig(props: {
|
|
|
162
298
|
);
|
|
163
299
|
return {
|
|
164
300
|
path: file,
|
|
165
|
-
dispose: () => fs.rmSync(
|
|
301
|
+
dispose: () => fs.rmSync(directory, { force: true, recursive: true }),
|
|
166
302
|
};
|
|
167
303
|
}
|
|
168
304
|
|
|
305
|
+
function normalizeCompilerOptionsForGeneratedTsconfig(
|
|
306
|
+
compilerOptions: Record<string, unknown>,
|
|
307
|
+
tsconfigDir: string,
|
|
308
|
+
): Record<string, unknown> {
|
|
309
|
+
const output = { ...compilerOptions };
|
|
310
|
+
for (const key of ["baseUrl", "declarationDir", "outDir", "rootDir"]) {
|
|
311
|
+
if (typeof output[key] === "string") {
|
|
312
|
+
output[key] = path.resolve(tsconfigDir, output[key]);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
for (const key of ["rootDirs", "typeRoots"]) {
|
|
316
|
+
if (Array.isArray(output[key])) {
|
|
317
|
+
output[key] = output[key].map((entry) =>
|
|
318
|
+
typeof entry === "string" ? path.resolve(tsconfigDir, entry) : entry,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (hasPaths(output.paths) && typeof output.baseUrl !== "string") {
|
|
323
|
+
output.baseUrl = tsconfigDir;
|
|
324
|
+
}
|
|
325
|
+
if (Array.isArray(output.plugins)) {
|
|
326
|
+
output.plugins = output.plugins.map((entry) =>
|
|
327
|
+
normalizePluginConfigForGeneratedTsconfig(entry, tsconfigDir),
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
return output;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function normalizePluginConfigForGeneratedTsconfig(
|
|
334
|
+
entry: unknown,
|
|
335
|
+
tsconfigDir: string,
|
|
336
|
+
): unknown {
|
|
337
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
338
|
+
return entry;
|
|
339
|
+
}
|
|
340
|
+
const output: Record<string, unknown> = { ...entry };
|
|
341
|
+
for (const key of ["config", "source", "transform"]) {
|
|
342
|
+
const value = output[key];
|
|
343
|
+
if (typeof value === "string" && isRelativeSpecifier(value)) {
|
|
344
|
+
output[key] = path.resolve(tsconfigDir, value);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return output;
|
|
348
|
+
}
|
|
349
|
+
|
|
169
350
|
function createAliasCompilerOptions(props: {
|
|
170
351
|
aliasPaths: Record<string, string[]>;
|
|
171
352
|
baseUrl: string;
|
|
@@ -183,6 +364,15 @@ function createAliasCompilerOptions(props: {
|
|
|
183
364
|
};
|
|
184
365
|
}
|
|
185
366
|
|
|
367
|
+
function hasPaths(value: unknown): boolean {
|
|
368
|
+
return (
|
|
369
|
+
typeof value === "object" &&
|
|
370
|
+
value !== null &&
|
|
371
|
+
!Array.isArray(value) &&
|
|
372
|
+
Object.keys(value).length !== 0
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
186
376
|
function readPaths(value: unknown): Record<string, string[]> {
|
|
187
377
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
188
378
|
return {};
|
|
@@ -260,13 +450,51 @@ function normalizeAliases(aliases: unknown): TtscTransformAlias[] {
|
|
|
260
450
|
return [];
|
|
261
451
|
}
|
|
262
452
|
|
|
453
|
+
function createTransformCacheKey(props: {
|
|
454
|
+
aliasPaths: Record<string, string[]>;
|
|
455
|
+
compilerOptions: Record<string, unknown>;
|
|
456
|
+
plugins?: ResolvedTtscUnpluginOptions["plugins"];
|
|
457
|
+
tsconfig: string;
|
|
458
|
+
}): string {
|
|
459
|
+
return stableStringify({
|
|
460
|
+
aliasPaths: props.aliasPaths,
|
|
461
|
+
compilerOptions: props.compilerOptions,
|
|
462
|
+
plugins: props.plugins,
|
|
463
|
+
tsconfig: path.resolve(props.tsconfig),
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function stableStringify(value: unknown): string {
|
|
468
|
+
if (Array.isArray(value)) {
|
|
469
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
470
|
+
}
|
|
471
|
+
if (value && typeof value === "object") {
|
|
472
|
+
return `{${Object.entries(value)
|
|
473
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
474
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`)
|
|
475
|
+
.join(",")}}`;
|
|
476
|
+
}
|
|
477
|
+
return JSON.stringify(value);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function isRelativeSpecifier(value: string): boolean {
|
|
481
|
+
return (
|
|
482
|
+
value === "." ||
|
|
483
|
+
value === ".." ||
|
|
484
|
+
value.startsWith("./") ||
|
|
485
|
+
value.startsWith("../") ||
|
|
486
|
+
value.startsWith(".\\") ||
|
|
487
|
+
value.startsWith("..\\")
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
263
491
|
function isAlias(value: unknown): value is TtscTransformAlias {
|
|
264
492
|
return (
|
|
265
493
|
typeof value === "object" &&
|
|
266
494
|
value !== null &&
|
|
267
495
|
"find" in value &&
|
|
268
496
|
"replacement" in value &&
|
|
269
|
-
|
|
497
|
+
typeof value.find === "string" &&
|
|
270
498
|
typeof value.replacement === "string"
|
|
271
499
|
);
|
|
272
500
|
}
|
|
@@ -296,6 +524,16 @@ function selectTransformedSource(props: {
|
|
|
296
524
|
throw new Error(`ttsc transform did not return output for ${props.file}`);
|
|
297
525
|
}
|
|
298
526
|
|
|
527
|
+
function reportSuccessDiagnostics(result: ITtscCompilerTransformation): void {
|
|
528
|
+
if (result.type !== "success" || result.diagnostics === undefined) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const text = formatDiagnostics(result.diagnostics);
|
|
532
|
+
if (text.length !== 0) {
|
|
533
|
+
process.stderr.write(`${text}\n`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
299
537
|
function formatDiagnostics(diagnostics: ITtscCompilerDiagnostic[]): string {
|
|
300
538
|
if (diagnostics.length === 0) {
|
|
301
539
|
return "ttsc transform failed";
|