@nitronjs/framework 0.1.24 → 0.2.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/lib/Build/CssBuilder.js +129 -0
- package/lib/Build/FileAnalyzer.js +395 -0
- package/lib/Build/HydrationBuilder.js +173 -0
- package/lib/Build/Manager.js +290 -943
- package/lib/Build/colors.js +10 -0
- package/lib/Build/jsxRuntime.js +116 -0
- package/lib/Build/plugins.js +264 -0
- package/lib/Console/Commands/BuildCommand.js +6 -5
- package/lib/Console/Commands/DevCommand.js +151 -311
- package/lib/Console/Stubs/page-hydration-dev.tsx +72 -0
- package/lib/Console/Stubs/page-hydration.tsx +9 -10
- package/lib/Console/Stubs/vendor-dev.tsx +50 -0
- package/lib/Core/Environment.js +29 -2
- package/lib/Core/Paths.js +12 -4
- package/lib/Database/Drivers/MySQLDriver.js +5 -4
- package/lib/Database/QueryBuilder.js +2 -3
- package/lib/Filesystem/Manager.js +32 -7
- package/lib/HMR/Server.js +87 -0
- package/lib/Http/Server.js +9 -5
- package/lib/Logging/Manager.js +68 -18
- package/lib/Route/Loader.js +3 -4
- package/lib/Route/Manager.js +24 -3
- package/lib/Runtime/Entry.js +26 -1
- package/lib/Session/File.js +18 -7
- package/lib/View/Client/hmr-client.js +166 -0
- package/lib/View/Client/spa.js +142 -0
- package/lib/View/Layout.js +94 -0
- package/lib/View/Manager.js +390 -46
- package/lib/index.d.ts +55 -0
- package/package.json +2 -1
- package/skeleton/.env.example +0 -2
- package/skeleton/app/Controllers/HomeController.js +27 -3
- package/skeleton/config/app.js +15 -14
- package/skeleton/config/session.js +1 -1
- package/skeleton/globals.d.ts +3 -63
- package/skeleton/resources/views/Site/Home.tsx +274 -50
- package/skeleton/tsconfig.json +5 -1
package/lib/Build/Manager.js
CHANGED
|
@@ -1,170 +1,57 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import crypto from "crypto";
|
|
2
3
|
import dotenv from "dotenv";
|
|
3
4
|
import fs from "fs";
|
|
4
5
|
import path from "path";
|
|
5
6
|
import esbuild from "esbuild";
|
|
6
|
-
import { parse } from "@babel/parser";
|
|
7
|
-
import traverse from "@babel/traverse";
|
|
8
|
-
import postcss from "postcss";
|
|
9
|
-
import tailwindPostcss from "@tailwindcss/postcss";
|
|
10
7
|
import Paths from "../Core/Paths.js";
|
|
8
|
+
import Environment from "../Core/Environment.js";
|
|
9
|
+
import Layout from "../View/Layout.js";
|
|
10
|
+
import JSX_RUNTIME from "./jsxRuntime.js";
|
|
11
|
+
import FileAnalyzer from "./FileAnalyzer.js";
|
|
12
|
+
import CssBuilder from "./CssBuilder.js";
|
|
13
|
+
import HydrationBuilder from "./HydrationBuilder.js";
|
|
14
|
+
import {
|
|
15
|
+
createPathAliasPlugin,
|
|
16
|
+
createOriginalJsxPlugin,
|
|
17
|
+
createVendorGlobalsPlugin,
|
|
18
|
+
createServerFunctionsPlugin,
|
|
19
|
+
createCssStubPlugin,
|
|
20
|
+
createMarkerPlugin,
|
|
21
|
+
createServerModuleBlockerPlugin
|
|
22
|
+
} from "./plugins.js";
|
|
23
|
+
import COLORS from "./colors.js";
|
|
11
24
|
|
|
12
25
|
dotenv.config({ quiet: true });
|
|
13
26
|
|
|
14
|
-
const COLORS = {
|
|
15
|
-
reset: "\x1b[0m",
|
|
16
|
-
dim: "\x1b[2m",
|
|
17
|
-
red: "\x1b[31m",
|
|
18
|
-
green: "\x1b[32m",
|
|
19
|
-
yellow: "\x1b[33m",
|
|
20
|
-
cyan: "\x1b[36m"
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const CLIENT_HOOKS = new Set([
|
|
24
|
-
"useState",
|
|
25
|
-
"useEffect",
|
|
26
|
-
"useRef",
|
|
27
|
-
"useReducer",
|
|
28
|
-
"useLayoutEffect",
|
|
29
|
-
"useCallback",
|
|
30
|
-
"useMemo"
|
|
31
|
-
]);
|
|
32
|
-
|
|
33
|
-
const MAX_DEPTH = 50;
|
|
34
|
-
|
|
35
|
-
function sanitizeName(name) {
|
|
36
|
-
return name.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const JSX_RUNTIME = `
|
|
40
|
-
import * as React from 'react';
|
|
41
|
-
import * as OriginalJsx from '__react_jsx_original__';
|
|
42
|
-
|
|
43
|
-
const CTX = Symbol.for('__nitron_view_context__');
|
|
44
|
-
const MARK = Symbol.for('__nitron_client_component__');
|
|
45
|
-
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
46
|
-
|
|
47
|
-
function getContext() {
|
|
48
|
-
return globalThis[CTX]?.getStore?.();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
globalThis.csrf = () => getContext()?.csrf || '';
|
|
52
|
-
|
|
53
|
-
globalThis.request = () => {
|
|
54
|
-
const ctx = getContext();
|
|
55
|
-
return ctx?.request || { params: {}, query: {}, url: '', method: 'GET', headers: {} };
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const DepthContext = React.createContext(false);
|
|
59
|
-
const componentCache = new WeakMap();
|
|
60
|
-
|
|
61
|
-
function sanitizeProps(obj, seen = new WeakSet()) {
|
|
62
|
-
if (obj == null) return obj;
|
|
63
|
-
|
|
64
|
-
const type = typeof obj;
|
|
65
|
-
if (type === 'function' || type === 'symbol') return undefined;
|
|
66
|
-
if (type === 'bigint') return obj.toString();
|
|
67
|
-
if (type !== 'object') return obj;
|
|
68
|
-
|
|
69
|
-
if (seen.has(obj)) return undefined;
|
|
70
|
-
seen.add(obj);
|
|
71
|
-
|
|
72
|
-
if (Array.isArray(obj)) {
|
|
73
|
-
return obj.map(item => {
|
|
74
|
-
const sanitized = sanitizeProps(item, seen);
|
|
75
|
-
return sanitized === undefined ? null : sanitized;
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (obj instanceof Date) return obj.toISOString();
|
|
80
|
-
if (obj._attributes && typeof obj._attributes === 'object') {
|
|
81
|
-
return sanitizeProps(obj._attributes, seen);
|
|
82
|
-
}
|
|
83
|
-
if (typeof obj.toJSON === 'function') {
|
|
84
|
-
return sanitizeProps(obj.toJSON(), seen);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const proto = Object.getPrototypeOf(obj);
|
|
88
|
-
if (proto !== Object.prototype && proto !== null) return undefined;
|
|
89
|
-
|
|
90
|
-
const result = {};
|
|
91
|
-
for (const key of Object.keys(obj)) {
|
|
92
|
-
if (UNSAFE_KEYS.has(key)) continue;
|
|
93
|
-
const value = sanitizeProps(obj[key], seen);
|
|
94
|
-
if (value !== undefined) result[key] = value;
|
|
95
|
-
}
|
|
96
|
-
return result;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function wrapWithDepth(children) {
|
|
100
|
-
return OriginalJsx.jsx(DepthContext.Provider, { value: true, children });
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function createIsland(Component, name) {
|
|
104
|
-
function IslandBoundary(props) {
|
|
105
|
-
// Nested client components render normally (already inside an island)
|
|
106
|
-
if (React.useContext(DepthContext)) {
|
|
107
|
-
return OriginalJsx.jsx(Component, props);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const id = React.useId();
|
|
111
|
-
const safeProps = sanitizeProps(props) || {};
|
|
112
|
-
|
|
113
|
-
const ctx = getContext();
|
|
114
|
-
if (ctx) {
|
|
115
|
-
ctx.props = ctx.props || {};
|
|
116
|
-
ctx.props[id] = safeProps;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// SSR: Render full HTML for SEO and initial paint
|
|
120
|
-
// Client: Will re-render with createRoot for animations to work
|
|
121
|
-
return OriginalJsx.jsx('div', {
|
|
122
|
-
'data-cid': id,
|
|
123
|
-
'data-island': name,
|
|
124
|
-
children: wrapWithDepth(OriginalJsx.jsx(Component, props))
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
IslandBoundary.displayName = 'Island(' + name + ')';
|
|
129
|
-
return IslandBoundary;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function getWrappedComponent(Component) {
|
|
133
|
-
if (!componentCache.has(Component)) {
|
|
134
|
-
const name = Component.displayName || Component.name || 'Anonymous';
|
|
135
|
-
componentCache.set(Component, createIsland(Component, name));
|
|
136
|
-
}
|
|
137
|
-
return componentCache.get(Component);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export function jsx(type, props, key) {
|
|
141
|
-
if (typeof type === 'function' && type[MARK]) {
|
|
142
|
-
return OriginalJsx.jsx(getWrappedComponent(type), props, key);
|
|
143
|
-
}
|
|
144
|
-
return OriginalJsx.jsx(type, props, key);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function jsxs(type, props, key) {
|
|
148
|
-
if (typeof type === 'function' && type[MARK]) {
|
|
149
|
-
return OriginalJsx.jsx(getWrappedComponent(type), props, key);
|
|
150
|
-
}
|
|
151
|
-
return OriginalJsx.jsxs(type, props, key);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export const Fragment = OriginalJsx.Fragment;
|
|
155
|
-
`;
|
|
156
|
-
|
|
157
27
|
class Builder {
|
|
158
|
-
#isDev =
|
|
159
|
-
#traverse = traverse.default;
|
|
28
|
+
#isDev = false;
|
|
160
29
|
#manifest = {};
|
|
161
30
|
#paths;
|
|
162
31
|
#stats = { user: 0, framework: 0, islands: 0, css: 0 };
|
|
163
|
-
#cache = {
|
|
32
|
+
#cache = {
|
|
33
|
+
imports: new Map(),
|
|
34
|
+
css: new Map(),
|
|
35
|
+
cssHashes: new Map(),
|
|
36
|
+
viewHashes: new Map(),
|
|
37
|
+
hydrationTemplate: null,
|
|
38
|
+
hydrationTemplateDev: null,
|
|
39
|
+
vendorBuilt: false,
|
|
40
|
+
spaBuilt: false,
|
|
41
|
+
hmrBuilt: false,
|
|
42
|
+
tailwindProcessor: null,
|
|
43
|
+
viewsChanged: false,
|
|
44
|
+
fileMeta: new Map(),
|
|
45
|
+
fileHashes: new Map()
|
|
46
|
+
};
|
|
47
|
+
#diskCachePath = path.join(Paths.nitronTemp, "build-cache.json");
|
|
48
|
+
#changedFiles = new Set();
|
|
49
|
+
#analyzer;
|
|
50
|
+
#cssBuilder;
|
|
51
|
+
#hydrationBuilder;
|
|
164
52
|
|
|
165
53
|
constructor() {
|
|
166
54
|
this.#paths = {
|
|
167
|
-
// User project paths
|
|
168
55
|
userViews: Paths.views,
|
|
169
56
|
userOutput: Paths.buildViews,
|
|
170
57
|
frameworkOutput: Paths.buildFrameworkViews,
|
|
@@ -173,40 +60,119 @@ class Builder {
|
|
|
173
60
|
jsOutput: Paths.publicJs,
|
|
174
61
|
jsxRuntime: Paths.jsxRuntime,
|
|
175
62
|
nitronTemp: Paths.nitronTemp,
|
|
176
|
-
// Framework paths
|
|
177
63
|
frameworkViews: Paths.frameworkViews,
|
|
178
64
|
templates: Paths.frameworkTemplates
|
|
179
65
|
};
|
|
66
|
+
|
|
67
|
+
this.#analyzer = new FileAnalyzer(this.#cache);
|
|
68
|
+
this.#cssBuilder = new CssBuilder(this.#cache, this.#isDev, this.#paths.cssInput, this.#paths.cssOutput);
|
|
69
|
+
this.#hydrationBuilder = new HydrationBuilder(this.#cache, this.#isDev, this.#paths.templates, this.#analyzer);
|
|
180
70
|
}
|
|
181
71
|
|
|
182
|
-
async run(only = null) {
|
|
72
|
+
async run(only = null, isDev = false, silent = false) {
|
|
73
|
+
this.#isDev = isDev;
|
|
74
|
+
Environment.setDev(isDev);
|
|
75
|
+
|
|
183
76
|
const startTime = Date.now();
|
|
77
|
+
this.#changedFiles.clear();
|
|
78
|
+
this.#manifest = {};
|
|
184
79
|
|
|
185
80
|
try {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (!only)
|
|
189
|
-
this.#cleanOutputDirs();
|
|
190
|
-
}
|
|
81
|
+
if (this.#isDev) this.#loadDiskCache();
|
|
82
|
+
if (!silent) console.log(`\n${COLORS.cyan}⚡ NitronJS Build${COLORS.reset}\n`);
|
|
83
|
+
if (!only) this.#cleanOutputDirs();
|
|
191
84
|
|
|
85
|
+
this.#cache.viewsChanged = false;
|
|
192
86
|
this.#writeJsxRuntime();
|
|
193
87
|
|
|
194
|
-
await
|
|
195
|
-
|
|
196
|
-
(!only || only === "css") ? this.#buildCss() : null
|
|
197
|
-
]);
|
|
88
|
+
if (!only || only === "views") await this.#buildViews();
|
|
89
|
+
if (!only || only === "css") await this.#buildCss();
|
|
198
90
|
|
|
91
|
+
if (this.#isDev) this.#saveDiskCache();
|
|
199
92
|
this.#cleanupTemp();
|
|
200
|
-
this.#printSummary(Date.now() - startTime);
|
|
201
|
-
|
|
93
|
+
if (!silent) this.#printSummary(Date.now() - startTime);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
success: true,
|
|
97
|
+
changedFiles: [...this.#changedFiles],
|
|
98
|
+
cssChanged: this.#stats.css > 0,
|
|
99
|
+
viewsChanged: this.#cache.viewsChanged,
|
|
100
|
+
time: Date.now() - startTime
|
|
101
|
+
};
|
|
202
102
|
} catch (error) {
|
|
203
103
|
this.#cleanupTemp();
|
|
204
|
-
console.log(`\n${COLORS.red}✖ Build failed: ${error.message}${COLORS.reset}\n`);
|
|
205
|
-
if (this.#isDev) console.error(error);
|
|
206
|
-
return false;
|
|
104
|
+
if (!silent) console.log(`\n${COLORS.red}✖ Build failed: ${error.message}${COLORS.reset}\n`);
|
|
105
|
+
if (this.#isDev && !silent) console.error(error);
|
|
106
|
+
return { success: false, error: error.message };
|
|
207
107
|
}
|
|
208
108
|
}
|
|
209
109
|
|
|
110
|
+
#loadDiskCache() {
|
|
111
|
+
try {
|
|
112
|
+
if (fs.existsSync(this.#diskCachePath)) {
|
|
113
|
+
const data = JSON.parse(fs.readFileSync(this.#diskCachePath, "utf8"));
|
|
114
|
+
|
|
115
|
+
if (data.fileMeta) {
|
|
116
|
+
for (const [key, value] of Object.entries(data.fileMeta)) {
|
|
117
|
+
value.imports = new Set(value.imports || []);
|
|
118
|
+
value.css = new Set(value.css || []);
|
|
119
|
+
this.#cache.fileMeta.set(key, value);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (data.fileHashes) {
|
|
123
|
+
for (const [key, value] of Object.entries(data.fileHashes)) {
|
|
124
|
+
this.#cache.fileHashes.set(key, value);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (data.viewHashes) {
|
|
128
|
+
for (const [key, value] of Object.entries(data.viewHashes)) {
|
|
129
|
+
this.#cache.viewHashes.set(key, value);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (data.cssHashes) {
|
|
133
|
+
for (const [key, value] of Object.entries(data.cssHashes)) {
|
|
134
|
+
this.#cache.cssHashes.set(key, value);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch {}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#saveDiskCache() {
|
|
142
|
+
try {
|
|
143
|
+
const cacheDir = path.dirname(this.#diskCachePath);
|
|
144
|
+
if (!fs.existsSync(cacheDir)) {
|
|
145
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const data = {
|
|
149
|
+
fileMeta: {},
|
|
150
|
+
fileHashes: {},
|
|
151
|
+
viewHashes: {},
|
|
152
|
+
cssHashes: {}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
for (const [key, value] of this.#cache.fileMeta) {
|
|
156
|
+
data.fileMeta[key] = {
|
|
157
|
+
...value,
|
|
158
|
+
imports: Array.from(value.imports || []),
|
|
159
|
+
css: Array.from(value.css || [])
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
for (const [key, value] of this.#cache.fileHashes) {
|
|
163
|
+
data.fileHashes[key] = value;
|
|
164
|
+
}
|
|
165
|
+
for (const [key, value] of this.#cache.viewHashes) {
|
|
166
|
+
data.viewHashes[key] = value;
|
|
167
|
+
}
|
|
168
|
+
for (const [key, value] of this.#cache.cssHashes) {
|
|
169
|
+
data.cssHashes[key] = value;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fs.writeFileSync(this.#diskCachePath, JSON.stringify(data));
|
|
173
|
+
} catch {}
|
|
174
|
+
}
|
|
175
|
+
|
|
210
176
|
#printSummary(duration) {
|
|
211
177
|
const { user, framework, islands, css } = this.#stats;
|
|
212
178
|
const lines = [];
|
|
@@ -233,20 +199,46 @@ class Builder {
|
|
|
233
199
|
}
|
|
234
200
|
|
|
235
201
|
async #buildViews() {
|
|
236
|
-
const [, userBundle, frameworkBundle] = await Promise.all([
|
|
202
|
+
const [, , , userBundle, frameworkBundle] = await Promise.all([
|
|
237
203
|
this.#buildVendor(),
|
|
204
|
+
this.#buildSpaRuntime(),
|
|
205
|
+
this.#buildHmrClient(),
|
|
238
206
|
this.#buildViewBundle("user", this.#paths.userViews, this.#paths.userOutput),
|
|
239
207
|
this.#buildViewBundle("framework", this.#paths.frameworkViews, this.#paths.frameworkOutput)
|
|
240
208
|
]);
|
|
241
209
|
|
|
242
|
-
|
|
210
|
+
const changedViews = new Set([
|
|
211
|
+
...(userBundle.changedFiles || []),
|
|
212
|
+
...(frameworkBundle.changedFiles || [])
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
for (const file of changedViews) {
|
|
216
|
+
this.#changedFiles.add(file);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const isFirstBuild = changedViews.size === 0 &&
|
|
220
|
+
(userBundle.entries.length > 0 || frameworkBundle.entries.length > 0);
|
|
221
|
+
|
|
222
|
+
await this.#buildHydrationBundles(
|
|
223
|
+
userBundle,
|
|
224
|
+
frameworkBundle,
|
|
225
|
+
isFirstBuild ? null : (changedViews.size > 0 ? changedViews : null)
|
|
226
|
+
);
|
|
227
|
+
|
|
243
228
|
this.#writeManifest();
|
|
244
229
|
}
|
|
245
230
|
|
|
246
231
|
async #buildVendor() {
|
|
232
|
+
const outfile = path.join(this.#paths.jsOutput, "vendor.js");
|
|
233
|
+
if (this.#cache.vendorBuilt && fs.existsSync(outfile)) return;
|
|
234
|
+
|
|
235
|
+
const vendorFile = this.#isDev
|
|
236
|
+
? path.join(this.#paths.templates, "vendor-dev.tsx")
|
|
237
|
+
: path.join(this.#paths.templates, "vendor.tsx");
|
|
238
|
+
|
|
247
239
|
await esbuild.build({
|
|
248
|
-
entryPoints: [
|
|
249
|
-
outfile
|
|
240
|
+
entryPoints: [vendorFile],
|
|
241
|
+
outfile,
|
|
250
242
|
bundle: true,
|
|
251
243
|
platform: "browser",
|
|
252
244
|
format: "iife",
|
|
@@ -255,26 +247,79 @@ class Builder {
|
|
|
255
247
|
minify: !this.#isDev,
|
|
256
248
|
jsx: "automatic"
|
|
257
249
|
});
|
|
250
|
+
this.#cache.vendorBuilt = true;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async #buildHmrClient() {
|
|
254
|
+
if (!this.#isDev) return;
|
|
255
|
+
|
|
256
|
+
const outfile = path.join(this.#paths.jsOutput, "hmr.js");
|
|
257
|
+
if (this.#cache.hmrBuilt && fs.existsSync(outfile)) return;
|
|
258
|
+
|
|
259
|
+
await esbuild.build({
|
|
260
|
+
entryPoints: [path.join(Paths.frameworkLib, "View/Client/hmr-client.js")],
|
|
261
|
+
outfile,
|
|
262
|
+
bundle: false,
|
|
263
|
+
platform: "browser",
|
|
264
|
+
format: "iife",
|
|
265
|
+
target: "es2020",
|
|
266
|
+
minify: false
|
|
267
|
+
});
|
|
268
|
+
this.#cache.hmrBuilt = true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async #buildSpaRuntime() {
|
|
272
|
+
const outfile = path.join(this.#paths.jsOutput, "spa.js");
|
|
273
|
+
if (this.#cache.spaBuilt && fs.existsSync(outfile)) return;
|
|
274
|
+
|
|
275
|
+
await esbuild.build({
|
|
276
|
+
entryPoints: [path.join(Paths.frameworkLib, "View/Client/spa.js")],
|
|
277
|
+
outfile,
|
|
278
|
+
bundle: true,
|
|
279
|
+
platform: "browser",
|
|
280
|
+
format: "iife",
|
|
281
|
+
target: "es2020",
|
|
282
|
+
minify: !this.#isDev
|
|
283
|
+
});
|
|
284
|
+
this.#cache.spaBuilt = true;
|
|
258
285
|
}
|
|
259
286
|
|
|
260
287
|
async #buildViewBundle(namespace, srcDir, outDir) {
|
|
261
288
|
if (!fs.existsSync(srcDir)) {
|
|
262
|
-
return { entries: [], meta: new Map(), srcDir, namespace };
|
|
289
|
+
return { entries: [], layouts: [], meta: new Map(), srcDir, namespace, changedFiles: [] };
|
|
263
290
|
}
|
|
264
291
|
|
|
265
|
-
const { entries, meta } = this.#discoverEntries(srcDir);
|
|
292
|
+
const { entries, layouts, meta } = this.#analyzer.discoverEntries(srcDir);
|
|
266
293
|
|
|
267
|
-
if (!entries.length) {
|
|
268
|
-
return { entries: [], meta: new Map(), srcDir, namespace };
|
|
294
|
+
if (!entries.length && !layouts.length) {
|
|
295
|
+
return { entries: [], layouts: [], meta: new Map(), srcDir, namespace, changedFiles: [] };
|
|
269
296
|
}
|
|
270
297
|
|
|
271
|
-
this.#addToManifest(entries, meta, srcDir, namespace);
|
|
272
|
-
|
|
273
|
-
|
|
298
|
+
this.#addToManifest(entries, layouts, meta, srcDir, namespace);
|
|
299
|
+
|
|
300
|
+
const allFiles = [...entries, ...layouts];
|
|
301
|
+
const changedFiles = [];
|
|
302
|
+
|
|
303
|
+
for (const file of allFiles) {
|
|
304
|
+
const content = await fs.promises.readFile(file, "utf8");
|
|
305
|
+
const hash = crypto.createHash("md5").update(content).digest("hex");
|
|
306
|
+
const cachedHash = this.#cache.viewHashes.get(file);
|
|
307
|
+
|
|
308
|
+
if (cachedHash !== hash) {
|
|
309
|
+
this.#cache.viewHashes.set(file, hash);
|
|
310
|
+
changedFiles.push(file);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (changedFiles.length) {
|
|
315
|
+
this.#cache.viewsChanged = true;
|
|
316
|
+
await this.#runEsbuild(changedFiles, outDir, { meta, outbase: srcDir });
|
|
317
|
+
await this.#postProcessMeta(changedFiles, srcDir, outDir);
|
|
318
|
+
}
|
|
274
319
|
|
|
275
320
|
this.#stats[namespace === "user" ? "user" : "framework"] = entries.length;
|
|
276
321
|
|
|
277
|
-
return { entries, meta, srcDir, namespace };
|
|
322
|
+
return { entries, layouts, meta, srcDir, namespace, changedFiles };
|
|
278
323
|
}
|
|
279
324
|
|
|
280
325
|
async #postProcessMeta(entries, srcDir, outDir) {
|
|
@@ -350,27 +395,13 @@ class Builder {
|
|
|
350
395
|
await Promise.all(entries.map(processEntry));
|
|
351
396
|
}
|
|
352
397
|
|
|
353
|
-
async #buildHydrationBundles(userBundle, frameworkBundle) {
|
|
354
|
-
const hydrationFiles =
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const clientComponents = this.#findClientComponents(viewPath, bundle.meta, new Set());
|
|
361
|
-
|
|
362
|
-
if (clientComponents.size) {
|
|
363
|
-
const hydrationFile = this.#generateHydrationFile(
|
|
364
|
-
viewPath,
|
|
365
|
-
bundle.srcDir,
|
|
366
|
-
clientComponents,
|
|
367
|
-
bundle.meta,
|
|
368
|
-
bundle.namespace
|
|
369
|
-
);
|
|
370
|
-
hydrationFiles.push(hydrationFile);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
398
|
+
async #buildHydrationBundles(userBundle, frameworkBundle, changedViews = null) {
|
|
399
|
+
const hydrationFiles = await this.#hydrationBuilder.build(
|
|
400
|
+
userBundle,
|
|
401
|
+
frameworkBundle,
|
|
402
|
+
this.#manifest,
|
|
403
|
+
changedViews
|
|
404
|
+
);
|
|
374
405
|
|
|
375
406
|
if (!hydrationFiles.length) {
|
|
376
407
|
return;
|
|
@@ -388,136 +419,26 @@ class Builder {
|
|
|
388
419
|
this.#stats.islands = hydrationFiles.length;
|
|
389
420
|
}
|
|
390
421
|
|
|
391
|
-
#findClientComponents(file, meta, seen, depth = 0) {
|
|
392
|
-
const result = new Set();
|
|
393
|
-
|
|
394
|
-
if (depth > MAX_DEPTH || seen.has(file)) {
|
|
395
|
-
return result;
|
|
396
|
-
}
|
|
397
|
-
seen.add(file);
|
|
398
|
-
|
|
399
|
-
const fileMeta = meta.get(file);
|
|
400
|
-
if (!fileMeta) {
|
|
401
|
-
return result;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (fileMeta.isClient) {
|
|
405
|
-
result.add(file);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
for (const importPath of fileMeta.imports) {
|
|
409
|
-
const resolvedPath = this.#resolveImport(file, importPath);
|
|
410
|
-
|
|
411
|
-
if (meta.has(resolvedPath)) {
|
|
412
|
-
const childComponents = this.#findClientComponents(resolvedPath, meta, seen, depth + 1);
|
|
413
|
-
for (const component of childComponents) {
|
|
414
|
-
result.add(component);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
return result;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
#generateHydrationFile(viewPath, srcDir, clientComponents, meta, namespace) {
|
|
423
|
-
if (!this.#cache.hydrationTemplate) {
|
|
424
|
-
const templatePath = path.join(this.#paths.templates, "page-hydration.tsx");
|
|
425
|
-
this.#cache.hydrationTemplate = fs.readFileSync(templatePath, "utf8");
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const viewRelative = path.relative(srcDir, viewPath).replace(/\.tsx$/, "").toLowerCase();
|
|
429
|
-
const outputDir = path.join(Paths.project, ".nitron/hydration", path.dirname(viewRelative));
|
|
430
|
-
const outputFile = path.join(outputDir, path.basename(viewRelative) + ".tsx");
|
|
431
|
-
|
|
432
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
433
|
-
|
|
434
|
-
const imports = [];
|
|
435
|
-
const manifestEntries = [];
|
|
436
|
-
let index = 0;
|
|
437
|
-
|
|
438
|
-
for (const componentPath of clientComponents) {
|
|
439
|
-
const componentMeta = meta.get(componentPath);
|
|
440
|
-
if (!componentMeta) continue;
|
|
441
|
-
|
|
442
|
-
const baseName = path.basename(componentPath, ".tsx");
|
|
443
|
-
const relativePath = path.relative(outputDir, componentPath)
|
|
444
|
-
.replace(/\\/g, "/")
|
|
445
|
-
.replace(/\.tsx$/, "");
|
|
446
|
-
|
|
447
|
-
if (componentMeta.hasDefault) {
|
|
448
|
-
const importName = sanitizeName(baseName) + "_" + index++;
|
|
449
|
-
imports.push(`import ${importName} from "${relativePath}";`);
|
|
450
|
-
manifestEntries.push(` "${baseName}": ${importName}`);
|
|
451
|
-
// Also add displayName entry
|
|
452
|
-
manifestEntries.push(` [${importName}.displayName || ${importName}.name || "${baseName}"]: ${importName}`);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
for (const namedExport of componentMeta.named || []) {
|
|
456
|
-
const importName = sanitizeName(namedExport) + "_" + index++;
|
|
457
|
-
imports.push(`import { ${namedExport} as ${importName} } from "${relativePath}";`);
|
|
458
|
-
manifestEntries.push(` "${namedExport}": ${importName}`);
|
|
459
|
-
// Also add displayName entry
|
|
460
|
-
manifestEntries.push(` [${importName}.displayName || ${importName}.name || "${namedExport}"]: ${importName}`);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const code = this.#cache.hydrationTemplate
|
|
465
|
-
.replace("// __COMPONENT_IMPORTS__", imports.join("\n"))
|
|
466
|
-
.replace(
|
|
467
|
-
"// __COMPONENT_MANIFEST__",
|
|
468
|
-
`Object.assign(componentManifest, {\n${manifestEntries.join(",\n")}\n});`
|
|
469
|
-
);
|
|
470
|
-
|
|
471
|
-
fs.writeFileSync(outputFile, code);
|
|
472
|
-
|
|
473
|
-
const manifestKey = `${namespace}:${viewRelative.replace(/\\/g, "/")}`;
|
|
474
|
-
if (this.#manifest[manifestKey]) {
|
|
475
|
-
this.#manifest[manifestKey].hydrationScript = `/js/${viewRelative.replace(/\\/g, "/")}.js`;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
return outputFile;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
422
|
async #buildCss() {
|
|
482
|
-
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const cssFiles = fs.readdirSync(this.#paths.cssInput).filter(f => f.endsWith(".css"));
|
|
487
|
-
if (!cssFiles.length) {
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
await Promise.all(cssFiles.map(async (filename) => {
|
|
492
|
-
const filePath = path.join(this.#paths.cssInput, filename);
|
|
493
|
-
if (!this.#cache.css.has(filePath)) {
|
|
494
|
-
const content = await fs.promises.readFile(filePath, "utf8");
|
|
495
|
-
this.#cache.css.set(filePath, content);
|
|
496
|
-
}
|
|
497
|
-
}));
|
|
498
|
-
|
|
499
|
-
const hasTailwind = this.#detectTailwind();
|
|
500
|
-
const processor = hasTailwind ? postcss([tailwindPostcss()]) : null;
|
|
501
|
-
|
|
502
|
-
await Promise.all(cssFiles.map(filename => this.#processCss(filename, hasTailwind, processor)));
|
|
503
|
-
|
|
504
|
-
this.#stats.css = cssFiles.length;
|
|
423
|
+
this.#stats.css = await this.#cssBuilder.build(this.#cache.viewsChanged);
|
|
505
424
|
}
|
|
506
425
|
|
|
507
426
|
async #runEsbuild(entries, outDir, options = {}) {
|
|
427
|
+
if (!entries.length) return;
|
|
428
|
+
|
|
508
429
|
const plugins = [
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
430
|
+
createCssStubPlugin(),
|
|
431
|
+
createMarkerPlugin(options, this.#isDev),
|
|
432
|
+
createPathAliasPlugin()
|
|
512
433
|
];
|
|
513
434
|
|
|
514
435
|
if (options.vendor) {
|
|
515
|
-
plugins.push(
|
|
516
|
-
plugins.push(
|
|
436
|
+
plugins.push(createVendorGlobalsPlugin());
|
|
437
|
+
plugins.push(createServerModuleBlockerPlugin());
|
|
517
438
|
}
|
|
518
439
|
|
|
519
440
|
if (options.serverFunctions) {
|
|
520
|
-
plugins.push(
|
|
441
|
+
plugins.push(createServerFunctionsPlugin());
|
|
521
442
|
}
|
|
522
443
|
|
|
523
444
|
const isNode = (options.platform ?? "node") === "node";
|
|
@@ -534,7 +455,9 @@ class Builder {
|
|
|
534
455
|
sourcemap: this.#isDev,
|
|
535
456
|
minify: !this.#isDev,
|
|
536
457
|
external: options.external ?? ["react", "react-dom", "react-dom/server"],
|
|
537
|
-
plugins
|
|
458
|
+
plugins,
|
|
459
|
+
write: true,
|
|
460
|
+
logLevel: "silent"
|
|
538
461
|
};
|
|
539
462
|
|
|
540
463
|
if (isNode) {
|
|
@@ -544,655 +467,78 @@ class Builder {
|
|
|
544
467
|
|
|
545
468
|
if (options.platform !== "browser") {
|
|
546
469
|
config.alias = { "react/jsx-runtime": this.#paths.jsxRuntime };
|
|
547
|
-
plugins.push(
|
|
470
|
+
plugins.push(createOriginalJsxPlugin());
|
|
548
471
|
}
|
|
549
472
|
|
|
550
473
|
await esbuild.build(config);
|
|
551
474
|
}
|
|
552
475
|
|
|
553
|
-
#
|
|
554
|
-
const
|
|
555
|
-
return {
|
|
556
|
-
name: "path-alias",
|
|
557
|
-
setup: (build) => {
|
|
558
|
-
// Handle @/* alias -> ./app/* with high priority
|
|
559
|
-
// Using filter with higher specificity to run before packages: "external"
|
|
560
|
-
build.onResolve({ filter: /^@\// }, async (args) => {
|
|
561
|
-
const relativePath = args.path.replace(/^@\//, "");
|
|
562
|
-
const absolutePath = path.join(root, "app", relativePath);
|
|
563
|
-
|
|
564
|
-
// Try with .js extension first, then without
|
|
565
|
-
const extensions = [".js", ".ts", ".jsx", ".tsx", ""];
|
|
566
|
-
for (const ext of extensions) {
|
|
567
|
-
const fullPath = absolutePath + ext;
|
|
568
|
-
if (fs.existsSync(fullPath)) {
|
|
569
|
-
// Return with explicit namespace to ensure it's bundled
|
|
570
|
-
return { path: fullPath, external: false };
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// If it's a directory, try index files
|
|
575
|
-
if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory()) {
|
|
576
|
-
for (const ext of [".js", ".ts", ".jsx", ".tsx"]) {
|
|
577
|
-
const indexPath = path.join(absolutePath, "index" + ext);
|
|
578
|
-
if (fs.existsSync(indexPath)) {
|
|
579
|
-
return { path: indexPath, external: false };
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
return { path: absolutePath + ".js", external: false };
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
#createOriginalJsxPlugin() {
|
|
591
|
-
return {
|
|
592
|
-
name: "original-jsx",
|
|
593
|
-
setup: (build) => {
|
|
594
|
-
build.onResolve({ filter: /^__react_jsx_original__$/ }, () => ({
|
|
595
|
-
path: "react/jsx-runtime",
|
|
596
|
-
external: true
|
|
597
|
-
}));
|
|
598
|
-
}
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
#createVendorGlobalsPlugin() {
|
|
603
|
-
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\\/]/g, "\\$&");
|
|
476
|
+
#addToManifest(entries, layouts, meta, baseDir, namespace) {
|
|
477
|
+
const layoutSet = new Set(layouts);
|
|
604
478
|
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
};
|
|
479
|
+
for (const file of entries) {
|
|
480
|
+
const fileMeta = meta.get(file);
|
|
481
|
+
const viewPath = path.relative(baseDir, file)
|
|
482
|
+
.replace(/\.tsx$/, "")
|
|
483
|
+
.replace(/\\/g, "/");
|
|
611
484
|
|
|
612
|
-
|
|
613
|
-
filter: new RegExp(`^${escapeRegex(pkg)}$`),
|
|
614
|
-
pkg,
|
|
615
|
-
global
|
|
616
|
-
}));
|
|
617
|
-
|
|
618
|
-
return {
|
|
619
|
-
name: "vendor-globals",
|
|
620
|
-
setup: (build) => {
|
|
621
|
-
for (const { filter, pkg, global } of patterns) {
|
|
622
|
-
build.onResolve({ filter }, () => ({
|
|
623
|
-
path: pkg,
|
|
624
|
-
namespace: "vendor-global"
|
|
625
|
-
}));
|
|
626
|
-
|
|
627
|
-
build.onLoad({ filter, namespace: "vendor-global" }, () => ({
|
|
628
|
-
contents: `module.exports = window.${global};`,
|
|
629
|
-
loader: "js"
|
|
630
|
-
}));
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
};
|
|
634
|
-
}
|
|
485
|
+
const key = `${namespace}:${viewPath.toLowerCase()}`;
|
|
635
486
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
name: "server-stub",
|
|
639
|
-
setup: (build) => {
|
|
640
|
-
build.onResolve({ filter: /lib\/Storage\.js$/ }, (args) => ({
|
|
641
|
-
path: args.path,
|
|
642
|
-
namespace: "storage-stub"
|
|
643
|
-
}));
|
|
644
|
-
|
|
645
|
-
build.onLoad({ filter: /.*/, namespace: "storage-stub" }, () => ({
|
|
646
|
-
contents: `export default { url: p => '/storage/' + (p.startsWith('/') ? p.slice(1) : p) };`,
|
|
647
|
-
loader: "js"
|
|
648
|
-
}));
|
|
649
|
-
|
|
650
|
-
build.onResolve(
|
|
651
|
-
{ filter: /lib\/(DB|Mail|Log|Hash|Environment|Server|Model|Validator)\.js$/ },
|
|
652
|
-
(args) => ({ path: args.path, namespace: "server-only" })
|
|
653
|
-
);
|
|
654
|
-
|
|
655
|
-
build.onLoad({ filter: /.*/, namespace: "server-only" }, (args) => {
|
|
656
|
-
const moduleName = args.path.split("/").pop()?.replace(".js", "") || "Module";
|
|
657
|
-
return {
|
|
658
|
-
contents: `const err = () => { throw new Error("${moduleName} is server-only") }; export default new Proxy({}, { get: err, apply: err });`,
|
|
659
|
-
loader: "js"
|
|
660
|
-
};
|
|
661
|
-
});
|
|
487
|
+
if (this.#manifest[key]) {
|
|
488
|
+
throw new Error(`Duplicate: ${key}`);
|
|
662
489
|
}
|
|
663
|
-
};
|
|
664
|
-
}
|
|
665
490
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
name: "server-functions",
|
|
669
|
-
setup: (build) => {
|
|
670
|
-
build.onLoad({ filter: /\.(tsx?|jsx?)$/ }, async (args) => {
|
|
671
|
-
if (args.path.includes("node_modules")) {
|
|
672
|
-
return null;
|
|
673
|
-
}
|
|
491
|
+
const layoutDisabled = fileMeta?.layoutDisabled === true;
|
|
492
|
+
const layoutChain = layoutDisabled ? [] : Layout.resolve(viewPath + ".tsx", baseDir);
|
|
674
493
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
494
|
+
const cssSet = new Set();
|
|
495
|
+
for (const layout of layoutChain) {
|
|
496
|
+
const layoutMeta = meta.get(layout.path);
|
|
497
|
+
if (layoutMeta?.css) {
|
|
498
|
+
for (const css of layoutMeta.css) {
|
|
499
|
+
cssSet.add(`/css/${path.basename(css)}`);
|
|
679
500
|
}
|
|
680
|
-
|
|
681
|
-
source = source.replace(
|
|
682
|
-
/\bcsrf\s*\(\s*\)/g,
|
|
683
|
-
"window.__NITRON_RUNTIME__.csrf"
|
|
684
|
-
);
|
|
685
|
-
|
|
686
|
-
source = source.replace(
|
|
687
|
-
/\broute\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
688
|
-
(_, routeName) => `window.__NITRON_RUNTIME__.routes["${routeName}"]`
|
|
689
|
-
);
|
|
690
|
-
|
|
691
|
-
const ext = args.path.split(".").pop();
|
|
692
|
-
const loader = ext === "tsx" ? "tsx" : ext === "ts" ? "ts" : ext === "jsx" ? "jsx" : "js";
|
|
693
|
-
|
|
694
|
-
return { contents: source, loader };
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
#createCssStubPlugin() {
|
|
701
|
-
return {
|
|
702
|
-
name: "css-stub",
|
|
703
|
-
setup: (build) => {
|
|
704
|
-
build.onResolve({ filter: /\.css$/ }, (args) => ({
|
|
705
|
-
path: args.path,
|
|
706
|
-
namespace: "css-stub"
|
|
707
|
-
}));
|
|
708
|
-
|
|
709
|
-
build.onLoad({ filter: /.*/, namespace: "css-stub" }, () => ({
|
|
710
|
-
contents: "",
|
|
711
|
-
loader: "js"
|
|
712
|
-
}));
|
|
713
|
-
}
|
|
714
|
-
};
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
#createMarkerPlugin(options) {
|
|
718
|
-
const isDev = this.#isDev;
|
|
719
|
-
|
|
720
|
-
return {
|
|
721
|
-
name: "client-marker",
|
|
722
|
-
setup: (build) => {
|
|
723
|
-
if (options.platform === "browser") {
|
|
724
|
-
return;
|
|
725
501
|
}
|
|
726
|
-
|
|
727
|
-
build.onLoad({ filter: /\.tsx$/ }, async (args) => {
|
|
728
|
-
const source = await fs.promises.readFile(args.path, "utf8");
|
|
729
|
-
|
|
730
|
-
if (!/^\s*["']use client["']/.test(source.slice(0, 50))) {
|
|
731
|
-
return null;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
let ast;
|
|
735
|
-
try {
|
|
736
|
-
ast = parse(source, {
|
|
737
|
-
sourceType: "module",
|
|
738
|
-
plugins: ["typescript", "jsx"]
|
|
739
|
-
});
|
|
740
|
-
} catch {
|
|
741
|
-
if (isDev) {
|
|
742
|
-
console.warn(`${COLORS.yellow}⚠ Parse: ${args.path}${COLORS.reset}`);
|
|
743
|
-
}
|
|
744
|
-
return null;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
const exports = this.#findExports(ast);
|
|
748
|
-
if (!exports.length) {
|
|
749
|
-
return null;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
const symbolCode = `Symbol.for('__nitron_client_component__')`;
|
|
753
|
-
let additionalCode = "\n";
|
|
754
|
-
|
|
755
|
-
for (const exp of exports) {
|
|
756
|
-
additionalCode += `try { Object.defineProperty(${exp.name}, ${symbolCode}, { value: true }); ${exp.name}.displayName = "${exp.name}"; } catch {}\n`;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
return { contents: source + additionalCode, loader: "tsx" };
|
|
760
|
-
});
|
|
761
502
|
}
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
async #processCss(filename, hasTailwind, processor) {
|
|
766
|
-
const inputPath = path.join(this.#paths.cssInput, filename);
|
|
767
|
-
const outputPath = path.join(this.#paths.cssOutput, filename);
|
|
768
503
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
let content = this.#cache.css.get(inputPath);
|
|
772
|
-
if (!content) {
|
|
773
|
-
content = await fs.promises.readFile(inputPath, "utf8");
|
|
774
|
-
this.#cache.css.set(inputPath, content);
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
if (!hasTailwind) {
|
|
778
|
-
await fs.promises.writeFile(outputPath, content);
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
const result = await processor.process(content, {
|
|
783
|
-
from: inputPath,
|
|
784
|
-
to: outputPath,
|
|
785
|
-
map: this.#isDev ? { inline: false } : false
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
await fs.promises.writeFile(outputPath, result.css);
|
|
789
|
-
|
|
790
|
-
if (result.map) {
|
|
791
|
-
await fs.promises.writeFile(`${outputPath}.map`, result.map.toString());
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
#detectTailwind() {
|
|
796
|
-
if (!fs.existsSync(this.#paths.cssInput)) {
|
|
797
|
-
return false;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
const tailwindPattern = /@(import\s+["']tailwindcss["']|tailwind\s+(base|components|utilities))/;
|
|
801
|
-
|
|
802
|
-
for (const filename of fs.readdirSync(this.#paths.cssInput).filter(f => f.endsWith(".css"))) {
|
|
803
|
-
const filePath = path.join(this.#paths.cssInput, filename);
|
|
804
|
-
|
|
805
|
-
let content = this.#cache.css.get(filePath);
|
|
806
|
-
if (!content) {
|
|
807
|
-
content = fs.readFileSync(filePath, "utf8");
|
|
808
|
-
this.#cache.css.set(filePath, content);
|
|
504
|
+
for (const css of this.#analyzer.collectCss(file, meta, new Set())) {
|
|
505
|
+
cssSet.add(`/css/${path.basename(css)}`);
|
|
809
506
|
}
|
|
810
507
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
return false;
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
#discoverEntries(baseDir) {
|
|
820
|
-
if (!fs.existsSync(baseDir)) {
|
|
821
|
-
return { entries: [], meta: new Map() };
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
const files = this.#findTsxFiles(baseDir);
|
|
825
|
-
const { graph, imported, importedBy } = this.#buildDependencyGraph(files);
|
|
826
|
-
|
|
827
|
-
const entries = [...graph.entries()]
|
|
828
|
-
.filter(([file, meta]) => !imported.has(file) && meta.hasDefault && meta.jsx)
|
|
829
|
-
.map(([file]) => file);
|
|
830
|
-
|
|
831
|
-
this.#validateGraph(graph, entries, importedBy);
|
|
832
|
-
|
|
833
|
-
return { entries, meta: graph };
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
#findTsxFiles(dir, result = []) {
|
|
837
|
-
if (!fs.existsSync(dir)) {
|
|
838
|
-
return result;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
for (const item of fs.readdirSync(dir)) {
|
|
842
|
-
const fullPath = path.join(dir, item);
|
|
843
|
-
const stat = fs.lstatSync(fullPath);
|
|
844
|
-
|
|
845
|
-
if (stat.isSymbolicLink()) {
|
|
846
|
-
continue;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
if (stat.isDirectory()) {
|
|
850
|
-
this.#findTsxFiles(fullPath, result);
|
|
851
|
-
} else if (fullPath.endsWith(".tsx")) {
|
|
852
|
-
result.push(fullPath);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
return result;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
#buildDependencyGraph(files) {
|
|
860
|
-
const graph = new Map();
|
|
861
|
-
const imported = new Set();
|
|
862
|
-
const importedBy = new Map();
|
|
863
|
-
|
|
864
|
-
for (const file of files) {
|
|
865
|
-
const meta = this.#analyzeFile(file);
|
|
866
|
-
graph.set(file, meta);
|
|
867
|
-
|
|
868
|
-
for (const importPath of meta.imports) {
|
|
869
|
-
const resolvedPath = this.#resolveImport(file, importPath);
|
|
870
|
-
|
|
871
|
-
if (this.#isInRoot(resolvedPath)) {
|
|
872
|
-
imported.add(resolvedPath);
|
|
873
|
-
|
|
874
|
-
if (!importedBy.has(resolvedPath)) {
|
|
875
|
-
importedBy.set(resolvedPath, []);
|
|
876
|
-
}
|
|
877
|
-
importedBy.get(resolvedPath).push(file);
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
return { graph, imported, importedBy };
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
#analyzeFile(filePath) {
|
|
886
|
-
const source = fs.readFileSync(filePath, "utf8");
|
|
887
|
-
|
|
888
|
-
let ast;
|
|
889
|
-
try {
|
|
890
|
-
ast = parse(source, {
|
|
891
|
-
sourceType: "module",
|
|
892
|
-
plugins: ["typescript", "jsx"]
|
|
893
|
-
});
|
|
894
|
-
} catch {
|
|
895
|
-
return {
|
|
896
|
-
imports: [],
|
|
897
|
-
css: [],
|
|
898
|
-
hasDefault: false,
|
|
899
|
-
named: [],
|
|
900
|
-
jsx: false,
|
|
901
|
-
needsClient: false,
|
|
902
|
-
isClient: false
|
|
508
|
+
this.#manifest[key] = {
|
|
509
|
+
css: [...cssSet],
|
|
510
|
+
layouts: layoutChain.map(l => l.name.toLowerCase()),
|
|
511
|
+
hydrationScript: null
|
|
903
512
|
};
|
|
904
513
|
}
|
|
905
514
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
const meta = {
|
|
909
|
-
imports: new Set(),
|
|
910
|
-
css: new Set(),
|
|
911
|
-
hasDefault: false,
|
|
912
|
-
named: [],
|
|
913
|
-
jsx: false,
|
|
914
|
-
needsClient: false,
|
|
915
|
-
isClient,
|
|
916
|
-
reactNamespace: null
|
|
917
|
-
};
|
|
918
|
-
|
|
919
|
-
this.#traverse(ast, {
|
|
920
|
-
ImportDeclaration: (p) => {
|
|
921
|
-
const source = p.node.source.value;
|
|
922
|
-
|
|
923
|
-
if (source.startsWith(".")) {
|
|
924
|
-
meta.imports.add(source);
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
if (source.endsWith(".css")) {
|
|
928
|
-
const resolved = path.resolve(path.dirname(filePath), source);
|
|
929
|
-
if (resolved.startsWith(Paths.project)) {
|
|
930
|
-
meta.css.add(resolved);
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
if (source === "react") {
|
|
935
|
-
for (const specifier of p.node.specifiers) {
|
|
936
|
-
if (specifier.type === "ImportSpecifier" && CLIENT_HOOKS.has(specifier.imported.name)) {
|
|
937
|
-
meta.needsClient = true;
|
|
938
|
-
}
|
|
939
|
-
if (specifier.type === "ImportNamespaceSpecifier" || specifier.type === "ImportDefaultSpecifier") {
|
|
940
|
-
meta.reactNamespace = specifier.local.name;
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
},
|
|
945
|
-
|
|
946
|
-
MemberExpression: (p) => {
|
|
947
|
-
if (!meta.needsClient &&
|
|
948
|
-
p.node.object.name === meta.reactNamespace &&
|
|
949
|
-
CLIENT_HOOKS.has(p.node.property.name)) {
|
|
950
|
-
meta.needsClient = true;
|
|
951
|
-
}
|
|
952
|
-
},
|
|
953
|
-
|
|
954
|
-
CallExpression: (p) => {
|
|
955
|
-
if (!meta.needsClient &&
|
|
956
|
-
p.node.callee.type === "Identifier" &&
|
|
957
|
-
/^use[A-Z]/.test(p.node.callee.name)) {
|
|
958
|
-
meta.needsClient = true;
|
|
959
|
-
}
|
|
960
|
-
},
|
|
961
|
-
|
|
962
|
-
ExportNamedDeclaration: (p) => this.#extractNamedExports(p, meta),
|
|
963
|
-
ExportDefaultDeclaration: () => { meta.hasDefault = true; },
|
|
964
|
-
JSXElement: () => { meta.jsx = true; },
|
|
965
|
-
JSXFragment: () => { meta.jsx = true; }
|
|
966
|
-
});
|
|
967
|
-
|
|
968
|
-
if (meta.needsClient && !meta.isClient) {
|
|
969
|
-
throw this.#createError('Missing "use client"', {
|
|
970
|
-
File: path.relative(Paths.project, filePath),
|
|
971
|
-
Fix: 'Add "use client" at top'
|
|
972
|
-
});
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
return {
|
|
976
|
-
...meta,
|
|
977
|
-
imports: [...meta.imports],
|
|
978
|
-
css: [...meta.css]
|
|
979
|
-
};
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
#extractNamedExports(path, meta) {
|
|
983
|
-
const declaration = path.node.declaration;
|
|
984
|
-
|
|
985
|
-
if (declaration?.type === "VariableDeclaration") {
|
|
986
|
-
for (const decl of declaration.declarations) {
|
|
987
|
-
if (decl.id.type === "Identifier" && decl.init?.type === "ArrowFunctionExpression") {
|
|
988
|
-
meta.named.push(decl.id.name);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
if (declaration?.type === "FunctionDeclaration" && declaration.id?.name) {
|
|
994
|
-
meta.named.push(declaration.id.name);
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
#findExports(ast) {
|
|
999
|
-
const exports = [];
|
|
1000
|
-
|
|
1001
|
-
this.#traverse(ast, {
|
|
1002
|
-
ExportDefaultDeclaration: (p) => {
|
|
1003
|
-
const declaration = p.node.declaration;
|
|
1004
|
-
let name;
|
|
1005
|
-
|
|
1006
|
-
if (declaration.type === "Identifier") {
|
|
1007
|
-
name = declaration.name;
|
|
1008
|
-
} else if (declaration.type === "FunctionDeclaration" && declaration.id?.name) {
|
|
1009
|
-
name = declaration.id.name;
|
|
1010
|
-
} else if (declaration.type === "CallExpression" &&
|
|
1011
|
-
["memo", "forwardRef", "lazy"].includes(declaration.callee?.name)) {
|
|
1012
|
-
name = declaration.arguments[0]?.name || "__default__";
|
|
1013
|
-
} else {
|
|
1014
|
-
name = "__default__";
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
exports.push({ name, isDefault: true });
|
|
1018
|
-
},
|
|
1019
|
-
|
|
1020
|
-
ExportNamedDeclaration: (p) => {
|
|
1021
|
-
for (const specifier of p.node.specifiers || []) {
|
|
1022
|
-
if (specifier.type === "ExportSpecifier") {
|
|
1023
|
-
const name = specifier.exported.name === "default"
|
|
1024
|
-
? specifier.local.name
|
|
1025
|
-
: specifier.exported.name;
|
|
1026
|
-
exports.push({
|
|
1027
|
-
name,
|
|
1028
|
-
isDefault: specifier.exported.name === "default"
|
|
1029
|
-
});
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
const declaration = p.node.declaration;
|
|
1034
|
-
|
|
1035
|
-
if (declaration?.type === "FunctionDeclaration" && declaration.id?.name) {
|
|
1036
|
-
exports.push({ name: declaration.id.name, isDefault: false });
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
if (declaration?.type === "VariableDeclaration") {
|
|
1040
|
-
for (const decl of declaration.declarations) {
|
|
1041
|
-
if (decl.id.type === "Identifier" && decl.init?.type === "ArrowFunctionExpression") {
|
|
1042
|
-
exports.push({ name: decl.id.name, isDefault: false });
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
});
|
|
1048
|
-
|
|
1049
|
-
return exports;
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
#validateGraph(graph, entries, importedBy) {
|
|
1053
|
-
const entrySet = new Set(entries);
|
|
1054
|
-
const relativePath = (p) => path.relative(Paths.project, p);
|
|
1055
|
-
|
|
1056
|
-
for (const [filePath, meta] of graph.entries()) {
|
|
1057
|
-
if (!meta.isClient) {
|
|
1058
|
-
continue;
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
if (entrySet.has(filePath) && meta.hasDefault && meta.jsx) {
|
|
1062
|
-
const importers = importedBy.get(filePath);
|
|
1063
|
-
if (importers?.length) {
|
|
1064
|
-
throw this.#createError("Client Entry Imported", {
|
|
1065
|
-
Entry: relativePath(filePath),
|
|
1066
|
-
By: relativePath(importers[0]),
|
|
1067
|
-
Fix: "Use as island"
|
|
1068
|
-
});
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
// Next.js pattern: Client component cannot import server component
|
|
1073
|
-
// Server component can render client component as children
|
|
1074
|
-
for (const importPath of meta.imports) {
|
|
1075
|
-
const resolvedPath = this.#resolveImport(filePath, importPath);
|
|
1076
|
-
const resolvedMeta = graph.get(resolvedPath);
|
|
1077
|
-
|
|
1078
|
-
// If importing a non-client component (server component)
|
|
1079
|
-
if (resolvedMeta && !resolvedMeta.isClient) {
|
|
1080
|
-
throw this.#createError("Boundary Violation", {
|
|
1081
|
-
Client: relativePath(filePath),
|
|
1082
|
-
Server: relativePath(resolvedPath),
|
|
1083
|
-
Fix: `Server components cannot be imported into client components. Use composition pattern: render "${path.basename(resolvedPath, ".tsx")}" as a parent and pass client component as children.`
|
|
1084
|
-
});
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
#addToManifest(entries, meta, baseDir, namespace) {
|
|
1091
|
-
for (const file of entries) {
|
|
1092
|
-
const fileMeta = meta.get(file);
|
|
1093
|
-
const viewPath = path.relative(baseDir, file)
|
|
515
|
+
for (const layout of layouts) {
|
|
516
|
+
const layoutPath = path.relative(baseDir, layout)
|
|
1094
517
|
.replace(/\.tsx$/, "")
|
|
1095
|
-
.toLowerCase()
|
|
1096
518
|
.replace(/\\/g, "/");
|
|
1097
519
|
|
|
1098
|
-
const key = `${namespace}:${
|
|
520
|
+
const key = `${namespace}:layout:${layoutPath.toLowerCase()}`;
|
|
1099
521
|
|
|
1100
522
|
if (this.#manifest[key]) {
|
|
1101
|
-
throw new Error(`Duplicate: ${key}`);
|
|
523
|
+
throw new Error(`Duplicate layout: ${key}`);
|
|
1102
524
|
}
|
|
1103
525
|
|
|
1104
|
-
const cssFiles = [...this.#collectCss(
|
|
526
|
+
const cssFiles = [...this.#analyzer.collectCss(layout, meta, new Set())]
|
|
1105
527
|
.map(cssPath => `/css/${path.basename(cssPath)}`);
|
|
1106
528
|
|
|
1107
529
|
this.#manifest[key] = {
|
|
1108
530
|
css: cssFiles,
|
|
1109
|
-
|
|
531
|
+
isLayout: true
|
|
1110
532
|
};
|
|
1111
533
|
}
|
|
1112
534
|
}
|
|
1113
535
|
|
|
1114
536
|
#writeManifest() {
|
|
1115
|
-
const manifestPath = path.join(Paths.
|
|
537
|
+
const manifestPath = path.join(Paths.build, "manifest.json");
|
|
1116
538
|
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
1117
539
|
fs.writeFileSync(manifestPath, JSON.stringify(this.#manifest, null, 2));
|
|
1118
540
|
}
|
|
1119
541
|
|
|
1120
|
-
#collectCss(file, meta, seen, depth = 0) {
|
|
1121
|
-
if (depth > MAX_DEPTH || seen.has(file)) {
|
|
1122
|
-
return new Set();
|
|
1123
|
-
}
|
|
1124
|
-
seen.add(file);
|
|
1125
|
-
|
|
1126
|
-
const fileMeta = meta.get(file);
|
|
1127
|
-
if (!fileMeta) {
|
|
1128
|
-
return new Set();
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
const result = new Set(fileMeta.css);
|
|
1132
|
-
|
|
1133
|
-
for (const importPath of fileMeta.imports) {
|
|
1134
|
-
const resolvedPath = this.#resolveImport(file, importPath);
|
|
1135
|
-
|
|
1136
|
-
if (meta.has(resolvedPath)) {
|
|
1137
|
-
const childCss = this.#collectCss(resolvedPath, meta, seen, depth + 1);
|
|
1138
|
-
for (const cssPath of childCss) {
|
|
1139
|
-
result.add(cssPath);
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
return result;
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
#resolveImport(fromFile, relativePath) {
|
|
1148
|
-
const cacheKey = `${fromFile}|${relativePath}`;
|
|
1149
|
-
const cached = this.#cache.imports.get(cacheKey);
|
|
1150
|
-
|
|
1151
|
-
if (cached) {
|
|
1152
|
-
return cached;
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
const resolved = path.resolve(path.dirname(fromFile), relativePath);
|
|
1156
|
-
|
|
1157
|
-
if (!this.#isInRoot(resolved)) {
|
|
1158
|
-
throw this.#createError("Path Traversal", {
|
|
1159
|
-
Import: relativePath,
|
|
1160
|
-
Outside: resolved
|
|
1161
|
-
});
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
const extensions = [".tsx", ".ts", ".jsx", ".js", "/index.tsx", "/index.ts", ""];
|
|
1165
|
-
|
|
1166
|
-
for (const ext of extensions) {
|
|
1167
|
-
const fullPath = resolved + ext;
|
|
1168
|
-
|
|
1169
|
-
if (ext === "" && fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
|
|
1170
|
-
this.#cache.imports.set(cacheKey, fullPath);
|
|
1171
|
-
return fullPath;
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
if (ext && fs.existsSync(fullPath)) {
|
|
1175
|
-
this.#cache.imports.set(cacheKey, fullPath);
|
|
1176
|
-
return fullPath;
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
this.#cache.imports.set(cacheKey, resolved);
|
|
1181
|
-
return resolved;
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
#isInRoot(filePath) {
|
|
1185
|
-
const normalized = path.normalize(filePath);
|
|
1186
|
-
const projectNormalized = path.normalize(Paths.project);
|
|
1187
|
-
const frameworkNormalized = path.normalize(Paths.framework);
|
|
1188
|
-
|
|
1189
|
-
// Allow both project paths and framework paths
|
|
1190
|
-
return normalized.startsWith(projectNormalized + path.sep) ||
|
|
1191
|
-
normalized === projectNormalized ||
|
|
1192
|
-
normalized.startsWith(frameworkNormalized + path.sep) ||
|
|
1193
|
-
normalized === frameworkNormalized;
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
542
|
#writeJsxRuntime() {
|
|
1197
543
|
fs.mkdirSync(path.dirname(this.#paths.jsxRuntime), { recursive: true });
|
|
1198
544
|
fs.writeFileSync(this.#paths.jsxRuntime, JSX_RUNTIME);
|
|
@@ -1205,8 +551,11 @@ class Builder {
|
|
|
1205
551
|
this.#paths.nitronTemp
|
|
1206
552
|
];
|
|
1207
553
|
|
|
554
|
+
const projectDir = path.normalize(Paths.project) + path.sep;
|
|
555
|
+
|
|
1208
556
|
for (const dir of dirsToClean) {
|
|
1209
|
-
|
|
557
|
+
const normalizedDir = path.normalize(dir);
|
|
558
|
+
if (!normalizedDir.startsWith(projectDir)) {
|
|
1210
559
|
throw new Error(`Unsafe path: ${dir}`);
|
|
1211
560
|
}
|
|
1212
561
|
|
|
@@ -1217,22 +566,20 @@ class Builder {
|
|
|
1217
566
|
}
|
|
1218
567
|
|
|
1219
568
|
#cleanupTemp() {
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
throw new Error(`Unsafe path: ${this.#paths.nitronTemp}`);
|
|
1223
|
-
}
|
|
1224
|
-
fs.rmSync(this.#paths.nitronTemp, { recursive: true, force: true });
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
#createError(title, details = {}) {
|
|
1229
|
-
let message = `\n${COLORS.red}✖ ${title}${COLORS.reset}\n\n`;
|
|
569
|
+
const projectDir = path.normalize(Paths.project) + path.sep;
|
|
570
|
+
const normalizedTemp = path.normalize(this.#paths.nitronTemp);
|
|
1230
571
|
|
|
1231
|
-
|
|
1232
|
-
|
|
572
|
+
if (!normalizedTemp.startsWith(projectDir)) {
|
|
573
|
+
throw new Error(`Unsafe path: ${this.#paths.nitronTemp}`);
|
|
1233
574
|
}
|
|
1234
575
|
|
|
1235
|
-
|
|
576
|
+
if (fs.existsSync(this.#paths.nitronTemp)) {
|
|
577
|
+
for (const entry of fs.readdirSync(this.#paths.nitronTemp)) {
|
|
578
|
+
if (entry === "build-cache.json") continue;
|
|
579
|
+
const fullPath = path.join(this.#paths.nitronTemp, entry);
|
|
580
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
581
|
+
}
|
|
582
|
+
}
|
|
1236
583
|
}
|
|
1237
584
|
}
|
|
1238
585
|
|