@vojtaholik/static-kit-core 1.0.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.
@@ -0,0 +1,8 @@
1
+ export { createStaticKitConfig, staticKitConfig } from "./vite-config.js";
2
+ export { svgSpritePlugin, pagesPreviewPlugin, buildPlugins } from "./plugins/index.js";
3
+ export { loadStaticKitConfig, normalizeBase, timeStamp } from "./utils/config.js";
4
+ export { processHtmlImports } from "./utils/html-imports.js";
5
+ export { scanDirectory, getInputEntries } from "./utils/file-scanner.js";
6
+ export type { StaticKitConfig, StaticKitOptions, PagesPreviewOptions } from "./types.js";
7
+ export type { SvgSpriteOptions, BuildPluginsOptions } from "./plugins/index.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAG1E,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,YAAY,EACb,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,SAAS,EACV,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,kBAAkB,EACnB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,aAAa,EACb,eAAe,EAChB,MAAM,yBAAyB,CAAC;AAGjC,YAAY,EACV,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAEpB,YAAY,EACV,gBAAgB,EAChB,mBAAmB,EACpB,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ // Main exports
2
+ export { createStaticKitConfig, staticKitConfig } from "./vite-config.js";
3
+ // Plugins
4
+ export { svgSpritePlugin, pagesPreviewPlugin, buildPlugins } from "./plugins/index.js";
5
+ // Utilities
6
+ export { loadStaticKitConfig, normalizeBase, timeStamp } from "./utils/config.js";
7
+ export { processHtmlImports } from "./utils/html-imports.js";
8
+ export { scanDirectory, getInputEntries } from "./utils/file-scanner.js";
@@ -0,0 +1,8 @@
1
+ import type { Plugin } from "vite";
2
+ import type { StaticKitConfig } from "../types.js";
3
+ export interface BuildPluginsOptions {
4
+ config: StaticKitConfig;
5
+ publicDir?: string;
6
+ }
7
+ export declare function buildPlugins(options: BuildPluginsOptions): Plugin[];
8
+ //# sourceMappingURL=build-plugins.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-plugins.d.ts","sourceRoot":"","sources":["../../src/plugins/build-plugins.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAInC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,eAAe,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AASD,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,EAAE,CAoKnE"}
@@ -0,0 +1,150 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import fg from "fast-glob";
4
+ import { processHtmlImports } from "../utils/html-imports.js";
5
+ import { normalizeBase, timeStamp } from "../utils/config.js";
6
+ async function createHtaccessFile(distPath) {
7
+ await fs.writeFile(path.join(distPath, ".htaccess"), "DirectoryIndex index.html\nRewriteEngine On\nRewriteCond %{REQUEST_FILENAME} !-f\nRewriteCond %{REQUEST_FILENAME} !-d\nRewriteRule ^([^.]+)$ $1.html [L]");
8
+ }
9
+ export function buildPlugins(options) {
10
+ const { config, publicDir = "public" } = options;
11
+ const normalizedBase = normalizeBase(config.build?.base);
12
+ return [
13
+ // Create .htaccess file
14
+ {
15
+ name: "create-htaccess-file",
16
+ apply: "build",
17
+ writeBundle: async () => {
18
+ await createHtaccessFile("dist");
19
+ },
20
+ },
21
+ // Copy static assets
22
+ {
23
+ name: "copy-static-assets",
24
+ apply: "build",
25
+ writeBundle: async () => {
26
+ try {
27
+ const srcPublicPath = path.resolve(publicDir);
28
+ const destPublicPath = path.resolve(`dist/${normalizedBase}`);
29
+ // Create dist/public directory
30
+ await fs.mkdir(destPublicPath, { recursive: true });
31
+ // Copy files from public/ to dist/public/ using fast-glob
32
+ const allFiles = await fg("**/*", {
33
+ cwd: srcPublicPath,
34
+ onlyFiles: true,
35
+ dot: true,
36
+ });
37
+ for (const file of allFiles) {
38
+ const srcPath = path.join(srcPublicPath, file);
39
+ const destPath = path.join(destPublicPath, file);
40
+ // Ensure destination directory exists
41
+ await fs.mkdir(path.dirname(destPath), { recursive: true });
42
+ await fs.copyFile(srcPath, destPath);
43
+ }
44
+ }
45
+ catch (error) {
46
+ // Public directory doesn't exist or can't be copied
47
+ console.warn("Could not copy public directory:", error);
48
+ }
49
+ },
50
+ },
51
+ // Process HTML files
52
+ {
53
+ name: "html-processor",
54
+ apply: "build",
55
+ writeBundle: async () => {
56
+ const distPath = path.resolve("dist");
57
+ // Find all HTML files in the dist directory using fast-glob
58
+ const findHtmlFiles = async (dir) => {
59
+ try {
60
+ return await fg("**/*.html", {
61
+ cwd: dir,
62
+ onlyFiles: true,
63
+ });
64
+ }
65
+ catch (error) {
66
+ return [];
67
+ }
68
+ };
69
+ try {
70
+ const htmlFiles = await findHtmlFiles(distPath);
71
+ for (const htmlFile of htmlFiles) {
72
+ const fullPath = path.join(distPath, htmlFile);
73
+ // Check if this is a file that should be moved (in src/pages structure)
74
+ if (htmlFile.startsWith("src/pages/")) {
75
+ const cleanFileName = htmlFile.replace(/^src\/pages\//, "");
76
+ const newPath = path.join(distPath, cleanFileName);
77
+ // Read the file, process it, and write to new location
78
+ const content = await fs.readFile(fullPath, "utf-8");
79
+ const sourceDir = path.dirname(path.resolve("src/pages", cleanFileName));
80
+ const processedContent = await processHtmlImports(content, sourceDir);
81
+ // Remove any existing CSS and JS links that Vite injected
82
+ const cleanedContent = processedContent
83
+ .replace(/<link[^>]*rel=["']stylesheet["'][^>]*>/g, "")
84
+ .replace(/<script[^>]*src=[^>]*><\/script>/g, "")
85
+ .replace(/<use href=\"\/sprite.svg/g, () => {
86
+ return `<use href=\"${normalizedBase}images/sprite.svg\?v=${timeStamp()}`;
87
+ })
88
+ // Normalize asset paths: Convert absolute paths to use configured base
89
+ // Transform /public/images/... → public/images/... (based on normalizedBase)
90
+ // Transform /images/... → public/images/... (shorthand syntax)
91
+ // This ensures all asset references work with the custom build structure
92
+ .replace(/src="\/public\//g, `src="${normalizedBase}`)
93
+ .replace(/href="\/public\//g, `href="${normalizedBase}`)
94
+ .replace(/src="\/images\//g, `src="${normalizedBase}images/`)
95
+ .replace(/href="\/images\//g, `href="${normalizedBase}images/`)
96
+ .trim();
97
+ // Calculate asset paths based on page depth and base config
98
+ const pathSegments = cleanFileName.split("/");
99
+ const depth = pathSegments.length - 1;
100
+ // For relative paths, go up directories then into configured base
101
+ const relativePath = depth > 0 ? "../".repeat(depth) : "";
102
+ const stylesPath = `${normalizedBase}css/styles.css?v=${timeStamp()}`;
103
+ const jsPath = `${normalizedBase}js/index.js?v=${timeStamp()}`;
104
+ // Create full HTML with correct paths
105
+ const fullHtml = `<!DOCTYPE html>
106
+ <html lang="${config.templates?.language || "en"}">
107
+ <head>
108
+ <meta charset="UTF-8">
109
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
110
+ <title>${cleanFileName.replace(".html", "")}</title>
111
+ <link rel="stylesheet" href="${stylesPath}">
112
+ </head>
113
+ <body>
114
+ ${cleanedContent}
115
+ <script type="module" src="${jsPath}"></script>
116
+ </body>
117
+ </html>`;
118
+ // Create directory if needed
119
+ await fs.mkdir(path.dirname(newPath), { recursive: true });
120
+ // Write to new location
121
+ await fs.writeFile(newPath, fullHtml);
122
+ // Remove old file
123
+ await fs.unlink(fullPath);
124
+ // Remove empty directories
125
+ let dirToCheck = path.dirname(fullPath);
126
+ while (dirToCheck !== distPath) {
127
+ try {
128
+ const items = await fs.readdir(dirToCheck);
129
+ if (items.length === 0) {
130
+ await fs.rmdir(dirToCheck);
131
+ dirToCheck = path.dirname(dirToCheck);
132
+ }
133
+ else {
134
+ break;
135
+ }
136
+ }
137
+ catch {
138
+ break;
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+ catch (error) {
145
+ console.error("Error processing HTML files:", error);
146
+ }
147
+ },
148
+ },
149
+ ];
150
+ }
@@ -0,0 +1,7 @@
1
+ export { svgSpritePlugin } from "./svg-sprite.js";
2
+ export { pagesPreviewPlugin } from "./pages-preview.js";
3
+ export { buildPlugins } from "./build-plugins.js";
4
+ export type { SvgSpriteOptions } from "./svg-sprite.js";
5
+ export type { PagesPreviewOptions } from "../types.js";
6
+ export type { BuildPluginsOptions } from "./build-plugins.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugins/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvD,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { svgSpritePlugin } from "./svg-sprite.js";
2
+ export { pagesPreviewPlugin } from "./pages-preview.js";
3
+ export { buildPlugins } from "./build-plugins.js";
@@ -0,0 +1,4 @@
1
+ import type { Plugin } from "vite";
2
+ import type { PagesPreviewOptions } from "../types.js";
3
+ export declare function pagesPreviewPlugin(options?: PagesPreviewOptions): Plugin;
4
+ //# sourceMappingURL=pages-preview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pages-preview.d.ts","sourceRoot":"","sources":["../../src/plugins/pages-preview.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAGnC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAwMvD,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,mBAAwB,GAAG,MAAM,CAuJ5E"}
@@ -0,0 +1,292 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import fg from "fast-glob";
4
+ import { processHtmlImports } from "../utils/html-imports.js";
5
+ async function scanPagesDirectory(pagesDir) {
6
+ try {
7
+ const htmlFiles = await fg("**/*.html", {
8
+ cwd: pagesDir,
9
+ onlyFiles: true,
10
+ ignore: ["node_modules/**"],
11
+ });
12
+ const pages = htmlFiles.map((file) => file.replace(".html", ""));
13
+ return pages.sort();
14
+ }
15
+ catch (error) {
16
+ // Directory doesn't exist or can't be read
17
+ return [];
18
+ }
19
+ }
20
+ function generatePagesIndex(pages, components, routePrefix, componentsRoutePrefix) {
21
+ // Group pages by directory
22
+ const pagesByDir = {};
23
+ const componentsByDir = {};
24
+ for (const page of pages) {
25
+ const parts = page.split(path.sep);
26
+ if (parts.length === 1) {
27
+ // Root level page
28
+ if (!pagesByDir[""])
29
+ pagesByDir[""] = [];
30
+ pagesByDir[""].push(page);
31
+ }
32
+ else {
33
+ // Nested page
34
+ const dir = parts.slice(0, -1).join("/");
35
+ if (!pagesByDir[dir])
36
+ pagesByDir[dir] = [];
37
+ pagesByDir[dir].push(page);
38
+ }
39
+ }
40
+ for (const component of components) {
41
+ const parts = component.split(path.sep);
42
+ if (parts.length === 1) {
43
+ // Root level component
44
+ if (!componentsByDir[""])
45
+ componentsByDir[""] = [];
46
+ componentsByDir[""].push(component);
47
+ }
48
+ else {
49
+ // Nested component
50
+ const dir = parts.slice(0, -1).join("/");
51
+ if (!componentsByDir[dir])
52
+ componentsByDir[dir] = [];
53
+ componentsByDir[dir].push(component);
54
+ }
55
+ }
56
+ // Generate HTML sections
57
+ let sectionsHtml = "";
58
+ // Root pages first
59
+ if (pagesByDir[""]) {
60
+ sectionsHtml += `
61
+ <section>
62
+ <h2>📄 Pages</h2>
63
+ <ul>
64
+ ${pagesByDir[""]
65
+ .map((page) => `<li><a href="${routePrefix}/${page}">${page}</a></li>`)
66
+ .join("\n ")}
67
+ </ul>
68
+ </section>`;
69
+ }
70
+ // Then subdirectory pages
71
+ const sortedPageDirs = Object.keys(pagesByDir)
72
+ .filter((dir) => dir !== "")
73
+ .sort();
74
+ for (const dir of sortedPageDirs) {
75
+ sectionsHtml += `
76
+ <section>
77
+ <h2>📁 pages/${dir}/</h2>
78
+ <ul>
79
+ ${pagesByDir[dir]
80
+ .map((page) => {
81
+ const fileName = page.split("/").pop();
82
+ return `<li><a href="${routePrefix}/${page}">${fileName}</a></li>`;
83
+ })
84
+ .join("\n ")}
85
+ </ul>
86
+ </section>`;
87
+ }
88
+ // Root components
89
+ if (componentsByDir[""]) {
90
+ sectionsHtml += `
91
+ <section>
92
+ <h2>🧩 Components</h2>
93
+ <ul>
94
+ ${componentsByDir[""]
95
+ .map((component) => `<li><a href="${componentsRoutePrefix}/${component}">${component}</a></li>`)
96
+ .join("\n ")}
97
+ </ul>
98
+ </section>`;
99
+ }
100
+ // Then subdirectory components
101
+ const sortedComponentDirs = Object.keys(componentsByDir)
102
+ .filter((dir) => dir !== "")
103
+ .sort();
104
+ for (const dir of sortedComponentDirs) {
105
+ sectionsHtml += `
106
+ <section>
107
+ <h2>📁 components/${dir}/</h2>
108
+ <ul>
109
+ ${componentsByDir[dir]
110
+ .map((component) => {
111
+ const fileName = component.split("/").pop();
112
+ return `<li><a href="${componentsRoutePrefix}/${component}">${fileName}</a></li>`;
113
+ })
114
+ .join("\n ")}
115
+ </ul>
116
+ </section>`;
117
+ }
118
+ return `<!DOCTYPE html>
119
+ <html lang="en">
120
+ <head>
121
+ <meta charset="UTF-8">
122
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
123
+ <title>Pages & Components Preview</title>
124
+ <script type="module" src="/@vite/client"></script>
125
+ <link rel="stylesheet" href="/style.css">
126
+ <style>
127
+ body {
128
+ font-family: system-ui, sans-serif;
129
+ max-width: 800px;
130
+ margin: 2rem auto;
131
+ padding: 0 1rem;
132
+ line-height: 1.6;
133
+ }
134
+ h1 {
135
+ color: #333;
136
+ border-bottom: 2px solid #eee;
137
+ padding-bottom: 0.5rem;
138
+ }
139
+ ul {
140
+ list-style: none;
141
+ padding: 0;
142
+ }
143
+ li {
144
+ margin: 0.5rem 0;
145
+ }
146
+ a {
147
+ display: block;
148
+ padding: 0.75rem 1rem;
149
+ text-decoration: none;
150
+ color: #0066cc;
151
+ border: 1px solid #ddd;
152
+ border-radius: 4px;
153
+ transition: all 0.2s;
154
+ }
155
+ a:hover {
156
+ background: #f5f5f5;
157
+ border-color: #0066cc;
158
+ }
159
+ </style>
160
+ </head>
161
+ <body>
162
+ <h1>📄 Pages & Components Preview</h1>
163
+ <p>Available pages and components in your file system:</p>
164
+ ${sectionsHtml}
165
+ </body>
166
+ </html>`;
167
+ }
168
+ function generateFullPage(pageName, pageContent) {
169
+ return `<!DOCTYPE html>
170
+ <html lang="en">
171
+ <head>
172
+ <meta charset="UTF-8">
173
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
174
+ <title>${pageName}</title>
175
+ <script type="module" src="/@vite/client"></script>
176
+ <link rel="icon" href="/favicon.ico">
177
+ <link rel="stylesheet" href="/src/styles/main.scss">
178
+ <script type="module" src="/src/js/index.ts"></script>
179
+ <script type="module" src="/svg-sprite-hmr.ts"></script>
180
+
181
+ </head>
182
+ <body>
183
+
184
+ ${pageContent}
185
+
186
+ <!-- <a href="/" class="back-link">← Back to all pages</a> -->
187
+ </body>
188
+ </html>`;
189
+ }
190
+ export function pagesPreviewPlugin(options = {}) {
191
+ const { pagesDir = "src/pages", componentsDir = "src/components", routePrefix = "/pages", componentsRoutePrefix = "/components", } = options;
192
+ return {
193
+ name: "vite-pages-preview",
194
+ apply: "serve",
195
+ configureServer(server) {
196
+ // Watch the pages and components directories for changes and force browser reload
197
+ const pagesPath = path.resolve(pagesDir);
198
+ const componentsPath = path.resolve(componentsDir);
199
+ server.watcher.add(pagesPath);
200
+ server.watcher.add(componentsPath);
201
+ const reloadBrowser = () => {
202
+ // Force a hard refresh of the browser
203
+ server.ws.send({
204
+ type: "full-reload",
205
+ });
206
+ // Also send update message for good measure
207
+ server.ws.send({
208
+ type: "update",
209
+ updates: [],
210
+ });
211
+ };
212
+ server.watcher.on("change", (filePath) => {
213
+ const resolvedPath = path.resolve(filePath);
214
+ if (resolvedPath.startsWith(pagesPath) ||
215
+ resolvedPath.startsWith(componentsPath)) {
216
+ console.log(`[pages-preview] File changed: ${filePath}`);
217
+ reloadBrowser();
218
+ }
219
+ });
220
+ server.watcher.on("add", (filePath) => {
221
+ const resolvedPath = path.resolve(filePath);
222
+ if ((resolvedPath.startsWith(pagesPath) ||
223
+ resolvedPath.startsWith(componentsPath)) &&
224
+ filePath.endsWith(".html")) {
225
+ console.log(`[pages-preview] File added: ${filePath}`);
226
+ reloadBrowser();
227
+ }
228
+ });
229
+ server.watcher.on("unlink", (filePath) => {
230
+ const resolvedPath = path.resolve(filePath);
231
+ if ((resolvedPath.startsWith(pagesPath) ||
232
+ resolvedPath.startsWith(componentsPath)) &&
233
+ filePath.endsWith(".html")) {
234
+ console.log(`[pages-preview] File deleted: ${filePath}`);
235
+ reloadBrowser();
236
+ }
237
+ });
238
+ server.middlewares.use(async (req, res, next) => {
239
+ const url = req.url;
240
+ // Handle preview index route
241
+ if (url === `/`) {
242
+ const pages = await scanPagesDirectory(pagesDir);
243
+ const components = await scanPagesDirectory(componentsDir);
244
+ const indexHtml = generatePagesIndex(pages, components, routePrefix, componentsRoutePrefix);
245
+ res.setHeader("Content-Type", "text/html");
246
+ res.end(indexHtml);
247
+ return;
248
+ }
249
+ // Handle individual page routes
250
+ if (url?.startsWith(`${routePrefix}/`) && url !== `${routePrefix}/`) {
251
+ const pageName = url
252
+ .replace(`${routePrefix}/`, "")
253
+ .replace(/\/$/, "");
254
+ const pageFile = path.join(pagesDir, `${pageName}.html`);
255
+ try {
256
+ const rawPageContent = await fs.readFile(pageFile, "utf-8");
257
+ // Process HTML imports before generating full page
258
+ const processedPageContent = await processHtmlImports(rawPageContent, path.dirname(pageFile));
259
+ const fullPageHtml = generateFullPage(pageName, processedPageContent);
260
+ res.setHeader("Content-Type", "text/html");
261
+ res.end(fullPageHtml);
262
+ return;
263
+ }
264
+ catch (error) {
265
+ // Page not found, continue to next middleware
266
+ }
267
+ }
268
+ // Handle individual component routes
269
+ if (url?.startsWith(`${componentsRoutePrefix}/`) &&
270
+ url !== `${componentsRoutePrefix}/`) {
271
+ const componentName = url
272
+ .replace(`${componentsRoutePrefix}/`, "")
273
+ .replace(/\/$/, "");
274
+ const componentFile = path.join(componentsDir, `${componentName}.html`);
275
+ try {
276
+ const rawComponentContent = await fs.readFile(componentFile, "utf-8");
277
+ // Process HTML imports before generating full page
278
+ const processedComponentContent = await processHtmlImports(rawComponentContent, path.dirname(componentFile));
279
+ const fullPageHtml = generateFullPage(`component: ${componentName}`, processedComponentContent);
280
+ res.setHeader("Content-Type", "text/html");
281
+ res.end(fullPageHtml);
282
+ return;
283
+ }
284
+ catch (error) {
285
+ // Component not found, continue to next middleware
286
+ }
287
+ }
288
+ next();
289
+ });
290
+ },
291
+ };
292
+ }
@@ -0,0 +1,8 @@
1
+ import type { Plugin } from "vite";
2
+ export interface SvgSpriteOptions {
3
+ iconsDir?: string;
4
+ outputPath?: string;
5
+ publicPath?: string;
6
+ }
7
+ export declare function svgSpritePlugin(options?: SvgSpriteOptions): Plugin[];
8
+ //# sourceMappingURL=svg-sprite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"svg-sprite.d.ts","sourceRoot":"","sources":["../../src/plugins/svg-sprite.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAInC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA4DD,wBAAgB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,MAAM,EAAE,CA0DxE"}
@@ -0,0 +1,98 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { optimize } from "svgo";
4
+ import { scanDirectory } from "../utils/file-scanner.js";
5
+ async function generateSvgSprite(iconsDir, outputPath) {
6
+ const svgFiles = await scanDirectory(iconsDir, [".svg"]);
7
+ if (svgFiles.length === 0) {
8
+ return;
9
+ }
10
+ const symbols = [];
11
+ for (const file of svgFiles) {
12
+ try {
13
+ const filePath = path.join(iconsDir, file);
14
+ const svgContent = await fs.readFile(filePath, "utf-8");
15
+ // Optimize with SVGO
16
+ const result = optimize(svgContent, {
17
+ plugins: [
18
+ {
19
+ name: "removeAttrs",
20
+ params: { attrs: "data-name" },
21
+ },
22
+ ],
23
+ });
24
+ // Extract the inner content of the SVG and create a symbol
25
+ const optimizedSvg = result.data;
26
+ const svgMatch = optimizedSvg.match(/<svg[^>]*>([\s\S]*?)<\/svg>/);
27
+ if (svgMatch) {
28
+ const iconName = path.basename(file, ".svg");
29
+ const innerContent = svgMatch[1];
30
+ // Extract viewBox from the original SVG
31
+ const viewBoxMatch = optimizedSvg.match(/viewBox="([^"]*)"/);
32
+ const viewBox = viewBoxMatch ? ` viewBox="${viewBoxMatch[1]}"` : "";
33
+ const symbol = `<symbol id="${iconName}"${viewBox}>${innerContent}</symbol>`;
34
+ symbols.push(symbol);
35
+ }
36
+ }
37
+ catch (error) {
38
+ console.warn(`Failed to process ${file}:`, error);
39
+ }
40
+ }
41
+ if (symbols.length > 0) {
42
+ const sprite = `<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
43
+ ${symbols.join("\n")}
44
+ </svg>`;
45
+ // Ensure the output directory exists
46
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
47
+ await fs.writeFile(outputPath, sprite);
48
+ console.log(`📦 Generated sprite with ${symbols.length} icons at ${outputPath}`);
49
+ }
50
+ }
51
+ export function svgSpritePlugin(options = {}) {
52
+ const { iconsDir = "src/icons", outputPath = "public/images/sprite.svg", publicPath = "public/" } = options;
53
+ return [
54
+ // Development plugin
55
+ {
56
+ name: "svg-sprite-dev",
57
+ apply: "serve",
58
+ configureServer(server) {
59
+ // Generate sprite on server start
60
+ generateSvgSprite(iconsDir, outputPath);
61
+ // Watch for changes in icons directory
62
+ server.watcher.add(`${iconsDir}/**/*.svg`);
63
+ const handleSpriteChange = async (file) => {
64
+ console.log("📁 File change detected:", file);
65
+ if (file.includes(iconsDir) && file.endsWith(".svg")) {
66
+ console.log("🎨 SVG file changed, regenerating sprite...");
67
+ await generateSvgSprite(iconsDir, outputPath);
68
+ const timestamp = Date.now();
69
+ console.log("📡 Sending HMR event with timestamp:", timestamp);
70
+ // Send a custom HMR update to invalidate SVG references
71
+ server.ws.send({
72
+ type: "custom",
73
+ event: "svg-sprite-updated",
74
+ data: { timestamp },
75
+ });
76
+ console.log("✅ SVG sprite updated and HMR event sent");
77
+ }
78
+ else {
79
+ console.log("⏭️ Not an SVG file, skipping");
80
+ }
81
+ };
82
+ server.watcher.on("change", handleSpriteChange);
83
+ server.watcher.on("add", handleSpriteChange);
84
+ server.watcher.on("unlink", handleSpriteChange);
85
+ },
86
+ },
87
+ // Build plugin
88
+ {
89
+ name: "svg-sprite-build",
90
+ apply: "build",
91
+ buildStart() {
92
+ // Generate sprite at build start
93
+ const buildOutputPath = outputPath.replace("public/", `dist/${publicPath}`);
94
+ return generateSvgSprite(iconsDir, buildOutputPath);
95
+ },
96
+ },
97
+ ];
98
+ }
@@ -0,0 +1,25 @@
1
+ export interface StaticKitConfig {
2
+ build?: {
3
+ base?: string;
4
+ output?: string;
5
+ };
6
+ templates?: {
7
+ language?: string;
8
+ };
9
+ }
10
+ export interface StaticKitOptions {
11
+ config?: StaticKitConfig;
12
+ pagesDir?: string;
13
+ componentsDir?: string;
14
+ iconsDir?: string;
15
+ stylesEntry?: string;
16
+ jsEntry?: string;
17
+ publicDir?: string;
18
+ }
19
+ export interface PagesPreviewOptions {
20
+ pagesDir?: string;
21
+ componentsDir?: string;
22
+ routePrefix?: string;
23
+ componentsRoutePrefix?: string;
24
+ }
25
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE;QACN,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,SAAS,CAAC,EAAE;QACV,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import type { StaticKitConfig } from "../types.js";
2
+ export declare function loadStaticKitConfig(root?: string): Promise<StaticKitConfig>;
3
+ export declare function normalizeBase(input?: string): string;
4
+ export declare function timeStamp(): string;
5
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,wBAAsB,mBAAmB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAqBjF;AAED,wBAAgB,aAAa,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAKpD;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC"}
@@ -0,0 +1,34 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ export async function loadStaticKitConfig(root) {
4
+ const projectRoot = root || process.cwd();
5
+ const baseConfigPath = path.join(projectRoot, "static-kit.config.json");
6
+ const localConfigPath = path.join(projectRoot, "static-kit.local.json");
7
+ const result = {};
8
+ try {
9
+ const raw = await fs.readFile(baseConfigPath, "utf8");
10
+ Object.assign(result, JSON.parse(raw));
11
+ }
12
+ catch {
13
+ // Config file doesn't exist or can't be read
14
+ }
15
+ try {
16
+ const rawLocal = await fs.readFile(localConfigPath, "utf8");
17
+ Object.assign(result, JSON.parse(rawLocal));
18
+ }
19
+ catch {
20
+ // Local config file doesn't exist or can't be read
21
+ }
22
+ return result;
23
+ }
24
+ export function normalizeBase(input) {
25
+ if (!input)
26
+ return "public/";
27
+ let base = input.trim();
28
+ if (!base.endsWith("/"))
29
+ base = base + "/";
30
+ return base;
31
+ }
32
+ export function timeStamp() {
33
+ return Date.now().toString().slice(0, 10);
34
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Scan directory for files with specified extensions using fast-glob
3
+ */
4
+ export declare function scanDirectory(dir: string, extensions: string[]): Promise<string[]>;
5
+ /**
6
+ * Get input entries for Vite build
7
+ */
8
+ export declare function getInputEntries(pagesDir?: string, jsDir?: string, stylesEntry?: string): Promise<Record<string, string>>;
9
+ //# sourceMappingURL=file-scanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-scanner.d.ts","sourceRoot":"","sources":["../../src/utils/file-scanner.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAAE,GACnB,OAAO,CAAC,MAAM,EAAE,CAAC,CAanB;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,QAAQ,GAAE,MAAoB,EAC9B,KAAK,GAAE,MAAiB,EACxB,WAAW,GAAE,MAA+B,mCAsB7C"}
@@ -0,0 +1,40 @@
1
+ import fg from "fast-glob";
2
+ /**
3
+ * Scan directory for files with specified extensions using fast-glob
4
+ */
5
+ export async function scanDirectory(dir, extensions) {
6
+ try {
7
+ const patterns = extensions.map((ext) => `**/*${ext}`);
8
+ const files = await fg(patterns, {
9
+ cwd: dir,
10
+ onlyFiles: true,
11
+ ignore: ["node_modules/**"],
12
+ });
13
+ return files;
14
+ }
15
+ catch (error) {
16
+ // Directory doesn't exist or can't be read
17
+ return [];
18
+ }
19
+ }
20
+ /**
21
+ * Get input entries for Vite build
22
+ */
23
+ export async function getInputEntries(pagesDir = "src/pages", jsDir = "src/js", stylesEntry = "src/styles/main.scss") {
24
+ const entries = {};
25
+ // Always include main SCSS
26
+ entries.main = stylesEntry;
27
+ // Dynamically scan for JS/TS files (including nested)
28
+ const jsFiles = await scanDirectory(jsDir, [".ts", ".js"]);
29
+ for (const file of jsFiles) {
30
+ const name = file.replace(/\.(ts|js)$/, "");
31
+ entries[`js/${name}`] = `${jsDir}/${file}`;
32
+ }
33
+ // Dynamically scan for HTML pages (including nested)
34
+ const pageFiles = await scanDirectory(pagesDir, [".html"]);
35
+ for (const file of pageFiles) {
36
+ const name = file.replace(".html", "");
37
+ entries[name] = `${pagesDir}/${file}`;
38
+ }
39
+ return entries;
40
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Process HTML imports in the given HTML string
3
+ * Supports both relative paths and @components/ prefix
4
+ */
5
+ export declare function processHtmlImports(html: string, dir: string): Promise<string>;
6
+ //# sourceMappingURL=html-imports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-imports.d.ts","sourceRoot":"","sources":["../../src/utils/html-imports.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,CAoEjB"}
@@ -0,0 +1,69 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ /**
4
+ * Process HTML imports in the given HTML string
5
+ * Supports both relative paths and @components/ prefix
6
+ */
7
+ export async function processHtmlImports(html, dir) {
8
+ const importRegex = /<!--\s*@import:\s*([\w./@-]+)\s*-->/g;
9
+ // Collect all matches first to avoid issues with changing string length
10
+ const matches = Array.from(html.matchAll(importRegex));
11
+ // Process matches in reverse order to maintain correct indices
12
+ for (let i = matches.length - 1; i >= 0; i--) {
13
+ const match = matches[i];
14
+ let importPath = match[1];
15
+ // Handle @components/ prefix for cleaner component imports
16
+ if (importPath.startsWith("@components/")) {
17
+ // Find the project root (go up from current dir until we find src/)
18
+ let currentDir = dir;
19
+ while (!currentDir.endsWith("src") &&
20
+ currentDir !== path.dirname(currentDir)) {
21
+ currentDir = path.dirname(currentDir);
22
+ }
23
+ if (currentDir.endsWith("src") || currentDir.includes("src")) {
24
+ const srcDir = currentDir.endsWith("src")
25
+ ? currentDir
26
+ : path.join(currentDir, "src");
27
+ importPath = importPath.replace("@components/", "components/");
28
+ const filePath = path.resolve(srcDir, importPath);
29
+ try {
30
+ const fileContent = await fs.readFile(filePath, "utf8");
31
+ html =
32
+ html.slice(0, match.index) +
33
+ fileContent +
34
+ html.slice(match.index + match[0].length);
35
+ continue;
36
+ }
37
+ catch (error) {
38
+ const errorMessage = `<!-- Import Error: Could not find component "${match[1]}"
39
+ Looked for: ${filePath}
40
+ Check the path and make sure the file exists -->`;
41
+ html =
42
+ html.slice(0, match.index) +
43
+ errorMessage +
44
+ html.slice(match.index + match[0].length);
45
+ continue;
46
+ }
47
+ }
48
+ }
49
+ // Standard relative path resolution
50
+ const filePath = path.resolve(dir, importPath);
51
+ try {
52
+ const fileContent = await fs.readFile(filePath, "utf8");
53
+ html =
54
+ html.slice(0, match.index) +
55
+ fileContent +
56
+ html.slice(match.index + match[0].length);
57
+ }
58
+ catch (error) {
59
+ const errorMessage = `<!-- Import Error: Could not find file "${match[1]}"
60
+ Looked for: ${filePath}
61
+ Check the path and make sure the file exists -->`;
62
+ html =
63
+ html.slice(0, match.index) +
64
+ errorMessage +
65
+ html.slice(match.index + match[0].length);
66
+ }
67
+ }
68
+ return html;
69
+ }
@@ -0,0 +1,5 @@
1
+ import type { UserConfigFnPromise } from "vite";
2
+ import type { StaticKitOptions } from "./types.js";
3
+ export declare function createStaticKitConfig(options?: StaticKitOptions): Promise<UserConfigFnPromise>;
4
+ export declare function staticKitConfig(options?: StaticKitOptions): Promise<UserConfigFnPromise>;
5
+ //# sourceMappingURL=vite-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite-config.d.ts","sourceRoot":"","sources":["../src/vite-config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAc,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAI5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,wBAAsB,qBAAqB,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA8ExG;AAGD,wBAAgB,eAAe,CAAC,OAAO,GAAE,gBAAqB,gCAE7D"}
@@ -0,0 +1,75 @@
1
+ import { defineConfig } from "vite";
2
+ import { loadStaticKitConfig, normalizeBase } from "./utils/config.js";
3
+ import { getInputEntries } from "./utils/file-scanner.js";
4
+ import { svgSpritePlugin, pagesPreviewPlugin, buildPlugins } from "./plugins/index.js";
5
+ export async function createStaticKitConfig(options = {}) {
6
+ const { config: userProvidedConfig, pagesDir = "src/pages", componentsDir = "src/components", iconsDir = "src/icons", stylesEntry = "src/styles/main.scss", jsEntry = "src/js", publicDir = "public" } = options;
7
+ // Load config from file system if not provided
8
+ const config = userProvidedConfig || await loadStaticKitConfig();
9
+ const normalizedBase = normalizeBase(config.build?.base);
10
+ return defineConfig(async ({ command }) => {
11
+ if (command === "serve") {
12
+ return {
13
+ plugins: [
14
+ pagesPreviewPlugin({
15
+ pagesDir,
16
+ componentsDir,
17
+ }),
18
+ ...svgSpritePlugin({
19
+ iconsDir,
20
+ outputPath: `${publicDir}/images/sprite.svg`,
21
+ publicPath: normalizedBase
22
+ }),
23
+ ],
24
+ publicDir, // Static assets during dev
25
+ };
26
+ }
27
+ // Build configuration with dynamic inputs
28
+ const inputEntries = await getInputEntries(pagesDir, jsEntry, stylesEntry);
29
+ console.log("📦 Building with entries:", Object.keys(inputEntries));
30
+ return {
31
+ build: {
32
+ outDir: config.build?.output || "dist",
33
+ emptyOutDir: true,
34
+ cssCodeSplit: false, // Single CSS file
35
+ rollupOptions: {
36
+ input: inputEntries,
37
+ output: {
38
+ entryFileNames: (chunkInfo) => {
39
+ if (chunkInfo.name?.startsWith("js/")) {
40
+ return `${normalizedBase}[name].js`;
41
+ }
42
+ return `${normalizedBase}[name].js`;
43
+ },
44
+ assetFileNames: (assetInfo) => {
45
+ if (assetInfo.name?.endsWith(".css")) {
46
+ return `${normalizedBase}css/styles.css`;
47
+ }
48
+ if (assetInfo.name === "sprite.svg") {
49
+ // Ensure sprite lands under public if emitted by Rollup
50
+ return `${normalizedBase}images/sprite.svg`;
51
+ }
52
+ return `${normalizedBase}assets/[name]-[hash][extname]`;
53
+ },
54
+ },
55
+ },
56
+ },
57
+ publicDir: "", // Disable default public dir copying
58
+ plugins: [
59
+ ...svgSpritePlugin({
60
+ iconsDir,
61
+ outputPath: `dist/${normalizedBase}images/sprite.svg`,
62
+ publicPath: normalizedBase
63
+ }),
64
+ ...buildPlugins({
65
+ config,
66
+ publicDir
67
+ }),
68
+ ],
69
+ };
70
+ });
71
+ }
72
+ // Convenience function that matches the current API
73
+ export function staticKitConfig(options = {}) {
74
+ return createStaticKitConfig(options);
75
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@vojtaholik/static-kit-core",
3
+ "version": "1.0.0",
4
+ "description": "Core library for Static Kit - simple static site framework",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./vite": {
14
+ "types": "./dist/vite-config.d.ts",
15
+ "import": "./dist/vite-config.js"
16
+ },
17
+ "./plugins": {
18
+ "types": "./dist/plugins/index.d.ts",
19
+ "import": "./dist/plugins/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist/",
24
+ "templates/"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc",
28
+ "dev": "tsc --watch",
29
+ "clean": "rm -rf dist"
30
+ },
31
+ "peerDependencies": {
32
+ "vite": "^7.0.0"
33
+ },
34
+ "dependencies": {
35
+ "fast-glob": "^3.3.3",
36
+ "svgo": "^4.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^24.2.1",
40
+ "typescript": "~5.9.2",
41
+ "vite": "^7.1.1"
42
+ },
43
+ "keywords": [
44
+ "static-site",
45
+ "vite",
46
+ "html",
47
+ "scss",
48
+ "typescript",
49
+ "components"
50
+ ],
51
+ "author": "Vojta Holik <vojta@hey.com>",
52
+ "license": "MIT",
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "https://github.com/vojtaholik/static-kit.git",
56
+ "directory": "packages/static-kit-core"
57
+ },
58
+ "homepage": "https://github.com/vojtaholik/static-kit#readme",
59
+ "bugs": {
60
+ "url": "https://github.com/vojtaholik/static-kit/issues"
61
+ }
62
+ }