@openload28/unplugin-tt 0.1.0-next.1.0.0.20260828.263

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 ADDED
@@ -0,0 +1,86 @@
1
+ # @openload28/unplugin-tt
2
+
3
+ `.tt`과 `.ttx` 모듈을 번들러에서 그대로 import합니다. 중간 `.ts`/`.tsx` 트리를 만들지 않고,
4
+ 번들러가 소스를 직접 읽습니다.
5
+
6
+ [unplugin](https://github.com/unjs/unplugin) 기반이라 구현은 하나이고 번들러별
7
+ 서브패스로 갈라져 나갑니다.
8
+
9
+ ```ts
10
+ // vite.config.ts
11
+ import { defineConfig } from "vite";
12
+ import tt from "@openload28/unplugin-tt/vite";
13
+
14
+ export default defineConfig({ plugins: [tt()] });
15
+ ```
16
+
17
+ ```ts
18
+ // src/main.ts — 평범한 TypeScript
19
+ import { Notice, render } from "./notice.tt";
20
+ ```
21
+
22
+ 컴파일러는 [`@openload28/tt-lang`](https://www.npmjs.com/package/@openload28/tt-lang)을 함께
23
+ 설치하면 자동으로 찾습니다 (`npm install --save-dev @openload28/tt-lang@next @openload28/unplugin-tt@next`). @openload28/tt-lang이
24
+ 없으면 PATH의 `ttc`(`cargo install --path .`)로 폴백합니다.
25
+
26
+ ## 서브패스
27
+
28
+ | import | 상태 |
29
+ |--------|------|
30
+ | `@openload28/unplugin-tt/vite` | 예제(`tt-interop`)로 검증 |
31
+ | `@openload28/unplugin-tt/esbuild` | 번들·실행 검증 |
32
+ | `@openload28/unplugin-tt/rollup`, `/rolldown`, `/webpack`, `/rspack`, `/farm` | unplugin이 제공하는 어댑터 — 미검증 |
33
+
34
+ `@openload28/unplugin-tt`를 그대로 import하면 `unplugin` 객체와 `vitePlugin`·
35
+ `esbuildPlugin` 같은 이름들이 나옵니다.
36
+
37
+ ## 동작
38
+
39
+ | 단계 | 하는 일 |
40
+ |------|---------|
41
+ | `resolveId` | `.tt`/`.ttx` 지정자를 파일 경로로 풀고 각각 `.ts`/`.tsx`를 덧붙인 가상 id를 돌려줍니다. `@tt/std`, `@tt/std/option`, `@tt/std/result`는 각각 가상 모듈 id로 바꿉니다 |
42
+ | `load` | `ttc -p --rewrite-imports off`의 출력을 돌려줍니다. 표준 라이브러리와 파이프 런타임은 모듈별 `ttc --emit-std types|option|result|runtime` 출력을 사용합니다 |
43
+
44
+ id에 `.ts` 또는 `.tsx`를 붙이는 이유는 **호스트의 TypeScript 처리에 그대로 태우기**
45
+ 위해서입니다. 덕분에 플러그인이 변환을 직접 하지 않습니다. 다만 esbuild의
46
+ `load`는 JavaScript만 반환할 수 있어서, 그 경로에는 소스 종류에 맞는 `ts`/`tsx`
47
+ loader를 명시합니다.
48
+
49
+ `--rewrite-imports off`인 것도 의도입니다. 지정자 재작성은 미리 컴파일하는
50
+ 파이프라인을 위한 기능이고, 여기서는 `.tt`이 그대로 남아야 이 플러그인이
51
+ 다음 모듈도 잡습니다.
52
+
53
+ 컴파일 에러는 ttc의 진단이 그대로 빌드 에러가 됩니다.
54
+
55
+ ```
56
+ [@openload28/unplugin-tt] src/notice.tt:22:16: match on variant Notice is not exhaustive:
57
+ missing "Warn" (add the missing arms or a final `_` arm)
58
+ ```
59
+
60
+ ## 옵션
61
+
62
+ | 옵션 | 기본값 | 설명 |
63
+ |------|--------|------|
64
+ | `compiler` | 설치된 `@openload28/tt-lang`의 바이너리, 없으면 `"ttc"` | ttc 실행 파일 경로 |
65
+ | `verify` | `true` | `false`면 `--no-verify`를 넘겨 방출물 자가 검사를 생략합니다 |
66
+
67
+ 타입 선언(`index.d.ts`와 서브패스별 `.d.ts`)을 함께 싣습니다 — 소비자가
68
+ `vite.config.ts`를 타입 검사에 넣어도 `tt()`의 옵션이 그대로 검사됩니다.
69
+
70
+ ## 타입은 별도입니다
71
+
72
+ 번들러 플러그인은 **런타임만** 해결합니다. `.ts` 파일이 `.tt`을 import할 때
73
+ 타입 검사와 정의 이동이 동작하려면 사이드카가 필요하고, 그건 ttc가 만듭니다.
74
+
75
+ ```sh
76
+ ttc --types src/ # .tt-types/<이름>.tt.d.ts + .map
77
+ ```
78
+
79
+ CLI에서 `ttc help workflow`를 실행하거나 [VSCode 확장](../../editors/vscode/README.md)을
80
+ 참조하세요. 확장은 저장할 때마다 사이드카를 갱신합니다.
81
+
82
+ ## 알려진 제약
83
+
84
+ - `enforce: "pre"`는 Rollup·esbuild에서 무시됩니다 (unplugin 문서의 지원 훅
85
+ 표). 그 두 곳에서는 플러그인 순서를 직접 앞에 두세요.
86
+ - `resolveId`는 Rspack·Rsbuild에서 최신 버전을 요구합니다.
package/esbuild.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { UnpluginInstance } from "unplugin";
2
+ import type { Options } from "./index.js";
3
+
4
+ declare const plugin: UnpluginInstance<Options | undefined>["esbuild"];
5
+ export default plugin;
package/esbuild.js ADDED
@@ -0,0 +1,3 @@
1
+ import unplugin from "./index.js";
2
+
3
+ export default unplugin.esbuild;
package/farm.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { UnpluginInstance } from "unplugin";
2
+ import type { Options } from "./index.js";
3
+
4
+ declare const plugin: UnpluginInstance<Options | undefined>["farm"];
5
+ export default plugin;
package/farm.js ADDED
@@ -0,0 +1,3 @@
1
+ import unplugin from "./index.js";
2
+
3
+ export default unplugin.farm;
package/index.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { UnpluginFactory, UnpluginInstance } from "unplugin";
2
+
3
+ /** Options accepted by every `@openload28/unplugin-tt` entry. */
4
+ export interface Options {
5
+ /** Path to the ttc binary (default: the installed `@openload28/tt-lang`, else `"ttc"`). */
6
+ compiler?: string;
7
+ /** Run ttc's output self-check (default: true). */
8
+ verify?: boolean;
9
+ }
10
+
11
+ export declare const unpluginFactory: UnpluginFactory<Options | undefined>;
12
+ export declare const unplugin: UnpluginInstance<Options | undefined>;
13
+ export default unplugin;
14
+
15
+ export declare const vitePlugin: UnpluginInstance<Options | undefined>["vite"];
16
+ export declare const rollupPlugin: UnpluginInstance<Options | undefined>["rollup"];
17
+ export declare const rolldownPlugin: UnpluginInstance<Options | undefined>["rolldown"];
18
+ export declare const webpackPlugin: UnpluginInstance<Options | undefined>["webpack"];
19
+ export declare const rspackPlugin: UnpluginInstance<Options | undefined>["rspack"];
20
+ export declare const esbuildPlugin: UnpluginInstance<Options | undefined>["esbuild"];
21
+ export declare const farmPlugin: UnpluginInstance<Options | undefined>["farm"];
package/index.js ADDED
@@ -0,0 +1,200 @@
1
+ /* --------------------------------------------------------------------------
2
+ * @openload28/unplugin-tt — `.tt` modules for every bundler unplugin supports.
3
+ *
4
+ * The plugin resolves `.tt` specifiers itself and compiles each file with
5
+ * `ttc` on the way in, so a project needs no intermediate `.ts` tree: the
6
+ * bundler reads the sources directly.
7
+ *
8
+ * Two deliberate details:
9
+ *
10
+ * - `ttc` runs with `--rewrite-imports off`. Rewriting exists for the
11
+ * ahead-of-time pipeline (where a `.tt` neighbour has already become a
12
+ * `.ts` file); here the specifier must stay `.tt` so this plugin resolves
13
+ * it too.
14
+ * - Module ids get a `.ts` suffix. ttc emits TypeScript, and the host's own
15
+ * TypeScript pass keys off the extension — this keeps the plugin out of
16
+ * that job entirely. esbuild is told the loader explicitly instead, since
17
+ * its `load` hook may only return JavaScript.
18
+ *
19
+ * Editor support is separate: `ttc --types` writes the declarations that let
20
+ * a `.ts` file import `.tt` without the type checker complaining.
21
+ * ----------------------------------------------------------------------- */
22
+ import { execFile } from "node:child_process";
23
+ import { createRequire } from "node:module";
24
+ import * as path from "node:path";
25
+ import { promisify } from "node:util";
26
+
27
+ import { createUnplugin } from "unplugin";
28
+
29
+ const run = promisify(execFile);
30
+
31
+ /**
32
+ * Default compiler: the prebuilt binary from an installed `@openload28/tt-lang` npm
33
+ * package when present (spawned directly — no per-call node launcher),
34
+ * otherwise `ttc` from PATH as before.
35
+ */
36
+ function defaultCompiler() {
37
+ try {
38
+ const require = createRequire(import.meta.url);
39
+ return require("@openload28/tt-lang").binaryPath();
40
+ } catch {
41
+ return "ttc";
42
+ }
43
+ }
44
+
45
+ /** Virtual suffix for ordinary `.tt` modules. */
46
+ const TS_SUFFIX = ".ts";
47
+ /** Virtual suffix for JSX-bearing `.ttx` modules. */
48
+ const TSX_SUFFIX = ".tsx";
49
+
50
+ const sourceSuffix = (file) => (file.endsWith(".ttx") ? TSX_SUFFIX : TS_SUFFIX);
51
+
52
+ const sourceFileOfId = (id) => {
53
+ if (id.endsWith(`.ttx${TSX_SUFFIX}`)) return id.slice(0, -TSX_SUFFIX.length);
54
+ if (id.endsWith(`.tt${TS_SUFFIX}`)) return id.slice(0, -TS_SUFFIX.length);
55
+ return null;
56
+ };
57
+
58
+ /** The bare specifier tt sources use for the standard library. */
59
+ const STD_MODULES = new Map([
60
+ ["@tt/std", "types"],
61
+ ["@tt/std/option", "option"],
62
+ ["@tt/std/result", "result"],
63
+ ["@tt/runtime", "runtime"],
64
+ ]);
65
+
66
+ /** Virtual module id for the standard library, per working directory. */
67
+ const stdId = (module) =>
68
+ path.resolve(process.cwd(), "__tt_std__", `${module}${TS_SUFFIX}`);
69
+
70
+ const stdModuleOfId = (id) => {
71
+ for (const module of STD_MODULES.values()) {
72
+ if (id === stdId(module)) return module;
73
+ }
74
+ return null;
75
+ };
76
+
77
+ const INLINE_MAP =
78
+ /\n\/\/# sourceMappingURL=data:application\/json;charset=utf-8;base64,([A-Za-z0-9+/=]+)\n?$/;
79
+
80
+ /**
81
+ * Splits ttc's inline source map back out of the printed output.
82
+ *
83
+ * The map travels inline because stdout carries one stream; a bundler wants
84
+ * it as a separate object so it can compose it with everything downstream.
85
+ * Output without a map is returned unchanged.
86
+ *
87
+ * @param {string} code
88
+ * @returns {{ code: string, map: object | null }}
89
+ */
90
+ function detachInlineSourceMap(code) {
91
+ const found = INLINE_MAP.exec(code);
92
+ if (found === null) return { code, map: null };
93
+ try {
94
+ const map = JSON.parse(Buffer.from(found[1], "base64").toString("utf8"));
95
+ return { code: code.slice(0, found.index + 1), map };
96
+ } catch {
97
+ // An unreadable map is not a reason to fail the build; the code is
98
+ // still exactly what ttc produced.
99
+ return { code, map: null };
100
+ }
101
+ }
102
+
103
+ /**
104
+ * @typedef {object} Options
105
+ * @property {string} [compiler] Path to the ttc binary (default: the
106
+ * installed `@openload28/tt-lang` package's binary, falling back to `"ttc"` on PATH).
107
+ * @property {boolean} [verify] Run ttc's output self-check (default: true).
108
+ * @property {boolean} [sourcemap] Ask ttc for a source map and hand it to the
109
+ * host, so a stack trace and a debugger point at the `.tt` (default: true).
110
+ */
111
+
112
+ /** @type {import("unplugin").UnpluginFactory<Options | undefined>} */
113
+ export const unpluginFactory = (options = {}) => {
114
+ const compiler = options.compiler ?? defaultCompiler();
115
+ const verify = options.verify ?? true;
116
+ const sourcemap = options.sourcemap ?? true;
117
+
118
+ return {
119
+ name: "@openload28/unplugin-tt",
120
+ // Ahead of the host's own resolution: `.tt` is not an extension it
121
+ // knows. Rollup and esbuild ignore `enforce`, where plugin order is the
122
+ // author's responsibility instead.
123
+ enforce: "pre",
124
+
125
+ resolveId(source, importer) {
126
+ // The standard library has no file: ttc prints it on demand, so it
127
+ // becomes a virtual module. Nothing lands in the project tree.
128
+ const stdModule = STD_MODULES.get(source);
129
+ if (stdModule !== undefined) return stdId(stdModule);
130
+ if (importer !== undefined && importer !== null) {
131
+ const importerModule = stdModuleOfId(importer);
132
+ if (importerModule !== null) {
133
+ if (source === "./option.js") return stdId("option");
134
+ if (source === "./result.js") return stdId("result");
135
+ }
136
+ }
137
+ if (!source.endsWith(".tt") && !source.endsWith(".ttx")) return null;
138
+
139
+ const file = path.isAbsolute(source)
140
+ ? source
141
+ : importer === undefined || importer === null
142
+ ? null
143
+ : path.resolve(path.dirname(importer), source);
144
+ return file === null ? null : `${file}${sourceSuffix(file)}`;
145
+ },
146
+
147
+ async load(id) {
148
+ const stdModule = stdModuleOfId(id);
149
+ if (stdModule !== null) {
150
+ const { stdout } = await run(compiler, ["--emit-std", stdModule, "--no-banner"], {
151
+ maxBuffer: 16 * 1024 * 1024,
152
+ });
153
+ return { code: stdout, map: null };
154
+ }
155
+ const file = sourceFileOfId(id);
156
+ if (file === null) return null;
157
+
158
+ const args = ["-p", "--rewrite-imports", "off"];
159
+ if (!verify) args.push("--no-verify");
160
+ // ttc prints the map into the output as a data: URL — the one form
161
+ // that survives a pipe. It is split back out here so the host gets a
162
+ // real map object and composes it with its own transforms.
163
+ if (sourcemap) args.push("--source-map", "inline");
164
+ args.push(file);
165
+
166
+ try {
167
+ const { stdout } = await run(compiler, args, { maxBuffer: 16 * 1024 * 1024 });
168
+ this.addWatchFile(file);
169
+ return detachInlineSourceMap(stdout);
170
+ } catch (error) {
171
+ // ttc reports `file:line:col: message` on stderr; surface that as
172
+ // the build error so the host shows the compiler's diagnostic.
173
+ const detail = String(error.stderr ?? error.message).trim();
174
+ this.error(detail.replace(/^ttc:\s*/, ""));
175
+ return null;
176
+ }
177
+ },
178
+
179
+ esbuild: {
180
+ // esbuild resolves and loads through its own filters, and its `load`
181
+ // may only return JavaScript — so narrow the filters to our ids and
182
+ // name the loader for the TypeScript ttc emits.
183
+ onResolveFilter: /(\.ttx?|^@tt\/(?:std(?:\/(?:option|result))?|runtime)$|\.\/(?:option|result)\.js$)/,
184
+ onLoadFilter: /(\.tt\.ts|\.ttx\.tsx|__tt_std__\/(?:types|option|result|runtime)\.ts)$/,
185
+ loader: (_code, id) => (id.endsWith(TSX_SUFFIX) ? "tsx" : "ts"),
186
+ },
187
+ };
188
+ };
189
+
190
+ export const unplugin = /* #__PURE__ */ createUnplugin(unpluginFactory);
191
+
192
+ export default unplugin;
193
+
194
+ export const vitePlugin = unplugin.vite;
195
+ export const rollupPlugin = unplugin.rollup;
196
+ export const rolldownPlugin = unplugin.rolldown;
197
+ export const webpackPlugin = unplugin.webpack;
198
+ export const rspackPlugin = unplugin.rspack;
199
+ export const esbuildPlugin = unplugin.esbuild;
200
+ export const farmPlugin = unplugin.farm;
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "@openload28/unplugin-tt",
3
+ "version": "0.1.0-next.1.0.0.20260828.263",
4
+ "description": "Compile .tt and .ttx modules with ttc in Vite, Rollup, webpack, Rspack, esbuild and Farm",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public",
8
+ "registry": "https://registry.npmjs.org/"
9
+ },
10
+ "type": "module",
11
+ "main": "./index.js",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./index.d.ts",
15
+ "default": "./index.js"
16
+ },
17
+ "./vite": {
18
+ "types": "./vite.d.ts",
19
+ "default": "./vite.js"
20
+ },
21
+ "./rollup": {
22
+ "types": "./rollup.d.ts",
23
+ "default": "./rollup.js"
24
+ },
25
+ "./rolldown": {
26
+ "types": "./rolldown.d.ts",
27
+ "default": "./rolldown.js"
28
+ },
29
+ "./webpack": {
30
+ "types": "./webpack.d.ts",
31
+ "default": "./webpack.js"
32
+ },
33
+ "./rspack": {
34
+ "types": "./rspack.d.ts",
35
+ "default": "./rspack.js"
36
+ },
37
+ "./esbuild": {
38
+ "types": "./esbuild.d.ts",
39
+ "default": "./esbuild.js"
40
+ },
41
+ "./farm": {
42
+ "types": "./farm.d.ts",
43
+ "default": "./farm.js"
44
+ }
45
+ },
46
+ "files": [
47
+ "index.js",
48
+ "index.d.ts",
49
+ "vite.js",
50
+ "vite.d.ts",
51
+ "rollup.js",
52
+ "rollup.d.ts",
53
+ "rolldown.js",
54
+ "rolldown.d.ts",
55
+ "webpack.js",
56
+ "webpack.d.ts",
57
+ "rspack.js",
58
+ "rspack.d.ts",
59
+ "esbuild.js",
60
+ "esbuild.d.ts",
61
+ "farm.js",
62
+ "farm.d.ts",
63
+ "README.md"
64
+ ],
65
+ "repository": {
66
+ "type": "git",
67
+ "url": "https://github.com/load28/tt",
68
+ "directory": "integrations/unplugin"
69
+ },
70
+ "keywords": [
71
+ "unplugin",
72
+ "vite-plugin",
73
+ "rollup-plugin",
74
+ "webpack-plugin",
75
+ "esbuild-plugin",
76
+ "tt",
77
+ "typescript"
78
+ ],
79
+ "dependencies": {
80
+ "unplugin": "^2.1.0"
81
+ },
82
+ "engines": {
83
+ "node": ">=18"
84
+ },
85
+ "types": "./index.d.ts"
86
+ }
package/rolldown.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { UnpluginInstance } from "unplugin";
2
+ import type { Options } from "./index.js";
3
+
4
+ declare const plugin: UnpluginInstance<Options | undefined>["rolldown"];
5
+ export default plugin;
package/rolldown.js ADDED
@@ -0,0 +1,3 @@
1
+ import unplugin from "./index.js";
2
+
3
+ export default unplugin.rolldown;
package/rollup.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { UnpluginInstance } from "unplugin";
2
+ import type { Options } from "./index.js";
3
+
4
+ declare const plugin: UnpluginInstance<Options | undefined>["rollup"];
5
+ export default plugin;
package/rollup.js ADDED
@@ -0,0 +1,3 @@
1
+ import unplugin from "./index.js";
2
+
3
+ export default unplugin.rollup;
package/rspack.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { UnpluginInstance } from "unplugin";
2
+ import type { Options } from "./index.js";
3
+
4
+ declare const plugin: UnpluginInstance<Options | undefined>["rspack"];
5
+ export default plugin;
package/rspack.js ADDED
@@ -0,0 +1,3 @@
1
+ import unplugin from "./index.js";
2
+
3
+ export default unplugin.rspack;
package/vite.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { UnpluginInstance } from "unplugin";
2
+ import type { Options } from "./index.js";
3
+
4
+ declare const plugin: UnpluginInstance<Options | undefined>["vite"];
5
+ export default plugin;
package/vite.js ADDED
@@ -0,0 +1,3 @@
1
+ import unplugin from "./index.js";
2
+
3
+ export default unplugin.vite;
package/webpack.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { UnpluginInstance } from "unplugin";
2
+ import type { Options } from "./index.js";
3
+
4
+ declare const plugin: UnpluginInstance<Options | undefined>["webpack"];
5
+ export default plugin;
package/webpack.js ADDED
@@ -0,0 +1,3 @@
1
+ import unplugin from "./index.js";
2
+
3
+ export default unplugin.webpack;