@noego/app 0.0.3 → 0.0.4
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/.claude/settings.local.json +3 -11
- package/package.json +15 -1
- package/src/args.js +1 -0
- package/src/build/bootstrap.js +114 -8
- package/src/build/context.js +2 -2
- package/src/build/helpers.js +10 -0
- package/src/build/openapi.js +9 -4
- package/src/build/runtime-manifest.js +22 -30
- package/src/build/server.js +34 -4
- package/src/cli.js +10 -5
- package/src/client.js +141 -0
- package/src/commands/build.js +66 -21
- package/src/commands/dev.js +624 -0
- package/src/commands/runtime-entry.ts +16 -0
- package/src/config.js +73 -553
- package/src/index.js +7 -0
- package/src/runtime/config-loader.js +203 -0
- package/src/runtime/html-parser.js +47 -0
- package/src/runtime/index.js +4 -0
- package/src/runtime/runtime.js +749 -0
- package/types/client.d.ts +23 -0
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"Read(//Users/shavauhngabay/dev/noblelaw/ui/**)",
|
|
8
|
-
"Read(//Users/shavauhngabay/dev/noblelaw/**)",
|
|
9
|
-
"mcp__chrome-devtools__take_screenshot",
|
|
10
|
-
"Bash(npm run build:ui:ssr:*)",
|
|
11
|
-
"mcp__chrome-devtools__navigate_page",
|
|
12
|
-
"Bash(node dist/hammer.js:*)",
|
|
13
|
-
"Bash(tee:*)",
|
|
14
|
-
"mcp__chrome-devtools__list_console_messages"
|
|
4
|
+
"Bash(cat:*)",
|
|
5
|
+
"Bash(curl:*)",
|
|
6
|
+
"Bash(noego dev)"
|
|
15
7
|
],
|
|
16
8
|
"deny": [],
|
|
17
9
|
"ask": []
|
package/package.json
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noego/app",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Production build tool for Dinner/Forge apps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"app": "./bin/app.js",
|
|
8
8
|
"noego": "./bin/app.js"
|
|
9
9
|
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./client": {
|
|
15
|
+
"import": "./src/client.js",
|
|
16
|
+
"types": "./types/client.d.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
10
19
|
"scripts": {
|
|
11
20
|
"build": "node ./bin/app.js build"
|
|
12
21
|
},
|
|
@@ -20,5 +29,10 @@
|
|
|
20
29
|
"picomatch": "^2.3.1",
|
|
21
30
|
"yaml": "^2.6.0"
|
|
22
31
|
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@noego/dinner": "*",
|
|
34
|
+
"@noego/forge": "*",
|
|
35
|
+
"express": "*"
|
|
36
|
+
},
|
|
23
37
|
"devDependencies": {}
|
|
24
38
|
}
|
package/src/args.js
CHANGED
package/src/build/bootstrap.js
CHANGED
|
@@ -2,29 +2,123 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Generates a bootstrap wrapper (
|
|
5
|
+
* Generates a bootstrap wrapper (no_ego.js) that sets up the runtime environment
|
|
6
6
|
* before importing the compiled application entry.
|
|
7
7
|
*
|
|
8
8
|
* This ensures:
|
|
9
|
+
* - NOEGO_CONFIGURATION is set with full resolved config
|
|
9
10
|
* - FORGE_ROOT is set to the dist directory (for consistent asset resolution)
|
|
10
|
-
* - Application works regardless of invocation directory (node dist/
|
|
11
|
+
* - Application works regardless of invocation directory (node dist/no_ego.js vs cd dist && node no_ego.js)
|
|
11
12
|
* - Environment-agnostic (staging, QA, production all work identically)
|
|
12
13
|
*/
|
|
13
14
|
export async function generateBootstrap(context) {
|
|
14
15
|
const { config, logger } = context;
|
|
15
16
|
|
|
17
|
+
// Build the config object that will be serialized to NOEGO_CONFIGURATION
|
|
18
|
+
// Paths are relative to project root, mirrored in dist (so dist/server/server.js mirrors src/server/server.ts)
|
|
19
|
+
const runtimeConfig = {
|
|
20
|
+
root: '__dirname', // Will be replaced in template with actual __dirname (dist directory)
|
|
21
|
+
mode: 'production',
|
|
22
|
+
outDir: config.outDir ? path.basename(config.outDir) : 'dist',
|
|
23
|
+
outDir_abs: '__dirname', // Will be replaced
|
|
24
|
+
app: {
|
|
25
|
+
boot: config.app?.boot,
|
|
26
|
+
boot_abs: config.app?.boot ? './' + path.relative(config.rootDir, config.app.boot_abs).replace(/\.ts$/, '.js') : null
|
|
27
|
+
},
|
|
28
|
+
server: config.server ? {
|
|
29
|
+
main: config.server.entry?.raw,
|
|
30
|
+
main_abs: config.server.entry?.absolute ? './' + path.relative(config.rootDir, config.server.entry.absolute).replace(/\.ts$/, '.js') : null,
|
|
31
|
+
controllers: config.server?.controllers,
|
|
32
|
+
controllers_abs: config.server?.controllersDir ? './' + path.relative(config.rootDir, config.server.controllersDir) : null,
|
|
33
|
+
controllers_base_path: config.server?.controllersDir ? './' + path.relative(config.rootDir, config.server.controllersDir) : null,
|
|
34
|
+
middleware: config.server?.middleware,
|
|
35
|
+
middleware_abs: config.server?.middlewareDir ? './' + path.relative(config.rootDir, config.server.middlewareDir) : null,
|
|
36
|
+
middleware_path: config.server?.middlewareDir ? './' + path.relative(config.rootDir, config.server.middlewareDir) : null,
|
|
37
|
+
openapi: config.server?.openapi,
|
|
38
|
+
openapi_abs: config.server?.openapiFile ? './' + path.relative(config.rootDir, config.server.openapiFile) : null,
|
|
39
|
+
openapi_path: config.server?.openapiFile ? './' + path.relative(config.rootDir, config.server.openapiFile) : null
|
|
40
|
+
} : undefined,
|
|
41
|
+
client: config.ui ? {
|
|
42
|
+
main: config.client?.main,
|
|
43
|
+
main_abs: config.client?.main_abs ? './' + path.relative(config.rootDir, config.client.main_abs).replace(/\.ts$/, '.js') : null,
|
|
44
|
+
shell: config.client?.shell,
|
|
45
|
+
shell_abs: config.client?.shell_abs ? './' + path.relative(config.rootDir, config.client.shell_abs) : null,
|
|
46
|
+
shell_path: config.client?.shell,
|
|
47
|
+
openapi: config.client?.openapi,
|
|
48
|
+
openapi_abs: config.client?.openapi_abs ? './' + path.relative(config.rootDir, config.client.openapi_abs) : null,
|
|
49
|
+
openapi_path: config.client?.openapi,
|
|
50
|
+
component_dir: config.client?.component_dir,
|
|
51
|
+
componentDir_abs: config.client?.componentDir_abs ? './' + path.relative(config.rootDir, config.client.componentDir_abs) : null,
|
|
52
|
+
assetDirs: config.client?.assetDirs || []
|
|
53
|
+
} : undefined,
|
|
54
|
+
dev: {
|
|
55
|
+
port: config.dev?.port || 3000,
|
|
56
|
+
backendPort: config.dev?.backendPort || 3001
|
|
57
|
+
},
|
|
58
|
+
assets: []
|
|
59
|
+
};
|
|
60
|
+
|
|
16
61
|
// Compute relative path from outDir to compiled entry
|
|
17
62
|
// The entry is copied to outDir root by syncRootEntry in server.js
|
|
18
|
-
const entryRel =
|
|
19
|
-
config.rootDir,
|
|
20
|
-
|
|
21
|
-
|
|
63
|
+
const entryRel = config.server?.entry?.absolute
|
|
64
|
+
? path.relative(config.rootDir, config.server.entry.absolute).replace(/\.ts$/, '.js')
|
|
65
|
+
: 'server/index.js';
|
|
66
|
+
|
|
67
|
+
// Serialize config, then replace __dirname placeholders
|
|
68
|
+
let configJson = JSON.stringify(runtimeConfig, null, 2);
|
|
69
|
+
configJson = configJson.replace(/"__dirname"/g, '__dirname');
|
|
22
70
|
|
|
23
71
|
const template = `import path from 'path';
|
|
24
72
|
import { fileURLToPath } from 'url';
|
|
25
73
|
|
|
26
74
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
75
|
|
|
76
|
+
// Set full configuration in environment
|
|
77
|
+
const runtimeConfig = ${configJson};
|
|
78
|
+
|
|
79
|
+
// Resolve all relative paths to absolute based on __dirname
|
|
80
|
+
if (runtimeConfig.app?.boot_abs) {
|
|
81
|
+
runtimeConfig.app.boot_abs = path.resolve(__dirname, runtimeConfig.app.boot_abs);
|
|
82
|
+
}
|
|
83
|
+
if (runtimeConfig.server) {
|
|
84
|
+
if (runtimeConfig.server.main_abs) {
|
|
85
|
+
runtimeConfig.server.main_abs = path.resolve(__dirname, runtimeConfig.server.main_abs);
|
|
86
|
+
}
|
|
87
|
+
if (runtimeConfig.server.controllers_abs) {
|
|
88
|
+
runtimeConfig.server.controllers_abs = path.resolve(__dirname, runtimeConfig.server.controllers_abs);
|
|
89
|
+
runtimeConfig.server.controllers_base_path = runtimeConfig.server.controllers_abs;
|
|
90
|
+
}
|
|
91
|
+
if (runtimeConfig.server.middleware_abs) {
|
|
92
|
+
runtimeConfig.server.middleware_abs = path.resolve(__dirname, runtimeConfig.server.middleware_abs);
|
|
93
|
+
runtimeConfig.server.middleware_path = runtimeConfig.server.middleware_abs;
|
|
94
|
+
}
|
|
95
|
+
if (runtimeConfig.server.openapi_abs) {
|
|
96
|
+
runtimeConfig.server.openapi_abs = path.resolve(__dirname, runtimeConfig.server.openapi_abs);
|
|
97
|
+
runtimeConfig.server.openapi_path = runtimeConfig.server.openapi_abs;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (runtimeConfig.client) {
|
|
101
|
+
if (runtimeConfig.client.main_abs) {
|
|
102
|
+
runtimeConfig.client.main_abs = path.resolve(__dirname, runtimeConfig.client.main_abs);
|
|
103
|
+
}
|
|
104
|
+
if (runtimeConfig.client.shell_abs) {
|
|
105
|
+
runtimeConfig.client.shell_abs = path.resolve(__dirname, runtimeConfig.client.shell_abs);
|
|
106
|
+
}
|
|
107
|
+
if (runtimeConfig.client.openapi_abs) {
|
|
108
|
+
runtimeConfig.client.openapi_abs = path.resolve(__dirname, runtimeConfig.client.openapi_abs);
|
|
109
|
+
}
|
|
110
|
+
if (runtimeConfig.client.componentDir_abs) {
|
|
111
|
+
runtimeConfig.client.componentDir_abs = path.resolve(__dirname, runtimeConfig.client.componentDir_abs);
|
|
112
|
+
}
|
|
113
|
+
// Resolve assetDirs paths
|
|
114
|
+
if (runtimeConfig.client.assetDirs) {
|
|
115
|
+
runtimeConfig.client.assetDirs = runtimeConfig.client.assetDirs.map(assetDir => ({
|
|
116
|
+
...assetDir,
|
|
117
|
+
absolutePath: path.resolve(__dirname, assetDir.absolutePath)
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
28
122
|
// Set Forge root explicitly (highest priority in resolveRuntimeRoot)
|
|
29
123
|
process.env.FORGE_ROOT = __dirname;
|
|
30
124
|
|
|
@@ -35,8 +129,20 @@ process.env.NOEGO_BUILT = 'true';
|
|
|
35
129
|
// Indicate Dinner is running in the built environment
|
|
36
130
|
process.env.DINNER_SERVED = 'true';
|
|
37
131
|
|
|
38
|
-
//
|
|
39
|
-
|
|
132
|
+
// Force production mode in config (not via NODE_ENV)
|
|
133
|
+
runtimeConfig.mode = 'production';
|
|
134
|
+
|
|
135
|
+
// Store config in env for @noego/app/client.js to read
|
|
136
|
+
process.env.NOEGO_CONFIGURATION = JSON.stringify(runtimeConfig);
|
|
137
|
+
|
|
138
|
+
// Import and run the runtime directly
|
|
139
|
+
import { runCombinedServices } from '@noego/app';
|
|
140
|
+
|
|
141
|
+
// Determine what services to run
|
|
142
|
+
const hasBackend = !!runtimeConfig.server?.main_abs;
|
|
143
|
+
const hasFrontend = !!runtimeConfig.client?.main_abs;
|
|
144
|
+
|
|
145
|
+
await runCombinedServices(runtimeConfig, { hasBackend, hasFrontend });
|
|
40
146
|
`;
|
|
41
147
|
|
|
42
148
|
const targetPath = path.join(config.outDir, 'no_ego.js');
|
package/src/build/context.js
CHANGED
|
@@ -3,8 +3,8 @@ import { createRequire } from 'node:module';
|
|
|
3
3
|
|
|
4
4
|
import { createLogger } from '../logger.js';
|
|
5
5
|
|
|
6
|
-
export function createBuildContext(config
|
|
7
|
-
const logger =
|
|
6
|
+
export function createBuildContext(config) {
|
|
7
|
+
const logger = createLogger();
|
|
8
8
|
const requireFromRoot = createRequire(path.join(config.rootDir, 'package.json'));
|
|
9
9
|
|
|
10
10
|
return {
|
package/src/build/helpers.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
export function resolvePath(input, baseDir) {
|
|
5
|
+
if (input == null || input === '') {
|
|
6
|
+
return path.resolve(baseDir);
|
|
7
|
+
}
|
|
8
|
+
if (path.isAbsolute(input)) {
|
|
9
|
+
return path.normalize(input);
|
|
10
|
+
}
|
|
11
|
+
return path.resolve(baseDir, input);
|
|
12
|
+
}
|
|
13
|
+
|
|
4
14
|
export async function resolveConfigFile(preferred, fallback) {
|
|
5
15
|
if (preferred) {
|
|
6
16
|
const absolutePreferred = path.resolve(preferred);
|
package/src/build/openapi.js
CHANGED
|
@@ -16,9 +16,11 @@ const HTTP_METHODS = new Set([
|
|
|
16
16
|
]);
|
|
17
17
|
|
|
18
18
|
export async function discoverProject(context) {
|
|
19
|
+
const serverOpenApiPath = context.config.server?.openapiFile;
|
|
20
|
+
const uiOpenApiPath = context.config.ui?.openapiFile;
|
|
19
21
|
const [serverDoc, uiDoc] = await Promise.all([
|
|
20
|
-
loadOpenApiDocumentIfPresent(context,
|
|
21
|
-
loadOpenApiDocumentIfPresent(context,
|
|
22
|
+
loadOpenApiDocumentIfPresent(context, serverOpenApiPath),
|
|
23
|
+
loadOpenApiDocumentIfPresent(context, uiOpenApiPath)
|
|
22
24
|
]);
|
|
23
25
|
|
|
24
26
|
const serverRoutes = collectRoutes(serverDoc);
|
|
@@ -114,6 +116,9 @@ async function loadOpenApiDocumentIfPresent(context, filePath) {
|
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
function collectRoutes(document) {
|
|
119
|
+
if (!document) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
117
122
|
const routes = [];
|
|
118
123
|
const inheritedMiddleware = ensureArray(document?.paths?.['x-middleware']);
|
|
119
124
|
|
|
@@ -123,7 +128,7 @@ function collectRoutes(document) {
|
|
|
123
128
|
return routes;
|
|
124
129
|
}
|
|
125
130
|
|
|
126
|
-
function parseModules(document, inheritedMiddleware = []) {
|
|
131
|
+
function parseModules(document = {}, inheritedMiddleware = []) {
|
|
127
132
|
const modulesConfig = document.modules || document.module;
|
|
128
133
|
if (!modulesConfig) return [];
|
|
129
134
|
|
|
@@ -151,7 +156,7 @@ function parseModules(document, inheritedMiddleware = []) {
|
|
|
151
156
|
return routes;
|
|
152
157
|
}
|
|
153
158
|
|
|
154
|
-
function parsePaths(document, inheritedMiddleware = []) {
|
|
159
|
+
function parsePaths(document = {}, inheritedMiddleware = []) {
|
|
155
160
|
const paths = document.paths;
|
|
156
161
|
if (!paths || typeof paths !== 'object') {
|
|
157
162
|
return [];
|
|
@@ -10,41 +10,31 @@ export async function writeRuntimeManifest(context, artifacts) {
|
|
|
10
10
|
mode: config.mode,
|
|
11
11
|
rootDir: config.rootDir,
|
|
12
12
|
outDir: config.outDir,
|
|
13
|
-
server: {
|
|
14
|
-
entry: config.server.entry
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
13
|
+
server: config.server ? {
|
|
14
|
+
entry: config.server.entry?.relativeToRoot ?? null,
|
|
15
|
+
controllersDir: config.server.controllersDir ? path.relative(config.rootDir, config.server.controllersDir) : null,
|
|
16
|
+
middlewareDir: config.server.middlewareDir ? path.relative(config.rootDir, config.server.middlewareDir) : null,
|
|
17
|
+
openapi: config.server.openapiFile ? path.relative(config.rootDir, config.server.openapiFile) : null,
|
|
18
|
+
sqlGlobs: Array.isArray(config.server.sqlGlobs) ? config.server.sqlGlobs.map((spec) => spec.pattern) : []
|
|
19
|
+
} : null,
|
|
20
|
+
ui: config.ui ? {
|
|
21
|
+
page: config.ui.page?.relativeToRoot ?? null,
|
|
22
|
+
openapi: config.ui.openapiFile ? path.relative(config.rootDir, config.ui.openapiFile) : null,
|
|
23
|
+
assets: Array.isArray(config.assets) ? config.assets.map((spec) => spec.pattern) : [],
|
|
24
|
+
clientExclude: Array.isArray(config.ui.clientExclude) ? config.ui.clientExclude.map((spec) => spec.pattern) : []
|
|
25
|
+
} : null,
|
|
26
|
+
discovery: artifacts.discovery ?? null,
|
|
27
|
+
client: artifacts.client ? {
|
|
28
|
+
manifest: artifacts.client.manifestPath ? relativeToOut(config, artifacts.client.manifestPath) : null,
|
|
29
|
+
entry: artifacts.client.entryGraph?.entry?.file
|
|
40
30
|
? `/assets/${toPosix(artifacts.client.entryGraph.entry.file)}`
|
|
41
31
|
: null
|
|
42
|
-
},
|
|
43
|
-
ssr: {
|
|
32
|
+
} : null,
|
|
33
|
+
ssr: artifacts.ssr ? {
|
|
44
34
|
manifest: artifacts.ssr.manifestPath
|
|
45
35
|
? relativeToOut(config, artifacts.ssr.manifestPath)
|
|
46
36
|
: null
|
|
47
|
-
}
|
|
37
|
+
} : null
|
|
48
38
|
};
|
|
49
39
|
|
|
50
40
|
await fs.writeFile(targetPath, JSON.stringify(payload, null, 2));
|
|
@@ -52,10 +42,12 @@ export async function writeRuntimeManifest(context, artifacts) {
|
|
|
52
42
|
}
|
|
53
43
|
|
|
54
44
|
function relativeToRoot(config, absolutePath) {
|
|
45
|
+
if (!absolutePath) return null;
|
|
55
46
|
return path.relative(config.rootDir, absolutePath);
|
|
56
47
|
}
|
|
57
48
|
|
|
58
49
|
function relativeToOut(config, absolutePath) {
|
|
50
|
+
if (!absolutePath) return null;
|
|
59
51
|
return path.relative(config.outDir, absolutePath);
|
|
60
52
|
}
|
|
61
53
|
|
package/src/build/server.js
CHANGED
|
@@ -13,6 +13,7 @@ export async function buildServer(context, discovery) {
|
|
|
13
13
|
await reorganizeServerOutputs(context);
|
|
14
14
|
await mirrorUiModules(context);
|
|
15
15
|
await syncRootEntry(context);
|
|
16
|
+
await syncAppBoot(context);
|
|
16
17
|
await copyServerAssets(context);
|
|
17
18
|
await fixImportExtensions(context.config.outDir);
|
|
18
19
|
}
|
|
@@ -70,7 +71,7 @@ async function reorganizeServerOutputs(context) {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
const compiledEntryRel = replaceExtension(
|
|
73
|
-
path.relative(config.rootDir, config.server.entry.absolute),
|
|
74
|
+
path.relative(config.server.rootDir, config.server.entry.absolute),
|
|
74
75
|
'.js'
|
|
75
76
|
);
|
|
76
77
|
const compiledEntryPath = path.join(serverOutDir, compiledEntryRel);
|
|
@@ -172,15 +173,44 @@ async function mirrorUiModules(context) {
|
|
|
172
173
|
|
|
173
174
|
async function syncRootEntry(context) {
|
|
174
175
|
const { config } = context;
|
|
175
|
-
const
|
|
176
|
+
const compiledEntryRelFromServerRoot = replaceExtension(
|
|
177
|
+
path.relative(config.server.rootDir, config.server.entry.absolute),
|
|
178
|
+
'.js'
|
|
179
|
+
);
|
|
180
|
+
const compiledEntryRelFromProjectRoot = replaceExtension(
|
|
176
181
|
path.relative(config.rootDir, config.server.entry.absolute),
|
|
177
182
|
'.js'
|
|
178
183
|
);
|
|
179
|
-
const sourcePath = path.join(config.layout.serverOutDir,
|
|
184
|
+
const sourcePath = path.join(config.layout.serverOutDir, compiledEntryRelFromServerRoot);
|
|
180
185
|
if (!(await pathExists(sourcePath))) {
|
|
181
186
|
return;
|
|
182
187
|
}
|
|
183
|
-
const destinationPath = path.join(config.outDir,
|
|
188
|
+
const destinationPath = path.join(config.outDir, compiledEntryRelFromProjectRoot);
|
|
189
|
+
await ensureParentDir(destinationPath);
|
|
190
|
+
await fs.copyFile(sourcePath, destinationPath);
|
|
191
|
+
await copyIfExists(`${sourcePath}.map`, `${destinationPath}.map`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function syncAppBoot(context) {
|
|
195
|
+
const { config } = context;
|
|
196
|
+
|
|
197
|
+
// Copy app.boot (e.g., index.ts -> dist/index.js) to maintain project structure
|
|
198
|
+
if (!config.app?.boot_abs) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const compiledBootRelFromProjectRoot = replaceExtension(
|
|
203
|
+
path.relative(config.rootDir, config.app.boot_abs),
|
|
204
|
+
'.js'
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// The compiled boot file should be in the tsOutDir, mirroring project structure
|
|
208
|
+
const sourcePath = path.join(config.layout.tsOutDir, compiledBootRelFromProjectRoot);
|
|
209
|
+
if (!(await pathExists(sourcePath))) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const destinationPath = path.join(config.outDir, compiledBootRelFromProjectRoot);
|
|
184
214
|
await ensureParentDir(destinationPath);
|
|
185
215
|
await fs.copyFile(sourcePath, destinationPath);
|
|
186
216
|
await copyIfExists(`${sourcePath}.map`, `${destinationPath}.map`);
|
package/src/cli.js
CHANGED
|
@@ -7,10 +7,11 @@ import { loadBuildConfig } from './config.js';
|
|
|
7
7
|
import { runBuild } from './commands/build.js';
|
|
8
8
|
import { runServe } from './commands/serve.js';
|
|
9
9
|
import { runPreview } from './commands/preview.js';
|
|
10
|
+
import { runDev } from './commands/dev.js';
|
|
10
11
|
|
|
11
|
-
async function
|
|
12
|
+
export async function runCli(argv = process.argv.slice(2)) {
|
|
12
13
|
try {
|
|
13
|
-
const { command, options } = parseCliArgs(
|
|
14
|
+
const { command, options } = parseCliArgs(argv);
|
|
14
15
|
|
|
15
16
|
if (options.help || !command) {
|
|
16
17
|
printHelpAndExit();
|
|
@@ -24,8 +25,7 @@ async function main() {
|
|
|
24
25
|
|
|
25
26
|
switch (command) {
|
|
26
27
|
case 'build': {
|
|
27
|
-
|
|
28
|
-
await runBuild(config);
|
|
28
|
+
await runBuild(options);
|
|
29
29
|
break;
|
|
30
30
|
}
|
|
31
31
|
case 'serve': {
|
|
@@ -33,6 +33,11 @@ async function main() {
|
|
|
33
33
|
await runServe(config);
|
|
34
34
|
break;
|
|
35
35
|
}
|
|
36
|
+
case 'dev': {
|
|
37
|
+
const config = await loadBuildConfig(options, { cwd: process.cwd() });
|
|
38
|
+
await runDev(config);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
36
41
|
case 'preview': {
|
|
37
42
|
const config = await loadBuildConfig(options, { cwd: process.cwd() });
|
|
38
43
|
await runPreview(config);
|
|
@@ -69,4 +74,4 @@ function cliError(message) {
|
|
|
69
74
|
return error;
|
|
70
75
|
}
|
|
71
76
|
|
|
72
|
-
await
|
|
77
|
+
await runCli();
|
package/src/client.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Runtime context - set by the runtime before calling server.main
|
|
5
|
+
let runtimeContext = null;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sets the runtime context (called by the runtime before server.main).
|
|
9
|
+
*
|
|
10
|
+
* @param {import('express').Express} app - Express application instance
|
|
11
|
+
* @param {object} config - Configuration object with resolved paths
|
|
12
|
+
* @param {import('http').Server} [httpServer] - HTTP server instance for WebSocket upgrades
|
|
13
|
+
*/
|
|
14
|
+
export function setContext(app, config, httpServer = null) {
|
|
15
|
+
runtimeContext = { app, config, httpServer };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Boots the backend server with Dinner (API) integration.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} assets - Dinner asset mappings
|
|
22
|
+
* @param {object} [options] - Optional configuration
|
|
23
|
+
* @param {function} [options.controller_builder] - Controller builder function
|
|
24
|
+
* @param {function} [options.controller_args_provider] - Controller args provider function
|
|
25
|
+
* @returns {Promise<object>} Dinner server instance
|
|
26
|
+
*/
|
|
27
|
+
export async function boot(assets, options = {}) {
|
|
28
|
+
if (!runtimeContext) {
|
|
29
|
+
throw new Error('Runtime context not set. Call setContext() before boot().');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { app, config } = runtimeContext;
|
|
33
|
+
|
|
34
|
+
// Resolve dependencies from user's project context
|
|
35
|
+
const root = config.root || process.cwd();
|
|
36
|
+
const require = createRequire(path.join(root, 'package.json'));
|
|
37
|
+
const { Server } = require('@noego/dinner');
|
|
38
|
+
|
|
39
|
+
// Setup Dinner (API server) only
|
|
40
|
+
const server = await Server.createServer({
|
|
41
|
+
openapi_path: config.server.openapi_path,
|
|
42
|
+
controllers_base_path: config.server.controllers_base_path,
|
|
43
|
+
middleware_path: config.server.middleware_path,
|
|
44
|
+
server: app,
|
|
45
|
+
ajv_formats: true,
|
|
46
|
+
assets: assets,
|
|
47
|
+
controller_builder: options.controller_builder || (async (Controller) => {
|
|
48
|
+
// Default: try to instantiate with new
|
|
49
|
+
return new Controller();
|
|
50
|
+
}),
|
|
51
|
+
controller_args_provider: options.controller_args_provider || (async (req, res) => ({ req, res })),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return server;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Boots the frontend server with Forge (SSR) integration.
|
|
59
|
+
*
|
|
60
|
+
* @returns {Promise<void>}
|
|
61
|
+
*/
|
|
62
|
+
export async function clientBoot() {
|
|
63
|
+
if (!runtimeContext) {
|
|
64
|
+
throw new Error('Runtime context not set. Call setContext() before client.boot().');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { app, config } = runtimeContext;
|
|
68
|
+
|
|
69
|
+
// Resolve dependencies from user's project context
|
|
70
|
+
const root = config.root || process.cwd();
|
|
71
|
+
const require = createRequire(path.join(root, 'package.json'));
|
|
72
|
+
const { createServer: createForgeServer } = require('@noego/forge/server');
|
|
73
|
+
|
|
74
|
+
// Setup Forge (SSR)
|
|
75
|
+
// Forge expects paths relative to viteOptions.root
|
|
76
|
+
const forgeOptions = {
|
|
77
|
+
viteOptions: {
|
|
78
|
+
root: root,
|
|
79
|
+
// Configure Vite server: use Express app (middleware mode) in development
|
|
80
|
+
server: config.mode !== 'production'
|
|
81
|
+
? {
|
|
82
|
+
middlewareMode: true, // Use Vite as middleware in Express, don't create separate server
|
|
83
|
+
hmr: {
|
|
84
|
+
clientPort: config.dev?.port || 3000,
|
|
85
|
+
// Pass HTTP server to Vite for WebSocket upgrades
|
|
86
|
+
server: runtimeContext.httpServer
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
: undefined
|
|
90
|
+
},
|
|
91
|
+
component_dir: config.client?.component_dir,
|
|
92
|
+
open_api_path: config.client?.openapi_path,
|
|
93
|
+
renderer: config.client?.shell_path,
|
|
94
|
+
middleware_path: config.server?.middleware_path,
|
|
95
|
+
development: config.mode !== 'production',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
await createForgeServer(app, forgeOptions);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Browser-side client initialization.
|
|
103
|
+
* Reads window.__CONFIGURATION__ and initializes the Forge app.
|
|
104
|
+
* This function works in the browser and should be called from client.ts.
|
|
105
|
+
*
|
|
106
|
+
* @returns {Promise<void>}
|
|
107
|
+
*/
|
|
108
|
+
export async function clientInit() {
|
|
109
|
+
// Check if we're in a browser environment
|
|
110
|
+
if (typeof window === 'undefined') {
|
|
111
|
+
throw new Error('client.init() can only be called in the browser');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Get configuration from window (injected by Forge HTML renderer)
|
|
115
|
+
const options = window.__CONFIGURATION__;
|
|
116
|
+
if (!options) {
|
|
117
|
+
console.warn('window.__CONFIGURATION__ not found. Forge app may not initialize correctly.');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Find root element
|
|
122
|
+
const root = document.getElementById('app');
|
|
123
|
+
if (!root) {
|
|
124
|
+
console.warn('Root element with id="app" not found.');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Dynamically import createApp from @noego/forge/client
|
|
129
|
+
// This will work because Vite bundles it properly
|
|
130
|
+
const { createApp } = await import('@noego/forge/client');
|
|
131
|
+
|
|
132
|
+
// Initialize the app
|
|
133
|
+
await createApp(root, options);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Export client.boot as a named export (server-side)
|
|
137
|
+
export const client = {
|
|
138
|
+
boot: clientBoot,
|
|
139
|
+
init: clientInit // Browser-side initialization
|
|
140
|
+
};
|
|
141
|
+
|