@ossy/cli 0.16.8 → 0.16.10

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/README.md CHANGED
@@ -9,7 +9,7 @@ Unified CLI for the Ossy platform: app dev/build and CMS workflows.
9
9
  | `init [dir]` | Scaffold a new Ossy app (default: current directory) |
10
10
  | `dev` | Start dev server with watch (uses `src/*.page.jsx` or `src/pages.jsx`, `src/config.js`) |
11
11
  | `build` | Production build |
12
- | `publish` | Queue a container deployment via `@ossy/deployment-tools`, then upload `resourceTemplates` and **site build artifacts** (S3 presign + CMS resource) when `workspaceId` is set (see below) |
12
+ | `publish` | Queue a container deployment via `@ossy/deployment-tools` (**temporary**; see below), then upload `resourceTemplates` and **site build artifacts** (S3 presign + CMS resource) when `workspaceId` is set |
13
13
  | `cms upload` | Upload resource templates only (same API as publish’s upload step) |
14
14
  | `cms validate` | Validate ossy config and resource templates |
15
15
 
@@ -26,6 +26,25 @@ Options: `--pages`, `--config`, `--destination`. See `@ossy/app` for details.
26
26
 
27
27
  Publishes a site by sending a deployment request to your platform queue (same as `npx @ossy/deployment-tools deployment deploy`). Run from the **website package** directory (where `src/config.js` lives) so domain/platform can be read automatically.
28
28
 
29
+ ### Temporary: no execution of `src/config.js` for CMS steps
30
+
31
+ After deploy succeeds, **`publish`** still needs **`workspaceId`**, **`apiUrl`** (optional, for absolute URLs), and **`resourceTemplates`** from `src/config.js`. Those values are **not** loaded with `import()` anymore: running the real config in plain Node would execute imports such as `@ossy/themes`, which are only meant to be resolved during **`ossy build`** (Rollup).
32
+
33
+ Instead, the CLI uses a **temporary** pipeline:
34
+
35
+ - **`workspaceId`** and **`apiUrl`** — read with **string-literal regexes** (same idea as deploy hints).
36
+ - **`resourceTemplates`** — parsed with **`@babel/parser`** from `export default { … }` when that array is **JSON-like literals only** (no function calls, variables, spreads, or template literals in that subtree).
37
+
38
+ If `resourceTemplates` cannot be extracted, template upload is skipped. This is a **stopgap** until publish is driven by build output or platform events (see **Future direction**).
39
+
40
+ ### Future direction (planned)
41
+
42
+ The explicit **`deployment deploy`** step (SQS / deployment queue from **`@ossy/deployment-tools`**) is **intended to go away**: the platform should **react to a website upload / artifact event** (e.g. after the container image or site bundle is published) and roll out without the CLI calling deploy. When that exists, **`publish`** can shrink to CMS-only steps (resource templates + site artifacts), or those can move to separate workflows entirely. Treat the current **deploy + follow-ups** combo as **temporary** coupling.
43
+
44
+ ---
45
+
46
+ Details:
47
+
29
48
  ```bash
30
49
  cd packages/my-website
31
50
  npx @ossy/cli publish \
@@ -39,8 +58,8 @@ npx @ossy/cli publish \
39
58
  - **`--config`** — Path to another `config.js` if not `./src/config.js`.
40
59
  - If `platform` is omitted but `domain` is set (from flags or config), it is inferred from `deployments.json` when that domain appears under exactly one `targetDeploymentPlatform`.
41
60
  - **`--all`** — Runs `deployment deploy-all` for the platform; requires `--platform` or `platform` in config.
42
- - **Resource templates** — After a successful deploy, the CLI loads `./src/config.js` (or `--config`). If it exports **`workspaceId`** and a non-empty **`resourceTemplates`** array, they are **POST**ed to **`{api}/resource-templates`** with the **`workspaceId`** header (same as the CMS). Skipped when **`--skip-resource-templates`** is set, or when `workspaceId` / `resourceTemplates` are missing.
43
- - **Site artifacts** — If **`workspaceId`** is set and **`build/`** exists next to `src/` (i.e. run **`npm run build`** first), the CLI calls **`/site-artifacts/presign-batch`**, **PUT**s each file to S3, then **`/site-artifacts/commit-batch`** so a **`@ossy/platform/site-artifact-batch`** resource is created in the CMS. Skipped with **`--skip-site-artifacts`**, when there is no **`build/`**, or when **`workspaceId`** is missing. Override the build output directory with **`--site-artifacts-build-dir`** (absolute or cwd-relative).
61
+ - **Resource templates** — After a successful deploy, the CLI reads **`workspaceId`** / **`resourceTemplates`** from `./src/config.js` (or `--config`) using the **static extraction** described above (not `import()`). If **`workspaceId`** is set and **`resourceTemplates`** is a non-empty array, they are **POST**ed to **`{api}/resource-templates`** with the **`workspaceId`** header (same as the CMS). Skipped when **`--skip-resource-templates`** is set, or when `workspaceId` / `resourceTemplates` are missing or not extractable.
62
+ - **Site artifacts** — Uses the same **static `workspaceId` / `apiUrl` extraction** as resource templates. If **`workspaceId`** is set and **`build/`** exists next to `src/` (i.e. run **`npm run build`** first), the CLI calls **`/site-artifacts/presign-batch`**, **PUT**s each file to S3, then **`/site-artifacts/commit-batch`** so a **`@ossy/platform/site-artifact-batch`** resource is created in the CMS. Skipped with **`--skip-site-artifacts`**, when there is no **`build/`**, or when **`workspaceId`** is missing. Override the build output directory with **`--site-artifacts-build-dir`** (absolute or cwd-relative).
44
63
  - **`--cms-authentication`** — Optional token used only for CMS/API steps (templates + site artifacts); defaults to **`--authentication`** (deployment token).
45
64
  - **`--api-url`** — Optional API base for CMS calls (e.g. `https://api.ossy.se/api/v0`). Otherwise **`OSSY_API_URL`**, else an **absolute** `apiUrl` from config, else `https://api.ossy.se/api/v0`. Relative app `apiUrl` values (e.g. `/@ossy`) are ignored unless you pass **`--api-url`** or set **`OSSY_API_URL`**.
