@macroforge/vite-plugin 0.1.4
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 +35 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +291 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @macroforge/vite-plugin
|
|
2
|
+
|
|
3
|
+
> **Warning:** This is a work in progress and probably won't work for you. Use at your own risk!
|
|
4
|
+
|
|
5
|
+
Vite plugin for macroforge compile-time TypeScript macros.
|
|
6
|
+
|
|
7
|
+
Part of the [macroforge](https://github.com/rymskip/macroforge-ts) project.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @macroforge/vite-plugin
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// vite.config.ts
|
|
19
|
+
import macroforge from "@macroforge/vite-plugin";
|
|
20
|
+
|
|
21
|
+
export default defineConfig({
|
|
22
|
+
plugins: [
|
|
23
|
+
macroforge({
|
|
24
|
+
typesOutputDir: ".macroforge/types",
|
|
25
|
+
metadataOutputDir: ".macroforge/meta",
|
|
26
|
+
generateTypes: true,
|
|
27
|
+
emitMetadata: true,
|
|
28
|
+
}),
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## License
|
|
34
|
+
|
|
35
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Plugin } from "vite";
|
|
2
|
+
export interface NapiMacrosPluginOptions {
|
|
3
|
+
include?: string | RegExp | (string | RegExp)[];
|
|
4
|
+
exclude?: string | RegExp | (string | RegExp)[];
|
|
5
|
+
generateTypes?: boolean;
|
|
6
|
+
typesOutputDir?: string;
|
|
7
|
+
emitMetadata?: boolean;
|
|
8
|
+
metadataOutputDir?: string;
|
|
9
|
+
}
|
|
10
|
+
declare function napiMacrosPlugin(options?: NapiMacrosPluginOptions): Plugin;
|
|
11
|
+
export default napiMacrosPlugin;
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAsC9B,MAAM,WAAW,uBAAuB;IACtC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAChD,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAChD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AA6LD,iBAAS,gBAAgB,CAAC,OAAO,GAAE,uBAA4B,GAAG,MAAM,CA0LvE;AAED,eAAe,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
const moduleRequire = createRequire(import.meta.url);
|
|
5
|
+
let tsModule;
|
|
6
|
+
try {
|
|
7
|
+
tsModule = moduleRequire("typescript");
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
tsModule = undefined;
|
|
11
|
+
console.warn("[@macroforge/vite-plugin] TypeScript not found. Generated .d.ts files will be skipped.");
|
|
12
|
+
}
|
|
13
|
+
const compilerOptionsCache = new Map();
|
|
14
|
+
let cachedRequire;
|
|
15
|
+
async function ensureRequire() {
|
|
16
|
+
if (typeof require !== "undefined") {
|
|
17
|
+
return require;
|
|
18
|
+
}
|
|
19
|
+
if (!cachedRequire) {
|
|
20
|
+
const { createRequire } = await import("module");
|
|
21
|
+
cachedRequire = createRequire(process.cwd() + "/");
|
|
22
|
+
// Expose on globalThis so native runtime loaders can use it
|
|
23
|
+
globalThis.require = cachedRequire;
|
|
24
|
+
}
|
|
25
|
+
return cachedRequire;
|
|
26
|
+
}
|
|
27
|
+
function loadMacroConfig(projectRoot) {
|
|
28
|
+
let current = projectRoot;
|
|
29
|
+
const fallback = { keepDecorators: false };
|
|
30
|
+
while (true) {
|
|
31
|
+
const candidate = path.join(current, "macroforge.json");
|
|
32
|
+
if (fs.existsSync(candidate)) {
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(candidate, "utf8");
|
|
35
|
+
const parsed = JSON.parse(raw);
|
|
36
|
+
return { keepDecorators: Boolean(parsed.keepDecorators) };
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return fallback;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const parent = path.dirname(current);
|
|
43
|
+
if (parent === current)
|
|
44
|
+
break;
|
|
45
|
+
current = parent;
|
|
46
|
+
}
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
function getCompilerOptions(projectRoot) {
|
|
50
|
+
if (!tsModule) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const cached = compilerOptionsCache.get(projectRoot);
|
|
54
|
+
if (cached) {
|
|
55
|
+
return cached;
|
|
56
|
+
}
|
|
57
|
+
let configPath;
|
|
58
|
+
try {
|
|
59
|
+
configPath = tsModule.findConfigFile(projectRoot, tsModule.sys.fileExists, "tsconfig.json");
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
configPath = undefined;
|
|
63
|
+
}
|
|
64
|
+
let options;
|
|
65
|
+
if (configPath) {
|
|
66
|
+
const configFile = tsModule.readConfigFile(configPath, tsModule.sys.readFile);
|
|
67
|
+
if (configFile.error) {
|
|
68
|
+
const formatted = tsModule.formatDiagnosticsWithColorAndContext([configFile.error], {
|
|
69
|
+
getCurrentDirectory: () => projectRoot,
|
|
70
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
71
|
+
getNewLine: () => tsModule.sys.newLine,
|
|
72
|
+
});
|
|
73
|
+
console.warn(`[@macroforge/vite-plugin] Failed to read tsconfig at ${configPath}\n${formatted}`);
|
|
74
|
+
options = {};
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const parsed = tsModule.parseJsonConfigFileContent(configFile.config, tsModule.sys, path.dirname(configPath));
|
|
78
|
+
options = parsed.options;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
options = {};
|
|
83
|
+
}
|
|
84
|
+
const normalized = {
|
|
85
|
+
...options,
|
|
86
|
+
declaration: true,
|
|
87
|
+
emitDeclarationOnly: true,
|
|
88
|
+
noEmitOnError: false,
|
|
89
|
+
incremental: false,
|
|
90
|
+
};
|
|
91
|
+
delete normalized.outDir;
|
|
92
|
+
delete normalized.outFile;
|
|
93
|
+
normalized.moduleResolution ??= tsModule.ModuleResolutionKind.Bundler;
|
|
94
|
+
normalized.module ??= tsModule.ModuleKind.ESNext;
|
|
95
|
+
normalized.target ??= tsModule.ScriptTarget.ESNext;
|
|
96
|
+
normalized.strict ??= true;
|
|
97
|
+
normalized.skipLibCheck ??= true;
|
|
98
|
+
compilerOptionsCache.set(projectRoot, normalized);
|
|
99
|
+
return normalized;
|
|
100
|
+
}
|
|
101
|
+
function emitDeclarationsFromCode(code, fileName, projectRoot) {
|
|
102
|
+
if (!tsModule) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const compilerOptions = getCompilerOptions(projectRoot);
|
|
106
|
+
if (!compilerOptions) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const normalizedFileName = path.resolve(fileName);
|
|
110
|
+
const sourceText = code;
|
|
111
|
+
const compilerHost = tsModule.createCompilerHost(compilerOptions, true);
|
|
112
|
+
compilerHost.getSourceFile = (requestedFileName, languageVersion) => {
|
|
113
|
+
if (path.resolve(requestedFileName) === normalizedFileName) {
|
|
114
|
+
return tsModule.createSourceFile(requestedFileName, sourceText, languageVersion, true);
|
|
115
|
+
}
|
|
116
|
+
const text = tsModule.sys.readFile(requestedFileName);
|
|
117
|
+
return text !== undefined
|
|
118
|
+
? tsModule.createSourceFile(requestedFileName, text, languageVersion, true)
|
|
119
|
+
: undefined;
|
|
120
|
+
};
|
|
121
|
+
compilerHost.readFile = (requestedFileName) => {
|
|
122
|
+
return path.resolve(requestedFileName) === normalizedFileName
|
|
123
|
+
? sourceText
|
|
124
|
+
: tsModule.sys.readFile(requestedFileName);
|
|
125
|
+
};
|
|
126
|
+
compilerHost.fileExists = (requestedFileName) => {
|
|
127
|
+
return (path.resolve(requestedFileName) === normalizedFileName ||
|
|
128
|
+
tsModule.sys.fileExists(requestedFileName));
|
|
129
|
+
};
|
|
130
|
+
let output;
|
|
131
|
+
const writeFile = (outputName, text) => {
|
|
132
|
+
if (outputName.endsWith(".d.ts")) {
|
|
133
|
+
output = text;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const program = tsModule.createProgram([normalizedFileName], compilerOptions, compilerHost);
|
|
137
|
+
const emitResult = program.emit(undefined, writeFile, undefined, true);
|
|
138
|
+
if (emitResult.emitSkipped && emitResult.diagnostics.length > 0) {
|
|
139
|
+
const formatted = tsModule.formatDiagnosticsWithColorAndContext(emitResult.diagnostics, {
|
|
140
|
+
getCurrentDirectory: () => projectRoot,
|
|
141
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
142
|
+
getNewLine: () => tsModule.sys.newLine,
|
|
143
|
+
});
|
|
144
|
+
console.warn(`[@macroforge/vite-plugin] Declaration emit failed for ${path.relative(projectRoot, fileName)}\n${formatted}`);
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
return output;
|
|
148
|
+
}
|
|
149
|
+
function napiMacrosPlugin(options = {}) {
|
|
150
|
+
let rustTransformer;
|
|
151
|
+
let projectRoot;
|
|
152
|
+
let macroConfig = { keepDecorators: false };
|
|
153
|
+
const generateTypes = options.generateTypes !== false; // Default to true
|
|
154
|
+
const typesOutputDir = options.typesOutputDir || "src/macros/generated";
|
|
155
|
+
const emitMetadata = options.emitMetadata !== false;
|
|
156
|
+
const metadataOutputDir = options.metadataOutputDir || typesOutputDir;
|
|
157
|
+
// Ensure directory exists
|
|
158
|
+
function ensureDir(dir) {
|
|
159
|
+
if (!fs.existsSync(dir)) {
|
|
160
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function writeTypeDefinitions(id, types) {
|
|
164
|
+
const relativePath = path.relative(projectRoot, id);
|
|
165
|
+
const parsed = path.parse(relativePath);
|
|
166
|
+
const outputBase = path.join(projectRoot, typesOutputDir, parsed.dir);
|
|
167
|
+
ensureDir(outputBase);
|
|
168
|
+
const targetPath = path.join(outputBase, `${parsed.name}.d.ts`);
|
|
169
|
+
try {
|
|
170
|
+
const existing = fs.existsSync(targetPath)
|
|
171
|
+
? fs.readFileSync(targetPath, "utf-8")
|
|
172
|
+
: null;
|
|
173
|
+
if (existing !== types) {
|
|
174
|
+
fs.writeFileSync(targetPath, types, "utf-8");
|
|
175
|
+
console.log(`[@macroforge/vite-plugin] Wrote types for ${relativePath} -> ${path.relative(projectRoot, targetPath)}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
console.error(`[@macroforge/vite-plugin] Failed to write type definitions for ${id}:`, error);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function writeMetadata(id, metadata) {
|
|
183
|
+
const relativePath = path.relative(projectRoot, id);
|
|
184
|
+
const parsed = path.parse(relativePath);
|
|
185
|
+
const outputBase = path.join(projectRoot, metadataOutputDir, parsed.dir);
|
|
186
|
+
ensureDir(outputBase);
|
|
187
|
+
const targetPath = path.join(outputBase, `${parsed.name}.macro-ir.json`);
|
|
188
|
+
try {
|
|
189
|
+
const existing = fs.existsSync(targetPath)
|
|
190
|
+
? fs.readFileSync(targetPath, "utf-8")
|
|
191
|
+
: null;
|
|
192
|
+
if (existing !== metadata) {
|
|
193
|
+
fs.writeFileSync(targetPath, metadata, "utf-8");
|
|
194
|
+
console.log(`[@macroforge/vite-plugin] Wrote metadata for ${relativePath} -> ${path.relative(projectRoot, targetPath)}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
console.error(`[@macroforge/vite-plugin] Failed to write metadata for ${id}:`, error);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function formatTransformError(error, id) {
|
|
202
|
+
const relative = projectRoot ? path.relative(projectRoot, id) || id : id;
|
|
203
|
+
if (error instanceof Error) {
|
|
204
|
+
const details = error.stack && error.stack.includes(error.message)
|
|
205
|
+
? error.stack
|
|
206
|
+
: `${error.message}\n${error.stack ?? ""}`;
|
|
207
|
+
return `[@macroforge/vite-plugin] Failed to transform ${relative}\n${details}`.trim();
|
|
208
|
+
}
|
|
209
|
+
return `[@macroforge/vite-plugin] Failed to transform ${relative}: ${String(error)}`;
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
name: "@macroforge/vite-plugin",
|
|
213
|
+
enforce: "pre",
|
|
214
|
+
configResolved(config) {
|
|
215
|
+
projectRoot = config.root;
|
|
216
|
+
macroConfig = loadMacroConfig(projectRoot);
|
|
217
|
+
// Load the Rust binary
|
|
218
|
+
try {
|
|
219
|
+
rustTransformer = moduleRequire("macroforge");
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
console.warn("[@macroforge/vite-plugin] Rust binary not found. Please run `npm run build:rust` first.");
|
|
223
|
+
console.warn(error);
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
async transform(code, id) {
|
|
227
|
+
await ensureRequire();
|
|
228
|
+
// Only transform TypeScript files
|
|
229
|
+
if (!id.endsWith(".ts") && !id.endsWith(".tsx")) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
// Skip node_modules by default
|
|
233
|
+
if (id.includes("node_modules")) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
// Check if Rust transformer is available
|
|
237
|
+
if (!rustTransformer || !rustTransformer.expandSync) {
|
|
238
|
+
// Return unchanged if transformer not available
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
const result = rustTransformer.expandSync(code, id, {
|
|
243
|
+
keepDecorators: macroConfig.keepDecorators,
|
|
244
|
+
});
|
|
245
|
+
// Report diagnostics
|
|
246
|
+
for (const diag of result.diagnostics) {
|
|
247
|
+
if (diag.level === "error") {
|
|
248
|
+
const message = `Macro error at ${id}:${diag.start ?? "?"}-${diag.end ?? "?"}: ${diag.message}`;
|
|
249
|
+
this.error(message);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
// Log warnings and info messages
|
|
253
|
+
console.warn(`[@macroforge/vite-plugin] ${diag.level}: ${diag.message}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (result && result.code) {
|
|
257
|
+
// TODO: Needs complete overhaul and dynamic attribute removal NO HARDCODING
|
|
258
|
+
// if (!macroConfig.keepDecorators) {
|
|
259
|
+
// result.code = result.code
|
|
260
|
+
// .replace(/\/\*\*\s*@derive[\s\S]*?\*\/\s*/gi, "")
|
|
261
|
+
// .replace(/\/\*\*\s*@debug[\s\S]*?\*\/\s*/gi, "");
|
|
262
|
+
// }
|
|
263
|
+
// Remove macro-only imports so SSR output doesn't load native bindings
|
|
264
|
+
result.code = result.code.replace(/\/\*\*\s*import\s+macro[\s\S]*?\*\/\s*/gi, "");
|
|
265
|
+
if (generateTypes) {
|
|
266
|
+
const emitted = emitDeclarationsFromCode(result.code, id, projectRoot);
|
|
267
|
+
if (emitted) {
|
|
268
|
+
writeTypeDefinitions(id, emitted);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (emitMetadata && result.metadata) {
|
|
272
|
+
writeMetadata(id, result.metadata);
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
code: result.code,
|
|
276
|
+
map: null, // expandSync does not generate source maps yet
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
if (error && typeof error === "object" && "plugin" in error) {
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
const message = formatTransformError(error, id);
|
|
285
|
+
this.error(message);
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
export default napiMacrosPlugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@macroforge/vite-plugin",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/rymskip/macroforge-ts.git"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"clean": "rm -rf dist",
|
|
17
|
+
"test": "npm run build && node --test tests/**/*.test.js"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"macroforge": "^0.1.4"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"typescript": "^5.9.3",
|
|
24
|
+
"vite": "^7.2.4"
|
|
25
|
+
}
|
|
26
|
+
}
|