@ossy/app 1.16.0 → 1.16.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/cli/build.task.js +19 -12
- package/cli/get-platform-files.task.js +21 -107
- package/cli/manifest-plugin.js +46 -16
- package/package.json +11 -11
package/cli/build.task.js
CHANGED
|
@@ -46,11 +46,21 @@ function ensureDir (dir) {
|
|
|
46
46
|
fs.mkdirSync(dir, { recursive: true })
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
// One source of truth for the per-entry name. Used both as the Rollup
|
|
50
|
+
// `input` key (which becomes the `[name]` token in `entryFileNames`) and
|
|
51
|
+
// as the basename of the generated stub. Flattening the full relative
|
|
52
|
+
// path with `__` keeps two files with the same basename in different
|
|
53
|
+
// folders (e.g. `apps/home.page.jsx` and `analytics/home.page.jsx`) from
|
|
54
|
+
// colliding on the Rollup input map — which the filesystem already
|
|
55
|
+
// guarantees can't happen at the relative-path level.
|
|
56
|
+
function entryNameFromSource (sourcePath, srcDir) {
|
|
50
57
|
const rel = path.relative(srcDir, sourcePath).replace(/\\/g, '/')
|
|
51
|
-
|
|
58
|
+
return rel.replace(/\//g, '__').replace(/\.(jsx?|tsx?|mjs|cjs)$/, '')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function stubFileNameFor (kind, sourcePath, srcDir) {
|
|
52
62
|
const ext = kind === 'page' ? '.entry.jsx' : '.entry.js'
|
|
53
|
-
return
|
|
63
|
+
return entryNameFromSource(sourcePath, srcDir) + ext
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
function relImport (fromAbs, toAbs) {
|
|
@@ -147,17 +157,12 @@ export async function build (cliArgs = []) {
|
|
|
147
157
|
const stubInputMap = {}
|
|
148
158
|
|
|
149
159
|
for (const entry of allEntries) {
|
|
150
|
-
const { kind, sourcePath
|
|
151
|
-
const
|
|
152
|
-
const stubAbs = path.join(stubDir,
|
|
160
|
+
const { kind, sourcePath } = entry
|
|
161
|
+
const inputName = entryNameFromSource(sourcePath, srcDir)
|
|
162
|
+
const stubAbs = path.join(stubDir, stubFileNameFor(kind, sourcePath, srcDir))
|
|
153
163
|
fs.writeFileSync(stubAbs, stubFor(kind, { stubAbs, sourceAbs: sourcePath }), 'utf8')
|
|
154
|
-
|
|
155
|
-
const inputName = path.basename(sourcePath).replace(/\.(jsx?|tsx?|mjs|cjs)$/, '')
|
|
156
|
-
if (stubInputMap[inputName]) {
|
|
157
|
-
throw new Error(`[@ossy/app][build] Duplicate entry name "${inputName}" between ${stubInputMap[inputName]} and ${stubAbs}.`)
|
|
158
|
-
}
|
|
159
164
|
stubInputMap[inputName] = stubAbs
|
|
160
|
-
entriesByStub.set(stubAbs, { kind, sourcePath
|
|
165
|
+
entriesByStub.set(stubAbs, { kind, sourcePath })
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
const configPath = path.resolve(srcDir, 'config.js')
|
|
@@ -197,6 +202,8 @@ export async function build (cliArgs = []) {
|
|
|
197
202
|
}),
|
|
198
203
|
manifestPlugin({
|
|
199
204
|
entriesByStub,
|
|
205
|
+
srcDir,
|
|
206
|
+
staticOutDir,
|
|
200
207
|
configValue,
|
|
201
208
|
manifestPath: path.join(buildPath, 'manifest.json'),
|
|
202
209
|
}),
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import { parse as babelParse } from '@babel/parser'
|
|
4
3
|
|
|
5
4
|
export const PAGE_FILE_PATTERN = /\.page\.(jsx?|tsx?)$/
|
|
6
5
|
export const API_FILE_PATTERN = /\.api\.(mjs|cjs|js)$/
|
|
@@ -10,9 +9,8 @@ export const TASK_FILE_PATTERN = /\.task\.(mjs|cjs|js)$/
|
|
|
10
9
|
* @typedef {'page' | 'api' | 'task'} EntryKind
|
|
11
10
|
*
|
|
12
11
|
* @typedef {object} PlatformEntry
|
|
13
|
-
* @property {EntryKind} kind
|
|
14
|
-
* @property {string} sourcePath
|
|
15
|
-
* @property {object} metadata Normalized metadata (always has `id`; pages always have `path`).
|
|
12
|
+
* @property {EntryKind} kind Which bucket this entry lives in.
|
|
13
|
+
* @property {string} sourcePath Absolute path to the user-authored source file.
|
|
16
14
|
*
|
|
17
15
|
* @typedef {object} PlatformFiles
|
|
18
16
|
* @property {PlatformEntry[]} pages Discovered `*.page.{jsx,tsx,...}` entries.
|
|
@@ -43,7 +41,14 @@ function classifyFile (absPath) {
|
|
|
43
41
|
return null
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Default `id` derived from a source file's location relative to `src/`.
|
|
46
|
+
* Used when a source module doesn't export `metadata.id`.
|
|
47
|
+
* `src/home.page.jsx` -> `home`
|
|
48
|
+
* `src/index.page.jsx` -> `home`
|
|
49
|
+
* `src/blog/post.page.jsx` -> `blog-post`
|
|
50
|
+
*/
|
|
51
|
+
export function metadataIdFromFile (absPath, srcDir) {
|
|
47
52
|
const rel = path.relative(srcDir, absPath).replace(/\\/g, '/')
|
|
48
53
|
const noExt = rel
|
|
49
54
|
.replace(PAGE_FILE_PATTERN, '')
|
|
@@ -53,110 +58,22 @@ function metadataIdFromFile (absPath, srcDir) {
|
|
|
53
58
|
return noExt.replace(/\//g, '-')
|
|
54
59
|
}
|
|
55
60
|
|
|
56
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Default URL path for a page when `metadata.path` isn't set.
|
|
63
|
+
* `home` is a special case — it lives at `/`.
|
|
64
|
+
*/
|
|
65
|
+
export function defaultPageRoute (id) {
|
|
57
66
|
return id === 'home' ? '/' : '/' + id
|
|
58
67
|
}
|
|
59
68
|
|
|
60
|
-
// Parses the static `metadata` export from a source file without executing it.
|
|
61
|
-
// Returns a plain JS-able shape: strings, numbers, booleans, arrays, and
|
|
62
|
-
// nested objects with string keys. Arbitrary expressions (function calls,
|
|
63
|
-
// references) are not supported here — pages must declare their metadata as
|
|
64
|
-
// a literal object expression so the build can read it without bundling.
|
|
65
|
-
function parseMetadataFromSource (absPath) {
|
|
66
|
-
const source = fs.readFileSync(absPath, 'utf8')
|
|
67
|
-
const ext = path.extname(absPath)
|
|
68
|
-
const plugins = ['importMeta']
|
|
69
|
-
if (ext === '.tsx' || ext === '.ts') plugins.push('typescript')
|
|
70
|
-
if (ext === '.jsx' || ext === '.tsx') plugins.push('jsx')
|
|
71
|
-
|
|
72
|
-
let ast
|
|
73
|
-
try {
|
|
74
|
-
ast = babelParse(source, {
|
|
75
|
-
sourceType: 'module',
|
|
76
|
-
allowImportExportEverywhere: true,
|
|
77
|
-
plugins,
|
|
78
|
-
})
|
|
79
|
-
} catch (err) {
|
|
80
|
-
throw new Error(`[@ossy/app][build] Failed to parse ${absPath}: ${err.message}`)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
for (const node of ast.program.body) {
|
|
84
|
-
if (node.type !== 'ExportNamedDeclaration') continue
|
|
85
|
-
const decl = node.declaration
|
|
86
|
-
if (!decl || decl.type !== 'VariableDeclaration') continue
|
|
87
|
-
for (const declarator of decl.declarations) {
|
|
88
|
-
if (declarator.id?.type !== 'Identifier') continue
|
|
89
|
-
if (declarator.id.name !== 'metadata') continue
|
|
90
|
-
const literal = literalFromNode(declarator.init)
|
|
91
|
-
if (literal === undefined) {
|
|
92
|
-
throw new Error(`[@ossy/app][build] metadata in ${absPath} must be a literal object expression.`)
|
|
93
|
-
}
|
|
94
|
-
return literal
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return null
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function literalFromNode (node) {
|
|
101
|
-
if (!node) return undefined
|
|
102
|
-
switch (node.type) {
|
|
103
|
-
case 'StringLiteral':
|
|
104
|
-
case 'NumericLiteral':
|
|
105
|
-
case 'BooleanLiteral':
|
|
106
|
-
return node.value
|
|
107
|
-
case 'NullLiteral':
|
|
108
|
-
return null
|
|
109
|
-
case 'TemplateLiteral':
|
|
110
|
-
if (node.expressions.length === 0) return node.quasis.map((q) => q.value.cooked).join('')
|
|
111
|
-
return undefined
|
|
112
|
-
case 'ArrayExpression': {
|
|
113
|
-
const out = []
|
|
114
|
-
for (const el of node.elements) {
|
|
115
|
-
if (el === null) { out.push(null); continue }
|
|
116
|
-
const value = literalFromNode(el)
|
|
117
|
-
if (value === undefined) return undefined
|
|
118
|
-
out.push(value)
|
|
119
|
-
}
|
|
120
|
-
return out
|
|
121
|
-
}
|
|
122
|
-
case 'ObjectExpression': {
|
|
123
|
-
const out = {}
|
|
124
|
-
for (const prop of node.properties) {
|
|
125
|
-
if (prop.type !== 'ObjectProperty') return undefined
|
|
126
|
-
let key
|
|
127
|
-
if (prop.key.type === 'Identifier') key = prop.key.name
|
|
128
|
-
else if (prop.key.type === 'StringLiteral') key = prop.key.value
|
|
129
|
-
else return undefined
|
|
130
|
-
const value = literalFromNode(prop.value)
|
|
131
|
-
if (value === undefined) return undefined
|
|
132
|
-
out[key] = value
|
|
133
|
-
}
|
|
134
|
-
return out
|
|
135
|
-
}
|
|
136
|
-
default:
|
|
137
|
-
return undefined
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function ensureMetadataForKind ({ kind, sourcePath, parsed, srcDir }) {
|
|
142
|
-
const fallbackId = metadataIdFromFile(sourcePath, srcDir)
|
|
143
|
-
const meta = { ...(parsed || {}) }
|
|
144
|
-
if (!meta.id) meta.id = fallbackId
|
|
145
|
-
if (kind === 'page' && meta.path === undefined) meta.path = defaultPageRoute(meta.id)
|
|
146
|
-
if (kind === 'task' && !meta.id && meta.type) meta.id = meta.type
|
|
147
|
-
return meta
|
|
148
|
-
}
|
|
149
|
-
|
|
150
69
|
/**
|
|
151
|
-
* Walks `srcDir` and returns the user-authored entries
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* placeholders the user is mid-creating.
|
|
70
|
+
* Walks `srcDir` and returns the user-authored entries the platform cares
|
|
71
|
+
* about, bucketed by kind. Empty (zero-byte) source files are skipped — we
|
|
72
|
+
* treat them as placeholders the user is mid-creating.
|
|
155
73
|
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* without triggering Rollup).
|
|
74
|
+
* Metadata isn't read here on purpose: the build pipeline reads it from the
|
|
75
|
+
* bundled output instead, which lets users compute `metadata.path` (and
|
|
76
|
+
* anything else) from imports, config, env vars, etc.
|
|
160
77
|
*
|
|
161
78
|
* @param {string} srcDir Absolute path to the project's `src/` directory.
|
|
162
79
|
* @returns {Promise<PlatformFiles>}
|
|
@@ -173,12 +90,9 @@ export default async function getPlatformFiles (srcDir) {
|
|
|
173
90
|
if (!stat.isFile() || stat.size === 0) continue
|
|
174
91
|
const kind = classifyFile(sourcePath)
|
|
175
92
|
if (!kind) continue
|
|
176
|
-
const parsed = parseMetadataFromSource(sourcePath)
|
|
177
|
-
const metadata = ensureMetadataForKind({ kind, sourcePath, parsed, srcDir })
|
|
178
93
|
out[kind === 'page' ? 'pages' : kind === 'api' ? 'apis' : 'tasks'].push({
|
|
179
94
|
kind,
|
|
180
95
|
sourcePath,
|
|
181
|
-
metadata,
|
|
182
96
|
})
|
|
183
97
|
}
|
|
184
98
|
return out
|
package/cli/manifest-plugin.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.js'
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* @typedef {import('./get-platform-files.task.js').PlatformEntry} PlatformEntry
|
|
@@ -7,9 +10,16 @@ import path from 'node:path'
|
|
|
7
10
|
* @typedef {object} ManifestPluginOptions
|
|
8
11
|
* @property {Map<string, PlatformEntry>} entriesByStub
|
|
9
12
|
* Maps each generated stub's absolute path (Rollup's `facadeModuleId`) to
|
|
10
|
-
* the `PlatformEntry` it was created for.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
+
* the `PlatformEntry` it was created for. Used to recover the entry's
|
|
14
|
+
* `kind` and source location after Rollup has bundled and hashed each
|
|
15
|
+
* chunk.
|
|
16
|
+
* @property {string} srcDir
|
|
17
|
+
* Absolute path to the project's `src/` — used to derive fallback `id`s
|
|
18
|
+
* from source file paths when `metadata.id` is missing.
|
|
19
|
+
* @property {string} staticOutDir
|
|
20
|
+
* Absolute path to Rollup's `output.dir` (typically
|
|
21
|
+
* `build/public/static/`). Bundled chunks live here, and we dynamically
|
|
22
|
+
* `import()` them to read their exported metadata.
|
|
13
23
|
* @property {object} configValue
|
|
14
24
|
* The serializable config object (typically `src/config.js`'s default
|
|
15
25
|
* export). Inlined under `manifest.config` so the platform server doesn't
|
|
@@ -44,6 +54,13 @@ import path from 'node:path'
|
|
|
44
54
|
* top-level key because it isn't an entry — it's the runtime context the
|
|
45
55
|
* platform threads into every page render.
|
|
46
56
|
*
|
|
57
|
+
* Metadata is read from the **bundled** output, not from the source file.
|
|
58
|
+
* Each entry stub re-exports `metadata` from the user's module, so once
|
|
59
|
+
* Rollup writes the chunk we can `await import(chunkUrl)` and read
|
|
60
|
+
* `mod.metadata` directly. This lets users compute metadata however they
|
|
61
|
+
* like (function calls, `Object.fromEntries`, values derived from imported
|
|
62
|
+
* config, etc.) — anything that runs at module load in Node will work.
|
|
63
|
+
*
|
|
47
64
|
* The plugin runs in the `writeBundle` hook (rather than `generateBundle` +
|
|
48
65
|
* `emitFile`) for two reasons:
|
|
49
66
|
*
|
|
@@ -51,22 +68,19 @@ import path from 'node:path'
|
|
|
51
68
|
* `output.dir` (`build/public/static/`). Rollup forbids relative paths
|
|
52
69
|
* in `emitFile({ type: 'asset', fileName })`, so we write the file
|
|
53
70
|
* directly with `fs` once Rollup has finished writing the chunks.
|
|
54
|
-
* 2.
|
|
55
|
-
*
|
|
71
|
+
* 2. We need the chunks on disk to dynamically `import()` them and read
|
|
72
|
+
* their exported `metadata`.
|
|
56
73
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* `manifest.entries` and stamps `entry: '/static/<chunk-name>'`.
|
|
60
|
-
* Duplicate ids within a `type` abort the build — routers key on `id`
|
|
61
|
-
* and would otherwise silently dispatch to the wrong entry.
|
|
74
|
+
* Duplicate ids within a `type` abort the build — routers key on `id` and
|
|
75
|
+
* would otherwise silently dispatch to the wrong entry.
|
|
62
76
|
*
|
|
63
77
|
* @param {ManifestPluginOptions} options
|
|
64
78
|
* @returns {import('rollup').Plugin}
|
|
65
79
|
*/
|
|
66
|
-
export function manifestPlugin ({ entriesByStub, configValue, manifestPath }) {
|
|
80
|
+
export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configValue, manifestPath }) {
|
|
67
81
|
return {
|
|
68
82
|
name: 'ossy-manifest',
|
|
69
|
-
writeBundle (_, bundle) {
|
|
83
|
+
async writeBundle (_, bundle) {
|
|
70
84
|
/** @type {ManifestEntry[]} */
|
|
71
85
|
const entries = []
|
|
72
86
|
const seenIds = { page: new Set(), api: new Set(), task: new Set() }
|
|
@@ -80,8 +94,23 @@ export function manifestPlugin ({ entriesByStub, configValue, manifestPath }) {
|
|
|
80
94
|
if (!entryInfo) continue
|
|
81
95
|
|
|
82
96
|
const url = '/static/' + fileName
|
|
83
|
-
const
|
|
84
|
-
|
|
97
|
+
const localPath = path.join(staticOutDir, fileName)
|
|
98
|
+
|
|
99
|
+
// Cache-bust the import so consecutive builds in a watch loop don't
|
|
100
|
+
// pick up a stale module from Node's loader cache.
|
|
101
|
+
const importHref = pathToFileURL(localPath).href + `?ts=${Date.now()}`
|
|
102
|
+
let mod
|
|
103
|
+
try {
|
|
104
|
+
mod = await import(importHref)
|
|
105
|
+
} catch (err) {
|
|
106
|
+
this.error(
|
|
107
|
+
`[@ossy/app][build] Failed to import bundled ${entryInfo.kind} entry ` +
|
|
108
|
+
`${entryInfo.sourcePath}: ${err && err.message ? err.message : err}`,
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const rawMeta = (mod && mod.metadata) || {}
|
|
113
|
+
const id = rawMeta.id || metadataIdFromFile(entryInfo.sourcePath, srcDir)
|
|
85
114
|
if (!id) {
|
|
86
115
|
this.error(`[@ossy/app][build] ${entryInfo.kind} entry missing metadata.id: ${entryInfo.sourcePath}`)
|
|
87
116
|
}
|
|
@@ -91,9 +120,10 @@ export function manifestPlugin ({ entriesByStub, configValue, manifestPath }) {
|
|
|
91
120
|
seenIds[entryInfo.kind].add(id)
|
|
92
121
|
|
|
93
122
|
if (entryInfo.kind === 'page') {
|
|
94
|
-
|
|
123
|
+
const pagePath = rawMeta.path !== undefined ? rawMeta.path : defaultPageRoute(id)
|
|
124
|
+
entries.push({ type: 'page', id, path: pagePath, title: rawMeta.title, entry: url })
|
|
95
125
|
} else if (entryInfo.kind === 'api') {
|
|
96
|
-
entries.push({ type: 'api', id, path:
|
|
126
|
+
entries.push({ type: 'api', id, path: rawMeta.path, entry: url })
|
|
97
127
|
} else {
|
|
98
128
|
entries.push({ type: 'task', id, entry: url })
|
|
99
129
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/app",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"source": "./src/index.js",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -33,15 +33,15 @@
|
|
|
33
33
|
"@babel/eslint-parser": "^7.15.8",
|
|
34
34
|
"@babel/preset-react": "^7.26.3",
|
|
35
35
|
"@babel/register": "^7.25.9",
|
|
36
|
-
"@ossy/connected-components": "^1.16.
|
|
37
|
-
"@ossy/design-system": "^1.16.
|
|
38
|
-
"@ossy/pages": "^1.16.
|
|
39
|
-
"@ossy/platform": "^1.15.
|
|
40
|
-
"@ossy/router": "^1.16.
|
|
41
|
-
"@ossy/router-react": "^1.16.
|
|
42
|
-
"@ossy/sdk": "^1.16.
|
|
43
|
-
"@ossy/sdk-react": "^1.16.
|
|
44
|
-
"@ossy/themes": "^1.16.
|
|
36
|
+
"@ossy/connected-components": "^1.16.2",
|
|
37
|
+
"@ossy/design-system": "^1.16.2",
|
|
38
|
+
"@ossy/pages": "^1.16.2",
|
|
39
|
+
"@ossy/platform": "^1.15.2",
|
|
40
|
+
"@ossy/router": "^1.16.2",
|
|
41
|
+
"@ossy/router-react": "^1.16.2",
|
|
42
|
+
"@ossy/sdk": "^1.16.2",
|
|
43
|
+
"@ossy/sdk-react": "^1.16.2",
|
|
44
|
+
"@ossy/themes": "^1.16.2",
|
|
45
45
|
"@rollup/plugin-alias": "^6.0.0",
|
|
46
46
|
"@rollup/plugin-babel": "6.1.0",
|
|
47
47
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
@@ -75,5 +75,5 @@
|
|
|
75
75
|
"README.md",
|
|
76
76
|
"tsconfig.json"
|
|
77
77
|
],
|
|
78
|
-
"gitHead": "
|
|
78
|
+
"gitHead": "00aca40bd8fcf8510c4a854cb388a780ddb751f7"
|
|
79
79
|
}
|