46
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/cli",
3
- "version": "0.16.8",
3
+ "version": "0.16.10",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ossy-se/packages.git"
@@ -17,7 +17,8 @@
17
17
  "license": "MIT",
18
18
  "bin": "./src/index.js",
19
19
  "dependencies": {
20
- "@ossy/app": "^0.15.8",
20
+ "@babel/parser": "^7.28.6",
21
+ "@ossy/app": "^0.15.10",
21
22
  "arg": "^5.0.2",
22
23
  "glob": "^10.3.10"
23
24
  },
@@ -29,5 +30,5 @@
29
30
  "/src",
30
31
  "README.md"
31
32
  ],
32
- "gitHead": "6514a64dbe2043b6f571404433779d9193a24895"
33
+ "gitHead": "cc54553550674b98f8a53bb3249307e06c4daa82"
33
34
  }
@@ -1,8 +1,138 @@
1
+ import { readFileSync } from 'fs'
1
2
  import { resolve } from 'path'
3
+ import { parse } from '@babel/parser'
2
4
  import { pathToFileURL } from 'url'
3
5
 
6
+ /** @returns {unknown | undefined} `undefined` if the AST cannot be reduced to JSON-like data */
7
+ function astNodeToLiteralValue (node) {
8
+ if (!node) return undefined
9
+ switch (node.type) {
10
+ case 'StringLiteral':
11
+ case 'BooleanLiteral':
12
+ return node.value
13
+ case 'NumericLiteral':
14
+ return node.value
15
+ case 'NullLiteral':
16
+ return null
17
+ case 'UnaryExpression':
18
+ if (node.operator === '-' && node.argument?.type === 'NumericLiteral') {
19
+ return -node.argument.value
20
+ }
21
+ return undefined
22
+ case 'ObjectExpression': {
23
+ const o = Object.create(null)
24
+ for (const p of node.properties) {
25
+ if (p.type !== 'ObjectProperty' || p.computed) return undefined
26
+ if (p.shorthand) return undefined
27
+ const key =
28
+ p.key.type === 'Identifier'
29
+ ? p.key.name
30
+ : p.key.type === 'StringLiteral'
31
+ ? p.key.value
32
+ : undefined
33
+ if (key === undefined) return undefined
34
+ const v = astNodeToLiteralValue(p.value)
35
+ if (v === undefined) return undefined
36
+ o[key] = v
37
+ }
38
+ return o
39
+ }
40
+ case 'ArrayExpression': {
41
+ const arr = []
42
+ for (const el of node.elements) {
43
+ if (el === null) {
44
+ arr.push(null)
45
+ continue
46
+ }
47
+ if (el.type === 'SpreadElement') return undefined
48
+ const v = astNodeToLiteralValue(el)
49
+ if (v === undefined) return undefined
50
+ arr.push(v)
51
+ }
52
+ return arr
53
+ }
54
+ default:
55
+ return undefined
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Reads `resourceTemplates` from `export default { ... }` when it is a literal array
61
+ * (no imports, calls, or templates). Matches typical OSSY website configs.
62
+ *
63
+ * @param {string} source
64
+ * @returns {unknown[] | undefined}
65
+ */
66
+ function extractResourceTemplatesFromSource (source) {
67
+ const ast = parse(source, {
68
+ sourceType: 'module',
69
+ allowAwaitOutsideFunction: true,
70
+ errorRecovery: false,
71
+ })
72
+
73
+ let obj = null
74
+ for (const stmt of ast.program.body) {
75
+ if (stmt.type !== 'ExportDefaultDeclaration') continue
76
+ const decl = stmt.declaration
77
+ if (decl.type === 'ObjectExpression') {
78
+ obj = decl
79
+ break
80
+ }
81
+ }
82
+ if (!obj) return undefined
83
+
84
+ for (const prop of obj.properties) {
85
+ if (prop.type !== 'ObjectProperty' || prop.computed) continue
86
+ const key =
87
+ prop.key.type === 'Identifier'
88
+ ? prop.key.name
89
+ : prop.key.type === 'StringLiteral'
90
+ ? prop.key.value
91
+ : null
92
+ if (key !== 'resourceTemplates') continue
93
+ if (prop.value.type !== 'ArrayExpression') return undefined
94
+ const v = astNodeToLiteralValue(prop.value)
95
+ return Array.isArray(v) ? v : undefined
96
+ }
97
+ return undefined
98
+ }
99
+
100
+ /**
101
+ * Reads fields needed for `ossy publish` follow-up steps **without executing** `config.js`.
102
+ * Avoids resolving imports such as `@ossy/themes` in CI (publish runs after deploy, outside Rollup).
103
+ *
104
+ * **Temporary** — prefer a build-time `publish-meta.json` or platform events once deploy is decoupled
105
+ * from the CLI; see `packages/cli/README.md` → *Publish* → *Temporary: no execution of src/config.js*.
106
+ *
107
+ * @param {string} configPath Absolute or cwd-relative path to `src/config.js`
108
+ * @returns {{ workspaceId?: string, apiUrl?: string, resourceTemplates?: unknown[] }}
109
+ */
110
+ export function readPublishFieldsFromWebsiteConfig (configPath) {
111
+ const abs = resolve(configPath)
112
+ const source = readFileSync(abs, 'utf8')
113
+
114
+ const pickString = (key) => {
115
+ const m = source.match(new RegExp(`\\b${key}\\s*:\\s*['"]([^'"]*)['"]`, 'm'))
116
+ return m ? m[1] : undefined
117
+ }
118
+
119
+ let resourceTemplates
120
+ try {
121
+ resourceTemplates = extractResourceTemplatesFromSource(source)
122
+ } catch {
123
+ resourceTemplates = undefined
124
+ }
125
+
126
+ return {
127
+ workspaceId: pickString('workspaceId'),
128
+ apiUrl: pickString('apiUrl'),
129
+ resourceTemplates,
130
+ }
131
+ }
132
+
4
133
  /**
5
- * Loads app `config.js` (ESM) for publish-side steps (e.g. resource template upload).
134
+ * @deprecated Prefer {@link readPublishFieldsFromWebsiteConfig} for publish dynamic import
135
+ * runs all side effects and `import`s in config (e.g. `@ossy/themes`), which breaks in plain Node.
6
136
  * @param {string} configPath Absolute or cwd-relative path to config file
7
137
  */
8
138
  export async function loadWebsiteConfig (configPath) {
@@ -1,5 +1,5 @@
1
1
  import { logInfo } from '../log.js'
2
- import { loadWebsiteConfig } from './load-website-config.js'
2
+ import { readPublishFieldsFromWebsiteConfig } from './load-website-config.js'
3
3
  import {
4
4
  postResourceTemplates,
5
5
  resolveApiBaseUrlForUpload,
@@ -13,9 +13,9 @@ export async function maybeUploadResourceTemplatesAfterPublish ({
13
13
  cmsToken,
14
14
  apiUrlFlag,
15
15
  }) {
16
- const config = await loadWebsiteConfig(configPath)
17
- const workspaceId = config?.workspaceId
18
- const resourceTemplates = config?.resourceTemplates
16
+ const config = readPublishFieldsFromWebsiteConfig(configPath)
17
+ const workspaceId = config.workspaceId
18
+ const resourceTemplates = config.resourceTemplates
19
19
 
20
20
  if (!workspaceId) {
21
21
  logInfo({
@@ -2,7 +2,7 @@ import { readdir, stat, readFile } from 'fs/promises'
2
2
  import { existsSync } from 'fs'
3
3
  import path from 'path'
4
4
  import { logInfo } from '../log.js'
5
- import { loadWebsiteConfig } from './load-website-config.js'
5
+ import { readPublishFieldsFromWebsiteConfig } from './load-website-config.js'
6
6
  import { resolveApiBaseUrlForUpload } from '../cms/upload-resource-templates.js'
7
7
 
8
8
  const MAX_FILES = 200
@@ -83,8 +83,8 @@ export async function maybeUploadSiteArtifactsAfterPublish ({
83
83
  apiUrlFlag,
84
84
  buildDir: buildDirOpt,
85
85
  }) {
86
- const config = await loadWebsiteConfig(configPath)
87
- const workspaceId = config?.workspaceId
86
+ const config = readPublishFieldsFromWebsiteConfig(configPath)
87
+ const workspaceId = config.workspaceId
88
88
 
89
89
  if (!workspaceId) {
90
90
  logInfo({
@@ -109,7 +109,7 @@ export async function maybeUploadSiteArtifactsAfterPublish ({
109
109
  const apiBaseUrl = resolveApiBaseUrlForUpload({
110
110
  flag: apiUrlFlag,
111
111
  envVar: process.env.OSSY_API_URL,
112
- configApiUrl: config?.apiUrl,
112
+ configApiUrl: config.apiUrl,
113
113
  })
114
114
 
115
115
  const files = await collectBuildFiles(buildDir)