@noego/app 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/AGENTS.md +457 -0
- package/bin/app.js +5 -0
- package/docs/design.md +107 -0
- package/package.json +24 -0
- package/src/args.js +180 -0
- package/src/build/bootstrap.js +43 -0
- package/src/build/client-modules.js +9 -0
- package/src/build/client.js +206 -0
- package/src/build/context.js +16 -0
- package/src/build/fix-imports.js +99 -0
- package/src/build/helpers.js +29 -0
- package/src/build/html.js +83 -0
- package/src/build/openapi.js +249 -0
- package/src/build/plugins/client-exclude.js +90 -0
- package/src/build/runtime-manifest.js +64 -0
- package/src/build/server.js +294 -0
- package/src/build/ssr.js +257 -0
- package/src/build/ui-common.js +188 -0
- package/src/build/vite.js +45 -0
- package/src/cli.js +72 -0
- package/src/commands/build.js +59 -0
- package/src/commands/preview.js +33 -0
- package/src/commands/serve.js +213 -0
- package/src/config.js +584 -0
- package/src/logger.js +16 -0
- package/src/utils/command.js +23 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import YAML from 'yaml';
|
|
6
|
+
|
|
7
|
+
const HTTP_METHODS = new Set([
|
|
8
|
+
'get',
|
|
9
|
+
'post',
|
|
10
|
+
'put',
|
|
11
|
+
'delete',
|
|
12
|
+
'patch',
|
|
13
|
+
'head',
|
|
14
|
+
'options',
|
|
15
|
+
'trace'
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export async function discoverProject(context) {
|
|
19
|
+
const [serverDoc, uiDoc] = await Promise.all([
|
|
20
|
+
loadOpenApiDocumentIfPresent(context, context.config.server.openapiFile),
|
|
21
|
+
loadOpenApiDocumentIfPresent(context, context.config.ui.openapiFile)
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const serverRoutes = collectRoutes(serverDoc);
|
|
25
|
+
const uiRoutes = collectRoutes(uiDoc);
|
|
26
|
+
|
|
27
|
+
const serverControllers = new Set();
|
|
28
|
+
const serverMiddleware = new Set();
|
|
29
|
+
for (const route of serverRoutes) {
|
|
30
|
+
if (route.controller) {
|
|
31
|
+
serverControllers.add(route.controller);
|
|
32
|
+
}
|
|
33
|
+
for (const middleware of route.middleware) {
|
|
34
|
+
serverMiddleware.add(middleware);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const uiViews = new Set();
|
|
39
|
+
const uiLayouts = new Set();
|
|
40
|
+
const uiMiddleware = new Set();
|
|
41
|
+
for (const route of uiRoutes) {
|
|
42
|
+
if (route.view) {
|
|
43
|
+
uiViews.add(route.view);
|
|
44
|
+
}
|
|
45
|
+
for (const layout of route.layout) {
|
|
46
|
+
uiLayouts.add(layout);
|
|
47
|
+
}
|
|
48
|
+
for (const middleware of route.middleware) {
|
|
49
|
+
uiMiddleware.add(middleware);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const fallback = uiDoc
|
|
54
|
+
? {
|
|
55
|
+
view: ensureString(uiDoc['x-fallback-view']),
|
|
56
|
+
layouts: ensureArray(uiDoc['x-fallback-layout']),
|
|
57
|
+
middleware: ensureArray(uiDoc['x-fallback-middleware'])
|
|
58
|
+
}
|
|
59
|
+
: { view: null, layouts: [], middleware: [] };
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
server: {
|
|
63
|
+
document: serverDoc,
|
|
64
|
+
routes: serverRoutes,
|
|
65
|
+
controllers: Array.from(serverControllers),
|
|
66
|
+
middleware: Array.from(serverMiddleware)
|
|
67
|
+
},
|
|
68
|
+
ui: {
|
|
69
|
+
document: uiDoc,
|
|
70
|
+
routes: uiRoutes,
|
|
71
|
+
views: Array.from(uiViews),
|
|
72
|
+
layouts: Array.from(uiLayouts),
|
|
73
|
+
middleware: Array.from(uiMiddleware),
|
|
74
|
+
fallback
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function loadOpenApiDocument(context, filePath) {
|
|
80
|
+
const absolutePath = path.resolve(filePath);
|
|
81
|
+
const content = await fs.readFile(absolutePath, 'utf8');
|
|
82
|
+
const parsed = YAML.parse(content);
|
|
83
|
+
|
|
84
|
+
if (isStitchConfig(parsed)) {
|
|
85
|
+
const StitchEngine = await loadStitchEngine(context);
|
|
86
|
+
const engine = new StitchEngine();
|
|
87
|
+
const result = engine.buildSync(absolutePath, { format: 'json' });
|
|
88
|
+
if (!result.success) {
|
|
89
|
+
throw new Error(`Stitch build failed for ${absolutePath}: ${result.error}`);
|
|
90
|
+
}
|
|
91
|
+
const data = typeof result.data === 'string' ? JSON.parse(result.data) : result.data;
|
|
92
|
+
return data;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!isOpenApiDocument(parsed)) {
|
|
96
|
+
throw new Error(`Unsupported OpenAPI document at ${absolutePath}: expected stitch or OpenAPI structure`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function loadOpenApiDocumentIfPresent(context, filePath) {
|
|
103
|
+
try {
|
|
104
|
+
return await loadOpenApiDocument(context, filePath);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (err && (err.code === 'ENOENT' || String(err.message || '').includes('no such file'))) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
// If the path is falsy or undefined, also treat as absent
|
|
110
|
+
if (!filePath) return null;
|
|
111
|
+
// For YAML parse errors or stitch failures, surface them
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function collectRoutes(document) {
|
|
117
|
+
const routes = [];
|
|
118
|
+
const inheritedMiddleware = ensureArray(document?.paths?.['x-middleware']);
|
|
119
|
+
|
|
120
|
+
routes.push(...parsePaths(document, inheritedMiddleware));
|
|
121
|
+
routes.push(...parseModules(document, inheritedMiddleware));
|
|
122
|
+
|
|
123
|
+
return routes;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseModules(document, inheritedMiddleware = []) {
|
|
127
|
+
const modulesConfig = document.modules || document.module;
|
|
128
|
+
if (!modulesConfig) return [];
|
|
129
|
+
|
|
130
|
+
const modules = Array.isArray(modulesConfig)
|
|
131
|
+
? modulesConfig
|
|
132
|
+
: Object.values(modulesConfig);
|
|
133
|
+
|
|
134
|
+
const routes = [];
|
|
135
|
+
for (const moduleConfig of modules) {
|
|
136
|
+
if (!moduleConfig || typeof moduleConfig !== 'object') continue;
|
|
137
|
+
|
|
138
|
+
const basePath = moduleConfig.basePath || '';
|
|
139
|
+
const baseLayouts = ensureArray(moduleConfig.baseLayouts);
|
|
140
|
+
const moduleMiddleware = ensureArray(moduleConfig['x-middleware']);
|
|
141
|
+
const combinedMiddleware = [...inheritedMiddleware, ...moduleMiddleware];
|
|
142
|
+
|
|
143
|
+
const moduleRoutes = parsePaths(moduleConfig, combinedMiddleware);
|
|
144
|
+
for (const route of moduleRoutes) {
|
|
145
|
+
route.path = joinPaths(basePath, route.path);
|
|
146
|
+
route.layout = [...baseLayouts, ...route.layout];
|
|
147
|
+
routes.push(route);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return routes;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parsePaths(document, inheritedMiddleware = []) {
|
|
155
|
+
const paths = document.paths;
|
|
156
|
+
if (!paths || typeof paths !== 'object') {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const routes = [];
|
|
161
|
+
const globalMiddleware = [
|
|
162
|
+
...inheritedMiddleware,
|
|
163
|
+
...ensureArray(paths['x-middleware'])
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
for (const [routePath, definition] of Object.entries(paths)) {
|
|
167
|
+
if (!routePath || typeof definition !== 'object') continue;
|
|
168
|
+
if (!routePath.startsWith('/')) continue;
|
|
169
|
+
|
|
170
|
+
for (const [method, config] of Object.entries(definition)) {
|
|
171
|
+
if (!HTTP_METHODS.has(method.toLowerCase())) continue;
|
|
172
|
+
routes.push(createRoute(routePath, method.toLowerCase(), config, globalMiddleware));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return routes;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function createRoute(routePath, method, config, inheritedMiddleware) {
|
|
180
|
+
const layout = ensureArray(config?.['x-layout']);
|
|
181
|
+
const middleware = [
|
|
182
|
+
...inheritedMiddleware,
|
|
183
|
+
...ensureArray(config?.['x-middleware'])
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
path: routePath,
|
|
188
|
+
method,
|
|
189
|
+
view: ensureString(config?.['x-view']),
|
|
190
|
+
layout,
|
|
191
|
+
middleware,
|
|
192
|
+
controller: ensureString(config?.['x-controller'])
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function ensureArray(value) {
|
|
197
|
+
if (value == null) return [];
|
|
198
|
+
if (Array.isArray(value)) {
|
|
199
|
+
return value.filter((item) => typeof item === 'string' && item.length > 0);
|
|
200
|
+
}
|
|
201
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
202
|
+
return [value];
|
|
203
|
+
}
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function ensureString(value) {
|
|
208
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function joinPaths(basePath, child) {
|
|
212
|
+
if (!basePath) return child;
|
|
213
|
+
if (!child) return basePath;
|
|
214
|
+
const base = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
|
|
215
|
+
const tail = child.startsWith('/') ? child : `/${child}`;
|
|
216
|
+
return `${base}${tail}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function isStitchConfig(document) {
|
|
220
|
+
return document && typeof document === 'object' && 'stitch' in document;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isOpenApiDocument(document) {
|
|
224
|
+
return document && typeof document === 'object' && ('paths' in document || 'modules' in document || 'module' in document);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function loadStitchEngine(context) {
|
|
228
|
+
try {
|
|
229
|
+
const mod = context.requireFromRoot('@noego/stitch');
|
|
230
|
+
const Engine =
|
|
231
|
+
mod?.StitchEngine ??
|
|
232
|
+
mod?.default?.StitchEngine ??
|
|
233
|
+
mod?.default;
|
|
234
|
+
if (Engine) return Engine;
|
|
235
|
+
} catch {
|
|
236
|
+
// Fallback to dynamic import
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const resolved = context.requireFromRoot.resolve('@noego/stitch');
|
|
240
|
+
const imported = await import(pathToFileURL(resolved).href);
|
|
241
|
+
const Engine =
|
|
242
|
+
imported?.StitchEngine ??
|
|
243
|
+
imported?.default?.StitchEngine ??
|
|
244
|
+
imported?.default;
|
|
245
|
+
if (!Engine) {
|
|
246
|
+
throw new Error('Unable to resolve StitchEngine from @noego/stitch');
|
|
247
|
+
}
|
|
248
|
+
return Engine;
|
|
249
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import picomatch from 'picomatch';
|
|
4
|
+
|
|
5
|
+
export function clientExcludePlugin(specs, { projectRoot }) {
|
|
6
|
+
if (!Array.isArray(specs) || specs.length === 0) {
|
|
7
|
+
return {
|
|
8
|
+
name: 'hammer-client-exclude'
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const matchers = specs.map((spec) => createMatcher(spec, projectRoot));
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
name: 'hammer-client-exclude',
|
|
16
|
+
enforce: 'pre',
|
|
17
|
+
load(id) {
|
|
18
|
+
if (!id || !isFile(id)) return null;
|
|
19
|
+
if (!shouldExclude(id, matchers)) return null;
|
|
20
|
+
const messagePath = toPosix(path.relative(projectRoot, id));
|
|
21
|
+
return generateStubModule(messagePath.startsWith('..') ? id : messagePath);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createMatcher(spec, projectRoot) {
|
|
27
|
+
const options = { dot: true };
|
|
28
|
+
if (spec.isAbsolute) {
|
|
29
|
+
const matcher = picomatch(toPosix(spec.pattern), options);
|
|
30
|
+
return (id) => matcher(toPosix(id));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const cwd = spec.cwd ? path.resolve(spec.cwd) : projectRoot;
|
|
34
|
+
const matcher = picomatch(toPosix(spec.pattern), options);
|
|
35
|
+
return (id) => {
|
|
36
|
+
const relative = path.relative(cwd, id);
|
|
37
|
+
if (relative.startsWith('..')) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return matcher(toPosix(relative));
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function shouldExclude(id, matchers) {
|
|
45
|
+
return matchers.some((matches) => {
|
|
46
|
+
try {
|
|
47
|
+
return matches(id);
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function generateStubModule(displayPath) {
|
|
55
|
+
const message = `Excluded from client bundle: ${displayPath}`;
|
|
56
|
+
return `
|
|
57
|
+
const error = new Error(${JSON.stringify(message)});
|
|
58
|
+
|
|
59
|
+
const thrower = () => {
|
|
60
|
+
throw error;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handler = {
|
|
64
|
+
apply() { throw error; },
|
|
65
|
+
construct() { throw error; },
|
|
66
|
+
get() { throw error; },
|
|
67
|
+
set() { throw error; },
|
|
68
|
+
has() { return false; }
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const callableProxy = new Proxy(thrower, handler);
|
|
72
|
+
const namespaceProxy = new Proxy({}, {
|
|
73
|
+
get() { throw error; },
|
|
74
|
+
set() { throw error; },
|
|
75
|
+
has() { return false; }
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export default callableProxy;
|
|
79
|
+
export const __namespace = namespaceProxy;
|
|
80
|
+
export const __esModule = true;
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function toPosix(input) {
|
|
85
|
+
return input.split(path.sep).join('/');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isFile(id) {
|
|
89
|
+
return !id.includes('\0');
|
|
90
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export async function writeRuntimeManifest(context, artifacts) {
|
|
5
|
+
const { config } = context;
|
|
6
|
+
const targetPath = path.join(config.layout.serverOutDir, 'runtime-manifest.json');
|
|
7
|
+
|
|
8
|
+
const payload = {
|
|
9
|
+
generatedAt: new Date().toISOString(),
|
|
10
|
+
mode: config.mode,
|
|
11
|
+
rootDir: config.rootDir,
|
|
12
|
+
outDir: config.outDir,
|
|
13
|
+
server: {
|
|
14
|
+
entry: config.server.entry.relativeToRoot,
|
|
15
|
+
controllersDir: path.relative(config.rootDir, config.server.controllersDir),
|
|
16
|
+
middlewareDir: path.relative(config.rootDir, config.server.middlewareDir),
|
|
17
|
+
openapi: path.relative(config.rootDir, config.server.openapiFile),
|
|
18
|
+
sqlGlobs: config.server.sqlGlobs.map((spec) => spec.pattern)
|
|
19
|
+
},
|
|
20
|
+
ui: {
|
|
21
|
+
page: config.ui.page.relativeToRoot,
|
|
22
|
+
openapi: path.relative(config.rootDir, config.ui.openapiFile),
|
|
23
|
+
assets: config.assets.map((spec) => spec.pattern),
|
|
24
|
+
clientExclude: config.ui.clientExclude.map((spec) => spec.pattern)
|
|
25
|
+
},
|
|
26
|
+
discovery: {
|
|
27
|
+
server: {
|
|
28
|
+
controllers: artifacts.discovery.server.controllers,
|
|
29
|
+
middleware: artifacts.discovery.server.middleware
|
|
30
|
+
},
|
|
31
|
+
ui: {
|
|
32
|
+
views: artifacts.discovery.ui.views,
|
|
33
|
+
layouts: artifacts.discovery.ui.layouts,
|
|
34
|
+
middleware: artifacts.discovery.ui.middleware
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
client: {
|
|
38
|
+
manifest: relativeToOut(config, artifacts.client.manifestPath),
|
|
39
|
+
entry: artifacts.client.entryGraph.entry?.file
|
|
40
|
+
? `/assets/${toPosix(artifacts.client.entryGraph.entry.file)}`
|
|
41
|
+
: null
|
|
42
|
+
},
|
|
43
|
+
ssr: {
|
|
44
|
+
manifest: artifacts.ssr.manifestPath
|
|
45
|
+
? relativeToOut(config, artifacts.ssr.manifestPath)
|
|
46
|
+
: null
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
await fs.writeFile(targetPath, JSON.stringify(payload, null, 2));
|
|
51
|
+
return targetPath;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function relativeToRoot(config, absolutePath) {
|
|
55
|
+
return path.relative(config.rootDir, absolutePath);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function relativeToOut(config, absolutePath) {
|
|
59
|
+
return path.relative(config.outDir, absolutePath);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function toPosix(value) {
|
|
63
|
+
return value.split(path.sep).join('/');
|
|
64
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
// No URL utilities needed when not vendoring Forge
|
|
4
|
+
|
|
5
|
+
import { runCommand } from '../utils/command.js';
|
|
6
|
+
import { fixImportExtensions } from './fix-imports.js';
|
|
7
|
+
|
|
8
|
+
export async function buildServer(context, discovery) {
|
|
9
|
+
const { logger } = context;
|
|
10
|
+
logger.info('Building server bundle');
|
|
11
|
+
|
|
12
|
+
await runTypeScriptCompiler(context);
|
|
13
|
+
await reorganizeServerOutputs(context);
|
|
14
|
+
await mirrorUiModules(context);
|
|
15
|
+
await syncRootEntry(context);
|
|
16
|
+
await copyServerAssets(context);
|
|
17
|
+
await fixImportExtensions(context.config.outDir);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function runTypeScriptCompiler(context) {
|
|
21
|
+
const { config, logger, requireFromRoot } = context;
|
|
22
|
+
const tsconfigPath = path.join(config.rootDir, 'tsconfig.json');
|
|
23
|
+
if (!(await pathExists(tsconfigPath))) {
|
|
24
|
+
throw new Error(`Cannot find tsconfig.json at ${tsconfigPath}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const tscBin = requireFromRoot.resolve('typescript/bin/tsc');
|
|
28
|
+
logger.info('Running TypeScript compiler');
|
|
29
|
+
|
|
30
|
+
await fs.rm(config.layout.tsOutDir, { recursive: true, force: true });
|
|
31
|
+
await fs.mkdir(config.layout.tsOutDir, { recursive: true });
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await runCommand(
|
|
35
|
+
process.execPath,
|
|
36
|
+
[
|
|
37
|
+
tscBin,
|
|
38
|
+
'--project',
|
|
39
|
+
tsconfigPath,
|
|
40
|
+
'--outDir',
|
|
41
|
+
config.layout.tsOutDir,
|
|
42
|
+
'--pretty',
|
|
43
|
+
'false',
|
|
44
|
+
'--noEmitOnError',
|
|
45
|
+
'false'
|
|
46
|
+
],
|
|
47
|
+
{ cwd: config.rootDir }
|
|
48
|
+
);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error?.code === 'COMMAND_FAILED') {
|
|
51
|
+
logger.warn('TypeScript reported errors; continuing with emitted output.');
|
|
52
|
+
} else {
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function reorganizeServerOutputs(context) {
|
|
59
|
+
const { config, logger } = context;
|
|
60
|
+
const { tsOutDir, serverOutDir, middlewareOutDir } = config.layout;
|
|
61
|
+
|
|
62
|
+
await fs.rm(serverOutDir, { recursive: true, force: true });
|
|
63
|
+
await fs.mkdir(serverOutDir, { recursive: true });
|
|
64
|
+
await fs.cp(tsOutDir, serverOutDir, { recursive: true });
|
|
65
|
+
|
|
66
|
+
const nestedServerDir = path.join(serverOutDir, 'server');
|
|
67
|
+
if (await pathExists(nestedServerDir)) {
|
|
68
|
+
await fs.cp(nestedServerDir, serverOutDir, { recursive: true, force: true });
|
|
69
|
+
await fs.rm(nestedServerDir, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const compiledEntryRel = replaceExtension(
|
|
73
|
+
path.relative(config.rootDir, config.server.entry.absolute),
|
|
74
|
+
'.js'
|
|
75
|
+
);
|
|
76
|
+
const compiledEntryPath = path.join(serverOutDir, compiledEntryRel);
|
|
77
|
+
if (!(await pathExists(compiledEntryPath))) {
|
|
78
|
+
throw new Error(`Compiled server entry not found at ${compiledEntryPath}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Mirror middleware directory into dist root for Dinner runtime lookup
|
|
82
|
+
await fs.rm(middlewareOutDir, { recursive: true, force: true });
|
|
83
|
+
const middlewareRel = path.relative(config.rootDir, config.server.middlewareDir);
|
|
84
|
+
if (!middlewareRel.startsWith('..')) {
|
|
85
|
+
const compiledMiddleware = path.join(serverOutDir, middlewareRel);
|
|
86
|
+
await copyTree(compiledMiddleware, middlewareOutDir);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
logger.info('Server TypeScript output staged');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Vendoring of @noego/forge removed to preserve 1:1 layout
|
|
93
|
+
|
|
94
|
+
async function copyServerAssets(context) {
|
|
95
|
+
const { config, logger, requireFromRoot } = context;
|
|
96
|
+
const globModule = requireFromRoot('glob');
|
|
97
|
+
const glob = typeof globModule === 'function' ? globModule : globModule.glob.bind(globModule);
|
|
98
|
+
|
|
99
|
+
// SQL files: must live next to compiled JS under dist/server/**
|
|
100
|
+
const copiedSql = new Set();
|
|
101
|
+
for (const spec of config.server.sqlGlobs) {
|
|
102
|
+
const matches = await glob(spec.pattern, {
|
|
103
|
+
cwd: spec.cwd,
|
|
104
|
+
absolute: true,
|
|
105
|
+
nodir: false,
|
|
106
|
+
dot: true
|
|
107
|
+
});
|
|
108
|
+
for (const absoluteSrc of matches) {
|
|
109
|
+
await copyRelativeTo(absoluteSrc, config.layout.serverOutDir, config, logger, {
|
|
110
|
+
seen: copiedSql,
|
|
111
|
+
stripSegments: ['server']
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Server OpenAPI + openapi directory (optional)
|
|
117
|
+
await copyRelativeTo(config.server.openapiFile, config.outDir, config, logger, { allowMissing: true });
|
|
118
|
+
const serverOpenapiDir = path.join(path.dirname(config.server.openapiFile), 'openapi');
|
|
119
|
+
await copyRelativeTo(serverOpenapiDir, config.outDir, config, logger, { allowMissing: true });
|
|
120
|
+
|
|
121
|
+
// UI OpenAPI + openapi directory (optional)
|
|
122
|
+
await copyRelativeTo(config.ui.openapiFile, config.outDir, config, logger, { allowMissing: true });
|
|
123
|
+
const uiOpenapiDir = path.join(path.dirname(config.ui.openapiFile), 'openapi');
|
|
124
|
+
await copyRelativeTo(uiOpenapiDir, config.outDir, config, logger, { allowMissing: true });
|
|
125
|
+
|
|
126
|
+
// Additional assets (ui/resources, database, etc.)
|
|
127
|
+
const copied = new Set();
|
|
128
|
+
for (const spec of config.assets) {
|
|
129
|
+
const matches = await glob(spec.pattern, {
|
|
130
|
+
cwd: spec.cwd,
|
|
131
|
+
absolute: true,
|
|
132
|
+
nodir: false,
|
|
133
|
+
dot: true
|
|
134
|
+
});
|
|
135
|
+
for (const absoluteSrc of matches) {
|
|
136
|
+
const relative = path.relative(config.rootDir, absoluteSrc);
|
|
137
|
+
if (relative.startsWith('..')) {
|
|
138
|
+
logger.warn(`Skipping asset outside project root: ${absoluteSrc}`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (copied.has(relative)) continue;
|
|
142
|
+
copied.add(relative);
|
|
143
|
+
|
|
144
|
+
const destination = path.join(config.outDir, relative);
|
|
145
|
+
const stat = await fs.stat(absoluteSrc);
|
|
146
|
+
if (stat.isDirectory()) {
|
|
147
|
+
await copyTree(absoluteSrc, destination);
|
|
148
|
+
} else {
|
|
149
|
+
await ensureParentDir(destination);
|
|
150
|
+
await fs.copyFile(absoluteSrc, destination);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
logger.info('Server runtime assets copied');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function mirrorUiModules(context) {
|
|
159
|
+
const { config } = context;
|
|
160
|
+
const destinationDir = config.layout.uiOutDir;
|
|
161
|
+
await fs.rm(destinationDir, { recursive: true, force: true }).catch(() => {});
|
|
162
|
+
|
|
163
|
+
// Overlay compiled UI outputs from tsc emit so .ts becomes .js under mirrored UI root
|
|
164
|
+
const compiledUiDir = path.join(
|
|
165
|
+
config.layout.serverOutDir,
|
|
166
|
+
path.relative(config.rootDir, config.ui.rootDir)
|
|
167
|
+
);
|
|
168
|
+
if (await pathExists(compiledUiDir)) {
|
|
169
|
+
await fs.cp(compiledUiDir, destinationDir, { recursive: true, force: true });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function syncRootEntry(context) {
|
|
174
|
+
const { config } = context;
|
|
175
|
+
const compiledEntryRel = replaceExtension(
|
|
176
|
+
path.relative(config.rootDir, config.server.entry.absolute),
|
|
177
|
+
'.js'
|
|
178
|
+
);
|
|
179
|
+
const sourcePath = path.join(config.layout.serverOutDir, compiledEntryRel);
|
|
180
|
+
if (!(await pathExists(sourcePath))) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const destinationPath = path.join(config.outDir, compiledEntryRel);
|
|
184
|
+
await ensureParentDir(destinationPath);
|
|
185
|
+
await fs.copyFile(sourcePath, destinationPath);
|
|
186
|
+
await copyIfExists(`${sourcePath}.map`, `${destinationPath}.map`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// No options wrapper; we mirror compiled UI files as-is.
|
|
190
|
+
|
|
191
|
+
async function copyRelativeTo(
|
|
192
|
+
sourcePath,
|
|
193
|
+
targetRoot,
|
|
194
|
+
config,
|
|
195
|
+
logger,
|
|
196
|
+
{ allowMissing = false, seen, stripSegments } = {}
|
|
197
|
+
) {
|
|
198
|
+
if (!(await pathExists(sourcePath))) {
|
|
199
|
+
if (!allowMissing) {
|
|
200
|
+
logger.warn(`Skipping missing path: ${sourcePath}`);
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
let relative = path.relative(config.rootDir, sourcePath);
|
|
205
|
+
if (relative.startsWith('..')) {
|
|
206
|
+
logger.warn(`Skipping path outside project root: ${sourcePath}`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (seen && seen.has(relative)) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (seen) {
|
|
213
|
+
seen.add(relative);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (Array.isArray(stripSegments) && stripSegments.length > 0) {
|
|
217
|
+
const relParts = relative.split(path.sep).filter(Boolean);
|
|
218
|
+
let match = true;
|
|
219
|
+
for (let i = 0; i < stripSegments.length; i += 1) {
|
|
220
|
+
if (relParts[i] !== stripSegments[i]) {
|
|
221
|
+
match = false;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (match) {
|
|
226
|
+
relative = relParts.slice(stripSegments.length).join(path.sep);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const destination = path.join(targetRoot, relative);
|
|
231
|
+
const stat = await fs.stat(sourcePath);
|
|
232
|
+
if (stat.isDirectory()) {
|
|
233
|
+
await copyTree(sourcePath, destination);
|
|
234
|
+
} else {
|
|
235
|
+
await ensureParentDir(destination);
|
|
236
|
+
await fs.copyFile(sourcePath, destination);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function copyTree(source, destination) {
|
|
241
|
+
if (!(await pathExists(source))) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
await fs.rm(destination, { recursive: true, force: true });
|
|
245
|
+
await fs.mkdir(path.dirname(destination), { recursive: true });
|
|
246
|
+
await fs.cp(source, destination, { recursive: true });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function ensureParentDir(targetPath) {
|
|
250
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function copyIfExists(source, destination) {
|
|
254
|
+
if (await pathExists(source)) {
|
|
255
|
+
await ensureParentDir(destination);
|
|
256
|
+
await fs.copyFile(source, destination);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function pathExists(target) {
|
|
261
|
+
try {
|
|
262
|
+
await fs.access(target);
|
|
263
|
+
return true;
|
|
264
|
+
} catch {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function replaceExtension(filePath, ext) {
|
|
270
|
+
const parsed = path.parse(filePath);
|
|
271
|
+
return path.join(parsed.dir, `${parsed.name}${ext}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function relativeFromRoot(config, absolutePath) {
|
|
275
|
+
const relative = path.relative(config.rootDir, absolutePath);
|
|
276
|
+
if (relative.startsWith('..')) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
return toPosix(relative);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function toPosix(value) {
|
|
283
|
+
return value.split(path.sep).join('/');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function replaceInFile(filePath, searchValue, replaceValue) {
|
|
287
|
+
if (!(await pathExists(filePath))) return;
|
|
288
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
289
|
+
if (!content.includes(searchValue)) return;
|
|
290
|
+
const updated = content.split(searchValue).join(replaceValue);
|
|
291
|
+
await fs.writeFile(filePath, updated, 'utf8');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// No vendoring helpers needed
|