driftjs-vite-plugin 0.0.1

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/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "driftjs-vite-plugin",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "main": "./dist/index-es.js",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./dist/index-es.js",
9
+ "require": "./dist/index-cjs.js"
10
+ }
11
+ },
12
+ "license": "MIT",
13
+ "dependencies": {
14
+ "driftjs-compiler": "0.0.1"
15
+ },
16
+ "peerDependencies": {
17
+ "vite": "8.1.5"
18
+ },
19
+ "devDependencies": {
20
+ "typescript": "^7.0.2",
21
+ "vite": "^8.1.5",
22
+ "vitest": "^4.1.10",
23
+ "driftjs-dom": "0.0.1"
24
+ },
25
+ "scripts": {
26
+ "build": "vite build",
27
+ "test": "vitest run"
28
+ }
29
+ }
package/src/index.ts ADDED
@@ -0,0 +1,148 @@
1
+ import type { Plugin } from 'vite';
2
+ import {
3
+ compile,
4
+ type CompiledModule,
5
+ } from 'driftjs-compiler';
6
+ import { DRIFT_EXT, type DriftPluginOptions } from '../types/index.js';
7
+
8
+ export type { DriftPluginOptions, DriftModule } from '../types/index.js';
9
+
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+ // ESM code generation
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+
14
+ /**
15
+ * Serializes constant values to JavaScript code literals.
16
+ */
17
+ function serializeValueToJS(val: unknown): string {
18
+ if (val === null || val === undefined) return String(val);
19
+ if (typeof val === 'function') return val.toString();
20
+ if (typeof val === 'object') {
21
+ if ('__drift_fn__' in (val as any)) {
22
+ const fnStr = (val as any).__drift_fn__;
23
+ return `{ __drift_fn__: ${typeof fnStr === 'function' ? fnStr.toString() : fnStr} }`;
24
+ }
25
+ if (Array.isArray(val)) {
26
+ return `[${val.map(serializeValueToJS).join(', ')}]`;
27
+ }
28
+ const entries = Object.entries(val as Record<string, any>)
29
+ .filter(([k]) => k !== 'start' && k !== 'end' && k !== 'loc')
30
+ .map(([k, v]) => `${JSON.stringify(k)}: ${serializeValueToJS(v)}`);
31
+ return `{ ${entries.join(', ')} }`;
32
+ }
33
+ return JSON.stringify(val);
34
+ }
35
+
36
+ function serializeConstants(constants: readonly unknown[]): string {
37
+ return `[\n ${constants.map(serializeValueToJS).join(',\n ')}\n ]`;
38
+ }
39
+
40
+ function generateESM(mod: CompiledModule, filePath: string): string {
41
+ const bytecodeJSON = JSON.stringify(Array.from(mod.bytecode));
42
+ const constantsJSON = serializeConstants(mod.constants);
43
+ const bindingsJSON = JSON.stringify(mod.reactiveBindings ?? []);
44
+ const declaredVarsJSON = JSON.stringify(mod.declaredVars ?? []);
45
+
46
+ const importStatements: string[] = [];
47
+ const scopeEntries: string[] = [];
48
+
49
+ if (mod.imports && mod.imports.length > 0) {
50
+ for (const imp of mod.imports) {
51
+ if (imp.isDefault) {
52
+ importStatements.push(`import ${imp.localName} from ${JSON.stringify(imp.source)};`);
53
+ } else if (imp.importedName) {
54
+ importStatements.push(`import { ${imp.importedName} as ${imp.localName} } from ${JSON.stringify(imp.source)};`);
55
+ }
56
+ scopeEntries.push(imp.localName);
57
+ }
58
+ }
59
+
60
+ const importsHeader = importStatements.length > 0 ? importStatements.join('\n') + '\n\n' : '';
61
+ const scopeObj = scopeEntries.length > 0 ? `{\n ${scopeEntries.join(',\n ')}\n }` : '{}';
62
+
63
+ return `\
64
+ // [DriftJS] Auto-generated from: ${filePath}
65
+ // Do not edit — regenerated on every save / build.
66
+ ${importsHeader}/** @type {import('driftjs-compiler').CompiledModule} */
67
+ const compiledModule = {
68
+ bytecode: new Uint32Array(${bytecodeJSON}),
69
+ constants: ${constantsJSON},
70
+ reactiveBindings: ${bindingsJSON},
71
+ declaredVars: ${declaredVarsJSON},
72
+ scope: ${scopeObj},
73
+ };
74
+
75
+ export default compiledModule;
76
+ `;
77
+ }
78
+
79
+ // ─────────────────────────────────────────────────────────────────────────────
80
+ // Plugin factory
81
+ // ─────────────────────────────────────────────────────────────────────────────
82
+
83
+ /**
84
+ * Vite plugin that transforms `.drift` template files into ESM modules.
85
+ *
86
+ * Each `.drift` file is compiled at build / serve time through the DriftJS
87
+ * pipeline and emitted as an ESM module exposing `render()`, `mount()`, and
88
+ * the raw `compiledModule`.
89
+ *
90
+ * @example
91
+ * // vite.config.ts
92
+ * import { driftPlugin } from '@driftjs/vite-plugin';
93
+ * export default defineConfig({ plugins: [driftPlugin()] });
94
+ *
95
+ * @example
96
+ * // app.ts
97
+ * import { mount } from './hero.drift';
98
+ * mount(document.getElementById('app')!, { title: 'Hello' });
99
+ */
100
+ export function driftPlugin(options: DriftPluginOptions = {}): Plugin {
101
+ const { debug = false } = options;
102
+
103
+ return {
104
+ name: 'vite-plugin-drift',
105
+
106
+ // Run before Vite's own asset / JSON transforms.
107
+ enforce: 'pre',
108
+
109
+ /**
110
+ * Transform hook: invoked for every file Vite processes.
111
+ * Compiles `.drift` source and returns synthetic ESM.
112
+ */
113
+ transform(src, id) {
114
+ const cleanId = id.split('?')[0] ?? id;
115
+ if (!cleanId.endsWith(DRIFT_EXT)) return null;
116
+
117
+ let mod: CompiledModule;
118
+ try {
119
+ mod = compile(src, debug);
120
+ } catch (err: unknown) {
121
+ const msg = err instanceof Error ? err.message : String(err);
122
+ this.error(`[DriftJS] Compilation failed in "${id}":\n${msg}`);
123
+ }
124
+
125
+ return {
126
+ code: generateESM(mod!, id),
127
+ // No meaningful source-map for generated code.
128
+ map: null,
129
+ };
130
+ },
131
+
132
+ /**
133
+ * HMR hook: invalidates and triggers a full-reload whenever a `.drift`
134
+ * source file is saved during `vite dev`.
135
+ */
136
+ handleHotUpdate({ file, server }) {
137
+ const cleanFile = file.split('?')[0] ?? file;
138
+ if (!cleanFile.endsWith(DRIFT_EXT)) return;
139
+
140
+ const mod = server.moduleGraph.getModuleById(file) || server.moduleGraph.getModuleById(cleanFile);
141
+ if (mod) server.moduleGraph.invalidateModule(mod);
142
+
143
+ server.ws.send({ type: 'full-reload', path: '*' });
144
+ },
145
+ };
146
+ }
147
+
148
+ export default driftPlugin;
@@ -0,0 +1,151 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { driftPlugin } from '../src/index.js';
3
+ import type { Plugin } from 'vite';
4
+
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+ // Helpers
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+
9
+ function makePlugin(opts = {}): Plugin {
10
+ return driftPlugin(opts) as Plugin;
11
+ }
12
+
13
+ /**
14
+ * Calls the plugin's `transform` hook with a `.drift` source string.
15
+ * Returns the generated code string, or null if the plugin skipped the file.
16
+ */
17
+ function transform(plugin: Plugin, src: string, id = 'test.drift'): string | null {
18
+ const hook = plugin.transform as (src: string, id: string) => { code: string; map: null } | null;
19
+ const result = hook.call({ error: (msg: string) => { throw new Error(msg); } } as any, src, id);
20
+ return result?.code ?? null;
21
+ }
22
+
23
+ // ─────────────────────────────────────────────────────────────────────────────
24
+ // Tests
25
+ // ─────────────────────────────────────────────────────────────────────────────
26
+
27
+ describe('driftPlugin – identity', () => {
28
+ it('ignores non-.drift files', () => {
29
+ const plugin = makePlugin();
30
+ const result = transform(plugin, '<div>hello</div>', 'app.ts');
31
+ expect(result).toBeNull();
32
+ });
33
+
34
+ it('transforms .drift files', () => {
35
+ const plugin = makePlugin();
36
+ const code = transform(plugin, '<p>Hello</p>');
37
+ expect(code).not.toBeNull();
38
+ });
39
+ });
40
+
41
+ describe('driftPlugin – emitted ESM structure', () => {
42
+ it('exports default compiledModule', () => {
43
+ const plugin = makePlugin();
44
+ const code = transform(plugin, '<h1>Drift</h1>')!;
45
+
46
+ expect(code).toContain('const compiledModule =');
47
+ expect(code).toContain('export default compiledModule;');
48
+ });
49
+
50
+ it('includes the source file path in a comment', () => {
51
+ const plugin = makePlugin();
52
+ const code = transform(plugin, '<div/>', '/project/hero.drift')!;
53
+ expect(code).toContain('hero.drift');
54
+ });
55
+
56
+ it('compiledModule has bytecode and constants arrays', () => {
57
+ const plugin = makePlugin();
58
+ const code = transform(plugin, '<div>test</div>')!;
59
+ expect(code).toMatch(/bytecode:\s*(new Uint32Array\(|\[)/);
60
+ expect(code).toMatch(/constants:\s*\[/);
61
+ });
62
+ });
63
+
64
+ describe('driftPlugin – compilation correctness', () => {
65
+ it('compiles a static element without throwing', () => {
66
+ const plugin = makePlugin();
67
+ expect(() => transform(plugin, '<section class="hero"><h1>Title</h1></section>')).not.toThrow();
68
+ });
69
+
70
+ it('compiles @if / @else directives', () => {
71
+ const plugin = makePlugin();
72
+ const src = '@if show { <p>Visible</p> } @else { <p>Hidden</p> }';
73
+ expect(() => transform(plugin, src)).not.toThrow();
74
+ });
75
+
76
+ it('compiles @for loops', () => {
77
+ const plugin = makePlugin();
78
+ const src = '<ul>@for item in items { <li>{item}</li> }</ul>';
79
+ expect(() => transform(plugin, src)).not.toThrow();
80
+ });
81
+
82
+ it('compiles interpolations', () => {
83
+ const plugin = makePlugin();
84
+ const src = '<p>{greeting}, {name}!</p>';
85
+ expect(() => transform(plugin, src)).not.toThrow();
86
+ });
87
+
88
+ it('surfaces DriftJS compilation errors as Vite build errors', () => {
89
+ const plugin = makePlugin();
90
+ // Unclosed tag — should throw via this.error()
91
+ expect(() => transform(plugin, '<div>')).toThrow(/DriftJS.*Compilation failed/i);
92
+ });
93
+ });
94
+
95
+ describe('driftPlugin – debug option', () => {
96
+ it('calls console.log when debug: true', () => {
97
+ const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
98
+ const plugin = makePlugin({ debug: true });
99
+ transform(plugin, '<p>Debug</p>');
100
+ expect(spy).toHaveBeenCalled();
101
+ spy.mockRestore();
102
+ });
103
+
104
+ it('does not call console.log when debug: false (default)', () => {
105
+ const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
106
+ const plugin = makePlugin();
107
+ transform(plugin, '<p>Quiet</p>');
108
+ expect(spy).not.toHaveBeenCalled();
109
+ spy.mockRestore();
110
+ });
111
+ });
112
+
113
+ describe('driftPlugin – HMR', () => {
114
+ it('triggers a full-reload for .drift files', () => {
115
+ const plugin = makePlugin();
116
+ const send = vi.fn();
117
+ const invalidateModule = vi.fn();
118
+ const fakeModule = { id: '/project/hero.drift' };
119
+
120
+ const ctx = {
121
+ file: '/project/hero.drift',
122
+ server: {
123
+ moduleGraph: {
124
+ getModuleById: vi.fn().mockReturnValue(fakeModule),
125
+ invalidateModule,
126
+ },
127
+ ws: { send },
128
+ },
129
+ };
130
+
131
+ const hook = plugin.handleHotUpdate as (ctx: typeof ctx) => void;
132
+ hook(ctx as any);
133
+
134
+ expect(invalidateModule).toHaveBeenCalledWith(fakeModule);
135
+ expect(send).toHaveBeenCalledWith({ type: 'full-reload', path: '*' });
136
+ });
137
+
138
+ it('ignores non-.drift files in HMR', () => {
139
+ const plugin = makePlugin();
140
+ const send = vi.fn();
141
+
142
+ const ctx = {
143
+ file: '/project/app.ts',
144
+ server: { moduleGraph: { getModuleById: vi.fn() }, ws: { send } },
145
+ };
146
+
147
+ const hook = plugin.handleHotUpdate as (ctx: typeof ctx) => void;
148
+ hook(ctx as any);
149
+ expect(send).not.toHaveBeenCalled();
150
+ });
151
+ });
package/tsconfig.json ADDED
@@ -0,0 +1 @@
1
+ { "extends": "../../tsconfig.json" }
package/types/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Options accepted by the `driftPlugin()` factory.
3
+ */
4
+
5
+ export interface DriftPluginOptions {
6
+ /**
7
+ * Emit verbose compiler debug output (transformed AST + bytecode) to the
8
+ * Vite dev-server console for every transformed .drift file.
9
+ * @default false
10
+ */
11
+ debug?: boolean;
12
+ }
13
+
14
+ /**
15
+ * Shape of the ESM module emitted by the plugin for every `.drift` file.
16
+ * Consumers can import these types when working with `.drift` imports in TS.
17
+ *
18
+ * @example
19
+ * import type { DriftModule } from 'driftjs-vite-plugin';
20
+ * import * as tpl from './hero.drift';
21
+ * const m: DriftModule = tpl;
22
+ */
23
+ export type DriftModule = import('driftjs-compiler').CompiledModule;
24
+
25
+ /** File extension this plugin owns. */
26
+ export const DRIFT_EXT = '.drift' as const;
package/vite.config.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { defineConfig } from 'vite';
2
+ import { resolve, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+
8
+ export default defineConfig({
9
+ test: {
10
+ environment: 'node',
11
+ },
12
+ build: {
13
+ outDir: 'dist',
14
+ minify: true,
15
+ emptyOutDir: true,
16
+ lib: {
17
+ formats: ['es', 'cjs'],
18
+ entry: resolve(__dirname, 'src/index.ts'),
19
+ name: 'DriftVitePlugin',
20
+ fileName: (format: string) => `index-${format}.js`,
21
+ },
22
+ rollupOptions: {
23
+ external: [
24
+ 'vite',
25
+ /^node:/,
26
+ /^@driftjs\//,
27
+ ],
28
+ },
29
+ },
30
+ });