xpine 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.d.ts +1 -0
- package/.gitattributes +2 -0
- package/README.md +39 -0
- package/base-path.mjs +5 -0
- package/index.mjs +6 -0
- package/modules/html/.d.ts +1 -0
- package/modules/html/index.js +28 -0
- package/modules/html/jsx-runtime.d.ts +1 -0
- package/modules/html/jsx-runtime.js +3 -0
- package/modules/html/package.json +13 -0
- package/package.json +26 -0
- package/src/auth.mjs +39 -0
- package/src/build/dev-server.mjs +9 -0
- package/src/build/spa.mjs +175 -0
- package/src/build/typescript-builder.mjs +170 -0
- package/src/build.mjs +270 -0
- package/src/express.mjs +117 -0
- package/src/runDevServer.mjs +71 -0
- package/src/scripts/build.mjs +5 -0
- package/src/scripts/run-dev.mjs +5 -0
- package/src/util/env.mjs +7 -0
- package/src/util/get-config.mjs +61 -0
- package/src/util/require.mjs +5 -0
- package/tsconfig.json +49 -0
package/.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module 'xpine';
|
package/.gitattributes
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# XPine - Alpine.js + JSX + Tailwind framework
|
|
2
|
+
|
|
3
|
+
Combines JSX with Alpine.js for a simpler, easier development experience.
|
|
4
|
+
|
|
5
|
+
## Get started
|
|
6
|
+
|
|
7
|
+
Add an xpine.config.mjs file to your root directory
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
export default {}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## API
|
|
14
|
+
|
|
15
|
+
### Build
|
|
16
|
+
|
|
17
|
+
`import { buildApp } from 'xpine';`
|
|
18
|
+
|
|
19
|
+
- Used for building production app
|
|
20
|
+
|
|
21
|
+
## Auth
|
|
22
|
+
|
|
23
|
+
`import { signUser, verifyUser } from 'xpine';`
|
|
24
|
+
|
|
25
|
+
## Dev server
|
|
26
|
+
|
|
27
|
+
`import { runDevServer } from 'xpine';`
|
|
28
|
+
|
|
29
|
+
## Config
|
|
30
|
+
|
|
31
|
+
`import { config } from 'xpine';`
|
|
32
|
+
|
|
33
|
+
## Env
|
|
34
|
+
|
|
35
|
+
`import { setupEnv } from 'xpine';`
|
|
36
|
+
|
|
37
|
+
## Router
|
|
38
|
+
|
|
39
|
+
`import { createXPineRouter } from 'xpine';`
|
package/base-path.mjs
ADDED
package/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module 'html';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export default class HTML {
|
|
2
|
+
|
|
3
|
+
static attributeObjectToString(props) {
|
|
4
|
+
if (!props) return '';
|
|
5
|
+
return Object.entries(props)
|
|
6
|
+
.filter(([, value]) => value !== null && value !== undefined)
|
|
7
|
+
.map(([key, value], index) => {
|
|
8
|
+
const start = index === 0 ? ' ' : '';
|
|
9
|
+
return `${start}${key}="${value}"`;
|
|
10
|
+
})
|
|
11
|
+
.join(' ');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static async fragment(props) {
|
|
15
|
+
const childrenResult = await Promise.all(props.children.flat());
|
|
16
|
+
return childrenResult.filter(Boolean).join('');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static async createElement(type, props, ...children) {
|
|
20
|
+
const childrenResult = await Promise.all(children.flat());
|
|
21
|
+
// Handle passing in another element
|
|
22
|
+
if (typeof type === 'function') {
|
|
23
|
+
const result = await type({ ...props, children: childrenResult, });
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
return `<${type}${this.attributeObjectToString(props)}>${childrenResult.filter(Boolean).join('')}</${type}>`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module 'html/jsx-runtime';
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xpine",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "index.mjs",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@tailwindcss/postcss": "^4.0.8",
|
|
7
|
+
"builtin-modules": "^4.0.0",
|
|
8
|
+
"chokidar": "^4.0.3",
|
|
9
|
+
"dotenv": "^16.4.7",
|
|
10
|
+
"esbuild": "^0.25.0",
|
|
11
|
+
"express": "^4.21.2",
|
|
12
|
+
"express-ws": "^5.0.2",
|
|
13
|
+
"fs-extra": "^11.3.0",
|
|
14
|
+
"glob": "^11.0.1",
|
|
15
|
+
"html": "file:./modules/html",
|
|
16
|
+
"jsonwebtoken": "^9.0.2",
|
|
17
|
+
"postcss": "^8.5.3",
|
|
18
|
+
"request-ip": "^3.3.0",
|
|
19
|
+
"tsx": "^4.19.3",
|
|
20
|
+
"typescript": "^5.7.3"
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"xpine-build": "./src/scripts/build.mjs"
|
|
24
|
+
},
|
|
25
|
+
"type": "module"
|
|
26
|
+
}
|
package/src/auth.mjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import jsonwebtoken from 'jsonwebtoken';
|
|
2
|
+
const { verify, sign, } = jsonwebtoken;
|
|
3
|
+
|
|
4
|
+
export async function signUser(email, username) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
sign(
|
|
7
|
+
{
|
|
8
|
+
user: {
|
|
9
|
+
email,
|
|
10
|
+
username,
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
process.env.JWT_PRIVATE_KEY,
|
|
15
|
+
{ expiresIn: '30d', },
|
|
16
|
+
(err, token) => {
|
|
17
|
+
if (err) reject(err);
|
|
18
|
+
resolve(token);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function verifyUser(token) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
// @ts-ignore
|
|
26
|
+
verify(token, process.env.JWT_PRIVATE_KEY, (err, authorizedData) => {
|
|
27
|
+
if (err) reject(err);
|
|
28
|
+
resolve(authorizedData);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getTokenFromRequest(req) {
|
|
34
|
+
// @ts-ignore
|
|
35
|
+
const { authorization, } = req.headers;
|
|
36
|
+
if (!authorization) return null;
|
|
37
|
+
const token = authorization.split(' ').pop() || '';
|
|
38
|
+
return token || null;
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const socketProtocol = 'ws:';
|
|
2
|
+
const echoSocketUrl = socketProtocol + '//' + window.location.hostname + ':3001/dev-server/';
|
|
3
|
+
const socket = new WebSocket(echoSocketUrl);
|
|
4
|
+
|
|
5
|
+
socket.addEventListener('message', async (msg) => {
|
|
6
|
+
if (msg.data === 'refresh:client') {
|
|
7
|
+
window.location.reload();
|
|
8
|
+
}
|
|
9
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Enables SPA like behavior for links
|
|
2
|
+
|
|
3
|
+
async function replaceDocumentContentsWithLinkResponse() {
|
|
4
|
+
const links = [...document.querySelectorAll('[data-spa]')];
|
|
5
|
+
for (const link of links) {
|
|
6
|
+
link.addEventListener('click', updatePageOnLinkClick);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function updatePageOnLinkClick(e) {
|
|
12
|
+
const targetHref = e?.target?.getAttribute('href');
|
|
13
|
+
if (!targetHref) return;
|
|
14
|
+
if (isExternalURL(targetHref) && !e?.target?.getAttribute('data-spa-crossorigin')) return;
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
try {
|
|
17
|
+
await getNewPageContent(targetHref);
|
|
18
|
+
if (targetHref === window.location.pathname) return;
|
|
19
|
+
window.history.pushState(
|
|
20
|
+
{
|
|
21
|
+
type: 'link-click',
|
|
22
|
+
targetHref,
|
|
23
|
+
},
|
|
24
|
+
'',
|
|
25
|
+
targetHref
|
|
26
|
+
);
|
|
27
|
+
const event = new CustomEvent('spa:updatePageURL', {
|
|
28
|
+
detail: {
|
|
29
|
+
href: targetHref,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
window.dispatchEvent(event);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error(err);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getNewPageContent(href) {
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(href);
|
|
41
|
+
const text = await res.text();
|
|
42
|
+
const parser = new DOMParser();
|
|
43
|
+
const dom = parser.parseFromString(text, 'text/html');
|
|
44
|
+
diffHead(dom);
|
|
45
|
+
const newScripts = removeBodyScripts(dom);
|
|
46
|
+
const clonedPersistentNodes = getPersistentNodesFromDocument(dom);
|
|
47
|
+
const body = dom.body.innerHTML;
|
|
48
|
+
document.body.innerHTML = body;
|
|
49
|
+
for (const clonedNode of clonedPersistentNodes) {
|
|
50
|
+
// @ts-ignore
|
|
51
|
+
const newNode = document.body.querySelector(`[data-persistent="${clonedNode?.getAttribute('data-persistent')}"]`);
|
|
52
|
+
newNode.replaceWith(clonedNode);
|
|
53
|
+
}
|
|
54
|
+
replaceAttributesOnDocumentBody(dom);
|
|
55
|
+
replaceDocumentContentsWithLinkResponse();
|
|
56
|
+
for (const script of newScripts) document.body.appendChild(script);
|
|
57
|
+
// Send an event
|
|
58
|
+
const event = new CustomEvent('spa:updatePageContent', {
|
|
59
|
+
detail: {
|
|
60
|
+
href,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
window.dispatchEvent(event);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(err);
|
|
66
|
+
console.error('Error getting link', href);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function diffHead(dom) {
|
|
71
|
+
const domHeadChildren = [...dom.head.children];
|
|
72
|
+
const currentHeadChildren = [...document.head.children];
|
|
73
|
+
const result = {
|
|
74
|
+
childrenToAdd: [],
|
|
75
|
+
childrenToRemove: [],
|
|
76
|
+
};
|
|
77
|
+
for (const domChild of domHeadChildren) {
|
|
78
|
+
const foundInCurrentChildren = currentHeadChildren.find(child => child.outerHTML === domChild.outerHTML);
|
|
79
|
+
if (!foundInCurrentChildren) result.childrenToAdd.push(domChild);
|
|
80
|
+
}
|
|
81
|
+
for (const currentChild of currentHeadChildren) {
|
|
82
|
+
const foundInDomChildren = domHeadChildren.find(child => child.outerHTML === currentChild.outerHTML);
|
|
83
|
+
if (!foundInDomChildren) result.childrenToRemove.push(currentChild);
|
|
84
|
+
}
|
|
85
|
+
applyHeadDiffsToDocument(result);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function applyHeadDiffsToDocument(diff) {
|
|
89
|
+
for (const child of diff.childrenToRemove) {
|
|
90
|
+
// Remove from head
|
|
91
|
+
document.head.removeChild(child);
|
|
92
|
+
}
|
|
93
|
+
for (const child of diff.childrenToAdd) {
|
|
94
|
+
// Handle script tags
|
|
95
|
+
if (child?.nodeName?.toLowerCase() == 'script') {
|
|
96
|
+
const scriptElement = document.createElement('script');
|
|
97
|
+
portAttributesToElement(child, scriptElement);
|
|
98
|
+
scriptElement.innerHTML = child.innerHTML;
|
|
99
|
+
document.head.appendChild(scriptElement);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// Add to the head
|
|
103
|
+
document.head.appendChild(child);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function removeBodyScripts(dom) {
|
|
108
|
+
const scripts = [...dom.body.querySelectorAll('script')];
|
|
109
|
+
const newElements = [];
|
|
110
|
+
for (const script of scripts) {
|
|
111
|
+
const element = document.createElement('script');
|
|
112
|
+
portAttributesToElement(script, element);
|
|
113
|
+
element.innerHTML = script.innerHTML;
|
|
114
|
+
dom.body.removeChild(script);
|
|
115
|
+
newElements.push(element);
|
|
116
|
+
}
|
|
117
|
+
return newElements;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function portAttributesToElement(oldEl, newEl) {
|
|
121
|
+
for (const attribute of oldEl.attributes) {
|
|
122
|
+
newEl.setAttribute(attribute.name, attribute.nodeValue);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getPersistentNodesFromDocument(dom) {
|
|
127
|
+
// Find persistent nodes
|
|
128
|
+
const persistentNodesFromDom = [...dom.querySelectorAll('[data-persistent]')].map(el => el.getAttribute('data-persistent'));
|
|
129
|
+
const persistentNodesFromBody = [...document.body.querySelectorAll('[data-persistent]')];
|
|
130
|
+
// Cross match the persistent nodes between current page and new page to find valid ones
|
|
131
|
+
const validPersistentNodes = persistentNodesFromBody.filter(el => {
|
|
132
|
+
return persistentNodesFromDom.includes(el.getAttribute('data-persistent'));
|
|
133
|
+
});
|
|
134
|
+
// Clone
|
|
135
|
+
return validPersistentNodes.map(el => el.cloneNode(true));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function replaceAttributesOnDocumentBody(dom) {
|
|
139
|
+
while (document.body.attributes.length > 0) {
|
|
140
|
+
document.body.removeAttribute(document.body.attributes[0].name);
|
|
141
|
+
}
|
|
142
|
+
// Set attributes on the body
|
|
143
|
+
for (const attribute of dom.body.attributes) {
|
|
144
|
+
document.body.setAttribute(attribute.name, attribute.nodeValue);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function handleBackButton() {
|
|
149
|
+
await getNewPageContent(window.history.state?.targetHref || window.location.href);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isRelativeURL(url) {
|
|
153
|
+
if (typeof url !== 'string') return false;
|
|
154
|
+
try {
|
|
155
|
+
new URL(url);
|
|
156
|
+
return false;
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (url?.startsWith('//')) return false;
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isExternalURL(url) {
|
|
164
|
+
if (isRelativeURL(url)) return false;
|
|
165
|
+
try {
|
|
166
|
+
const computedURL = new URL(url);
|
|
167
|
+
return computedURL?.origin !== window.location?.origin;
|
|
168
|
+
} catch {
|
|
169
|
+
return false; // Not a URL
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
window.addEventListener('DOMContentLoaded', replaceDocumentContentsWithLinkResponse);
|
|
174
|
+
window.addEventListener('popstate', handleBackButton);
|
|
175
|
+
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// node arg should initially be the source node
|
|
7
|
+
export function getImportDeclarationNodes(node, sourceFile, importDeclarations = []) {
|
|
8
|
+
if (node.kind === ts.SyntaxKind.ImportDeclaration) {
|
|
9
|
+
let importPath = null;
|
|
10
|
+
node.forEachChild(child => {
|
|
11
|
+
if (child.kind === ts.SyntaxKind.StringLiteral) {
|
|
12
|
+
importPath = child.getText(sourceFile).replace(/[\"\']/g, '');
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
importDeclarations.push({
|
|
16
|
+
node,
|
|
17
|
+
importPath,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
node.forEachChild(child => {
|
|
21
|
+
getImportDeclarationNodes(child, sourceFile, importDeclarations);
|
|
22
|
+
});
|
|
23
|
+
return importDeclarations;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function stripImportsNodesFromFile(importsToStrip, content, importDeclarations) {
|
|
27
|
+
let outputContent = content;
|
|
28
|
+
const filteredImports = importDeclarations.filter(item => importsToStrip.includes(item.importPath));
|
|
29
|
+
for (const importItem of filteredImports) {
|
|
30
|
+
outputContent = outputContent.slice(0, importItem.node.pos) + outputContent.slice(importItem.node.end);
|
|
31
|
+
}
|
|
32
|
+
return outputContent;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function findDataAttributesAndFunctions(
|
|
36
|
+
node,
|
|
37
|
+
sourceFile,
|
|
38
|
+
foundDataAttributes = [],
|
|
39
|
+
foundFunctions = []
|
|
40
|
+
) {
|
|
41
|
+
if (node.kind === ts.SyntaxKind.JsxAttribute) {
|
|
42
|
+
const attribute = getAttributeValuePair(node, sourceFile);
|
|
43
|
+
if (attribute?.[0] === 'x-data' && attribute?.[1]) {
|
|
44
|
+
// We found a data attribute,
|
|
45
|
+
// so now we have to search throughout the file for a function declaration of the same name
|
|
46
|
+
foundDataAttributes.push(attribute[1].replace(/[^a-zA-Z0-9]/g, ''));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (node.kind === ts.SyntaxKind.SourceFile) {
|
|
50
|
+
node.forEachChild(child => {
|
|
51
|
+
if ([ts.SyntaxKind.FunctionDeclaration, ts.SyntaxKind.VariableDeclaration].includes(child.kind)) {
|
|
52
|
+
const functions = getFunctionDeclarationValue(child, sourceFile);
|
|
53
|
+
foundFunctions.push(functions);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
node.forEachChild(child => {
|
|
59
|
+
findDataAttributesAndFunctions(child, sourceFile, foundDataAttributes, foundFunctions);
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
foundDataAttributes,
|
|
63
|
+
foundFunctions,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getAttributeValuePair(node, sourceFile) {
|
|
68
|
+
const result = [];
|
|
69
|
+
node.forEachChild(child => {
|
|
70
|
+
result.push(child.getText(sourceFile));
|
|
71
|
+
});
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getFunctionDeclarationValue(node, sourceFile) {
|
|
76
|
+
const result = {
|
|
77
|
+
name: null,
|
|
78
|
+
isDefaultExport: false,
|
|
79
|
+
hasExport: false,
|
|
80
|
+
};
|
|
81
|
+
node.forEachChild(child => {
|
|
82
|
+
if (child.kind === ts.SyntaxKind.ExportKeyword) {
|
|
83
|
+
result.hasExport = true;
|
|
84
|
+
}
|
|
85
|
+
if (child.kind === ts.SyntaxKind.DefaultKeyword) {
|
|
86
|
+
result.isDefaultExport = true;
|
|
87
|
+
}
|
|
88
|
+
if (child.kind === ts.SyntaxKind.Identifier) {
|
|
89
|
+
result.name = child.getText(sourceFile);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Convert entry point files into one big bundle file
|
|
96
|
+
export function convertEntryPointsToSingleFile(entryPoints, tempWritePath) {
|
|
97
|
+
fs.writeFileSync(
|
|
98
|
+
tempWritePath,
|
|
99
|
+
entryPoints
|
|
100
|
+
.map(entry => `import "${entry}"`)
|
|
101
|
+
.join(';\n')
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function removeClientScriptInTSXFile(pathName) {
|
|
106
|
+
const content = fs.readFileSync(pathName, 'utf-8');
|
|
107
|
+
const source = ts.createSourceFile(
|
|
108
|
+
pathName,
|
|
109
|
+
content,
|
|
110
|
+
ts.ScriptTarget.Latest
|
|
111
|
+
);
|
|
112
|
+
let toRemoveFrom;
|
|
113
|
+
let clientDataStart;
|
|
114
|
+
const clientImportsToHoist = [];
|
|
115
|
+
const clientImportsToReplace = [];
|
|
116
|
+
source.forEachChild((child) => {
|
|
117
|
+
if (child.kind === ts.SyntaxKind.ExpressionStatement) {
|
|
118
|
+
const text = child.getText(source);
|
|
119
|
+
// Remove from here
|
|
120
|
+
if (text.startsWith('<script')) {
|
|
121
|
+
toRemoveFrom = child.pos;
|
|
122
|
+
clientDataStart = child.end;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Hoist the client imports to above the <script /> tag
|
|
126
|
+
if (child.kind === ts.SyntaxKind.ImportDeclaration && child.pos >= clientDataStart) {
|
|
127
|
+
const text = child.getText(source);
|
|
128
|
+
child.forEachChild(child => {
|
|
129
|
+
if (child.kind === ts.SyntaxKind.StringLiteral) {
|
|
130
|
+
// Keep track of client imports needed for path replacement to be absolute
|
|
131
|
+
const text = child.getText(source);
|
|
132
|
+
const importPath = text.replace(/[\"\']/g, '');
|
|
133
|
+
const isRelativeImport = importPath.startsWith('.') || importPath.startsWith('/');
|
|
134
|
+
if (!isRelativeImport) return;
|
|
135
|
+
clientImportsToReplace.push({
|
|
136
|
+
old: importPath,
|
|
137
|
+
new: path.join(path.dirname(pathName), importPath),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
clientImportsToHoist.push(text);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
let clientContent = !isNaN(clientDataStart) ? content.slice(clientDataStart) : '';
|
|
145
|
+
for (const clientImport of clientImportsToReplace) {
|
|
146
|
+
clientContent = clientContent.replace(clientImport.old, clientImport.new);
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
content: !isNaN(toRemoveFrom) ?
|
|
150
|
+
clientImportsToHoist.join('\n') + content.slice(0, toRemoveFrom) :
|
|
151
|
+
content,
|
|
152
|
+
clientContent,
|
|
153
|
+
fullContent: content,
|
|
154
|
+
toRemoveFrom,
|
|
155
|
+
clientDataStart,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function printRecursiveFrom(
|
|
160
|
+
node, indentLevel, sourceFile
|
|
161
|
+
) {
|
|
162
|
+
const indentation = '-'.repeat(indentLevel);
|
|
163
|
+
const syntaxKind = ts.SyntaxKind[node.kind];
|
|
164
|
+
const nodeText = node.getText(sourceFile);
|
|
165
|
+
console.log(`${indentation}${syntaxKind}: ${nodeText}`);
|
|
166
|
+
|
|
167
|
+
node.forEachChild(child =>
|
|
168
|
+
printRecursiveFrom(child, indentLevel + 1, sourceFile)
|
|
169
|
+
);
|
|
170
|
+
}
|
package/src/build.mjs
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { build } from 'esbuild';
|
|
4
|
+
import builtinModules from 'builtin-modules';
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
import {
|
|
7
|
+
convertEntryPointsToSingleFile,
|
|
8
|
+
findDataAttributesAndFunctions,
|
|
9
|
+
removeClientScriptInTSXFile
|
|
10
|
+
} from './build/typescript-builder.mjs';
|
|
11
|
+
import { globSync } from 'glob';
|
|
12
|
+
import postcss from 'postcss';
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
import tailwindPostcss from '@tailwindcss/postcss';
|
|
15
|
+
import { config } from './util/get-config.mjs';
|
|
16
|
+
import basePath from '../base-path.mjs';
|
|
17
|
+
|
|
18
|
+
// Extensions to look for in the bundle
|
|
19
|
+
const extensions = ['.ts', '.tsx'];
|
|
20
|
+
const packageJson = JSON.parse(fs.readFileSync(config.packageJsonPath, 'utf-8'));
|
|
21
|
+
const allPackages = Object.keys(packageJson.devDependencies).concat(Object.keys(packageJson.dependencies));
|
|
22
|
+
const allPackagesIncludingNode = allPackages.concat(builtinModules);
|
|
23
|
+
|
|
24
|
+
export async function buildApp(isDev = false) {
|
|
25
|
+
try {
|
|
26
|
+
const srcDirFiles = globSync(config.srcDir + '/**/*.{js,ts,tsx,jsx}');
|
|
27
|
+
const { componentData, dataFiles, } = await buildAppFiles(srcDirFiles, isDev);
|
|
28
|
+
const alpineDataFile = await buildAlpineDataFile(componentData, dataFiles);
|
|
29
|
+
await buildClientSideFiles([alpineDataFile], isDev);
|
|
30
|
+
fs.removeSync(config.distTempFolder);
|
|
31
|
+
await buildCSS();
|
|
32
|
+
await buildPublicFolderSymlinks();
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error('Build failed');
|
|
35
|
+
console.error(err);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function buildAppFiles(files, isDev) {
|
|
40
|
+
const componentData = [];
|
|
41
|
+
const dataFiles = [];
|
|
42
|
+
// Filter out client side files and files that aren't of the allowed extensions
|
|
43
|
+
const backendFiles = files
|
|
44
|
+
.filter((file) => extensions.find(ext => file.endsWith(ext)))
|
|
45
|
+
.filter((file) => !file.startsWith(config.publicDir));
|
|
46
|
+
fs.ensureDirSync(config.distDir);
|
|
47
|
+
// Build backend/SSR TSX modules
|
|
48
|
+
await build({
|
|
49
|
+
entryPoints: backendFiles,
|
|
50
|
+
format: 'esm',
|
|
51
|
+
platform: 'node',
|
|
52
|
+
outdir: config.distDir,
|
|
53
|
+
bundle: true,
|
|
54
|
+
sourcemap: isDev ? 'inline' : false,
|
|
55
|
+
external: allPackages,
|
|
56
|
+
jsx: 'transform',
|
|
57
|
+
minify: !isDev,
|
|
58
|
+
plugins: [
|
|
59
|
+
{
|
|
60
|
+
name: 'add-dot-js',
|
|
61
|
+
setup(build) {
|
|
62
|
+
build.onResolve({ filter: /.*/, }, args => {
|
|
63
|
+
const hasAtSign = args.path.startsWith('@');
|
|
64
|
+
const isPackage = hasAtSign ?
|
|
65
|
+
allPackagesIncludingNode.includes(args.path) :
|
|
66
|
+
allPackagesIncludingNode.includes(args.path.split('/').shift());
|
|
67
|
+
if (args.importer && !isPackage) {
|
|
68
|
+
// If we're doing an index import we need /index.js
|
|
69
|
+
const calculatedDir = path.join(args.resolveDir, args.path);
|
|
70
|
+
let existsAsFile = false;
|
|
71
|
+
for (const extension of extensions) {
|
|
72
|
+
const asFile = calculatedDir + extension;
|
|
73
|
+
const exists = fs.existsSync(asFile);
|
|
74
|
+
if (exists) existsAsFile = true;
|
|
75
|
+
}
|
|
76
|
+
let outputPath = args.path + (existsAsFile ? '' : '/index') + '.js';
|
|
77
|
+
outputPath = args.path.endsWith('.js') || args.path.endsWith('.mjs') ? args.path : outputPath;
|
|
78
|
+
return { path: outputPath, external: true, };
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'insert-html-banner-and-remove-client-scripts',
|
|
85
|
+
setup(build) {
|
|
86
|
+
build.onLoad({ filter: /.tsx/, }, args => {
|
|
87
|
+
const cleanedContent = removeClientScriptInTSXFile(args.path);
|
|
88
|
+
const htmlImportStart = 'import html from \'html\';\n';
|
|
89
|
+
const newContent = `${htmlImportStart}${cleanedContent.content}`;
|
|
90
|
+
componentData.push({
|
|
91
|
+
...args,
|
|
92
|
+
contents: `${htmlImportStart}${cleanedContent.fullContent}`,
|
|
93
|
+
clientContent: cleanedContent.clientContent,
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
contents: newContent,
|
|
97
|
+
loader: 'tsx',
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'get-data-files',
|
|
104
|
+
setup(build) {
|
|
105
|
+
build.onLoad({ filter: /\.data\.(js|mjs|ts)$/, }, args => {
|
|
106
|
+
const contents = fs.readFileSync(args.path, 'utf-8');
|
|
107
|
+
dataFiles.push({
|
|
108
|
+
...args,
|
|
109
|
+
contents,
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
contents,
|
|
113
|
+
loader: 'ts',
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
await logSize(config.distDir, 'app');
|
|
121
|
+
return {
|
|
122
|
+
componentData,
|
|
123
|
+
dataFiles,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Build client side files
|
|
128
|
+
async function buildClientSideFiles(alpineDataFiles = [], isDev) {
|
|
129
|
+
// Write the temp file to use
|
|
130
|
+
const tempFilePath = path.join(config.distTempFolder, './app.ts');
|
|
131
|
+
fs.ensureFileSync(tempFilePath);
|
|
132
|
+
// Get all ts/js files in public folder but ignore the pages scripts
|
|
133
|
+
const pagesScriptsGlob = config.publicDir + '/scripts/pages/**/*.{js,ts}';
|
|
134
|
+
const clientFiles = globSync(
|
|
135
|
+
config.publicDir + '/**/*.{js,ts}',
|
|
136
|
+
{
|
|
137
|
+
ignore: pagesScriptsGlob,
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
// Filter out all public/pages files
|
|
141
|
+
convertEntryPointsToSingleFile(alpineDataFiles.concat(clientFiles), tempFilePath);
|
|
142
|
+
// Write dev server code to the temp file path
|
|
143
|
+
if (isDev) writeDevServerClientSideCode(tempFilePath);
|
|
144
|
+
// Write spa functionality to the temp file path
|
|
145
|
+
writeSpaClientSideCode(tempFilePath);
|
|
146
|
+
await build({
|
|
147
|
+
entryPoints: [tempFilePath],
|
|
148
|
+
bundle: true,
|
|
149
|
+
outdir: config.distPublicScriptsDir,
|
|
150
|
+
minify: !isDev,
|
|
151
|
+
sourcemap: isDev ? 'inline' : false,
|
|
152
|
+
});
|
|
153
|
+
// Build pages files scripts
|
|
154
|
+
const pagesFiles = globSync(pagesScriptsGlob);
|
|
155
|
+
await build({
|
|
156
|
+
entryPoints: pagesFiles || [],
|
|
157
|
+
bundle: true,
|
|
158
|
+
outdir: config.distPublicScriptsDir + '/pages',
|
|
159
|
+
minify: !isDev,
|
|
160
|
+
sourcemap: isDev ? 'inline' : false,
|
|
161
|
+
});
|
|
162
|
+
await logSize(config.distPublicDir, 'client');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function writeDevServerClientSideCode(tempFilePath) {
|
|
166
|
+
const devServerPath = path.join(basePath, './src/build/dev-server.mjs');
|
|
167
|
+
const content = fs.readFileSync(devServerPath, 'utf-8');
|
|
168
|
+
fs.appendFileSync(tempFilePath, `\n` + content);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function writeSpaClientSideCode(tempFilePath) {
|
|
172
|
+
const spaPath = path.join(basePath, './src/build/spa.mjs');
|
|
173
|
+
const content = fs.readFileSync(spaPath, 'utf-8');
|
|
174
|
+
fs.appendFileSync(tempFilePath, `\n` + content);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function buildAlpineDataFile(componentData, dataFiles) {
|
|
178
|
+
const output = {
|
|
179
|
+
imports: [
|
|
180
|
+
'import Alpine from \'alpinejs\';'
|
|
181
|
+
],
|
|
182
|
+
code: [],
|
|
183
|
+
end: [
|
|
184
|
+
'window.Alpine = Alpine;'
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
const dataFunctionResults = {
|
|
188
|
+
foundDataAttributes: [],
|
|
189
|
+
foundFunctions: [],
|
|
190
|
+
foundImports: [],
|
|
191
|
+
};
|
|
192
|
+
const componentsAndDataFiles = componentData.concat(dataFiles);
|
|
193
|
+
for (const component of componentsAndDataFiles) {
|
|
194
|
+
// Single source file
|
|
195
|
+
const sourceFile = ts.createSourceFile(
|
|
196
|
+
component.path,
|
|
197
|
+
component.contents,
|
|
198
|
+
ts.ScriptTarget.Latest
|
|
199
|
+
);
|
|
200
|
+
const dataFunctionResult = findDataAttributesAndFunctions(sourceFile, sourceFile);
|
|
201
|
+
dataFunctionResults.foundDataAttributes.push(...dataFunctionResult.foundDataAttributes);
|
|
202
|
+
const foundFunctionsWithPath = dataFunctionResult.foundFunctions.map(item => {
|
|
203
|
+
return {
|
|
204
|
+
...item,
|
|
205
|
+
path: component.path,
|
|
206
|
+
content: component.clientContent,
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
dataFunctionResults.foundFunctions.push(...foundFunctionsWithPath);
|
|
210
|
+
}
|
|
211
|
+
const validDataFunctions = dataFunctionResults.foundFunctions.filter(item => {
|
|
212
|
+
return dataFunctionResults.foundDataAttributes.includes(item.name) && item.content;
|
|
213
|
+
});
|
|
214
|
+
for (const dataFunction of validDataFunctions) {
|
|
215
|
+
if (!dataFunction.hasExport) continue;
|
|
216
|
+
output.code.push(dataFunction.content);
|
|
217
|
+
output.code.push(`Alpine.data('${dataFunction.name}', ${dataFunction.name});`);
|
|
218
|
+
}
|
|
219
|
+
const result = output.imports.join('\n') + '\n' + output.code.join('\n') + '\n' + output.end.join('\n');
|
|
220
|
+
fs.ensureFileSync(config.alpineDataPath);
|
|
221
|
+
fs.writeFileSync(config.alpineDataPath, result);
|
|
222
|
+
return config.alpineDataPath;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function buildCSS() {
|
|
226
|
+
const cssFiles = globSync(config.srcDir + '/**/*.css');
|
|
227
|
+
for (const file of cssFiles) {
|
|
228
|
+
const fileContents = fs.readFileSync(file, 'utf-8');
|
|
229
|
+
const result = await postcss([tailwindPostcss()]).process(fileContents, { from: file, });
|
|
230
|
+
// Write to dist folder
|
|
231
|
+
const newPath = file.replace(config.srcDir, config.distDir);
|
|
232
|
+
fs.ensureFileSync(newPath);
|
|
233
|
+
fs.writeFileSync(newPath, result.css);
|
|
234
|
+
}
|
|
235
|
+
logSize(config.distPublicDir, 'css');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// We need to symlink the non CSS or JS files from the src/public folder into the dist folder
|
|
239
|
+
export async function buildPublicFolderSymlinks() {
|
|
240
|
+
const files = globSync(config.publicDir + '/**/*.*', {
|
|
241
|
+
ignore: '/**/*.{css,js,ts,tsx,jsx}',
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Create symlinks in dist directory
|
|
245
|
+
for (const file of files) {
|
|
246
|
+
try {
|
|
247
|
+
const newPath = file.replace(config.srcDir, config.distDir);
|
|
248
|
+
const splitNewPath = newPath.split('/');
|
|
249
|
+
splitNewPath.pop();
|
|
250
|
+
const newDir = splitNewPath.join('/');
|
|
251
|
+
fs.ensureDirSync(newDir);
|
|
252
|
+
fs.symlinkSync(file, newPath);
|
|
253
|
+
} catch { }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function logSize(pathName, type, validExtensions = ['.js', '.css']) {
|
|
258
|
+
const files = globSync(pathName + '/**/*' + (type === 'css' ? '.css' : ''));
|
|
259
|
+
const fileSizes = files.map((file) => {
|
|
260
|
+
if (!validExtensions.find(ext => file.endsWith(ext))) return false;
|
|
261
|
+
return {
|
|
262
|
+
file,
|
|
263
|
+
size: (fs.statSync(file).size) / (1024 * 1000),
|
|
264
|
+
};
|
|
265
|
+
}).filter(Boolean);
|
|
266
|
+
const totalSize = fileSizes.reduce((total, current) => {
|
|
267
|
+
return current.size + total;
|
|
268
|
+
}, 0);
|
|
269
|
+
console.info(`[${totalSize.toFixed(3)} MB] Built ${type}`);
|
|
270
|
+
}
|
package/src/express.mjs
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { globSync } from 'glob';
|
|
3
|
+
import { config } from './util/get-config.mjs';
|
|
4
|
+
import { verifyUser } from './auth.mjs';
|
|
5
|
+
import require from './util/require.mjs';
|
|
6
|
+
import requestIP from 'request-ip';
|
|
7
|
+
|
|
8
|
+
const doctypeHTML = '<!DOCTYPE html>';
|
|
9
|
+
|
|
10
|
+
export async function createRouter() {
|
|
11
|
+
const methods = ['get', 'post', 'put', 'patch', 'delete'];
|
|
12
|
+
const router = express.Router();
|
|
13
|
+
const routes = globSync(config.pagesDir + '/**/*.{tsx,ts}');
|
|
14
|
+
const routeMap = routes.map(route => {
|
|
15
|
+
return {
|
|
16
|
+
route: route.split(config.pagesDir).pop().replace('.tsx', '').replace('.js', '').replace('.ts', '').replace('/index', '/'),
|
|
17
|
+
path: route.replace(config.srcDir, config.distDir).replace('.tsx', '.js').replace('.ts', '.js'),
|
|
18
|
+
originalRoute: route,
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
const routeResults = [];
|
|
22
|
+
|
|
23
|
+
for (const route of routeMap) {
|
|
24
|
+
const isJSX = route.originalRoute.endsWith('.tsx') || route.originalRoute.endsWith('.jsx');
|
|
25
|
+
// Import route
|
|
26
|
+
const routeItem = process.env.NODE_ENV === 'development' ? null : require(route.path).default;
|
|
27
|
+
// Configure result,methods for the route
|
|
28
|
+
const slugRoute = route.route.toLowerCase().replace(/[ ]/g, '');
|
|
29
|
+
const foundMethod = methods.find(method => slugRoute.endsWith(`.${method}`));
|
|
30
|
+
const isDynamicRoute = slugRoute.match(/\[(.*)\]/g);
|
|
31
|
+
let formattedRouteItem = slugRoute;
|
|
32
|
+
if (foundMethod) formattedRouteItem = formattedRouteItem.split('.').shift();
|
|
33
|
+
// Handle dynamic routing
|
|
34
|
+
if (isDynamicRoute) {
|
|
35
|
+
const result = [...formattedRouteItem.matchAll(/(\[)(.*?)(\])/g)];
|
|
36
|
+
for (const match of result) {
|
|
37
|
+
formattedRouteItem = formattedRouteItem.replace(match[0], ':' + match[2]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Push to the route results array
|
|
41
|
+
routeResults.push({
|
|
42
|
+
formattedRouteItem,
|
|
43
|
+
foundMethod,
|
|
44
|
+
route,
|
|
45
|
+
});
|
|
46
|
+
router[foundMethod || 'get'](formattedRouteItem, async (req, res) => {
|
|
47
|
+
try {
|
|
48
|
+
// Check if it's a string response from the routeItem or is a different response
|
|
49
|
+
if (routeItem) {
|
|
50
|
+
if (isJSX) {
|
|
51
|
+
res.send(doctypeHTML + (await routeItem(req, res)));
|
|
52
|
+
} else {
|
|
53
|
+
await routeItem(req, res);
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const defaultRouteImport = require(route.path).default;
|
|
58
|
+
// Require every time only if in development mode
|
|
59
|
+
if (isJSX) {
|
|
60
|
+
res.send(doctypeHTML + (await defaultRouteImport(req, res)));
|
|
61
|
+
} else {
|
|
62
|
+
await defaultRouteImport(req, res);
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(err);
|
|
66
|
+
res.status(err?.status || 500).send(err?.message || 'Error');
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
router,
|
|
72
|
+
routeResults,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function verifyUserMiddleware(req, _res, next) {
|
|
77
|
+
// @ts-ignore
|
|
78
|
+
const { usertoken, } = req.cookies;
|
|
79
|
+
if (!usertoken) {
|
|
80
|
+
req.user = null;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const { user, } = await verifyUser(usertoken);
|
|
84
|
+
req.user = user;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
req.user = null;
|
|
87
|
+
}
|
|
88
|
+
next();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function createXPineRouter(app, beforeErrorRoute) {
|
|
92
|
+
app.use(express.static(config.distPublicDir));
|
|
93
|
+
app.use(verifyUserMiddleware);
|
|
94
|
+
app.use(requestIP.mw());
|
|
95
|
+
|
|
96
|
+
const { router, routeResults, } = await createRouter();
|
|
97
|
+
app.use(function replaceableRouter(req, res, next) {
|
|
98
|
+
router(req, res, next);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const found404 = routeResults?.find(item => item?.formattedRouteItem === '/404');
|
|
102
|
+
const import404 = found404 ? require(found404.route.path).default : null;
|
|
103
|
+
|
|
104
|
+
if (beforeErrorRoute) beforeErrorRoute(app);
|
|
105
|
+
|
|
106
|
+
// error handler
|
|
107
|
+
app.use(async function (req, res) {
|
|
108
|
+
// render the error page
|
|
109
|
+
res.status(404);
|
|
110
|
+
|
|
111
|
+
if (import404) {
|
|
112
|
+
res.send(doctypeHTML + (await import404(req, res)));
|
|
113
|
+
} else {
|
|
114
|
+
res.send('Error');
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import expressWs from 'express-ws';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import EventEmitter from 'events';
|
|
5
|
+
import chokidar from 'chokidar';
|
|
6
|
+
import { buildApp } from './build.mjs';
|
|
7
|
+
import { globSync } from 'glob';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { config } from './util/get-config.mjs';
|
|
10
|
+
import require from './util/require.mjs';
|
|
11
|
+
|
|
12
|
+
export async function runDevServer() {
|
|
13
|
+
const startServer = require(config.serverAppPath);
|
|
14
|
+
|
|
15
|
+
// Initial server set up
|
|
16
|
+
await buildApp(true);
|
|
17
|
+
let appServer = await startServer.default();
|
|
18
|
+
|
|
19
|
+
// Watch files
|
|
20
|
+
const watcher = chokidar.watch(config.srcDir, {
|
|
21
|
+
ignoreInitial: true,
|
|
22
|
+
// Ignore map and prisma files
|
|
23
|
+
ignored: (pathName) => pathName.endsWith('.map') || pathName.startsWith(path.join(config.serverDir, './prisma')),
|
|
24
|
+
});
|
|
25
|
+
watcher.on('all', async (event, path) => {
|
|
26
|
+
const shouldReloadServer = (path.startsWith(config.serverDir) && !path.startsWith(config.runDir)) ||
|
|
27
|
+
['add', 'unlink'].includes(event) && path.startsWith(config.pagesDir);
|
|
28
|
+
if (shouldReloadServer) {
|
|
29
|
+
// We modified files in the server, restart the server
|
|
30
|
+
appServer.server.close();
|
|
31
|
+
await buildApp(true);
|
|
32
|
+
// Clear requires for startServer, then restart the server
|
|
33
|
+
delete require.cache[config.serverAppPath];
|
|
34
|
+
const startServer = require(config.serverAppPath);
|
|
35
|
+
appServer = await startServer.default();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// Clear all import cache for components
|
|
39
|
+
const files = globSync(config.distDir + '/**/*.{tsx,ts,js,jsx}', {
|
|
40
|
+
ignore: [config.serverDistDir + '/**/*.*'],
|
|
41
|
+
});
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
delete require.cache[file];
|
|
44
|
+
}
|
|
45
|
+
await buildApp(true);
|
|
46
|
+
refreshEmitter.emit('refresh');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Create web socket sever
|
|
50
|
+
const wsApp = express();
|
|
51
|
+
const wsServer = http.createServer(wsApp);
|
|
52
|
+
expressWs(wsApp, wsServer);
|
|
53
|
+
class RefreshEmitter extends EventEmitter { };
|
|
54
|
+
const refreshEmitter = new RefreshEmitter();
|
|
55
|
+
|
|
56
|
+
// @ts-ignore
|
|
57
|
+
wsApp.ws('/dev-server', function (ws) {
|
|
58
|
+
refreshEmitter.removeAllListeners();
|
|
59
|
+
refreshEmitter.on('refresh', () => {
|
|
60
|
+
ws.send('refresh:client');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const wsPort = 3001;
|
|
65
|
+
wsServer.listen(wsPort);
|
|
66
|
+
wsServer.on('listening', () => {
|
|
67
|
+
console.info(`Dev server listening on port ${wsPort}`);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
package/src/util/env.mjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import require from './require.mjs';
|
|
3
|
+
|
|
4
|
+
const rootDir = process.cwd();
|
|
5
|
+
|
|
6
|
+
export function fromRoot(pathName) {
|
|
7
|
+
if (!pathName) return rootDir;
|
|
8
|
+
return path.join(rootDir, pathName);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const userConfig = require(path.join(process.cwd(), './xpine.config.mjs')).default;
|
|
12
|
+
|
|
13
|
+
const configDefaults = {
|
|
14
|
+
rootDir: fromRoot(),
|
|
15
|
+
srcDir: fromRoot('./src'),
|
|
16
|
+
distDir: fromRoot('./dist'),
|
|
17
|
+
packageJsonPath: fromRoot('package.json'),
|
|
18
|
+
// We need to use getters here in the event someone wants to change folders, such as the dist folder
|
|
19
|
+
get distPublicDir() {
|
|
20
|
+
return path.join(this.distDir, './public');
|
|
21
|
+
},
|
|
22
|
+
get distPublicScriptsDir() {
|
|
23
|
+
return path.join(this.distPublicDir, './scripts');
|
|
24
|
+
},
|
|
25
|
+
get distTempFolder() {
|
|
26
|
+
return path.join(this.distDir, './temp');
|
|
27
|
+
},
|
|
28
|
+
get clientJSBundlePath() {
|
|
29
|
+
return path.join(this.distPublicScriptsDir, './app.js');
|
|
30
|
+
},
|
|
31
|
+
get alpineDataPath() {
|
|
32
|
+
return path.join(this.distTempFolder, './alpine-data.ts');
|
|
33
|
+
},
|
|
34
|
+
get serverDistDir() {
|
|
35
|
+
return path.join(this.distDir, './server');
|
|
36
|
+
},
|
|
37
|
+
// Important dirs/paths
|
|
38
|
+
get pagesDir() {
|
|
39
|
+
return path.join(this.srcDir, './pages');
|
|
40
|
+
},
|
|
41
|
+
get publicDir() {
|
|
42
|
+
return path.join(this.srcDir, './public');
|
|
43
|
+
},
|
|
44
|
+
get serverDir() {
|
|
45
|
+
return path.join(this.srcDir, './server');
|
|
46
|
+
},
|
|
47
|
+
get runDir() {
|
|
48
|
+
return path.join(this.serverDir, './run');
|
|
49
|
+
},
|
|
50
|
+
get serverAppPath() {
|
|
51
|
+
return path.join(this.serverDir, './app.ts');
|
|
52
|
+
},
|
|
53
|
+
get globalCSSFile() {
|
|
54
|
+
return path.join(this.publicDir, './styles/global.css');
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const config = {
|
|
59
|
+
...configDefaults,
|
|
60
|
+
...userConfig,
|
|
61
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"jsx": "preserve",
|
|
4
|
+
"jsxFragmentFactory": "html.fragment",
|
|
5
|
+
"jsxFactory": "html.createElement",
|
|
6
|
+
"lib": [
|
|
7
|
+
"dom",
|
|
8
|
+
"dom.iterable",
|
|
9
|
+
"esnext"
|
|
10
|
+
],
|
|
11
|
+
"allowJs": false,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"strict": false,
|
|
14
|
+
"forceConsistentCasingInFileNames": false,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
"esModuleInterop": true,
|
|
17
|
+
"target": "esnext",
|
|
18
|
+
"module": "ESNext",
|
|
19
|
+
"moduleResolution": "node",
|
|
20
|
+
"resolveJsonModule": true,
|
|
21
|
+
"isolatedModules": true,
|
|
22
|
+
"jsxImportSource": "html",
|
|
23
|
+
"incremental": true,
|
|
24
|
+
"allowImportingTsExtensions": true,
|
|
25
|
+
},
|
|
26
|
+
"include": [
|
|
27
|
+
"**/*.ts",
|
|
28
|
+
"**/*.tsx",
|
|
29
|
+
"postcss.config.mjs",
|
|
30
|
+
"tailwind.config.js",
|
|
31
|
+
"eslint.config.mjs",
|
|
32
|
+
"index.mjs",
|
|
33
|
+
"src/runDevServer.mjs",
|
|
34
|
+
"src/express.mjs",
|
|
35
|
+
"src/build.mjs",
|
|
36
|
+
"src/auth.mjs",
|
|
37
|
+
"src/util/require.mjs",
|
|
38
|
+
"src/util/get-config.mjs",
|
|
39
|
+
"src/util/env.mjs",
|
|
40
|
+
"src/scripts/run-dev.mjs",
|
|
41
|
+
"src/scripts/build.mjs",
|
|
42
|
+
"src/build/typescript-builder.mjs"
|
|
43
|
+
],
|
|
44
|
+
"exclude": [
|
|
45
|
+
"node_modules",
|
|
46
|
+
"dist-frontend",
|
|
47
|
+
"**/*.js"
|
|
48
|
+
],
|
|
49
|
+
}
|