@lithent/lithent-template-vite 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 superlucky84
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # Lithent Template Vite Plugin
2
+
3
+ Vite plugin that compiles Lithent templates embedded inside JSX/TSX-like files.
4
+ It is built on top of `@lithent/lithent-template-parser` and understands both
5
+ inline template literals and full-document `.ljsx` / `.ltsx` modules.
6
+
7
+ Lithent templates intentionally mimic JSX ergonomics, yet remain independent of
8
+ the JSX transform. They give Lithent projects a declarative syntax for complex
9
+ conditional and iterative markup while compiling straight to `h()` calls.
10
+
11
+ > **Status:** Experimental. Behaviour and options may change as the tooling
12
+ > evolves.
13
+
14
+ ## Features
15
+
16
+ - **Document pipeline** – scans entire source files, replaces Lithent markup
17
+ with generated `h()` calls, and forwards source maps downstream.
18
+ - **Custom extensions** – default support for `.ljsx` and `.ltsx`, with full
19
+ control over additional extensions and associated esbuild loaders.
20
+ - **HTML entry transform** – `<script type="module" src="*.ltsx">` tags are
21
+ automatically rewritten to inline imports so the browser receives valid JS.
22
+ - **Tooling-friendly** – exposes warnings, preserves source maps in production,
23
+ and keeps untouched code intact.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pnpm add -D @lithent/lithent-template-vite
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```ts
34
+ // vite.config.ts
35
+ import { defineConfig } from 'vite';
36
+ import lithentTemplateVite from '@lithent/lithent-template-vite';
37
+
38
+ export default defineConfig({
39
+ plugins: [lithentTemplateVite()],
40
+ });
41
+ ```
42
+
43
+ With the default settings the plugin looks for `.ljsx` and `.ltsx` files,
44
+ rewrites HTML entry points, and delegates template compilation to the parser.
45
+
46
+ If you prefer a static HTML entry in development, the safest option is to inline
47
+ the import yourself so the browser receives JavaScript immediately:
48
+
49
+ ```html
50
+ <script type="module">import "/src/dev/main.ltsx";</script>
51
+ ```
52
+
53
+ This pattern works in both dev and build modes and avoids relying on HTML entry
54
+ rewrites.
55
+
56
+ ## Options
57
+
58
+ ```ts
59
+ lithentTemplateVite({
60
+ include: [/\.ltsx$/, /\.foo$/],
61
+ extensions: ['.ltsx', '.foo'],
62
+ extensionLoaders: {
63
+ '.foo': 'ts',
64
+ },
65
+ });
66
+ ```
67
+
68
+ - `include` (RegExp | RegExp[]) – extra guard for files to transform. By
69
+ default a pattern is generated for every configured extension.
70
+ - `extensions` (string[]) – list of file extensions to treat as Lithent
71
+ templates. Values are normalised (leading `.` is optional). Defaults to
72
+ `['.ljsx', '.ltsx']`.
73
+ - `extensionLoaders` (Record<string, LoaderValue>) – override the esbuild loader
74
+ used for a given extension (`'js' | 'jsx' | 'ts' | 'tsx' | 'json' | 'css'`).
75
+ When omitted, the loader is inferred from the extension suffix (e.g. `.ltsx`
76
+ → `'ts'`).
77
+
78
+ ## Example Workflow
79
+
80
+ ```ts
81
+ // src/dev/init.js
82
+ import './main.ltsx';
83
+
84
+ // src/dev/main.ltsx
85
+ import { render } from 'lithent';
86
+
87
+ export const App = () => (
88
+ <div class="app">
89
+ <h1>Hello Lithent</h1>
90
+ <TodoList l-for={todo in todos} todo={todo} />
91
+ </div>
92
+ );
93
+
94
+ if ('document' in globalThis) {
95
+ render(<App />, document.getElementById('app'));
96
+ }
97
+ ```
98
+
99
+ The plugin turns the template section inside `App` into pure `h()` calls. All
100
+ other code (hooks, logic, imports) stays untouched.
101
+
102
+ ## Development & Tests
103
+
104
+ ```bash
105
+ # Launch the playground (packages/lithentTemplateVite/html)
106
+ pnpm --filter @lithent/lithent-template-vite dev
107
+
108
+ # Run Vitest
109
+ pnpm --filter @lithent/lithent-template-vite test
110
+ ```
111
+
112
+ ## License
113
+
114
+ MIT © Lithent contributors
@@ -0,0 +1,2 @@
1
+ export { lithentTemplateVite as default, lithentTemplateVite } from './plugin';
2
+ export type { LithentTemplateViteOptions } from './plugin';
package/dist/index.mjs ADDED
@@ -0,0 +1,123 @@
1
+ import { readFile as v } from "node:fs/promises";
2
+ import p from "node:path";
3
+ import { normalizePath as h, transformWithEsbuild as A } from "vite";
4
+ import { transformDocument as P } from "@lithent/lithent-template-parser";
5
+ const R = [".ljsx", ".ltsx"], S = (e) => e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), z = (e) => e.startsWith(".") ? e : `.${e}`, D = (e) => {
6
+ const s = (e && e.length > 0 ? e : Array.from(R)).map(z);
7
+ return Array.from(new Set(s));
8
+ }, y = (e) => e.endsWith("tsx") || e.endsWith("ts") ? "ts" : (e.endsWith("jsx"), "js"), L = (e, r) => e ? Array.isArray(e) ? e : [e] : r, j = (e, r) => e.some((s) => s.test(r)), O = (e, r) => {
9
+ const [s, m] = e.split("?", 2);
10
+ if (m) {
11
+ const i = new URLSearchParams(m).get($);
12
+ if (i)
13
+ return h(i);
14
+ }
15
+ return s && x(s, r) ? h(s) : null;
16
+ }, x = (e, r) => r.find((s) => e.endsWith(s)) ?? null, E = (e, r) => {
17
+ const s = new Set(e ?? []);
18
+ for (const m of r)
19
+ s.add(m);
20
+ return Array.from(s);
21
+ }, I = (e, r) => ({
22
+ ...e ?? {},
23
+ ...r
24
+ }), $ = "lithent:original";
25
+ function M(e = {}) {
26
+ const r = D(e.extensions), s = r.reduce((t, n) => {
27
+ var c;
28
+ const a = ((c = e.extensionLoaders) == null ? void 0 : c[n]) ?? y(n);
29
+ return t[n] = a, t;
30
+ }, {}), m = r.map(
31
+ (t) => new RegExp(`${S(t)}$`)
32
+ ), w = L(e.include, m);
33
+ let i;
34
+ const g = (t) => {
35
+ const n = new URLSearchParams({
36
+ [$]: t
37
+ });
38
+ return `${t}?${n.toString()}`;
39
+ };
40
+ return [{
41
+ name: "lithent:template-vite",
42
+ enforce: "pre",
43
+ config(t) {
44
+ var n, a, c, l, o, d, f;
45
+ return {
46
+ resolve: {
47
+ extensions: E(
48
+ (n = t.resolve) == null ? void 0 : n.extensions,
49
+ r
50
+ )
51
+ },
52
+ optimizeDeps: {
53
+ extensions: E(
54
+ (a = t.optimizeDeps) == null ? void 0 : a.extensions,
55
+ r
56
+ ),
57
+ esbuildOptions: {
58
+ ...((c = t.optimizeDeps) == null ? void 0 : c.esbuildOptions) ?? {},
59
+ loader: I(
60
+ typeof ((o = (l = t.optimizeDeps) == null ? void 0 : l.esbuildOptions) == null ? void 0 : o.loader) == "object" ? (f = (d = t.optimizeDeps) == null ? void 0 : d.esbuildOptions) == null ? void 0 : f.loader : void 0,
61
+ s
62
+ )
63
+ }
64
+ }
65
+ };
66
+ },
67
+ configResolved(t) {
68
+ i = t;
69
+ },
70
+ async resolveId(t, n, a) {
71
+ if (!x(t, r))
72
+ return null;
73
+ const l = (i == null ? void 0 : i.root) ?? process.cwd();
74
+ let o;
75
+ if (typeof this.resolve == "function") {
76
+ const d = await this.resolve(t, n, {
77
+ skipSelf: !0,
78
+ ...a
79
+ });
80
+ d != null && d.id && (o = d.id);
81
+ }
82
+ return o || (p.isAbsolute(t) ? o = t : t.startsWith("/") ? o = p.join(l, t.slice(1)) : n && (t.startsWith("./") || t.startsWith("../")) ? o = p.resolve(p.dirname(n), t) : o = p.resolve(l, t)), o = h(o), j(w, o) ? g(o) : null;
83
+ },
84
+ async load(t) {
85
+ const n = O(t, r);
86
+ if (!n)
87
+ return null;
88
+ const a = x(n, r);
89
+ if (!a)
90
+ return null;
91
+ this.addWatchFile(n);
92
+ const c = await v(n, "utf-8"), l = P(c, {
93
+ source: n,
94
+ generateSourceMap: (i == null ? void 0 : i.command) === "build"
95
+ }), o = this.warn ? this.warn.bind(this) : console.warn;
96
+ if (l.errors.length > 0)
97
+ for (const { error: u } of l.errors) {
98
+ const W = u.line || u.column ? ` (line ${u.line}, column ${u.column})` : "";
99
+ o(
100
+ `[@lithent/lithent-template-vite] Failed to compile template in ${n}${W}: ${u.message}`
101
+ );
102
+ }
103
+ const d = s[a] ?? y(a), f = l.transformed ? l.code : c, b = await A(
104
+ f,
105
+ n,
106
+ {
107
+ loader: d,
108
+ sourcemap: (i == null ? void 0 : i.command) === "build",
109
+ sourcefile: n
110
+ }
111
+ );
112
+ return {
113
+ code: b.code,
114
+ map: b.map ?? null
115
+ };
116
+ }
117
+ }];
118
+ }
119
+ export {
120
+ M as default,
121
+ M as lithentTemplateVite
122
+ };
123
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","sources":["../src/plugin.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport type { Plugin, PluginOption, ResolvedConfig, UserConfig } from 'vite';\nimport { normalizePath, transformWithEsbuild } from 'vite';\nimport { transformDocument } from '@lithent/lithent-template-parser';\n\ntype LoaderValue = 'js' | 'jsx' | 'ts' | 'tsx' | 'json' | 'css';\ntype LoaderMap = Record<string, LoaderValue>;\n\nexport interface LithentTemplateViteOptions {\n include?: RegExp | RegExp[];\n extensions?: string[];\n extensionLoaders?: Record<string, LoaderValue>;\n}\n\nconst DEFAULT_EXTENSIONS = ['.ljsx', '.ltsx'] as const;\n\nconst escapeRegExp = (value: string): string =>\n value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\nconst normalizeExtension = (ext: string): string =>\n ext.startsWith('.') ? ext : `.${ext}`;\n\nconst resolveExtensions = (extensions?: string[]): string[] => {\n const base =\n extensions && extensions.length > 0\n ? extensions\n : Array.from(DEFAULT_EXTENSIONS);\n const normalized = base.map(normalizeExtension);\n return Array.from(new Set(normalized));\n};\n\nconst inferLoader = (ext: string): LoaderValue => {\n if (ext.endsWith('tsx') || ext.endsWith('ts')) {\n return 'ts';\n }\n if (ext.endsWith('jsx')) {\n return 'js';\n }\n return 'js';\n};\n\nconst toRegExpArray = (\n value: RegExp | RegExp[] | undefined,\n fallback: RegExp[]\n): RegExp[] => {\n if (!value) {\n return fallback;\n }\n return Array.isArray(value) ? value : [value];\n};\n\nconst shouldProcess = (patterns: RegExp[], filePath: string): boolean => {\n return patterns.some(pattern => pattern.test(filePath));\n};\n\nconst extractOriginalPath = (\n id: string,\n extensions: string[]\n): string | null => {\n const [filepath, query] = id.split('?', 2);\n if (query) {\n const params = new URLSearchParams(query);\n const original = params.get(VIRTUAL_QUERY);\n if (original) {\n return normalizePath(original);\n }\n }\n if (filepath && matchExtension(filepath, extensions)) {\n return normalizePath(filepath);\n }\n return null;\n};\n\nconst matchExtension = (\n filePath: string,\n extensions: string[]\n): string | null => {\n return extensions.find(ext => filePath.endsWith(ext)) ?? null;\n};\n\nconst mergeExtensions = (\n original: string[] | undefined,\n additions: readonly string[]\n): string[] => {\n const set = new Set(original ?? []);\n for (const ext of additions) {\n set.add(ext);\n }\n return Array.from(set);\n};\n\nconst mergeLoaderMap = (\n original: LoaderMap | undefined,\n additions: LoaderMap\n): LoaderMap => {\n return {\n ...(original ?? {}),\n ...additions,\n };\n};\n\nconst VIRTUAL_QUERY = 'lithent:original';\n\nexport function lithentTemplateVite(\n options: LithentTemplateViteOptions = {}\n): PluginOption {\n const extensions = resolveExtensions(options.extensions);\n const extensionLoaders: LoaderMap = extensions.reduce((acc, ext) => {\n const loader = options.extensionLoaders?.[ext] ?? inferLoader(ext);\n acc[ext] = loader;\n return acc;\n }, {} as LoaderMap);\n const defaultInclude = extensions.map(\n ext => new RegExp(`${escapeRegExp(ext)}$`)\n );\n const includePatterns = toRegExpArray(options.include, defaultInclude);\n let config: ResolvedConfig | undefined;\n\n const createVirtualId = (originalPath: string): string => {\n const query = new URLSearchParams({\n [VIRTUAL_QUERY]: originalPath,\n });\n return `${originalPath}?${query.toString()}`;\n };\n\n const templatePlugin: Plugin = {\n name: 'lithent:template-vite',\n enforce: 'pre',\n config(userConfig: UserConfig) {\n return {\n resolve: {\n extensions: mergeExtensions(\n userConfig.resolve?.extensions,\n extensions\n ),\n },\n optimizeDeps: {\n extensions: mergeExtensions(\n userConfig.optimizeDeps?.extensions,\n extensions\n ),\n esbuildOptions: {\n ...(userConfig.optimizeDeps?.esbuildOptions ?? {}),\n loader: mergeLoaderMap(\n typeof userConfig.optimizeDeps?.esbuildOptions?.loader ===\n 'object'\n ? (userConfig.optimizeDeps?.esbuildOptions?.loader as LoaderMap)\n : undefined,\n extensionLoaders\n ),\n },\n },\n };\n },\n configResolved(resolved) {\n config = resolved;\n },\n async resolveId(source, importer, options) {\n const ext = matchExtension(source, extensions);\n if (!ext) {\n return null;\n }\n\n const rootDir = config?.root ?? process.cwd();\n let resolvedPath: string | undefined;\n\n if (typeof this.resolve === 'function') {\n const resolved = await this.resolve(source, importer, {\n skipSelf: true,\n ...options,\n });\n if (resolved?.id) {\n resolvedPath = resolved.id;\n }\n }\n\n if (!resolvedPath) {\n if (path.isAbsolute(source)) {\n resolvedPath = source;\n } else if (source.startsWith('/')) {\n resolvedPath = path.join(rootDir, source.slice(1));\n } else if (\n importer &&\n (source.startsWith('./') || source.startsWith('../'))\n ) {\n resolvedPath = path.resolve(path.dirname(importer), source);\n } else {\n resolvedPath = path.resolve(rootDir, source);\n }\n }\n\n resolvedPath = normalizePath(resolvedPath);\n\n if (!shouldProcess(includePatterns, resolvedPath)) {\n return null;\n }\n\n return createVirtualId(resolvedPath);\n },\n async load(id) {\n const originalPath = extractOriginalPath(id, extensions);\n if (!originalPath) {\n return null;\n }\n\n const ext = matchExtension(originalPath, extensions);\n if (!ext) {\n return null;\n }\n\n this.addWatchFile(originalPath);\n\n const raw = await readFile(originalPath, 'utf-8');\n const result = transformDocument(raw, {\n source: originalPath,\n generateSourceMap: config?.command === 'build',\n });\n\n const warn = this.warn ? this.warn.bind(this) : console.warn;\n if (result.errors.length > 0) {\n for (const { error } of result.errors) {\n const location =\n error.line || error.column\n ? ` (line ${error.line}, column ${error.column})`\n : '';\n warn(\n `[@lithent/lithent-template-vite] Failed to compile template in ${originalPath}${location}: ${error.message}`\n );\n }\n }\n\n const loader = extensionLoaders[ext] ?? inferLoader(ext);\n const transformedCode = result.transformed ? result.code : raw;\n const esbuildResult = await transformWithEsbuild(\n transformedCode,\n originalPath,\n {\n loader,\n sourcemap: config?.command === 'build',\n sourcefile: originalPath,\n }\n );\n\n return {\n code: esbuildResult.code,\n map: esbuildResult.map ?? null,\n };\n },\n };\n\n return [templatePlugin];\n}\n\nexport default lithentTemplateVite;\n"],"names":["DEFAULT_EXTENSIONS","escapeRegExp","value","normalizeExtension","ext","resolveExtensions","extensions","normalized","inferLoader","toRegExpArray","fallback","shouldProcess","patterns","filePath","pattern","extractOriginalPath","id","filepath","query","original","VIRTUAL_QUERY","normalizePath","matchExtension","mergeExtensions","additions","set","mergeLoaderMap","lithentTemplateVite","options","extensionLoaders","acc","loader","_a","defaultInclude","includePatterns","config","createVirtualId","originalPath","userConfig","_b","_c","_e","_d","_g","_f","resolved","source","importer","rootDir","resolvedPath","path","raw","readFile","result","transformDocument","warn","error","location","transformedCode","esbuildResult","transformWithEsbuild"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ (function(l,p){typeof exports=="object"&&typeof module<"u"?p(exports,require("node:fs/promises"),require("node:path"),require("vite"),require("@lithent/lithent-template-parser")):typeof define=="function"&&define.amd?define(["exports","node:fs/promises","node:path","vite","@lithent/lithent-template-parser"],p):(l=typeof globalThis<"u"?globalThis:l||self,p(l.lithentTemplateVite={},l.promises,l.path,l.vite,l.lithentTemplateParser))})(this,function(l,p,f,x,z){"use strict";const T=[".ljsx",".ltsx"],W=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),A=e=>e.startsWith(".")?e:`.${e}`,R=e=>{const r=(e&&e.length>0?e:Array.from(T)).map(A);return Array.from(new Set(r))},P=e=>e.endsWith("tsx")||e.endsWith("ts")?"ts":(e.endsWith("jsx"),"js"),g=(e,s)=>e?Array.isArray(e)?e:[e]:s,j=(e,s)=>e.some(r=>r.test(s)),D=(e,s)=>{const[r,m]=e.split("?",2);if(m){const o=new URLSearchParams(m).get(E);if(o)return x.normalizePath(o)}return r&&w(r,s)?x.normalizePath(r):null},w=(e,s)=>s.find(r=>e.endsWith(r))??null,b=(e,s)=>{const r=new Set(e??[]);for(const m of s)r.add(m);return Array.from(r)},L=(e,s)=>({...e??{},...s}),E="lithent:original";function $(e={}){const s=R(e.extensions),r=s.reduce((t,n)=>{var c;const a=((c=e.extensionLoaders)==null?void 0:c[n])??P(n);return t[n]=a,t},{}),m=s.map(t=>new RegExp(`${W(t)}$`)),v=g(e.include,m);let o;const O=t=>{const n=new URLSearchParams({[E]:t});return`${t}?${n.toString()}`};return[{name:"lithent:template-vite",enforce:"pre",config(t){var n,a,c,d,i,u,y;return{resolve:{extensions:b((n=t.resolve)==null?void 0:n.extensions,s)},optimizeDeps:{extensions:b((a=t.optimizeDeps)==null?void 0:a.extensions,s),esbuildOptions:{...((c=t.optimizeDeps)==null?void 0:c.esbuildOptions)??{},loader:L(typeof((i=(d=t.optimizeDeps)==null?void 0:d.esbuildOptions)==null?void 0:i.loader)=="object"?(y=(u=t.optimizeDeps)==null?void 0:u.esbuildOptions)==null?void 0:y.loader:void 0,r)}}}},configResolved(t){o=t},async resolveId(t,n,a){if(!w(t,s))return null;const d=(o==null?void 0:o.root)??process.cwd();let i;if(typeof this.resolve=="function"){const u=await this.resolve(t,n,{skipSelf:!0,...a});u!=null&&u.id&&(i=u.id)}return i||(f.isAbsolute(t)?i=t:t.startsWith("/")?i=f.join(d,t.slice(1)):n&&(t.startsWith("./")||t.startsWith("../"))?i=f.resolve(f.dirname(n),t):i=f.resolve(d,t)),i=x.normalizePath(i),j(v,i)?O(i):null},async load(t){const n=D(t,s);if(!n)return null;const a=w(n,s);if(!a)return null;this.addWatchFile(n);const c=await p.readFile(n,"utf-8"),d=z.transformDocument(c,{source:n,generateSourceMap:(o==null?void 0:o.command)==="build"}),i=this.warn?this.warn.bind(this):console.warn;if(d.errors.length>0)for(const{error:h}of d.errors){const q=h.line||h.column?` (line ${h.line}, column ${h.column})`:"";i(`[@lithent/lithent-template-vite] Failed to compile template in ${n}${q}: ${h.message}`)}const u=r[a]??P(a),y=d.transformed?d.code:c,S=await x.transformWithEsbuild(y,n,{loader:u,sourcemap:(o==null?void 0:o.command)==="build",sourcefile:n});return{code:S.code,map:S.map??null}}}]}l.default=$,l.lithentTemplateVite=$,Object.defineProperties(l,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
2
+ //# sourceMappingURL=index.umd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.umd.js","sources":["../src/plugin.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport type { Plugin, PluginOption, ResolvedConfig, UserConfig } from 'vite';\nimport { normalizePath, transformWithEsbuild } from 'vite';\nimport { transformDocument } from '@lithent/lithent-template-parser';\n\ntype LoaderValue = 'js' | 'jsx' | 'ts' | 'tsx' | 'json' | 'css';\ntype LoaderMap = Record<string, LoaderValue>;\n\nexport interface LithentTemplateViteOptions {\n include?: RegExp | RegExp[];\n extensions?: string[];\n extensionLoaders?: Record<string, LoaderValue>;\n}\n\nconst DEFAULT_EXTENSIONS = ['.ljsx', '.ltsx'] as const;\n\nconst escapeRegExp = (value: string): string =>\n value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\nconst normalizeExtension = (ext: string): string =>\n ext.startsWith('.') ? ext : `.${ext}`;\n\nconst resolveExtensions = (extensions?: string[]): string[] => {\n const base =\n extensions && extensions.length > 0\n ? extensions\n : Array.from(DEFAULT_EXTENSIONS);\n const normalized = base.map(normalizeExtension);\n return Array.from(new Set(normalized));\n};\n\nconst inferLoader = (ext: string): LoaderValue => {\n if (ext.endsWith('tsx') || ext.endsWith('ts')) {\n return 'ts';\n }\n if (ext.endsWith('jsx')) {\n return 'js';\n }\n return 'js';\n};\n\nconst toRegExpArray = (\n value: RegExp | RegExp[] | undefined,\n fallback: RegExp[]\n): RegExp[] => {\n if (!value) {\n return fallback;\n }\n return Array.isArray(value) ? value : [value];\n};\n\nconst shouldProcess = (patterns: RegExp[], filePath: string): boolean => {\n return patterns.some(pattern => pattern.test(filePath));\n};\n\nconst extractOriginalPath = (\n id: string,\n extensions: string[]\n): string | null => {\n const [filepath, query] = id.split('?', 2);\n if (query) {\n const params = new URLSearchParams(query);\n const original = params.get(VIRTUAL_QUERY);\n if (original) {\n return normalizePath(original);\n }\n }\n if (filepath && matchExtension(filepath, extensions)) {\n return normalizePath(filepath);\n }\n return null;\n};\n\nconst matchExtension = (\n filePath: string,\n extensions: string[]\n): string | null => {\n return extensions.find(ext => filePath.endsWith(ext)) ?? null;\n};\n\nconst mergeExtensions = (\n original: string[] | undefined,\n additions: readonly string[]\n): string[] => {\n const set = new Set(original ?? []);\n for (const ext of additions) {\n set.add(ext);\n }\n return Array.from(set);\n};\n\nconst mergeLoaderMap = (\n original: LoaderMap | undefined,\n additions: LoaderMap\n): LoaderMap => {\n return {\n ...(original ?? {}),\n ...additions,\n };\n};\n\nconst VIRTUAL_QUERY = 'lithent:original';\n\nexport function lithentTemplateVite(\n options: LithentTemplateViteOptions = {}\n): PluginOption {\n const extensions = resolveExtensions(options.extensions);\n const extensionLoaders: LoaderMap = extensions.reduce((acc, ext) => {\n const loader = options.extensionLoaders?.[ext] ?? inferLoader(ext);\n acc[ext] = loader;\n return acc;\n }, {} as LoaderMap);\n const defaultInclude = extensions.map(\n ext => new RegExp(`${escapeRegExp(ext)}$`)\n );\n const includePatterns = toRegExpArray(options.include, defaultInclude);\n let config: ResolvedConfig | undefined;\n\n const createVirtualId = (originalPath: string): string => {\n const query = new URLSearchParams({\n [VIRTUAL_QUERY]: originalPath,\n });\n return `${originalPath}?${query.toString()}`;\n };\n\n const templatePlugin: Plugin = {\n name: 'lithent:template-vite',\n enforce: 'pre',\n config(userConfig: UserConfig) {\n return {\n resolve: {\n extensions: mergeExtensions(\n userConfig.resolve?.extensions,\n extensions\n ),\n },\n optimizeDeps: {\n extensions: mergeExtensions(\n userConfig.optimizeDeps?.extensions,\n extensions\n ),\n esbuildOptions: {\n ...(userConfig.optimizeDeps?.esbuildOptions ?? {}),\n loader: mergeLoaderMap(\n typeof userConfig.optimizeDeps?.esbuildOptions?.loader ===\n 'object'\n ? (userConfig.optimizeDeps?.esbuildOptions?.loader as LoaderMap)\n : undefined,\n extensionLoaders\n ),\n },\n },\n };\n },\n configResolved(resolved) {\n config = resolved;\n },\n async resolveId(source, importer, options) {\n const ext = matchExtension(source, extensions);\n if (!ext) {\n return null;\n }\n\n const rootDir = config?.root ?? process.cwd();\n let resolvedPath: string | undefined;\n\n if (typeof this.resolve === 'function') {\n const resolved = await this.resolve(source, importer, {\n skipSelf: true,\n ...options,\n });\n if (resolved?.id) {\n resolvedPath = resolved.id;\n }\n }\n\n if (!resolvedPath) {\n if (path.isAbsolute(source)) {\n resolvedPath = source;\n } else if (source.startsWith('/')) {\n resolvedPath = path.join(rootDir, source.slice(1));\n } else if (\n importer &&\n (source.startsWith('./') || source.startsWith('../'))\n ) {\n resolvedPath = path.resolve(path.dirname(importer), source);\n } else {\n resolvedPath = path.resolve(rootDir, source);\n }\n }\n\n resolvedPath = normalizePath(resolvedPath);\n\n if (!shouldProcess(includePatterns, resolvedPath)) {\n return null;\n }\n\n return createVirtualId(resolvedPath);\n },\n async load(id) {\n const originalPath = extractOriginalPath(id, extensions);\n if (!originalPath) {\n return null;\n }\n\n const ext = matchExtension(originalPath, extensions);\n if (!ext) {\n return null;\n }\n\n this.addWatchFile(originalPath);\n\n const raw = await readFile(originalPath, 'utf-8');\n const result = transformDocument(raw, {\n source: originalPath,\n generateSourceMap: config?.command === 'build',\n });\n\n const warn = this.warn ? this.warn.bind(this) : console.warn;\n if (result.errors.length > 0) {\n for (const { error } of result.errors) {\n const location =\n error.line || error.column\n ? ` (line ${error.line}, column ${error.column})`\n : '';\n warn(\n `[@lithent/lithent-template-vite] Failed to compile template in ${originalPath}${location}: ${error.message}`\n );\n }\n }\n\n const loader = extensionLoaders[ext] ?? inferLoader(ext);\n const transformedCode = result.transformed ? result.code : raw;\n const esbuildResult = await transformWithEsbuild(\n transformedCode,\n originalPath,\n {\n loader,\n sourcemap: config?.command === 'build',\n sourcefile: originalPath,\n }\n );\n\n return {\n code: esbuildResult.code,\n map: esbuildResult.map ?? null,\n };\n },\n };\n\n return [templatePlugin];\n}\n\nexport default lithentTemplateVite;\n"],"names":["DEFAULT_EXTENSIONS","escapeRegExp","value","normalizeExtension","ext","resolveExtensions","extensions","normalized","inferLoader","toRegExpArray","fallback","shouldProcess","patterns","filePath","pattern","extractOriginalPath","id","filepath","query","original","VIRTUAL_QUERY","normalizePath","matchExtension","mergeExtensions","additions","set","mergeLoaderMap","lithentTemplateVite","options","extensionLoaders","acc","loader","_a","defaultInclude","includePatterns","config","createVirtualId","originalPath","userConfig","_b","_c","_e","_d","_g","_f","resolved","source","importer","rootDir","resolvedPath","path","raw","readFile","result","transformDocument","warn","error","location","transformedCode","esbuildResult","transformWithEsbuild"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { PluginOption } from 'vite';
2
+ type LoaderValue = 'js' | 'jsx' | 'ts' | 'tsx' | 'json' | 'css';
3
+ export interface LithentTemplateViteOptions {
4
+ include?: RegExp | RegExp[];
5
+ extensions?: string[];
6
+ extensionLoaders?: Record<string, LoaderValue>;
7
+ }
8
+ export declare function lithentTemplateVite(options?: LithentTemplateViteOptions): PluginOption;
9
+ export default lithentTemplateVite;
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "type": "module",
3
+ "name": "@lithent/lithent-template-vite",
4
+ "version": "0.1.0",
5
+ "description": "Vite plugin that compiles Lithent’s JSX-like template syntax into h() calls.",
6
+ "main": "./dist/index.mjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.umd.js"
14
+ }
15
+ },
16
+ "dependencies": {
17
+ "@lithent/lithent-template-parser": "0.1.0"
18
+ },
19
+ "devDependencies": {
20
+ "lithent": "1.20.1"
21
+ },
22
+ "keywords": [
23
+ "lithent",
24
+ "vite",
25
+ "template"
26
+ ],
27
+ "author": "superlucky84",
28
+ "license": "MIT",
29
+ "homepage": "https://github.com/superlucky84/lithent",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/superlucky84/lithent.git"
33
+ },
34
+ "peerDependencies": {
35
+ "vite": "^5.0.0"
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "src"
40
+ ],
41
+ "volta": {
42
+ "node": "20.3.0"
43
+ },
44
+ "scripts": {
45
+ "dev": "vite --config vite.config.plugin.ts",
46
+ "dev:lib": "vite",
47
+ "build": "vite build",
48
+ "watch": "vite build --watch",
49
+ "test": "vitest run --reporter verbose",
50
+ "test:dev": "vitest"
51
+ }
52
+ }
@@ -0,0 +1,44 @@
1
+ import { h, mount } from 'lithent';
2
+
3
+ void h;
4
+
5
+ interface TodoItem {
6
+ text: string;
7
+ done: boolean;
8
+ }
9
+
10
+ const todos: TodoItem[] = [
11
+ { text: 'Check plugin output', done: true },
12
+ { text: 'Verify l-for transformation', done: false },
13
+ { text: 'Ensure nested JSX works', done: false },
14
+ ];
15
+
16
+ export function TemplateDemo() {
17
+ const todo = {} as TodoItem;
18
+ const index = 0;
19
+ void todo;
20
+ void index;
21
+
22
+ return (
23
+ <section class="todo-demo">
24
+ <h2>Todos ({(todos as any).length})</h2>
25
+ <ul>
26
+ <li l-for={(todo, index) in todos} class="todo-item">
27
+ <span class="todo-index">{index + 1}.</span>
28
+ <span class="todo-text">{todo.text}</span>
29
+ <strong l-if={todo.done} class="todo-status done">
30
+ Done11
31
+ </strong>
32
+ <strong l-else class="todo-status pending">
33
+ asdglkj a
34
+ </strong>
35
+ </li>
36
+ </ul>
37
+ <ul class="summary">
38
+ {todos.map(todo => (
39
+ <li class="summary-item">{todo.text.toUpperCase()}</li>
40
+ ))}
41
+ </ul>
42
+ </section>
43
+ );
44
+ }
@@ -0,0 +1 @@
1
+ import './main.ltsx';
@@ -0,0 +1,48 @@
1
+ import { TemplateDemo } from './App';
2
+ import { compile } from '@lithent/lithent-template-parser';
3
+ import { render, h } from 'lithent';
4
+
5
+ const source = `
6
+ <section class="todo-demo">
7
+ <h2>Todos ({todos.length})</h2>
8
+ <ul>
9
+ <li l-for={(todo, index) in todos} class="todo-item">
10
+ <span class="todo-index">{index + 1}.</span>
11
+ <span class="todo-text">{todo.text}</span>
12
+ <strong l-if={todo.done} class="todo-status done">Done</strong>
13
+ <strong l-else class="todo-status pending">Pending</strong>
14
+ </li>
15
+ </ul>
16
+ <div class="summary">
17
+ {todos.map(todo => (
18
+ <span class="summary-item">{todo.text.toUpperCase()}</span>
19
+ ))}
20
+ </div>
21
+ </section>
22
+ `;
23
+
24
+ const compileResult = compile(source);
25
+ console.log('[template-parser] code:', compileResult.code);
26
+
27
+ const Demo = TemplateDemo;
28
+
29
+ console.log('[template-parser] AST:', compileResult.ast);
30
+
31
+ if ('document' in globalThis) {
32
+ const doc = (globalThis as any).document as any;
33
+ const codeBlock = doc.getElementById('code-output');
34
+ const astBlock = doc.getElementById('ast-output');
35
+ const app = doc.getElementById('preview');
36
+
37
+ if (codeBlock) {
38
+ codeBlock.textContent = compileResult.code ?? '// no code';
39
+ }
40
+
41
+ if (astBlock) {
42
+ console.log(compileResult.ast);
43
+ }
44
+
45
+ if (app) {
46
+ render(<Demo />, app);
47
+ }
48
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { lithentTemplateVite as default, lithentTemplateVite } from './plugin';
2
+ export type { LithentTemplateViteOptions } from './plugin';
package/src/plugin.ts ADDED
@@ -0,0 +1,255 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import type { Plugin, PluginOption, ResolvedConfig, UserConfig } from 'vite';
4
+ import { normalizePath, transformWithEsbuild } from 'vite';
5
+ import { transformDocument } from '@lithent/lithent-template-parser';
6
+
7
+ type LoaderValue = 'js' | 'jsx' | 'ts' | 'tsx' | 'json' | 'css';
8
+ type LoaderMap = Record<string, LoaderValue>;
9
+
10
+ export interface LithentTemplateViteOptions {
11
+ include?: RegExp | RegExp[];
12
+ extensions?: string[];
13
+ extensionLoaders?: Record<string, LoaderValue>;
14
+ }
15
+
16
+ const DEFAULT_EXTENSIONS = ['.ljsx', '.ltsx'] as const;
17
+
18
+ const escapeRegExp = (value: string): string =>
19
+ value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
20
+
21
+ const normalizeExtension = (ext: string): string =>
22
+ ext.startsWith('.') ? ext : `.${ext}`;
23
+
24
+ const resolveExtensions = (extensions?: string[]): string[] => {
25
+ const base =
26
+ extensions && extensions.length > 0
27
+ ? extensions
28
+ : Array.from(DEFAULT_EXTENSIONS);
29
+ const normalized = base.map(normalizeExtension);
30
+ return Array.from(new Set(normalized));
31
+ };
32
+
33
+ const inferLoader = (ext: string): LoaderValue => {
34
+ if (ext.endsWith('tsx') || ext.endsWith('ts')) {
35
+ return 'ts';
36
+ }
37
+ if (ext.endsWith('jsx')) {
38
+ return 'js';
39
+ }
40
+ return 'js';
41
+ };
42
+
43
+ const toRegExpArray = (
44
+ value: RegExp | RegExp[] | undefined,
45
+ fallback: RegExp[]
46
+ ): RegExp[] => {
47
+ if (!value) {
48
+ return fallback;
49
+ }
50
+ return Array.isArray(value) ? value : [value];
51
+ };
52
+
53
+ const shouldProcess = (patterns: RegExp[], filePath: string): boolean => {
54
+ return patterns.some(pattern => pattern.test(filePath));
55
+ };
56
+
57
+ const extractOriginalPath = (
58
+ id: string,
59
+ extensions: string[]
60
+ ): string | null => {
61
+ const [filepath, query] = id.split('?', 2);
62
+ if (query) {
63
+ const params = new URLSearchParams(query);
64
+ const original = params.get(VIRTUAL_QUERY);
65
+ if (original) {
66
+ return normalizePath(original);
67
+ }
68
+ }
69
+ if (filepath && matchExtension(filepath, extensions)) {
70
+ return normalizePath(filepath);
71
+ }
72
+ return null;
73
+ };
74
+
75
+ const matchExtension = (
76
+ filePath: string,
77
+ extensions: string[]
78
+ ): string | null => {
79
+ return extensions.find(ext => filePath.endsWith(ext)) ?? null;
80
+ };
81
+
82
+ const mergeExtensions = (
83
+ original: string[] | undefined,
84
+ additions: readonly string[]
85
+ ): string[] => {
86
+ const set = new Set(original ?? []);
87
+ for (const ext of additions) {
88
+ set.add(ext);
89
+ }
90
+ return Array.from(set);
91
+ };
92
+
93
+ const mergeLoaderMap = (
94
+ original: LoaderMap | undefined,
95
+ additions: LoaderMap
96
+ ): LoaderMap => {
97
+ return {
98
+ ...(original ?? {}),
99
+ ...additions,
100
+ };
101
+ };
102
+
103
+ const VIRTUAL_QUERY = 'lithent:original';
104
+
105
+ export function lithentTemplateVite(
106
+ options: LithentTemplateViteOptions = {}
107
+ ): PluginOption {
108
+ const extensions = resolveExtensions(options.extensions);
109
+ const extensionLoaders: LoaderMap = extensions.reduce((acc, ext) => {
110
+ const loader = options.extensionLoaders?.[ext] ?? inferLoader(ext);
111
+ acc[ext] = loader;
112
+ return acc;
113
+ }, {} as LoaderMap);
114
+ const defaultInclude = extensions.map(
115
+ ext => new RegExp(`${escapeRegExp(ext)}$`)
116
+ );
117
+ const includePatterns = toRegExpArray(options.include, defaultInclude);
118
+ let config: ResolvedConfig | undefined;
119
+
120
+ const createVirtualId = (originalPath: string): string => {
121
+ const query = new URLSearchParams({
122
+ [VIRTUAL_QUERY]: originalPath,
123
+ });
124
+ return `${originalPath}?${query.toString()}`;
125
+ };
126
+
127
+ const templatePlugin: Plugin = {
128
+ name: 'lithent:template-vite',
129
+ enforce: 'pre',
130
+ config(userConfig: UserConfig) {
131
+ return {
132
+ resolve: {
133
+ extensions: mergeExtensions(
134
+ userConfig.resolve?.extensions,
135
+ extensions
136
+ ),
137
+ },
138
+ optimizeDeps: {
139
+ extensions: mergeExtensions(
140
+ userConfig.optimizeDeps?.extensions,
141
+ extensions
142
+ ),
143
+ esbuildOptions: {
144
+ ...(userConfig.optimizeDeps?.esbuildOptions ?? {}),
145
+ loader: mergeLoaderMap(
146
+ typeof userConfig.optimizeDeps?.esbuildOptions?.loader ===
147
+ 'object'
148
+ ? (userConfig.optimizeDeps?.esbuildOptions?.loader as LoaderMap)
149
+ : undefined,
150
+ extensionLoaders
151
+ ),
152
+ },
153
+ },
154
+ };
155
+ },
156
+ configResolved(resolved) {
157
+ config = resolved;
158
+ },
159
+ async resolveId(source, importer, options) {
160
+ const ext = matchExtension(source, extensions);
161
+ if (!ext) {
162
+ return null;
163
+ }
164
+
165
+ const rootDir = config?.root ?? process.cwd();
166
+ let resolvedPath: string | undefined;
167
+
168
+ if (typeof this.resolve === 'function') {
169
+ const resolved = await this.resolve(source, importer, {
170
+ skipSelf: true,
171
+ ...options,
172
+ });
173
+ if (resolved?.id) {
174
+ resolvedPath = resolved.id;
175
+ }
176
+ }
177
+
178
+ if (!resolvedPath) {
179
+ if (path.isAbsolute(source)) {
180
+ resolvedPath = source;
181
+ } else if (source.startsWith('/')) {
182
+ resolvedPath = path.join(rootDir, source.slice(1));
183
+ } else if (
184
+ importer &&
185
+ (source.startsWith('./') || source.startsWith('../'))
186
+ ) {
187
+ resolvedPath = path.resolve(path.dirname(importer), source);
188
+ } else {
189
+ resolvedPath = path.resolve(rootDir, source);
190
+ }
191
+ }
192
+
193
+ resolvedPath = normalizePath(resolvedPath);
194
+
195
+ if (!shouldProcess(includePatterns, resolvedPath)) {
196
+ return null;
197
+ }
198
+
199
+ return createVirtualId(resolvedPath);
200
+ },
201
+ async load(id) {
202
+ const originalPath = extractOriginalPath(id, extensions);
203
+ if (!originalPath) {
204
+ return null;
205
+ }
206
+
207
+ const ext = matchExtension(originalPath, extensions);
208
+ if (!ext) {
209
+ return null;
210
+ }
211
+
212
+ this.addWatchFile(originalPath);
213
+
214
+ const raw = await readFile(originalPath, 'utf-8');
215
+ const result = transformDocument(raw, {
216
+ source: originalPath,
217
+ generateSourceMap: config?.command === 'build',
218
+ });
219
+
220
+ const warn = this.warn ? this.warn.bind(this) : console.warn;
221
+ if (result.errors.length > 0) {
222
+ for (const { error } of result.errors) {
223
+ const location =
224
+ error.line || error.column
225
+ ? ` (line ${error.line}, column ${error.column})`
226
+ : '';
227
+ warn(
228
+ `[@lithent/lithent-template-vite] Failed to compile template in ${originalPath}${location}: ${error.message}`
229
+ );
230
+ }
231
+ }
232
+
233
+ const loader = extensionLoaders[ext] ?? inferLoader(ext);
234
+ const transformedCode = result.transformed ? result.code : raw;
235
+ const esbuildResult = await transformWithEsbuild(
236
+ transformedCode,
237
+ originalPath,
238
+ {
239
+ loader,
240
+ sourcemap: config?.command === 'build',
241
+ sourcefile: originalPath,
242
+ }
243
+ );
244
+
245
+ return {
246
+ code: esbuildResult.code,
247
+ map: esbuildResult.map ?? null,
248
+ };
249
+ },
250
+ };
251
+
252
+ return [templatePlugin];
253
+ }
254
+
255
+ export default lithentTemplateVite;
@@ -0,0 +1,12 @@
1
+ export const View = (todos) => (
2
+ <li l-for={(todo, index) in todos} class="todo-item">
3
+ <span class="todo-index">{index + 1}.</span>
4
+ <span class="todo-text">{todo.text}</span>
5
+ <strong l-if={todo.done} class="todo-status done">
6
+ Completed
7
+ </strong>
8
+ <strong l-else class="todo-status pending">
9
+ Pending
10
+ </strong>
11
+ </li>
12
+ );
@@ -0,0 +1,12 @@
1
+ export const View = (todos) => (
2
+ <li l-for={(todo, index) in todos} class="todo-item">
3
+ <span class="todo-index">{index + 1}.</span>
4
+ <span class="todo-text">{todo.text}</span>
5
+ <strong l-if={todo.done} class="todo-status done">
6
+ Completed
7
+ </strong>
8
+ <strong l-else class="todo-status pending">
9
+ Pending
10
+ </strong>
11
+ </li>
12
+ );
@@ -0,0 +1,252 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { beforeEach, describe, expect, it } from 'vitest';
5
+ import { resolveConfig } from 'vite';
6
+ import type { ConfigEnv, Plugin, PluginOption, ResolvedConfig } from 'vite';
7
+ import { lithentTemplateVite } from '../plugin';
8
+
9
+ interface TransformResult {
10
+ code: string;
11
+ map: any;
12
+ }
13
+
14
+ const normalizePluginOption = (option: PluginOption): Plugin[] => {
15
+ if (Array.isArray(option)) {
16
+ return option.flatMap(item => normalizePluginOption(item));
17
+ }
18
+ if (!option) {
19
+ return [];
20
+ }
21
+ return [option as Plugin];
22
+ };
23
+
24
+ const createPlugins = (opts?: Parameters<typeof lithentTemplateVite>[0]) => {
25
+ const option = lithentTemplateVite(opts);
26
+ return normalizePluginOption(option).filter(Boolean);
27
+ };
28
+
29
+ type MinimalPlugin = Pick<
30
+ Plugin,
31
+ 'config' | 'configResolved' | 'resolveId' | 'load' | 'transform'
32
+ >;
33
+
34
+ const runHook = async (
35
+ plugin: MinimalPlugin,
36
+ hookName: keyof MinimalPlugin,
37
+ context: Record<string, unknown>,
38
+ ...args: unknown[]
39
+ ): Promise<unknown> => {
40
+ const hook = plugin[hookName];
41
+ if (!hook) {
42
+ return undefined;
43
+ }
44
+
45
+ const invoke = async (entry: unknown): Promise<unknown> => {
46
+ if (typeof entry === 'function') {
47
+ return await (entry as (...a: unknown[]) => unknown).apply(context, args);
48
+ }
49
+ if (
50
+ entry &&
51
+ typeof entry === 'object' &&
52
+ 'handler' in entry &&
53
+ typeof (entry as { handler: unknown }).handler === 'function'
54
+ ) {
55
+ return await (
56
+ entry as { handler: (...a: unknown[]) => unknown }
57
+ ).handler.apply(context, args);
58
+ }
59
+ return undefined;
60
+ };
61
+
62
+ if (Array.isArray(hook)) {
63
+ let last: unknown;
64
+ for (const entry of hook) {
65
+ last = await invoke(entry);
66
+ }
67
+ return last;
68
+ }
69
+
70
+ return await invoke(hook);
71
+ };
72
+
73
+ const createPluginContext = () => ({
74
+ warn: () => {},
75
+ addWatchFile: () => {},
76
+ getWatchFiles: () => [] as string[],
77
+ resolve: async () => null,
78
+ load: async () => null,
79
+ transform: async () => null,
80
+ });
81
+
82
+ const setupPlugins = async (
83
+ plugins: Plugin[],
84
+ command: 'serve' | 'build' = 'serve'
85
+ ): Promise<ResolvedConfig> => {
86
+ const context = createPluginContext();
87
+ const env: ConfigEnv = {
88
+ command,
89
+ mode: command === 'serve' ? 'development' : 'production',
90
+ isSsrBuild: command === 'build',
91
+ };
92
+
93
+ const userConfig = {} as Parameters<typeof resolveConfig>[0];
94
+
95
+ for (const plugin of plugins) {
96
+ const result = await runHook(plugin, 'config', context, userConfig, env);
97
+ if (result && typeof result === 'object') {
98
+ Object.assign(userConfig, result as object);
99
+ }
100
+ }
101
+
102
+ const resolved = (await resolveConfig(
103
+ userConfig,
104
+ command,
105
+ env.mode
106
+ )) as ResolvedConfig;
107
+
108
+ await Promise.all(
109
+ plugins.map(plugin => runHook(plugin, 'configResolved', context, resolved))
110
+ );
111
+
112
+ return resolved;
113
+ };
114
+
115
+ const runTransforms = async (
116
+ plugins: Plugin[],
117
+ initial: string,
118
+ id: string
119
+ ): Promise<TransformResult> => {
120
+ const context = createPluginContext();
121
+
122
+ let resolvedId = id;
123
+ for (const plugin of plugins) {
124
+ if (!plugin.resolveId) {
125
+ continue;
126
+ }
127
+ const resolution = await runHook(
128
+ plugin,
129
+ 'resolveId',
130
+ context,
131
+ resolvedId,
132
+ undefined,
133
+ {}
134
+ );
135
+ if (resolution == null) {
136
+ continue;
137
+ }
138
+ if (typeof resolution === 'string') {
139
+ resolvedId = resolution;
140
+ break;
141
+ }
142
+ if (typeof resolution === 'object' && 'id' in resolution) {
143
+ resolvedId = (resolution as { id: string }).id;
144
+ break;
145
+ }
146
+ }
147
+
148
+ let code = initial;
149
+ let map: any = null;
150
+
151
+ for (const plugin of plugins) {
152
+ if (!plugin.load) {
153
+ continue;
154
+ }
155
+ const loaded = await runHook(plugin, 'load', context, resolvedId);
156
+ if (loaded == null) {
157
+ continue;
158
+ }
159
+ if (typeof loaded === 'string') {
160
+ code = loaded;
161
+ map = null;
162
+ } else if (loaded && typeof loaded === 'object') {
163
+ const output = loaded as { code?: string; map?: unknown };
164
+ if (typeof output.code === 'string') {
165
+ code = output.code;
166
+ }
167
+ map = output.map ?? null;
168
+ }
169
+ break;
170
+ }
171
+
172
+ for (const plugin of plugins) {
173
+ if (!plugin.transform) {
174
+ continue;
175
+ }
176
+ const transformed = await runHook(
177
+ plugin,
178
+ 'transform',
179
+ context,
180
+ code,
181
+ resolvedId
182
+ );
183
+ if (transformed == null) {
184
+ continue;
185
+ }
186
+ if (typeof transformed === 'string') {
187
+ code = transformed;
188
+ map = null;
189
+ } else if (transformed && typeof transformed === 'object') {
190
+ const output = transformed as { code?: string; map?: unknown };
191
+ if (typeof output.code === 'string') {
192
+ code = output.code;
193
+ }
194
+ map = output.map ?? null;
195
+ }
196
+ }
197
+
198
+ return { code, map };
199
+ };
200
+
201
+ describe('lithentTemplateVite plugin', () => {
202
+ let plugins: Plugin[];
203
+ const __filename = fileURLToPath(import.meta.url);
204
+ const __dirname = path.dirname(__filename);
205
+
206
+ beforeEach(async () => {
207
+ plugins = createPlugins();
208
+ await setupPlugins(plugins);
209
+ });
210
+
211
+ it('transforms l-for template with plain text content', async () => {
212
+ const filePath = path.resolve(__dirname, './fixtures/view.ltsx');
213
+ const code = await readFile(filePath, 'utf-8');
214
+ const result = await runTransforms(plugins, code, filePath);
215
+
216
+ expect(result.code).toContain('todos.map((todo, index) =>');
217
+ expect(result.code).toMatch(
218
+ /h\("strong", { class: "todo-status pending" }, "Pending\\n\s*"\)/
219
+ );
220
+ });
221
+
222
+ it('skips transformation for non-matching files', async () => {
223
+ const code = `<div>test</div>`;
224
+ const result = await runTransforms(plugins, code, 'script.js');
225
+
226
+ expect(result.code).toBe(code);
227
+ });
228
+
229
+ it('does not transform .tsx files by default', async () => {
230
+ const code = `
231
+ import { useState } from 'react';
232
+ export const Example = ({ todos }) => (
233
+ <div>
234
+ <li l-for={(todo, index) in todos} class="todo-item">
235
+ <span>{todo.text}</span>
236
+ </li>
237
+ </div>
238
+ );
239
+ `;
240
+ const result = await runTransforms(plugins, code, 'component.tsx');
241
+ expect(result.code).toBe(code);
242
+ });
243
+
244
+ it('allows configuring custom extensions', async () => {
245
+ const customPlugins = createPlugins({ extensions: ['.foo'] });
246
+ await setupPlugins(customPlugins);
247
+ const filePath = path.resolve(__dirname, './fixtures/view.foo');
248
+ const code = await readFile(filePath, 'utf-8');
249
+ const result = await runTransforms(customPlugins, code, filePath);
250
+ expect(result.code).toContain('todos.map((todo, index) =>');
251
+ });
252
+ });