@ossy/platform 1.14.1 → 1.14.2
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/package.json +6 -5
- package/src/getPlatformFiles.task.js +50 -0
- package/src/server.js +216 -113
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/platform",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.2",
|
|
4
4
|
"description": "Ossy application server runtime",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,14 +16,15 @@
|
|
|
16
16
|
"./worker-runtime": "./src/worker-runtime.js"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
|
-
"start": "PORT=3003 node ./src/server.js"
|
|
19
|
+
"start": "PORT=3003 node -e \"import('./src/server.js').then(m => m.startServer())\""
|
|
20
20
|
},
|
|
21
21
|
"keywords": [],
|
|
22
22
|
"author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@ossy/
|
|
26
|
-
"@ossy/
|
|
25
|
+
"@ossy/connected-components": "^1.15.2",
|
|
26
|
+
"@ossy/router": "^1.15.2",
|
|
27
|
+
"@ossy/sdk": "^1.15.2",
|
|
27
28
|
"cookie-parser": "^1.4.7",
|
|
28
29
|
"express": ">=5.0.0 <6.0.0",
|
|
29
30
|
"morgan": ">=1.10.1 <2.0.0",
|
|
@@ -33,5 +34,5 @@
|
|
|
33
34
|
"files": [
|
|
34
35
|
"src"
|
|
35
36
|
],
|
|
36
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "6a15f3ac4cafdb190beed11e4a93adc0ba28c245"
|
|
37
38
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
export const PAGE_FILE_PATTERN = /\.page\.(jsx?|tsx?)$/
|
|
5
|
+
export const API_FILE_PATTERN = /\.api\.(mjs|cjs|js)$/
|
|
6
|
+
export const TASK_FILE_PATTERN = /\.task\.(mjs|cjs|js)$/
|
|
7
|
+
export const RESOURCE_TEMPLATE_FILE_PATTERN = /\.resource\.(mjs|cjs|js)$/
|
|
8
|
+
|
|
9
|
+
function discoverFilesByPattern (srcDir, filePattern) {
|
|
10
|
+
const dir = path.resolve(srcDir)
|
|
11
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
12
|
+
return []
|
|
13
|
+
}
|
|
14
|
+
const files = []
|
|
15
|
+
const walk = (d) => {
|
|
16
|
+
const entries = fs.readdirSync(d, { withFileTypes: true })
|
|
17
|
+
for (const e of entries) {
|
|
18
|
+
const full = path.join(d, e.name)
|
|
19
|
+
if (e.isDirectory()) walk(full)
|
|
20
|
+
else if (filePattern.test(e.name)) files.push(full)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
walk(dir)
|
|
24
|
+
return files
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
export default async function getPlatformFiles(_folder) {
|
|
29
|
+
const cwd = process.cwd()
|
|
30
|
+
const folder = path.resolve(cwd, _folder)
|
|
31
|
+
|
|
32
|
+
const configPath = fs.existsSync(path.resolve(folder, 'config.js')) ? path.resolve(folder, 'config.js') : null
|
|
33
|
+
const middlewarePath = fs.existsSync(path.resolve(folder, 'middleware.js')) ? path.resolve(folder, 'middleware.js') : null
|
|
34
|
+
|
|
35
|
+
const pageFiles = discoverFilesByPattern(folder, PAGE_FILE_PATTERN)
|
|
36
|
+
const apiFiles = discoverFilesByPattern(folder, API_FILE_PATTERN)
|
|
37
|
+
const taskFiles = discoverFilesByPattern(folder, TASK_FILE_PATTERN)
|
|
38
|
+
const resourceTemplatesFiles = discoverFilesByPattern(folder, TASK_FILE_PATTERN)
|
|
39
|
+
|
|
40
|
+
const entries = [
|
|
41
|
+
...(middlewarePath ? [middlewarePath] : []),
|
|
42
|
+
...(configPath ? [configPath] : []),
|
|
43
|
+
...pageFiles,
|
|
44
|
+
...apiFiles,
|
|
45
|
+
...taskFiles,
|
|
46
|
+
...resourceTemplatesFiles,
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
return entries
|
|
50
|
+
}
|
package/src/server.js
CHANGED
|
@@ -1,59 +1,142 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
|
-
import path from 'path'
|
|
3
|
-
import {
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
4
|
import express from 'express'
|
|
5
5
|
import morgan from 'morgan'
|
|
6
6
|
import { Router as OssyRouter } from '@ossy/router'
|
|
7
7
|
import cookieParser from 'cookie-parser'
|
|
8
8
|
import { ProxyInternal } from './proxy-internal.js'
|
|
9
|
+
import getPlatformFiles, {
|
|
10
|
+
PAGE_FILE_PATTERN,
|
|
11
|
+
API_FILE_PATTERN,
|
|
12
|
+
} from './getPlatformFiles.task.js'
|
|
9
13
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
const DEFAULT_PORT = 3000
|
|
15
|
+
|
|
16
|
+
function parsePortFromArgv (argv) {
|
|
17
|
+
const flagIdx = argv.findIndex((a) => a === '--port' || a === '-p')
|
|
18
|
+
if (flagIdx !== -1 && argv[flagIdx + 1]) return argv[flagIdx + 1]
|
|
19
|
+
const eq = argv.find((a) => a.startsWith('--port='))
|
|
20
|
+
if (eq) return eq.split('=')[1]
|
|
21
|
+
return undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolvePort (explicit) {
|
|
25
|
+
const raw = explicit ?? parsePortFromArgv(process.argv) ?? process.env.PORT
|
|
26
|
+
const parsed = Number.parseInt(String(raw ?? ''), 10)
|
|
27
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PORT
|
|
18
28
|
}
|
|
19
29
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
async function importModule (absPath) {
|
|
31
|
+
return import(pathToFileURL(absPath).href)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function loadRoutes (buildDir) {
|
|
35
|
+
const files = await getPlatformFiles(buildDir)
|
|
36
|
+
const pages = []
|
|
37
|
+
const apis = []
|
|
38
|
+
const pageModules = new Map()
|
|
39
|
+
const apiModules = new Map()
|
|
40
|
+
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
const basename = path.basename(file)
|
|
43
|
+
const isPage = PAGE_FILE_PATTERN.test(basename)
|
|
44
|
+
const isApi = API_FILE_PATTERN.test(basename)
|
|
45
|
+
if (!isPage && !isApi) continue
|
|
46
|
+
|
|
47
|
+
const mod = await importModule(file)
|
|
48
|
+
const metadata = mod.metadata || {}
|
|
49
|
+
const id = metadata.id
|
|
50
|
+
const routePath = metadata.path
|
|
51
|
+
if (!id || !routePath) continue
|
|
52
|
+
|
|
53
|
+
const entry = { id, path: routePath, title: metadata.title }
|
|
54
|
+
if (isPage) {
|
|
55
|
+
pages.push(entry)
|
|
56
|
+
pageModules.set(id, mod)
|
|
57
|
+
} else {
|
|
58
|
+
apis.push(entry)
|
|
59
|
+
apiModules.set(id, mod)
|
|
60
|
+
}
|
|
28
61
|
}
|
|
62
|
+
|
|
63
|
+
return { pages, apis, pageModules, apiModules }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function loadConfig (buildDir) {
|
|
67
|
+
const configPath = path.resolve(buildDir, 'config.js')
|
|
68
|
+
if (!fs.existsSync(configPath)) return {}
|
|
69
|
+
const mod = await importModule(configPath)
|
|
70
|
+
return mod.default || {}
|
|
29
71
|
}
|
|
30
72
|
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// SSR is optional at startup; page requests return 503 when unavailable.
|
|
73
|
+
async function loadMiddleware (buildDir) {
|
|
74
|
+
const mwPath = path.resolve(buildDir, 'middleware.js')
|
|
75
|
+
if (!fs.existsSync(mwPath)) return []
|
|
76
|
+
const mod = await importModule(mwPath)
|
|
77
|
+
const value = mod.default
|
|
78
|
+
if (Array.isArray(value)) return value
|
|
79
|
+
if (typeof value === 'function') return [value]
|
|
80
|
+
return []
|
|
40
81
|
}
|
|
41
82
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
83
|
+
function cloneSerializable (value) {
|
|
84
|
+
return value == null ? value : JSON.parse(JSON.stringify(value))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function renderPage ({ Component, metadata, appConfig }) {
|
|
88
|
+
const [{ createElement }, { renderToString }, { App }] = await Promise.all([
|
|
89
|
+
import('react'),
|
|
90
|
+
import('react-dom/server'),
|
|
91
|
+
import('@ossy/connected-components'),
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
const appProps = {
|
|
95
|
+
...appConfig,
|
|
96
|
+
theme: cloneSerializable(appConfig.theme),
|
|
97
|
+
themes: cloneSerializable(appConfig.themes),
|
|
98
|
+
resourceTemplates: cloneSerializable(appConfig.resourceTemplates),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const element = createElement(
|
|
102
|
+
'html',
|
|
103
|
+
{ lang: appProps.htmlLang || appProps.defaultLanguage || 'en' },
|
|
104
|
+
createElement(
|
|
105
|
+
'head',
|
|
106
|
+
null,
|
|
107
|
+
createElement('meta', { charSet: 'utf-8' }),
|
|
108
|
+
createElement('title', null, metadata?.title || appProps.documentTitle || ''),
|
|
109
|
+
),
|
|
110
|
+
createElement(App, appProps, createElement(Component, appProps)),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return '<!doctype html>' + renderToString(element)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function startServer (options = {}) {
|
|
117
|
+
const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd()
|
|
118
|
+
const buildDir = path.resolve(cwd, options.buildDir || 'build')
|
|
119
|
+
const publicDir = path.resolve(buildDir, 'public')
|
|
120
|
+
const port = resolvePort(options.port)
|
|
121
|
+
|
|
122
|
+
if (!fs.existsSync(buildDir)) {
|
|
123
|
+
throw new Error(`[@ossy/platform][server] Build directory not found: ${buildDir}. Run \`app build\` first.`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const [config, userMiddleware, routes] = await Promise.all([
|
|
127
|
+
loadConfig(buildDir),
|
|
128
|
+
loadMiddleware(buildDir),
|
|
129
|
+
loadRoutes(buildDir),
|
|
130
|
+
])
|
|
131
|
+
|
|
132
|
+
const { pages, apis, pageModules, apiModules } = routes
|
|
133
|
+
|
|
134
|
+
const app = express()
|
|
135
|
+
|
|
136
|
+
app.use(morgan('tiny'))
|
|
137
|
+
app.use(express.json({ strict: false }))
|
|
138
|
+
app.use(cookieParser(process.env.OSSY_COOKIE_SECRET || 'default_secret'))
|
|
139
|
+
app.use((req, _res, next) => {
|
|
57
140
|
const userSettings = JSON.parse(req.signedCookies?.['x-ossy-user-settings'] || '{}')
|
|
58
141
|
req.userAppSettings = userSettings
|
|
59
142
|
if (userSettings.workspaceId && !req.get('workspaceId')) {
|
|
@@ -62,82 +145,102 @@ const middleware = [
|
|
|
62
145
|
const cookieHeader = req.headers.cookie
|
|
63
146
|
req.isAuthenticated = cookieHeader ? cookieHeader.includes('auth=') : false
|
|
64
147
|
next()
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
express.static(
|
|
68
|
-
ProxyInternal()
|
|
69
|
-
]
|
|
70
|
-
|
|
71
|
-
app.use(middleware)
|
|
72
|
-
|
|
73
|
-
let supportedLanguages = buildTimeConfig?.supportedLanguages
|
|
74
|
-
let defaultLanguage = buildTimeConfig?.defaultLanguage
|
|
75
|
-
if ((!supportedLanguages || supportedLanguages.length <= 1) && sitePageList.length > 0) {
|
|
76
|
-
const firstPage = sitePageList[0]
|
|
77
|
-
if (firstPage && typeof firstPage.path === 'object' && firstPage.path != null) {
|
|
78
|
-
supportedLanguages = Object.keys(firstPage.path)
|
|
79
|
-
defaultLanguage = defaultLanguage || supportedLanguages[0]
|
|
80
|
-
}
|
|
81
|
-
}
|
|
148
|
+
})
|
|
149
|
+
for (const mw of userMiddleware) app.use(mw)
|
|
150
|
+
if (fs.existsSync(publicDir)) app.use(express.static(publicDir))
|
|
151
|
+
app.use(ProxyInternal())
|
|
82
152
|
|
|
83
|
-
const apiRouter = OssyRouter.of({ pages:
|
|
84
|
-
const pageRouter = OssyRouter.of({
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
app.all('*all', async (req, res) => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (typeof ssrRender !== 'function') {
|
|
106
|
-
res.status(503).type('text').send('SSR runtime unavailable')
|
|
153
|
+
const apiRouter = OssyRouter.of({ pages: apis })
|
|
154
|
+
const pageRouter = OssyRouter.of({
|
|
155
|
+
pages,
|
|
156
|
+
supportedLanguages: Array.isArray(config.supportedLanguages) ? config.supportedLanguages : [],
|
|
157
|
+
defaultLanguage: config.defaultLanguage,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
app.all('*all', async (req, res) => {
|
|
161
|
+
const requestUrl = req.originalUrl || '/'
|
|
162
|
+
try {
|
|
163
|
+
const apiRoute = apiRouter.getPageByUrl(requestUrl)
|
|
164
|
+
if (apiRoute) {
|
|
165
|
+
const mod = apiModules.get(apiRoute.id)
|
|
166
|
+
const handler = mod?.default
|
|
167
|
+
if (typeof handler === 'function') {
|
|
168
|
+
await handler(req, res)
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
174
|
+
res.status(404).send('Not found')
|
|
107
175
|
return
|
|
108
176
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
177
|
+
|
|
178
|
+
const pageRoute = pageRouter.getPageByUrl(requestUrl)
|
|
179
|
+
if (pageRoute) {
|
|
180
|
+
const mod = pageModules.get(pageRoute.id)
|
|
181
|
+
const Component = mod?.default
|
|
182
|
+
if (typeof Component !== 'function') {
|
|
183
|
+
res.status(503).type('text').send('SSR runtime unavailable')
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
const appConfig = {
|
|
187
|
+
...config,
|
|
188
|
+
url: requestUrl,
|
|
189
|
+
theme: config?.theme || 'light',
|
|
190
|
+
isAuthenticated: !!req.isAuthenticated,
|
|
191
|
+
workspaceId: config?.workspaceId,
|
|
192
|
+
apiUrl: config?.apiUrl,
|
|
193
|
+
pages: pages.map((page) => ({ id: page?.id, path: page?.path })),
|
|
194
|
+
pageId: pageRoute.id,
|
|
195
|
+
sidebarPrimaryCollapsed: false,
|
|
196
|
+
}
|
|
197
|
+
const html = await renderPage({
|
|
198
|
+
Component,
|
|
199
|
+
metadata: mod.metadata,
|
|
200
|
+
appConfig,
|
|
201
|
+
})
|
|
202
|
+
res.status(200).type('html').send(html)
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
res.status(404).send('Not found')
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.error('[@ossy/platform][server] Request handling failed:', err)
|
|
209
|
+
if (!res.headersSent) {
|
|
210
|
+
res.status(500).type('text').send('Internal Server Error')
|
|
119
211
|
}
|
|
120
|
-
const html = await ssrRender(pageRoute.id, {
|
|
121
|
-
...appConfig,
|
|
122
|
-
theme: appConfig?.theme == null ? appConfig?.theme : JSON.parse(JSON.stringify(appConfig.theme)),
|
|
123
|
-
themes: appConfig?.themes == null ? appConfig?.themes : JSON.parse(JSON.stringify(appConfig.themes)),
|
|
124
|
-
resourceTemplates:
|
|
125
|
-
appConfig?.resourceTemplates == null
|
|
126
|
-
? appConfig?.resourceTemplates
|
|
127
|
-
: JSON.parse(JSON.stringify(appConfig.resourceTemplates)),
|
|
128
|
-
}, {})
|
|
129
|
-
res.status(200).type('html').send(html)
|
|
130
|
-
return
|
|
131
212
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
const server = await new Promise((resolve, reject) => {
|
|
216
|
+
const httpServer = app.listen(port)
|
|
217
|
+
httpServer.once('listening', () => resolve(httpServer))
|
|
218
|
+
httpServer.once('error', reject)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
console.log(`[@ossy/platform][server] Running on http://localhost:${port}`)
|
|
222
|
+
console.log('[@ossy/platform][server] Press Ctrl+C to stop.')
|
|
223
|
+
|
|
224
|
+
const closeServer = () => new Promise((resolve) => server.close(() => resolve()))
|
|
225
|
+
|
|
226
|
+
let shuttingDown = false
|
|
227
|
+
const handleShutdown = async (signal) => {
|
|
228
|
+
if (shuttingDown) return
|
|
229
|
+
shuttingDown = true
|
|
230
|
+
console.log(`\n[@ossy/platform][server] Received ${signal}, shutting down...`)
|
|
231
|
+
try {
|
|
232
|
+
await closeServer()
|
|
233
|
+
} finally {
|
|
234
|
+
process.exit(0)
|
|
137
235
|
}
|
|
138
236
|
}
|
|
139
|
-
})
|
|
140
237
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
238
|
+
process.on('SIGINT', () => handleShutdown('SIGINT'))
|
|
239
|
+
process.on('SIGTERM', () => handleShutdown('SIGTERM'))
|
|
240
|
+
|
|
241
|
+
const lifetime = new Promise(() => {})
|
|
242
|
+
|
|
243
|
+
return { app, server, port, close: closeServer, lifetime }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export default startServer
|