@harpy-js/core 0.4.7
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/README.md +326 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +53 -0
- package/dist/client/Link.d.ts +5 -0
- package/dist/client/Link.js +62 -0
- package/dist/client/__tests__/getActiveItemId.test.d.ts +1 -0
- package/dist/client/__tests__/getActiveItemId.test.js +38 -0
- package/dist/client/getActiveItemId.d.ts +7 -0
- package/dist/client/getActiveItemId.js +55 -0
- package/dist/client/use-i18n.d.ts +7 -0
- package/dist/client/use-i18n.js +64 -0
- package/dist/core/__tests__/component-analyzer.test.d.ts +1 -0
- package/dist/core/__tests__/component-analyzer.test.js +151 -0
- package/dist/core/__tests__/hydration-manifest.test.d.ts +1 -0
- package/dist/core/__tests__/hydration-manifest.test.js +211 -0
- package/dist/core/__tests__/jsx.engine.test.d.ts +1 -0
- package/dist/core/__tests__/jsx.engine.test.js +118 -0
- package/dist/core/app-setup.d.ts +7 -0
- package/dist/core/app-setup.js +79 -0
- package/dist/core/auto-register.module.d.ts +9 -0
- package/dist/core/auto-register.module.js +18 -0
- package/dist/core/auto-wrap-middleware.d.ts +4 -0
- package/dist/core/auto-wrap-middleware.js +130 -0
- package/dist/core/client-component-wrapper.d.ts +5 -0
- package/dist/core/client-component-wrapper.js +37 -0
- package/dist/core/client-hydration.d.ts +2 -0
- package/dist/core/client-hydration.js +93 -0
- package/dist/core/client-wrapper-browser.d.ts +2 -0
- package/dist/core/client-wrapper-browser.js +22 -0
- package/dist/core/component-analyzer.d.ts +4 -0
- package/dist/core/component-analyzer.js +98 -0
- package/dist/core/component-auto-wrapper.d.ts +2 -0
- package/dist/core/component-auto-wrapper.js +63 -0
- package/dist/core/component-client-wrapper.d.ts +4 -0
- package/dist/core/component-client-wrapper.js +80 -0
- package/dist/core/hydration-generator.d.ts +2 -0
- package/dist/core/hydration-generator.js +98 -0
- package/dist/core/hydration-manifest.d.ts +7 -0
- package/dist/core/hydration-manifest.js +83 -0
- package/dist/core/hydration.d.ts +16 -0
- package/dist/core/hydration.js +72 -0
- package/dist/core/jsx.engine.d.ts +9 -0
- package/dist/core/jsx.engine.js +161 -0
- package/dist/core/live-reload-client.js +32 -0
- package/dist/core/live-reload.controller.d.ts +10 -0
- package/dist/core/live-reload.controller.js +38 -0
- package/dist/core/navigation.service.d.ts +18 -0
- package/dist/core/navigation.service.js +206 -0
- package/dist/core/router.module.d.ts +2 -0
- package/dist/core/router.module.js +21 -0
- package/dist/core/static-assets.controller.d.ts +4 -0
- package/dist/core/static-assets.controller.js +51 -0
- package/dist/core/types/nav.types.d.ts +22 -0
- package/dist/core/types/nav.types.js +2 -0
- package/dist/core/views/layout.d.ts +8 -0
- package/dist/core/views/layout.js +35 -0
- package/dist/decorators/jsx.decorator.d.ts +26 -0
- package/dist/decorators/jsx.decorator.js +10 -0
- package/dist/decorators/layout.decorator.d.ts +4 -0
- package/dist/decorators/layout.decorator.js +29 -0
- package/dist/i18n/__tests__/i18n.helper.test.d.ts +1 -0
- package/dist/i18n/__tests__/i18n.helper.test.js +105 -0
- package/dist/i18n/__tests__/i18n.interceptor.test.d.ts +1 -0
- package/dist/i18n/__tests__/i18n.interceptor.test.js +195 -0
- package/dist/i18n/__tests__/i18n.module.test.d.ts +1 -0
- package/dist/i18n/__tests__/i18n.module.test.js +83 -0
- package/dist/i18n/__tests__/i18n.service.test.d.ts +1 -0
- package/dist/i18n/__tests__/i18n.service.test.js +109 -0
- package/dist/i18n/__tests__/t.test.d.ts +1 -0
- package/dist/i18n/__tests__/t.test.js +66 -0
- package/dist/i18n/i18n-module.options.d.ts +10 -0
- package/dist/i18n/i18n-module.options.js +4 -0
- package/dist/i18n/i18n-switcher.controller.d.ts +12 -0
- package/dist/i18n/i18n-switcher.controller.js +80 -0
- package/dist/i18n/i18n-types.d.ts +8 -0
- package/dist/i18n/i18n-types.js +2 -0
- package/dist/i18n/i18n.helper.d.ts +14 -0
- package/dist/i18n/i18n.helper.js +70 -0
- package/dist/i18n/i18n.interceptor.d.ts +9 -0
- package/dist/i18n/i18n.interceptor.js +99 -0
- package/dist/i18n/i18n.module.d.ts +5 -0
- package/dist/i18n/i18n.module.js +51 -0
- package/dist/i18n/i18n.service.d.ts +12 -0
- package/dist/i18n/i18n.service.js +61 -0
- package/dist/i18n/index.d.ts +10 -0
- package/dist/i18n/index.js +20 -0
- package/dist/i18n/locale.decorator.d.ts +1 -0
- package/dist/i18n/locale.decorator.js +8 -0
- package/dist/i18n/t.d.ts +3 -0
- package/dist/i18n/t.js +16 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +40 -0
- package/package.json +79 -0
- package/scripts/analyze-styles.ts +124 -0
- package/scripts/auto-wrap-exports.ts +239 -0
- package/scripts/build-css.ts +38 -0
- package/scripts/build-hydration.ts +313 -0
- package/scripts/build-page-styles.ts +43 -0
- package/scripts/copy-assets.ts +34 -0
- package/scripts/dev.sh +3 -0
- package/scripts/dev.ts +257 -0
- package/src/cli.ts +71 -0
- package/src/client/Link.tsx +62 -0
- package/src/client/__tests__/getActiveItemId.test.ts +49 -0
- package/src/client/getActiveItemId.ts +54 -0
- package/src/client/use-i18n.ts +111 -0
- package/src/core/__tests__/component-analyzer.test.ts +141 -0
- package/src/core/__tests__/hydration-manifest.test.ts +223 -0
- package/src/core/__tests__/jsx.engine.test.ts +137 -0
- package/src/core/app-setup.ts +114 -0
- package/src/core/auto-register.module.ts +30 -0
- package/src/core/auto-wrap-middleware.ts +165 -0
- package/src/core/client-component-wrapper.ts +72 -0
- package/src/core/client-hydration.tsx +99 -0
- package/src/core/client-wrapper-browser.ts +40 -0
- package/src/core/component-analyzer.ts +89 -0
- package/src/core/component-auto-wrapper.ts +68 -0
- package/src/core/component-client-wrapper.ts +112 -0
- package/src/core/hydration-generator.ts +94 -0
- package/src/core/hydration-manifest.ts +79 -0
- package/src/core/hydration.ts +70 -0
- package/src/core/jsx.engine.ts +205 -0
- package/src/core/live-reload-client.js +32 -0
- package/src/core/live-reload.controller.ts +55 -0
- package/src/core/navigation.service.ts +257 -0
- package/src/core/router.module.ts +9 -0
- package/src/core/static-assets.controller.ts +19 -0
- package/src/core/types/nav.types.ts +53 -0
- package/src/core/views/layout.tsx +61 -0
- package/src/decorators/jsx.decorator.ts +49 -0
- package/src/decorators/layout.decorator.ts +66 -0
- package/src/i18n/__tests__/i18n.helper.test.ts +126 -0
- package/src/i18n/__tests__/i18n.interceptor.test.ts +229 -0
- package/src/i18n/__tests__/i18n.module.test.ts +98 -0
- package/src/i18n/__tests__/i18n.service.test.ts +129 -0
- package/src/i18n/__tests__/t.test.ts +88 -0
- package/src/i18n/i18n-module.options.ts +53 -0
- package/src/i18n/i18n-switcher.controller.ts +99 -0
- package/src/i18n/i18n-types.ts +56 -0
- package/src/i18n/i18n.helper.ts +75 -0
- package/src/i18n/i18n.interceptor.ts +114 -0
- package/src/i18n/i18n.module.ts +45 -0
- package/src/i18n/i18n.service.ts +95 -0
- package/src/i18n/index.ts +37 -0
- package/src/i18n/locale.decorator.ts +10 -0
- package/src/i18n/t.ts +62 -0
- package/src/index.ts +31 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build script to:
|
|
5
|
+
* 1. Analyze components for 'use client' directive
|
|
6
|
+
* 2. Generate separate hydration entry files
|
|
7
|
+
* 3. Bundle them with esbuild to dist/chunks with cache-busted names
|
|
8
|
+
* 4. Create a manifest file for server-side lookup
|
|
9
|
+
* 5. Create shared vendor bundle for React/ReactDOM to eliminate duplication
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execSync } from "child_process";
|
|
13
|
+
import * as crypto from "crypto";
|
|
14
|
+
import * as fs from "fs";
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
|
|
17
|
+
const PROJECT_ROOT = process.cwd();
|
|
18
|
+
const SRC_DIR = path.join(PROJECT_ROOT, "src");
|
|
19
|
+
const DIST_DIR = path.join(PROJECT_ROOT, "dist");
|
|
20
|
+
const HYDRATION_ENTRIES_DIR = path.join(SRC_DIR, ".hydration-entries");
|
|
21
|
+
const CHUNKS_DIR = path.join(DIST_DIR, "chunks");
|
|
22
|
+
// Write manifest directly to dist (no need for temp since this runs after nest build)
|
|
23
|
+
const MANIFEST_FILE = path.join(DIST_DIR, "hydration-manifest.json");
|
|
24
|
+
const VENDOR_BUNDLE = "vendor.js";
|
|
25
|
+
|
|
26
|
+
// Check if running in production mode (add cache-busting hashes only in production)
|
|
27
|
+
const IS_PRODUCTION = process.argv.includes("--prod");
|
|
28
|
+
|
|
29
|
+
interface ComponentFile {
|
|
30
|
+
filePath: string;
|
|
31
|
+
componentName: string;
|
|
32
|
+
isNamedExport: boolean; // Track if it's a named export
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface HydrationManifest {
|
|
36
|
+
[componentName: string]: string; // componentName -> chunkFileName
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Find all files with 'use client' directive
|
|
41
|
+
*/
|
|
42
|
+
function findClientComponents(): ComponentFile[] {
|
|
43
|
+
const components: ComponentFile[] = [];
|
|
44
|
+
const extensions = [".ts", ".tsx"];
|
|
45
|
+
|
|
46
|
+
function walkDir(dir: string) {
|
|
47
|
+
if (!fs.existsSync(dir)) return;
|
|
48
|
+
|
|
49
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
50
|
+
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
const fullPath = path.join(dir, entry.name);
|
|
53
|
+
|
|
54
|
+
if (entry.isDirectory()) {
|
|
55
|
+
if (!entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
56
|
+
walkDir(fullPath);
|
|
57
|
+
}
|
|
58
|
+
} else if (extensions.includes(path.extname(entry.name))) {
|
|
59
|
+
try {
|
|
60
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
61
|
+
|
|
62
|
+
// Check for 'use client' directive at the start of the file
|
|
63
|
+
if (/^['"]use client['"]/.test(content.trim())) {
|
|
64
|
+
const fileName = path.basename(fullPath, path.extname(fullPath));
|
|
65
|
+
// Convert kebab-case to PascalCase
|
|
66
|
+
const componentName = fileName
|
|
67
|
+
.split("-")
|
|
68
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
69
|
+
.join("");
|
|
70
|
+
|
|
71
|
+
// Detect if it's a named export by checking for patterns like:
|
|
72
|
+
// export function ComponentName or export const ComponentName
|
|
73
|
+
const hasNamedExport = new RegExp(
|
|
74
|
+
`export\\s+(function|const|class)\\s+${componentName}\\b`,
|
|
75
|
+
).test(content);
|
|
76
|
+
|
|
77
|
+
components.push({
|
|
78
|
+
filePath: fullPath,
|
|
79
|
+
componentName,
|
|
80
|
+
isNamedExport: hasNamedExport,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error(`Error reading file ${fullPath}:`, error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
walkDir(SRC_DIR);
|
|
91
|
+
return components;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Generate hydration entry file for a client component
|
|
96
|
+
* Uses window.React from vendor bundle
|
|
97
|
+
*/
|
|
98
|
+
function generateHydrationEntry(component: ComponentFile): string {
|
|
99
|
+
const relativePath = path.relative(HYDRATION_ENTRIES_DIR, component.filePath);
|
|
100
|
+
const importPath = `./${relativePath.replace(/\\\\/g, "/")}`;
|
|
101
|
+
|
|
102
|
+
// Generate appropriate import statement based on export type
|
|
103
|
+
const importStatement = component.isNamedExport
|
|
104
|
+
? `import { ${component.componentName} } from '${importPath}';`
|
|
105
|
+
: `import ${component.componentName} from '${importPath}';`;
|
|
106
|
+
|
|
107
|
+
const content = `
|
|
108
|
+
// Use React from vendor bundle
|
|
109
|
+
const React = window.React;
|
|
110
|
+
const { hydrateRoot } = window.ReactDOM;
|
|
111
|
+
|
|
112
|
+
${importStatement}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Auto-generated hydration entry for ${component.componentName}
|
|
116
|
+
*/
|
|
117
|
+
|
|
118
|
+
// Find all hydration containers for this component
|
|
119
|
+
const containers = document.querySelectorAll('[id^="hydrate-${component.componentName}"]');
|
|
120
|
+
|
|
121
|
+
containers.forEach((container) => {
|
|
122
|
+
const containerElement = container as HTMLElement;
|
|
123
|
+
const propsElement = document.getElementById(\`\${container.id}-props\`);
|
|
124
|
+
|
|
125
|
+
let props = {};
|
|
126
|
+
if (propsElement) {
|
|
127
|
+
try {
|
|
128
|
+
props = JSON.parse(propsElement.textContent || '{}');
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error('Error parsing props:', error);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
hydrateRoot(containerElement, React.createElement(${component.componentName}, props));
|
|
136
|
+
console.log('[Hydration] Hydrated ${component.componentName}');
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('[Hydration] Failed to hydrate ${component.componentName}:', error);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
`.trim();
|
|
142
|
+
|
|
143
|
+
return content;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Generate cache-busted filename (production only)
|
|
148
|
+
* In dev mode, uses consistent filenames so the browser doesn't re-download
|
|
149
|
+
*/
|
|
150
|
+
function generateChunkFilename(componentName: string): string {
|
|
151
|
+
if (IS_PRODUCTION) {
|
|
152
|
+
// Production: use cache-busting hash
|
|
153
|
+
const hash = crypto.randomBytes(8).toString("hex");
|
|
154
|
+
return `${componentName}.${hash}.js`;
|
|
155
|
+
} else {
|
|
156
|
+
// Development: use consistent filename
|
|
157
|
+
return `${componentName}.js`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Main build process
|
|
163
|
+
*/
|
|
164
|
+
function main(): void {
|
|
165
|
+
// Ensure hydration entries directory exists
|
|
166
|
+
if (!fs.existsSync(HYDRATION_ENTRIES_DIR)) {
|
|
167
|
+
fs.mkdirSync(HYDRATION_ENTRIES_DIR, { recursive: true });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log("š Detecting client components...");
|
|
171
|
+
const clientComponents = findClientComponents();
|
|
172
|
+
|
|
173
|
+
if (clientComponents.length === 0) {
|
|
174
|
+
console.log("ā ļø No client components found");
|
|
175
|
+
// Still ensure chunks directory exists and clear manifest
|
|
176
|
+
if (!fs.existsSync(CHUNKS_DIR)) {
|
|
177
|
+
fs.mkdirSync(CHUNKS_DIR, { recursive: true });
|
|
178
|
+
}
|
|
179
|
+
fs.writeFileSync(MANIFEST_FILE, JSON.stringify({}, null, 2), "utf-8");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(`ā
Found ${clientComponents.length} client component(s):`);
|
|
184
|
+
clientComponents.forEach((c) => console.log(` - ${c.componentName}`));
|
|
185
|
+
|
|
186
|
+
// Create hydration entries directory
|
|
187
|
+
if (!fs.existsSync(HYDRATION_ENTRIES_DIR)) {
|
|
188
|
+
fs.mkdirSync(HYDRATION_ENTRIES_DIR, { recursive: true });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Ensure chunks directory exists
|
|
192
|
+
if (!fs.existsSync(CHUNKS_DIR)) {
|
|
193
|
+
fs.mkdirSync(CHUNKS_DIR, { recursive: true });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log("\nš Generating hydration entries...");
|
|
197
|
+
|
|
198
|
+
// Generate hydration entry files
|
|
199
|
+
const entryFiles: { path: string; componentName: string }[] = [];
|
|
200
|
+
for (const component of clientComponents) {
|
|
201
|
+
const entryContent = generateHydrationEntry(component);
|
|
202
|
+
const entryPath = path.join(
|
|
203
|
+
HYDRATION_ENTRIES_DIR,
|
|
204
|
+
`${component.componentName}.tsx`,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
fs.writeFileSync(entryPath, entryContent, "utf-8");
|
|
208
|
+
entryFiles.push({
|
|
209
|
+
path: entryPath,
|
|
210
|
+
componentName: component.componentName,
|
|
211
|
+
});
|
|
212
|
+
console.log(` ā ${component.componentName}.tsx`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Build shared vendor bundle first
|
|
216
|
+
console.log("\nš¦ Building shared vendor bundle...");
|
|
217
|
+
const vendorEntryPath = path.join(HYDRATION_ENTRIES_DIR, "_vendor.js");
|
|
218
|
+
const vendorContent = `
|
|
219
|
+
import React from 'react';
|
|
220
|
+
import ReactDOM from 'react-dom/client';
|
|
221
|
+
|
|
222
|
+
// Expose React and ReactDOM globally for component chunks
|
|
223
|
+
window.React = React;
|
|
224
|
+
window.ReactDOM = ReactDOM;
|
|
225
|
+
`.trim();
|
|
226
|
+
|
|
227
|
+
fs.writeFileSync(vendorEntryPath, vendorContent, "utf-8");
|
|
228
|
+
|
|
229
|
+
const vendorOutputPath = path.join(CHUNKS_DIR, VENDOR_BUNDLE);
|
|
230
|
+
try {
|
|
231
|
+
const vendorCommand = `npx esbuild "${vendorEntryPath}" --bundle --minify --target=es2020 --format=iife --outfile="${vendorOutputPath}" --platform=browser --tree-shaking=true --define:process.env.NODE_ENV=\\"production\\"`;
|
|
232
|
+
execSync(vendorCommand, { stdio: "inherit" });
|
|
233
|
+
console.log(` ā vendor.js (React + ReactDOM)`);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error(` ā Failed to bundle vendor:`, error);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Bundle each entry file separately with cache-busted names
|
|
240
|
+
console.log("\nš¦ Bundling hydration scripts...");
|
|
241
|
+
|
|
242
|
+
// Create React shim files for aliasing
|
|
243
|
+
const SHIMS_DIR = path.join(DIST_DIR, ".shims");
|
|
244
|
+
if (!fs.existsSync(SHIMS_DIR)) {
|
|
245
|
+
fs.mkdirSync(SHIMS_DIR, { recursive: true });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Create shim for react
|
|
249
|
+
const reactShimPath = path.join(SHIMS_DIR, "react.js");
|
|
250
|
+
fs.writeFileSync(reactShimPath, "module.exports = window.React;", "utf-8");
|
|
251
|
+
|
|
252
|
+
// Create shim for react-dom
|
|
253
|
+
const reactDomShimPath = path.join(SHIMS_DIR, "react-dom.js");
|
|
254
|
+
fs.writeFileSync(
|
|
255
|
+
reactDomShimPath,
|
|
256
|
+
"module.exports = window.ReactDOM;",
|
|
257
|
+
"utf-8",
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Create shim for react-dom/client
|
|
261
|
+
const reactDomClientShimPath = path.join(SHIMS_DIR, "react-dom-client.js");
|
|
262
|
+
fs.writeFileSync(
|
|
263
|
+
reactDomClientShimPath,
|
|
264
|
+
"module.exports = window.ReactDOM;",
|
|
265
|
+
"utf-8",
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Create shim for react/jsx-runtime
|
|
269
|
+
const jsxRuntimeShimPath = path.join(SHIMS_DIR, "jsx-runtime.js");
|
|
270
|
+
fs.writeFileSync(
|
|
271
|
+
jsxRuntimeShimPath,
|
|
272
|
+
`
|
|
273
|
+
const React = window.React;
|
|
274
|
+
module.exports = {
|
|
275
|
+
jsx: React.createElement,
|
|
276
|
+
jsxs: React.createElement,
|
|
277
|
+
Fragment: React.Fragment
|
|
278
|
+
};`,
|
|
279
|
+
"utf-8",
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const manifest: HydrationManifest = {};
|
|
283
|
+
|
|
284
|
+
for (const entry of entryFiles) {
|
|
285
|
+
const chunkFilename = generateChunkFilename(entry.componentName);
|
|
286
|
+
const outputPath = path.join(CHUNKS_DIR, chunkFilename);
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
// Use aliases to redirect React imports to window.React from vendor bundle
|
|
290
|
+
const command = `npx esbuild "${entry.path}" --bundle --minify --target=es2020 --format=iife --keep-names --outfile="${outputPath}" --platform=browser --tree-shaking=true --define:process.env.NODE_ENV=\\"production\\" --alias:react=${reactShimPath} --alias:react-dom=${reactDomShimPath} --alias:react-dom/client=${reactDomClientShimPath} --alias:react/jsx-runtime=${jsxRuntimeShimPath}`;
|
|
291
|
+
execSync(command, { stdio: "inherit" });
|
|
292
|
+
manifest[entry.componentName] = chunkFilename;
|
|
293
|
+
console.log(` ā ${entry.componentName} -> ${chunkFilename}`);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error(` ā Failed to bundle ${entry.componentName}:`, error);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Write manifest file for server-side lookup
|
|
301
|
+
console.log("\nš Writing hydration manifest...");
|
|
302
|
+
fs.writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2), "utf-8");
|
|
303
|
+
console.log(` ā Manifest written to ${MANIFEST_FILE}`);
|
|
304
|
+
|
|
305
|
+
// Clean up temporary entries directory
|
|
306
|
+
if (fs.existsSync(HYDRATION_ENTRIES_DIR)) {
|
|
307
|
+
fs.rmSync(HYDRATION_ENTRIES_DIR, { recursive: true });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
console.log("\n⨠Hydration build complete!");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
main();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Build script for common CSS file
|
|
4
|
+
* Generates a single styles.css with all Tailwind styles for the entire app
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
|
|
11
|
+
const projectRoot = process.cwd();
|
|
12
|
+
const distDir = path.join(projectRoot, "dist");
|
|
13
|
+
const stylesDir = path.join(distDir, "styles");
|
|
14
|
+
const srcAssetsDir = path.join(projectRoot, "src/assets");
|
|
15
|
+
const outputCssPath = path.join(stylesDir, "styles.css");
|
|
16
|
+
|
|
17
|
+
async function main(): Promise<void> {
|
|
18
|
+
console.log("šØ Building styles...");
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// Ensure styles directory exists
|
|
22
|
+
if (!fs.existsSync(stylesDir)) {
|
|
23
|
+
fs.mkdirSync(stylesDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Compile Tailwind CSS
|
|
27
|
+
console.log(" Compiling Tailwind CSS...");
|
|
28
|
+
execSync(
|
|
29
|
+
`NODE_ENV=production postcss ${path.join(srcAssetsDir, "styles.css")} -o ${outputCssPath}`,
|
|
30
|
+
{
|
|
31
|
+
stdio: "inherit",
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
console.log(` ā Generated styles.css`);
|
|
36
|
+
console.log("⨠Styles build complete!");
|
|
37
|
+
} catch (error: any) {
|
|
38
|
+
console.error("ā CSS generation failed:", error.message);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
main();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
|
|
4
|
+
const src = "public";
|
|
5
|
+
const dest = path.join("dist", "public");
|
|
6
|
+
|
|
7
|
+
if (!fs.existsSync(src)) {
|
|
8
|
+
console.log("No public folder to copy");
|
|
9
|
+
process.exit(0);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (fs.existsSync(dest)) {
|
|
13
|
+
fs.rmSync(dest, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const copy = (source: string, target: string): void => {
|
|
17
|
+
fs.mkdirSync(target, { recursive: true });
|
|
18
|
+
|
|
19
|
+
for (const entry of fs.readdirSync(source)) {
|
|
20
|
+
const srcPath = path.join(source, entry);
|
|
21
|
+
const destPath = path.join(target, entry);
|
|
22
|
+
const stat = fs.statSync(srcPath);
|
|
23
|
+
|
|
24
|
+
if (stat.isDirectory()) {
|
|
25
|
+
copy(srcPath, destPath);
|
|
26
|
+
} else {
|
|
27
|
+
fs.copyFileSync(srcPath, destPath);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
copy(src, dest);
|
|
33
|
+
|
|
34
|
+
console.log("Assets copied successfully");
|
package/scripts/dev.sh
ADDED
package/scripts/dev.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as http from "http";
|
|
6
|
+
|
|
7
|
+
let nestProcess: any = null;
|
|
8
|
+
let isRebuilding = false;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Trigger browser reload by sending notification to LiveReloadController
|
|
12
|
+
*/
|
|
13
|
+
function triggerBrowserReload(): void {
|
|
14
|
+
const options = {
|
|
15
|
+
hostname: "127.0.0.1",
|
|
16
|
+
port: 3000,
|
|
17
|
+
path: "/__harpy/live-reload/trigger",
|
|
18
|
+
method: "POST",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const req = http.request(options, () => {
|
|
22
|
+
// Silently succeed
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
req.on("error", () => {
|
|
26
|
+
// Silently fail - server might not be ready yet
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
req.end();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function runCommand(cmd: string, args: string[] = []): Promise<void> {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const proc = spawn(cmd, args, { stdio: "inherit", shell: true });
|
|
35
|
+
proc.on("close", (code) => {
|
|
36
|
+
if (code !== 0) {
|
|
37
|
+
reject(new Error(`Command failed with code ${code}`));
|
|
38
|
+
} else {
|
|
39
|
+
resolve();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function buildHydration(): Promise<void> {
|
|
46
|
+
console.log("š§ Building hydration...");
|
|
47
|
+
try {
|
|
48
|
+
await runCommand("pnpm", ["build:hydration"]);
|
|
49
|
+
console.log("ā
Hydration built");
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error("ā Hydration build failed:", error);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function autoWrap(): Promise<void> {
|
|
57
|
+
console.log("š Auto-wrapping client components...");
|
|
58
|
+
try {
|
|
59
|
+
await runCommand("pnpm", ["auto-wrap"]);
|
|
60
|
+
console.log("ā
Auto-wrap complete");
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error("ā Auto-wrap failed:", error);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function buildStyles(): Promise<void> {
|
|
68
|
+
console.log("šØ Building styles...");
|
|
69
|
+
try {
|
|
70
|
+
await runCommand("pnpm", ["build:styles"]);
|
|
71
|
+
console.log("ā
Styles built");
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error("ā Styles build failed:", error);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function startNestServer(): Promise<void> {
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
console.log("š Starting NestJS server from compiled dist...");
|
|
81
|
+
// Run compiled dist/main.js instead of using ts-node
|
|
82
|
+
// This ensures auto-wrapped components are used
|
|
83
|
+
nestProcess = spawn("node", ["--watch", "dist/main.js"], {
|
|
84
|
+
stdio: "pipe",
|
|
85
|
+
shell: false,
|
|
86
|
+
cwd: process.cwd(),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
let resolved = false;
|
|
90
|
+
let isFirstStart = true;
|
|
91
|
+
|
|
92
|
+
// Capture output to detect when server is ready
|
|
93
|
+
nestProcess.stdout?.on("data", (data: Buffer) => {
|
|
94
|
+
const output = data.toString();
|
|
95
|
+
process.stdout.write(output);
|
|
96
|
+
|
|
97
|
+
// Detect when NestJS application successfully started
|
|
98
|
+
if (output.includes("Nest application successfully started")) {
|
|
99
|
+
if (!resolved && isFirstStart) {
|
|
100
|
+
// First start - resolve the promise
|
|
101
|
+
resolved = true;
|
|
102
|
+
isFirstStart = false;
|
|
103
|
+
resolve();
|
|
104
|
+
} else if (!isFirstStart && !isRebuilding) {
|
|
105
|
+
// Subsequent restarts - rebuild assets
|
|
106
|
+
setTimeout(async () => {
|
|
107
|
+
if (isRebuilding) return;
|
|
108
|
+
isRebuilding = true;
|
|
109
|
+
console.log("\nš NestJS rebuild detected, rebuilding assets...");
|
|
110
|
+
try {
|
|
111
|
+
await buildHydration();
|
|
112
|
+
await autoWrap();
|
|
113
|
+
await buildStyles();
|
|
114
|
+
console.log("ā
Assets rebuilt\n");
|
|
115
|
+
triggerBrowserReload();
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error("ā Asset rebuild failed:", error);
|
|
118
|
+
} finally {
|
|
119
|
+
isRebuilding = false;
|
|
120
|
+
}
|
|
121
|
+
}, 100);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
nestProcess.stderr?.on("data", (data: Buffer) => {
|
|
127
|
+
process.stderr.write(data);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Fallback timeout in case the detection fails
|
|
131
|
+
setTimeout(() => {
|
|
132
|
+
if (!resolved) {
|
|
133
|
+
resolved = true;
|
|
134
|
+
isFirstStart = false;
|
|
135
|
+
resolve();
|
|
136
|
+
}
|
|
137
|
+
}, 8000);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function watchSourceChanges(): void {
|
|
142
|
+
console.log("š Watching source files for changes...");
|
|
143
|
+
|
|
144
|
+
let debounceTimer: NodeJS.Timeout | null = null;
|
|
145
|
+
const watchedFiles = new Set<string>();
|
|
146
|
+
|
|
147
|
+
// Watch src for CSS changes only (TS/TSX changes are handled by NestJS watch + stdout detection)
|
|
148
|
+
fs.watch("src", { recursive: true }, async (eventType, filename) => {
|
|
149
|
+
if (!filename || isRebuilding) return;
|
|
150
|
+
|
|
151
|
+
// Ignore .hydration-entries changes
|
|
152
|
+
if (filename.includes(".hydration-entries")) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Only watch CSS files (TS/TSX changes trigger nest rebuild which we catch above)
|
|
157
|
+
if (!filename.endsWith(".css")) return;
|
|
158
|
+
|
|
159
|
+
// Skip if we just rebuilt for this file (avoid duplicate rebuilds)
|
|
160
|
+
if (watchedFiles.has(filename)) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
watchedFiles.add(filename);
|
|
165
|
+
|
|
166
|
+
// Debounce: wait 1000ms after last change before rebuilding
|
|
167
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
168
|
+
|
|
169
|
+
debounceTimer = setTimeout(async () => {
|
|
170
|
+
isRebuilding = true;
|
|
171
|
+
console.log(`\nš CSS file changed: ${filename}`);
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await buildStyles();
|
|
175
|
+
console.log("ā
Styles rebuilt\n");
|
|
176
|
+
triggerBrowserReload();
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error("Build error:", error);
|
|
179
|
+
} finally {
|
|
180
|
+
watchedFiles.delete(filename);
|
|
181
|
+
isRebuilding = false;
|
|
182
|
+
}
|
|
183
|
+
}, 1000);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let tscProcess: any = null;
|
|
188
|
+
|
|
189
|
+
async function startTypeScriptWatch(): Promise<void> {
|
|
190
|
+
return new Promise((resolve) => {
|
|
191
|
+
console.log("āļø Starting TypeScript compiler in watch mode...");
|
|
192
|
+
tscProcess = spawn("pnpm", ["nest", "build", "--watch"], {
|
|
193
|
+
stdio: "pipe",
|
|
194
|
+
shell: true,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
let resolved = false;
|
|
198
|
+
|
|
199
|
+
tscProcess.stdout?.on("data", (data: Buffer) => {
|
|
200
|
+
const output = data.toString();
|
|
201
|
+
process.stdout.write(output);
|
|
202
|
+
|
|
203
|
+
// Resolve once first compilation is done
|
|
204
|
+
if (output.includes("Found 0 errors. Watching") && !resolved) {
|
|
205
|
+
resolved = true;
|
|
206
|
+
resolve();
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
tscProcess.stderr?.on("data", (data: Buffer) => {
|
|
211
|
+
process.stderr.write(data);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function main(): Promise<void> {
|
|
217
|
+
try {
|
|
218
|
+
console.log("š¦ Initializing development environment...\n");
|
|
219
|
+
|
|
220
|
+
// First: Start TypeScript compiler in watch mode
|
|
221
|
+
await startTypeScriptWatch();
|
|
222
|
+
|
|
223
|
+
// Build initial assets after first compilation
|
|
224
|
+
console.log("\nš§ Building hydration assets...");
|
|
225
|
+
await buildHydration();
|
|
226
|
+
await autoWrap();
|
|
227
|
+
await buildStyles();
|
|
228
|
+
|
|
229
|
+
// Now start the node server with compiled dist files
|
|
230
|
+
await startNestServer();
|
|
231
|
+
|
|
232
|
+
console.log("\nā
Development server ready!\n");
|
|
233
|
+
|
|
234
|
+
// Watch for source changes
|
|
235
|
+
watchSourceChanges();
|
|
236
|
+
|
|
237
|
+
// Handle graceful shutdown
|
|
238
|
+
process.on("SIGINT", async () => {
|
|
239
|
+
console.log("\n\nš Stopping development server...");
|
|
240
|
+
if (tscProcess) tscProcess.kill();
|
|
241
|
+
if (nestProcess) nestProcess.kill();
|
|
242
|
+
process.exit(0);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
process.on("SIGTERM", async () => {
|
|
246
|
+
console.log("\n\nš Stopping development server...");
|
|
247
|
+
if (tscProcess) tscProcess.kill();
|
|
248
|
+
if (nestProcess) nestProcess.kill();
|
|
249
|
+
process.exit(0);
|
|
250
|
+
});
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.error("Fatal error:", error);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
main();
|