@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 +21 -0
- package/README.md +114 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.mjs +123 -0
- package/dist/index.mjs.map +1 -0
- package/dist/index.umd.js +2 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/plugin.d.ts +9 -0
- package/package.json +52 -0
- package/src/dev/App.ltsx +44 -0
- package/src/dev/init.js +1 -0
- package/src/dev/main.ltsx +48 -0
- package/src/index.ts +2 -0
- package/src/plugin.ts +255 -0
- package/src/tests/fixtures/view.foo +12 -0
- package/src/tests/fixtures/view.ltsx +12 -0
- package/src/tests/plugin.test.ts +252 -0
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
|
package/dist/index.d.ts
ADDED
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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"}
|
package/dist/plugin.d.ts
ADDED
|
@@ -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
|
+
}
|
package/src/dev/App.ltsx
ADDED
|
@@ -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
|
+
}
|
package/src/dev/init.js
ADDED
|
@@ -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
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
|
+
});
|