thunderous-server 0.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.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/bin/thunderous +10 -0
- package/dist/index.cjs +69 -0
- package/dist/index.d.cts +85 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.js +40 -0
- package/package.json +63 -0
- package/src/build.ts +134 -0
- package/src/cli.ts +24 -0
- package/src/config.ts +61 -0
- package/src/dev.ts +113 -0
- package/src/generate.ts +451 -0
- package/src/index.ts +2 -0
- package/src/meta.ts +80 -0
- package/src/utilities.ts +26 -0
- package/src/vendorize.ts +190 -0
- package/types/global.d.ts +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jonathan DeWitt
|
|
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,129 @@
|
|
|
1
|
+
# Thunderous Server
|
|
2
|
+
|
|
3
|
+
> [!CAUTION]
|
|
4
|
+
> This project is experimental. It is not ready for production use at this time.
|
|
5
|
+
|
|
6
|
+
**Thunderous Server is a web framework and static generator designed to support and supplement _plain old HTML_.**
|
|
7
|
+
|
|
8
|
+
**What it does NOT do:**
|
|
9
|
+
|
|
10
|
+
- No special file extensions
|
|
11
|
+
- No unusual mixed syntax
|
|
12
|
+
- No complex route config
|
|
13
|
+
- No compiler magic
|
|
14
|
+
|
|
15
|
+
**What it DOES:**
|
|
16
|
+
|
|
17
|
+
- HTML is enhanced to support server-side templating and layout composition.
|
|
18
|
+
- TypeScript is compiled for browser runtimes
|
|
19
|
+
- NPM dependencies are extracted to static assets and referenced via import maps
|
|
20
|
+
|
|
21
|
+
All you need to set up your app is a source folder (`src` by default) with an `index.html` file. Routing is achieved natively, through the folder structure
|
|
22
|
+
|
|
23
|
+
## Examples
|
|
24
|
+
|
|
25
|
+
**\_layout.html**:
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<!--
|
|
29
|
+
Since this file begins with an underscore, it will not be included
|
|
30
|
+
in the static output. Its contents may be used by other HTML files
|
|
31
|
+
that reference it, but it will never be served directly.
|
|
32
|
+
-->
|
|
33
|
+
<html>
|
|
34
|
+
<head>
|
|
35
|
+
<title>Thunderous Server</title>
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<!--
|
|
39
|
+
<slot> tags are usually reserved for shadow DOM, but they are
|
|
40
|
+
appropriate here since they serve a similar purpose. Any HTML
|
|
41
|
+
file that includes this layout will insert its content into this
|
|
42
|
+
slot.
|
|
43
|
+
-->
|
|
44
|
+
<slot></slot>
|
|
45
|
+
</body>
|
|
46
|
+
</html>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**index.html**:
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<!--
|
|
53
|
+
This is a "processing instruction", and there's already support
|
|
54
|
+
in XML and the DOM spec. Browsers just parse them as comments,
|
|
55
|
+
but Thunderous Server will strip this away before it reaches
|
|
56
|
+
the browser anyway.
|
|
57
|
+
-->
|
|
58
|
+
<?layout href="_layout.html">
|
|
59
|
+
|
|
60
|
+
<ul>
|
|
61
|
+
<!--
|
|
62
|
+
This is evaluated in-place and replaced with the result.
|
|
63
|
+
This is server-side only; you'll never see <script expr>
|
|
64
|
+
in the source markup served to the browser.
|
|
65
|
+
-->
|
|
66
|
+
<script expr>
|
|
67
|
+
list.map((item) => html` <li>test ${label} - ${item}</li> `);
|
|
68
|
+
</script>
|
|
69
|
+
</ul>
|
|
70
|
+
|
|
71
|
+
<!--
|
|
72
|
+
Thunderous supports SSR web components by rendering declarative
|
|
73
|
+
shadow DOM in the source markup sent to the browser. When
|
|
74
|
+
MyComponent.define() is called on the server, it will inject
|
|
75
|
+
this element with <my-component>'s rendered output.
|
|
76
|
+
-->
|
|
77
|
+
<my-component></my-component>
|
|
78
|
+
|
|
79
|
+
<!--
|
|
80
|
+
This is ordinary client-only code that remains in the source markup
|
|
81
|
+
sent to the browser.
|
|
82
|
+
-->
|
|
83
|
+
<script>
|
|
84
|
+
console.log('Hello from the browser!');
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<!--
|
|
88
|
+
This is also client-only. Client-side modules are processed to
|
|
89
|
+
"vendorize" NPM dependencies. That is, imports are ejected as
|
|
90
|
+
assets the browser can see and use. This does NOT bundle; instead,
|
|
91
|
+
it generates an import map. It will only vendorize the dependencies
|
|
92
|
+
that are actually imported.
|
|
93
|
+
-->
|
|
94
|
+
<script type="module">
|
|
95
|
+
console.log('Hello from a module!');
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<!--
|
|
99
|
+
This runs on both the client and server. Isomorphic scripts are
|
|
100
|
+
always type="module". Since it runs on the client, it will also
|
|
101
|
+
vendorize NPM dependencies.
|
|
102
|
+
-->
|
|
103
|
+
<script isomorphic>
|
|
104
|
+
import { MyComponent } from './scripts';
|
|
105
|
+
MyComponent.define('my-component');
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<!--
|
|
109
|
+
This runs on the server only. Server scripts are always modules.
|
|
110
|
+
Exported values are available in <script expr> tags.
|
|
111
|
+
-->
|
|
112
|
+
<script server>
|
|
113
|
+
export default {
|
|
114
|
+
label: 'Item',
|
|
115
|
+
list: [1, 2, 3, 4, 5],
|
|
116
|
+
};
|
|
117
|
+
</script>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## TODO
|
|
121
|
+
|
|
122
|
+
- [ ] Create (or find existing) extension that supports:
|
|
123
|
+
- [ ] TypeScript inside `<script>` tags
|
|
124
|
+
- [ ] IDE navigation in `<script server>` and `<script expr>` tags (go to definition, find references, etc.)
|
|
125
|
+
- [ ] Auto-imports for `<script server>`, `<script isomorphic>`, and `<script type="module">`
|
|
126
|
+
- [ ] Lint against multiple `<script server>` tags to avoid export collisions
|
|
127
|
+
- [ ] Typechecking to ensure the default export in `<script server>` is `Record<PropertyKey, unknown>`
|
|
128
|
+
- [ ] OPTIONAL: Lint and autofix for trailing `;` in `<script expr>` tags
|
|
129
|
+
_e.g., `('hello';)` is an invalid expression, so `<script expr>'hello';</script>` is technically incorrect. That said, Thunderous Server does handle this currently by stripping it from the content before it's evaluated._
|
package/bin/thunderous
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Register tsx loaders in-process (no child process spawn needed)
|
|
4
|
+
import { register as registerEsm } from 'tsx/esm/api';
|
|
5
|
+
import { register as registerCjs } from 'tsx/cjs/api';
|
|
6
|
+
registerCjs();
|
|
7
|
+
registerEsm();
|
|
8
|
+
|
|
9
|
+
// Dynamically import the TypeScript CLI entry point
|
|
10
|
+
await import('../src/cli.ts');
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
escapeHtml: () => escapeHtml,
|
|
24
|
+
getMeta: () => getMeta,
|
|
25
|
+
raw: () => raw
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/meta.ts
|
|
30
|
+
var metaState = {
|
|
31
|
+
config: {
|
|
32
|
+
name: "",
|
|
33
|
+
baseDir: "",
|
|
34
|
+
outDir: ""
|
|
35
|
+
},
|
|
36
|
+
pathname: "/",
|
|
37
|
+
breadcrumbs: [],
|
|
38
|
+
title: "",
|
|
39
|
+
name: "",
|
|
40
|
+
filename: ""
|
|
41
|
+
};
|
|
42
|
+
var getMeta = () => {
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
...metaState,
|
|
45
|
+
config: { ...metaState.config },
|
|
46
|
+
breadcrumbs: [...metaState.breadcrumbs]
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/utilities.ts
|
|
51
|
+
var escapeHtml = (str) => {
|
|
52
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
53
|
+
};
|
|
54
|
+
var RawHtml = class {
|
|
55
|
+
value;
|
|
56
|
+
constructor(value) {
|
|
57
|
+
this.value = value;
|
|
58
|
+
}
|
|
59
|
+
toString() {
|
|
60
|
+
return this.value;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var raw = (str) => new RawHtml(str);
|
|
64
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
65
|
+
0 && (module.exports = {
|
|
66
|
+
escapeHtml,
|
|
67
|
+
getMeta,
|
|
68
|
+
raw
|
|
69
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The type for the default export from `thunderous.config.ts`.
|
|
3
|
+
*/
|
|
4
|
+
type ThunderousConfig = {
|
|
5
|
+
/**
|
|
6
|
+
* The name of the site.
|
|
7
|
+
*/
|
|
8
|
+
name: string;
|
|
9
|
+
/**
|
|
10
|
+
* The base directory where HTML files and static assets are served.
|
|
11
|
+
*/
|
|
12
|
+
baseDir: string;
|
|
13
|
+
/**
|
|
14
|
+
* The output directory for builds.
|
|
15
|
+
*/
|
|
16
|
+
outDir: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A breadcrumb (name and path) for a given page.
|
|
21
|
+
*/
|
|
22
|
+
type Breadcrumb = {
|
|
23
|
+
/**
|
|
24
|
+
* The name of the page.
|
|
25
|
+
*/
|
|
26
|
+
name: string;
|
|
27
|
+
/**
|
|
28
|
+
* The full URL path to the page.
|
|
29
|
+
*/
|
|
30
|
+
pathname: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Metadata about the current page being rendered.
|
|
34
|
+
*/
|
|
35
|
+
type Meta = {
|
|
36
|
+
/**
|
|
37
|
+
* The configuration object from `thunderous.config.ts`.
|
|
38
|
+
*/
|
|
39
|
+
config: ThunderousConfig;
|
|
40
|
+
/**
|
|
41
|
+
* The pathname of the current page being rendered.
|
|
42
|
+
*/
|
|
43
|
+
pathname: string;
|
|
44
|
+
/**
|
|
45
|
+
* The breadcrumbs of the current page being rendered.
|
|
46
|
+
*/
|
|
47
|
+
breadcrumbs: Breadcrumb[];
|
|
48
|
+
/**
|
|
49
|
+
* The inferred title of the current page being rendered.
|
|
50
|
+
*
|
|
51
|
+
* This is derived from the filename, replacing hyphens and underscores
|
|
52
|
+
* with spaces, and capitalizing the first letter of each word.
|
|
53
|
+
*/
|
|
54
|
+
title: string;
|
|
55
|
+
/**
|
|
56
|
+
* The name of the current page being rendered.
|
|
57
|
+
*
|
|
58
|
+
* This is the filename without the extension.
|
|
59
|
+
*/
|
|
60
|
+
name: string;
|
|
61
|
+
/**
|
|
62
|
+
* The filename of the current page being rendered.
|
|
63
|
+
*
|
|
64
|
+
* This is the full filename including the extension.
|
|
65
|
+
*/
|
|
66
|
+
filename: string;
|
|
67
|
+
};
|
|
68
|
+
/** Get metadata about the current page being rendered. */
|
|
69
|
+
declare const getMeta: () => Meta;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Escape a string for safe insertion into HTML.
|
|
73
|
+
*/
|
|
74
|
+
declare const escapeHtml: (str: string) => string;
|
|
75
|
+
declare class RawHtml {
|
|
76
|
+
value: string;
|
|
77
|
+
constructor(value: string);
|
|
78
|
+
toString(): string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Wrap a string so it bypasses automatic HTML escaping in `<script expr>`.
|
|
82
|
+
*/
|
|
83
|
+
declare const raw: (str: string) => RawHtml;
|
|
84
|
+
|
|
85
|
+
export { escapeHtml, getMeta, raw };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The type for the default export from `thunderous.config.ts`.
|
|
3
|
+
*/
|
|
4
|
+
type ThunderousConfig = {
|
|
5
|
+
/**
|
|
6
|
+
* The name of the site.
|
|
7
|
+
*/
|
|
8
|
+
name: string;
|
|
9
|
+
/**
|
|
10
|
+
* The base directory where HTML files and static assets are served.
|
|
11
|
+
*/
|
|
12
|
+
baseDir: string;
|
|
13
|
+
/**
|
|
14
|
+
* The output directory for builds.
|
|
15
|
+
*/
|
|
16
|
+
outDir: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A breadcrumb (name and path) for a given page.
|
|
21
|
+
*/
|
|
22
|
+
type Breadcrumb = {
|
|
23
|
+
/**
|
|
24
|
+
* The name of the page.
|
|
25
|
+
*/
|
|
26
|
+
name: string;
|
|
27
|
+
/**
|
|
28
|
+
* The full URL path to the page.
|
|
29
|
+
*/
|
|
30
|
+
pathname: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Metadata about the current page being rendered.
|
|
34
|
+
*/
|
|
35
|
+
type Meta = {
|
|
36
|
+
/**
|
|
37
|
+
* The configuration object from `thunderous.config.ts`.
|
|
38
|
+
*/
|
|
39
|
+
config: ThunderousConfig;
|
|
40
|
+
/**
|
|
41
|
+
* The pathname of the current page being rendered.
|
|
42
|
+
*/
|
|
43
|
+
pathname: string;
|
|
44
|
+
/**
|
|
45
|
+
* The breadcrumbs of the current page being rendered.
|
|
46
|
+
*/
|
|
47
|
+
breadcrumbs: Breadcrumb[];
|
|
48
|
+
/**
|
|
49
|
+
* The inferred title of the current page being rendered.
|
|
50
|
+
*
|
|
51
|
+
* This is derived from the filename, replacing hyphens and underscores
|
|
52
|
+
* with spaces, and capitalizing the first letter of each word.
|
|
53
|
+
*/
|
|
54
|
+
title: string;
|
|
55
|
+
/**
|
|
56
|
+
* The name of the current page being rendered.
|
|
57
|
+
*
|
|
58
|
+
* This is the filename without the extension.
|
|
59
|
+
*/
|
|
60
|
+
name: string;
|
|
61
|
+
/**
|
|
62
|
+
* The filename of the current page being rendered.
|
|
63
|
+
*
|
|
64
|
+
* This is the full filename including the extension.
|
|
65
|
+
*/
|
|
66
|
+
filename: string;
|
|
67
|
+
};
|
|
68
|
+
/** Get metadata about the current page being rendered. */
|
|
69
|
+
declare const getMeta: () => Meta;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Escape a string for safe insertion into HTML.
|
|
73
|
+
*/
|
|
74
|
+
declare const escapeHtml: (str: string) => string;
|
|
75
|
+
declare class RawHtml {
|
|
76
|
+
value: string;
|
|
77
|
+
constructor(value: string);
|
|
78
|
+
toString(): string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Wrap a string so it bypasses automatic HTML escaping in `<script expr>`.
|
|
82
|
+
*/
|
|
83
|
+
declare const raw: (str: string) => RawHtml;
|
|
84
|
+
|
|
85
|
+
export { escapeHtml, getMeta, raw };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/meta.ts
|
|
2
|
+
var metaState = {
|
|
3
|
+
config: {
|
|
4
|
+
name: "",
|
|
5
|
+
baseDir: "",
|
|
6
|
+
outDir: ""
|
|
7
|
+
},
|
|
8
|
+
pathname: "/",
|
|
9
|
+
breadcrumbs: [],
|
|
10
|
+
title: "",
|
|
11
|
+
name: "",
|
|
12
|
+
filename: ""
|
|
13
|
+
};
|
|
14
|
+
var getMeta = () => {
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
...metaState,
|
|
17
|
+
config: { ...metaState.config },
|
|
18
|
+
breadcrumbs: [...metaState.breadcrumbs]
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/utilities.ts
|
|
23
|
+
var escapeHtml = (str) => {
|
|
24
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
25
|
+
};
|
|
26
|
+
var RawHtml = class {
|
|
27
|
+
value;
|
|
28
|
+
constructor(value) {
|
|
29
|
+
this.value = value;
|
|
30
|
+
}
|
|
31
|
+
toString() {
|
|
32
|
+
return this.value;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var raw = (str) => new RawHtml(str);
|
|
36
|
+
export {
|
|
37
|
+
escapeHtml,
|
|
38
|
+
getMeta,
|
|
39
|
+
raw
|
|
40
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "thunderous-server",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "A simple server to enhance Thunderous components with SSR.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Jonathan DeWitt <jon.dewitt@thunder.solutions>",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"bin": {
|
|
12
|
+
"thunderous": "./bin/thunderous"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"/dist",
|
|
16
|
+
"/bin",
|
|
17
|
+
"/src",
|
|
18
|
+
"/types",
|
|
19
|
+
"/package.json",
|
|
20
|
+
"/README.md"
|
|
21
|
+
],
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@eslint/css": "^0.13.0",
|
|
24
|
+
"@eslint/js": "^9.38.0",
|
|
25
|
+
"@eslint/json": "^0.13.2",
|
|
26
|
+
"@total-typescript/ts-reset": "^0.6.1",
|
|
27
|
+
"@types/connect-livereload": "^0.6.3",
|
|
28
|
+
"@types/express": "^5.0.3",
|
|
29
|
+
"@types/livereload": "^0.9.5",
|
|
30
|
+
"@types/node": "^24.9.1",
|
|
31
|
+
"@types/resolve": "^1.20.6",
|
|
32
|
+
"@typescript-eslint/eslint-plugin": "^8.46.2",
|
|
33
|
+
"connect-livereload": "^0.6.1",
|
|
34
|
+
"eslint": "^9.38.0",
|
|
35
|
+
"livereload": "^0.10.3",
|
|
36
|
+
"prettier": "^3.6.2",
|
|
37
|
+
"serve": "^14.2.5",
|
|
38
|
+
"tsx": "^4.20.6",
|
|
39
|
+
"typescript": "^5.9.3",
|
|
40
|
+
"typescript-eslint": "^8.46.2",
|
|
41
|
+
"undici-types": "^7.16.0"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"es-module-lexer": "^1.7.0",
|
|
45
|
+
"express": "^5.1.0",
|
|
46
|
+
"resolve": "^1.22.11",
|
|
47
|
+
"resolve.exports": "^2.0.3"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"thunderous": ">=2.4.1"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup --no-clean",
|
|
54
|
+
"demo": "cd demo && npm run",
|
|
55
|
+
"demo:install": "cd demo && npm install",
|
|
56
|
+
"lint": "eslint .",
|
|
57
|
+
"lint:fix": "eslint . --fix",
|
|
58
|
+
"format": "prettier --check . --ignore-path ../../.gitignore",
|
|
59
|
+
"format:fix": "prettier --write . --ignore-path ../../.gitignore",
|
|
60
|
+
"typecheck": "tsc --noEmit",
|
|
61
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/build.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { cpSync, existsSync, rmSync, writeFileSync, readFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join, resolve, dirname, relative } from 'path';
|
|
3
|
+
import {
|
|
4
|
+
bootstrapThunderous,
|
|
5
|
+
processFiles,
|
|
6
|
+
generateStaticTemplate,
|
|
7
|
+
transpileTsFile,
|
|
8
|
+
generateImportMap,
|
|
9
|
+
injectImportMap,
|
|
10
|
+
} from './generate';
|
|
11
|
+
import { config } from './config';
|
|
12
|
+
|
|
13
|
+
/** Make the directory if it does not exist. Not worth installing fs-extra just for this. */
|
|
14
|
+
export const ensureDirSync = (dirPath: string) => {
|
|
15
|
+
if (!existsSync(dirPath)) {
|
|
16
|
+
mkdirSync(dirPath, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build the static site.
|
|
22
|
+
*/
|
|
23
|
+
export const build = () => {
|
|
24
|
+
console.log('\x1b[36m🔨 Building static site...\x1b[0m\n');
|
|
25
|
+
|
|
26
|
+
const baseDir = resolve(config.baseDir);
|
|
27
|
+
const outDir = resolve(config.outDir);
|
|
28
|
+
|
|
29
|
+
if (!existsSync(baseDir)) {
|
|
30
|
+
console.error(`\x1b[31mError: Base directory "${baseDir}" does not exist.\x1b[0m`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Clean and create output directory
|
|
35
|
+
if (existsSync(outDir)) {
|
|
36
|
+
rmSync(outDir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
ensureDirSync(outDir);
|
|
39
|
+
|
|
40
|
+
// Copy all files (excluding build files which we'll process separately)
|
|
41
|
+
console.log(`\x1b[90mCopying static assets...\x1b[0m`);
|
|
42
|
+
|
|
43
|
+
cpSync(baseDir, outDir, {
|
|
44
|
+
recursive: true,
|
|
45
|
+
filter: (src) => !/\.server\.(ts|js)$/.test(src),
|
|
46
|
+
});
|
|
47
|
+
bootstrapThunderous();
|
|
48
|
+
|
|
49
|
+
// Transpile TypeScript files FIRST and collect entry points
|
|
50
|
+
console.log(`\x1b[90mTranspiling TypeScript files...\x1b[0m`);
|
|
51
|
+
const entryFiles: string[] = [];
|
|
52
|
+
processFiles({
|
|
53
|
+
dir: outDir,
|
|
54
|
+
filter: (filePath) => /(?<!\.d|\.tmp|\.server)\.ts$/.test(filePath),
|
|
55
|
+
callback: (filePath) => {
|
|
56
|
+
const relPath = relative(outDir, filePath);
|
|
57
|
+
console.log(`\x1b[90mTranspiling: ${relPath}\x1b[0m`);
|
|
58
|
+
try {
|
|
59
|
+
const jsPath = transpileTsFile(filePath);
|
|
60
|
+
entryFiles.push(jsPath);
|
|
61
|
+
console.log(`\x1b[32m✓ Transpiled: ${relPath} → ${relative(outDir, jsPath)}\x1b[0m`);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(`\x1b[31m✗ Error transpiling ${filePath}:\x1b[0m`, error);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Process HTML files AFTER transpilation
|
|
69
|
+
console.log(`\x1b[90mProcessing HTML files...\x1b[0m`);
|
|
70
|
+
const htmlEntryFiles: string[] = [];
|
|
71
|
+
const cleanupFunctions: Array<() => void> = [];
|
|
72
|
+
const errors: string[] = [];
|
|
73
|
+
processFiles({
|
|
74
|
+
dir: baseDir,
|
|
75
|
+
filter: (filePath) => filePath.endsWith('.html') && !filePath.split('/').pop()!.startsWith('_'),
|
|
76
|
+
callback: (filePath) => {
|
|
77
|
+
console.log(`\x1b[90mProcessing: ${relative(baseDir, filePath)}\x1b[0m`);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const result = generateStaticTemplate(filePath);
|
|
81
|
+
const markup = result.markup;
|
|
82
|
+
|
|
83
|
+
// Collect client entry files from this HTML
|
|
84
|
+
htmlEntryFiles.push(...result.clientEntryFiles);
|
|
85
|
+
cleanupFunctions.push(result.cleanup);
|
|
86
|
+
|
|
87
|
+
// Calculate the output path
|
|
88
|
+
const relativePath = relative(baseDir, filePath);
|
|
89
|
+
const outputPath = join(outDir, relativePath);
|
|
90
|
+
|
|
91
|
+
// Ensure output directory exists
|
|
92
|
+
ensureDirSync(dirname(outputPath));
|
|
93
|
+
|
|
94
|
+
// Write the processed HTML
|
|
95
|
+
writeFileSync(outputPath, markup, 'utf-8');
|
|
96
|
+
|
|
97
|
+
console.log(`\x1b[32m✓ Generated: ${relativePath}\x1b[0m`);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error(`\x1b[31m✗ Error processing ${filePath}:\x1b[0m`, error);
|
|
100
|
+
errors.push(filePath);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Vendorize node modules based on collected entry files
|
|
106
|
+
const allEntryFiles = [...htmlEntryFiles, ...entryFiles];
|
|
107
|
+
if (allEntryFiles.length > 0) {
|
|
108
|
+
const importMapJson = generateImportMap(allEntryFiles);
|
|
109
|
+
|
|
110
|
+
// Inject import map into HTML files
|
|
111
|
+
processFiles({
|
|
112
|
+
dir: outDir,
|
|
113
|
+
filter: (filePath) => filePath.endsWith('.html'),
|
|
114
|
+
callback: (filePath) => {
|
|
115
|
+
let html = readFileSync(filePath, 'utf-8');
|
|
116
|
+
html = injectImportMap(html, importMapJson);
|
|
117
|
+
writeFileSync(filePath, html, 'utf-8');
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
console.log(`\x1b[32m✓ Import map injected into HTML files\x1b[0m`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Clean up temporary files from HTML processing
|
|
124
|
+
for (const cleanup of cleanupFunctions) {
|
|
125
|
+
cleanup();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (errors.length > 0) {
|
|
129
|
+
console.error(`\n\x1b[31m❌ Build failed with ${errors.length} error(s).\x1b[0m`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(`\n\x1b[32m✅ Static site built successfully in "${outDir}"\x1b[0m`);
|
|
134
|
+
};
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { build } from './build';
|
|
2
|
+
import { dev } from './dev';
|
|
3
|
+
|
|
4
|
+
const args = process.argv.slice(2);
|
|
5
|
+
|
|
6
|
+
if (args[0] === 'dev') {
|
|
7
|
+
try {
|
|
8
|
+
dev();
|
|
9
|
+
} catch (error) {
|
|
10
|
+
console.error('Failed to start server:', error);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
} else if (args[0] === 'build') {
|
|
14
|
+
try {
|
|
15
|
+
build();
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error('\x1b[31mBuild failed:\x1b[0m', error);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
} else {
|
|
21
|
+
console.log('Usage:');
|
|
22
|
+
console.log(' --dev Start the development server');
|
|
23
|
+
console.log(' --build Build the static site');
|
|
24
|
+
}
|