@omega.js/desktop 0.50.0 → 0.51.0
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 +1 -1
- package/dist/assets/css/tokens/_index.scss +1 -1
- package/dist/assets/themes/base/_includes/frontend/sections/account-section-header.html +4 -1
- package/dist/assets/themes/base/_includes/frontend/sections/footer.html +13 -6
- package/dist/assets/themes/base/_includes/frontend/sections/nav.html +10 -8
- package/dist/assets/themes/base/_includes/global/sections/account.html +3 -1
- package/dist/assets/themes/base/_includes/global/sections/app-sidebar.html +12 -8
- package/dist/assets/themes/base/_includes/global/sections/app-topbar.html +10 -6
- package/dist/assets/themes/base/_includes/global/sections/page-header.html +8 -4
- package/dist/assets/themes/base/_layouts/backend/pages/dashboard/index.html +24 -24
- package/dist/assets/themes/base/_layouts/frontend/pages/about.html +10 -10
- package/dist/assets/themes/base/_layouts/frontend/pages/account/index.html +35 -35
- package/dist/assets/themes/base/_layouts/frontend/pages/alternatives/index.html +4 -4
- package/dist/assets/themes/base/_layouts/frontend/pages/auth/signin.html +1 -1
- package/dist/assets/themes/base/_layouts/frontend/pages/auth/signup.html +4 -4
- package/dist/assets/themes/base/_layouts/frontend/pages/contact.html +3 -3
- package/dist/assets/themes/base/_layouts/frontend/pages/download.html +34 -33
- package/dist/assets/themes/base/_layouts/frontend/pages/extension/index.html +11 -11
- package/dist/assets/themes/base/_layouts/frontend/pages/legal/document.html +1 -1
- package/dist/assets/themes/base/_layouts/frontend/pages/status.html +1 -1
- package/dist/assets/themes/base/_layouts/frontend/pages/team/index.html +9 -7
- package/dist/assets/themes/base/_layouts/frontend/pages/team/member.html +5 -3
- package/dist/assets/themes/base/_sections/about/letter/section.html +1 -1
- package/dist/assets/themes/base/_sections/about/letter/section.json5 +4 -4
- package/dist/assets/themes/base/_sections/marketing/bento/section.html +1 -1
- package/dist/assets/themes/base/_sections/marketing/bento/section.json5 +15 -15
- package/dist/assets/themes/base/_sections/marketing/cta/section.json5 +1 -1
- package/dist/assets/themes/base/_sections/marketing/hero/section.html +6 -6
- package/dist/assets/themes/base/_sections/marketing/hero/section.json5 +10 -10
- package/dist/assets/themes/base/_sections/marketing/product-demo/section.html +1 -1
- package/dist/assets/themes/base/_sections/marketing/product-demo/section.json5 +2 -2
- package/dist/assets/themes/base/_sections/marketing/stats/section.html +1 -1
- package/dist/assets/themes/base/_sections/marketing/stats/section.json5 +5 -5
- package/dist/assets/themes/base/_sections/marketing/trusted-by/section.html +1 -1
- package/dist/assets/themes/base/_sections/marketing/trusted-by/section.json5 +2 -2
- package/dist/assets/themes/neobrutalism/_layouts/frontend/pages/index.html +10 -10
- package/dist/assets/themes/newsflash/_layouts/frontend/pages/index.html +10 -10
- package/dist/assets/themes/newsflash/_sections/marketing/desks/section.html +2 -2
- package/dist/assets/themes/newsflash/_sections/marketing/desks/section.json5 +4 -4
- package/dist/build.js +69 -28
- package/dist/cli-run.js +20 -13
- package/dist/cli.js +12 -7
- package/dist/commands/build.js +0 -2
- package/dist/commands/deploy.js +116 -44
- package/dist/commands/finalize-release.js +5 -4
- package/dist/commands/launch.js +12 -7
- package/dist/commands/lib/deploy-precheck.js +66 -34
- package/dist/commands/lib/ensure-target.js +71 -11
- package/dist/commands/package.js +0 -1
- package/dist/commands/publish.js +11 -4
- package/dist/commands/release.js +99 -252
- package/dist/commands/runner.js +42 -5
- package/dist/commands/sign-windows.js +46 -15
- package/dist/commands/test.js +4 -3
- package/dist/commands/validate-certs.js +291 -115
- package/dist/config/page-template.html +4 -0
- package/dist/defaults/.github/workflows/build.yml +144 -64
- package/dist/defaults/AGENTS.md +3 -2
- package/dist/defaults/_.gitignore +4 -2
- package/dist/defaults/config/certs/README.md +25 -45
- package/dist/defaults/config/omega.json5 +66 -32
- package/dist/defaults/hooks/deploy/pre.js +10 -0
- package/dist/defaults/src/assets/scss/main.scss +12 -2
- package/dist/defaults/src/integrations/tray/index.js +1 -1
- package/dist/gulp/main.js +13 -25
- package/dist/gulp/tasks/audit.js +18 -6
- package/dist/gulp/tasks/build-config.js +181 -60
- package/dist/gulp/tasks/bundle.js +108 -36
- package/dist/gulp/tasks/release.js +86 -4
- package/dist/gulp/tasks/sass.js +11 -0
- package/dist/hooks/lib/notarize-tools.js +137 -0
- package/dist/hooks/notarize-artifacts.js +57 -0
- package/dist/hooks/notarize.js +59 -8
- package/dist/lib/auth-persistence.js +25 -0
- package/dist/lib/client-bridge.js +11 -8
- package/dist/lib/deep-link.js +15 -8
- package/dist/lib/protocol.js +7 -1
- package/dist/lib/restart-manager/install.js +6 -3
- package/dist/lib/sign-helpers/auto-unlock.js +65 -27
- package/dist/lib/sign-helpers/console-lock.js +34 -0
- package/dist/lib/sign-helpers/exec-with-limit.js +68 -0
- package/dist/lib/sign-helpers/resolve-icons.js +6 -4
- package/dist/lib/tray.js +7 -6
- package/dist/main.js +16 -0
- package/dist/preload.js +8 -0
- package/dist/renderer.js +16 -4
- package/dist/runner/job-started.js +104 -0
- package/dist/test/fixtures/consumer-app/config/omega.json5 +7 -0
- package/dist/test/fixtures/consumer-app/package.json +1 -1
- package/dist/test/fixtures/consumer-app/src/assets/js/components/main/index.js +7 -1
- package/dist/test/harness/main-entry.js +2 -1
- package/dist/test/harness/renderer-preload.js +12 -3
- package/dist/test/runners/boot.js +101 -11
- package/dist/test/runners/electron.js +1 -1
- package/dist/test/suites/boot/consumer-app-boots.test.js +54 -0
- package/dist/test/suites/build/audit.test.js +45 -6
- package/dist/test/suites/build/auth-persistence-resolve.test.js +119 -0
- package/dist/test/suites/build/auto-unlock.test.js +105 -0
- package/dist/test/suites/build/boot-runner-timeout.test.js +253 -0
- package/dist/test/suites/build/brand-scss.test.js +106 -0
- package/dist/test/suites/build/build-config.test.js +146 -36
- package/dist/test/suites/build/build-json-bake.test.js +245 -0
- package/dist/test/suites/build/build-verbs.test.js +48 -15
- package/dist/test/suites/build/build-workflow-jobs.test.js +190 -0
- package/dist/test/suites/build/cli.test.js +32 -10
- package/dist/test/suites/build/config-schema.test.js +4 -4
- package/dist/test/suites/build/console-lock.test.js +51 -0
- package/dist/test/suites/build/defaults-scaffold.test.js +71 -2
- package/dist/test/suites/build/deploy-direct.test.js +241 -0
- package/dist/test/suites/build/deploy-dispatch.test.js +287 -0
- package/dist/test/suites/build/deploy-hook.test.js +169 -0
- package/dist/test/suites/build/ensure-target.test.js +14 -2
- package/dist/test/suites/build/env-delivery.test.js +19 -10
- package/dist/test/suites/build/env-watch.test.js +18 -7
- package/dist/test/suites/build/esm-only-dependency.test.js +127 -0
- package/dist/test/suites/build/exec-with-limit.test.js +53 -0
- package/dist/test/suites/build/finalize-release.test.js +2 -2
- package/dist/test/suites/build/get-config.test.js +120 -8
- package/dist/test/suites/build/github-utils.test.js +12 -6
- package/dist/test/suites/build/license-stamp.test.js +6 -4
- package/dist/test/suites/build/manager.test.js +88 -68
- package/dist/test/suites/build/manifest-deps.test.js +116 -0
- package/dist/test/suites/build/merge-line-files.test.js +27 -4
- package/dist/test/suites/build/notarize-artifacts.test.js +135 -0
- package/dist/test/suites/build/notarize-tools.test.js +38 -0
- package/dist/test/suites/build/notarize.test.js +207 -0
- package/dist/test/suites/build/release-pipeline.test.js +38 -60
- package/dist/test/suites/build/release-skipped-upload.test.js +107 -0
- package/dist/test/suites/build/resolve-icons.test.js +35 -35
- package/dist/test/suites/build/runner-job-guard.test.js +182 -0
- package/dist/test/suites/build/runner.test.js +25 -2
- package/dist/test/suites/build/sentry.test.js +8 -3
- package/dist/test/suites/build/setup-scripts.test.js +3 -0
- package/dist/test/suites/build/sign-windows.test.js +92 -8
- package/dist/test/suites/build/test-stealth.test.js +17 -11
- package/dist/test/suites/build/url-helpers.test.js +37 -17
- package/dist/test/suites/build/validate-certs.test.js +428 -55
- package/dist/test/suites/build/validate-config.test.js +3 -3
- package/dist/test/suites/main/auth-flow.test.js +12 -0
- package/dist/test/suites/main/auth-persistence.test.js +14 -17
- package/dist/test/suites/main/auto-updater.test.js +2 -2
- package/dist/test/suites/main/boot-sequence.test.js +1 -1
- package/dist/test/suites/main/client-bridge.integration.test.js +5 -82
- package/dist/test/suites/main/client-bridge.test.js +19 -6
- package/dist/test/suites/main/deep-link.test.js +54 -0
- package/dist/test/suites/main/startup-paths-and-ua.test.js +1 -1
- package/dist/test/suites/main/url-helpers.test.js +81 -72
- package/dist/test/suites/renderer/cross-context-helpers.test.js +19 -16
- package/dist/utils/build-pipeline.js +10 -10
- package/dist/utils/github.js +12 -51
- package/dist/utils/load-env.js +66 -0
- package/dist/utils/mode-helpers.js +43 -111
- package/dist/utils/platform.js +37 -0
- package/dist/utils/runner-env.js +2 -1
- package/dist/utils/runner-job-guard.js +149 -0
- package/dist/utils/ship-keys.js +52 -0
- package/dist/utils/test-stealth.js +5 -3
- package/dist/utils/url-helpers.js +33 -17
- package/dist/vendor/config/bundle-id.js +53 -0
- package/dist/vendor/config/client-config.js +141 -0
- package/dist/vendor/config/company.js +334 -15
- package/dist/vendor/config/dev-facts.js +48 -0
- package/dist/vendor/config/env-delivery.js +231 -9
- package/dist/vendor/config/env-retired.js +137 -0
- package/dist/vendor/config/env-rules.js +22 -3
- package/dist/vendor/config/env-schema.js +234 -117
- package/dist/vendor/config/env.js +55 -26
- package/dist/vendor/config/environment.js +189 -0
- package/dist/vendor/config/hooks.js +13 -11
- package/dist/vendor/config/index.js +121 -44
- package/dist/vendor/config/load.js +366 -115
- package/dist/vendor/config/merge.js +2 -2
- package/dist/vendor/config/order.js +3 -3
- package/dist/vendor/config/platforms.js +276 -0
- package/dist/vendor/config/repo.js +226 -104
- package/dist/vendor/config/retired-keys.js +232 -27
- package/dist/vendor/config/schema.js +525 -99
- package/dist/vendor/config/site-global.js +63 -53
- package/dist/vendor/config/targets.js +187 -0
- package/dist/vendor/config/validate.js +119 -59
- package/dist/vendor/devkit/argv.js +118 -0
- package/dist/vendor/devkit/attach-log-file.js +21 -13
- package/dist/vendor/devkit/brand-tokens.js +278 -0
- package/dist/vendor/devkit/brand-version.js +264 -0
- package/dist/vendor/devkit/build-json.js +91 -0
- package/dist/vendor/devkit/bundle.js +48 -0
- package/dist/vendor/devkit/certificate-expiry.js +108 -0
- package/dist/vendor/devkit/certs.js +16 -194
- package/dist/vendor/devkit/ci-workflows.js +124 -8
- package/dist/vendor/devkit/cli-router.js +3 -3
- package/dist/vendor/devkit/defaults-engine.js +69 -7
- package/dist/vendor/devkit/deploy-follow.js +297 -0
- package/dist/vendor/devkit/deploy-precheck.js +23 -8
- package/dist/vendor/devkit/deploy-record.js +11 -27
- package/dist/vendor/devkit/deploy-snapshot.js +661 -0
- package/dist/vendor/devkit/deploy.js +445 -75
- package/dist/vendor/devkit/git-auth.js +73 -0
- package/dist/vendor/devkit/git-remote.js +95 -0
- package/dist/vendor/devkit/github-repo.js +290 -0
- package/dist/vendor/devkit/local.js +47 -0
- package/dist/vendor/devkit/merge-line-files.js +23 -16
- package/dist/vendor/devkit/omega-bin.js +18 -3
- package/dist/vendor/devkit/pack-local.js +391 -0
- package/dist/vendor/devkit/preludes/index.js +120 -0
- package/dist/vendor/devkit/preludes/origin-heal.js +156 -0
- package/dist/vendor/devkit/service-account.js +43 -0
- package/dist/vendor/devkit/ship-plan.js +112 -0
- package/dist/vendor/devkit/signing-env.js +180 -0
- package/dist/vendor/devkit/signing-tree.js +92 -0
- package/dist/vendor/devkit/target-seams.js +142 -0
- package/dist/vendor/devkit/target-secrets.js +235 -50
- package/dist/vendor/devkit/test/esm-only-fixture.js +48 -0
- package/dist/vendor/devkit/test/fixtures/esm-only-package/browser.js +19 -0
- package/dist/vendor/devkit/test/fixtures/esm-only-package/index.js +20 -0
- package/dist/vendor/devkit/test/fixtures/esm-only-package/package.json +13 -0
- package/dist/vendor/monitoring/env.js +20 -10
- package/dist/vendor/monitoring/main.js +1 -1
- package/dist/vendor/monitoring/preload.js +1 -1
- package/dist/vendor/monitoring/renderer.js +1 -1
- package/docs/analytics.md +1 -1
- package/docs/auto-updater.md +5 -5
- package/docs/boot-sequence.md +1 -1
- package/docs/build-system.md +15 -7
- package/docs/client-bridge.md +9 -7
- package/docs/config-schema.md +4 -4
- package/docs/css.md +8 -2
- package/docs/deep-link.md +12 -4
- package/docs/environment-detection.md +32 -24
- package/docs/hooks.md +3 -1
- package/docs/icons.md +7 -7
- package/docs/index.md +61 -23
- package/docs/installer-options.md +24 -21
- package/docs/logging.md +5 -5
- package/docs/releasing.md +25 -17
- package/docs/runner.md +40 -5
- package/docs/shared/brands.md +12 -6
- package/docs/shared/breaking-changes.md +375 -21
- package/docs/shared/config.md +757 -199
- package/docs/shared/deploys.md +194 -91
- package/docs/shared/icons.md +18 -0
- package/docs/shared/local-dev.md +24 -6
- package/docs/shared/logging.md +9 -6
- package/docs/shared/monitoring.md +27 -13
- package/docs/shared/publishing.md +3 -3
- package/docs/shared/rulings.md +2 -2
- package/docs/shared/testing.md +1 -1
- package/docs/shared/theming.md +26 -1
- package/docs/shared/translation.md +49 -7
- package/docs/shared/updates.md +1 -1
- package/docs/signing.md +59 -33
- package/docs/test-framework.md +10 -5
- package/docs/themes.md +15 -1
- package/package.json +18 -12
- package/bin/omega-desktop +0 -2
- package/dist/commands/push-secrets.js +0 -141
- package/dist/test/suites/build/deliver-certs.test.js +0 -95
- package/dist/test/suites/build/derive-signing-env.test.js +0 -122
- package/dist/test/suites/build/push-secrets.test.js +0 -226
- package/dist/test/suites/build/resolve-signing-cert.test.js +0 -342
- package/dist/utils/deliver-certs.js +0 -69
- package/dist/utils/derive-signing-env.js +0 -56
- package/dist/utils/resolve-signing-cert.js +0 -175
- package/dist/vendor/config/desktop-artifacts.js +0 -110
- package/dist/vendor/config/instances.js +0 -208
- /package/dist/defaults/config/icons/{macos → mac}/dmg.png +0 -0
- /package/dist/defaults/config/icons/{macos → mac}/icon.png +0 -0
- /package/dist/defaults/config/icons/{macos → mac}/tray.png +0 -0
|
@@ -33,17 +33,19 @@
|
|
|
33
33
|
* declaring a desktop target derives nothing, since the release may not
|
|
34
34
|
* exist yet. WHICH repo those links address is not decided here: it is
|
|
35
35
|
* [repo.js](repo.js)'s `releasesRepo`, the one home of the brand's public
|
|
36
|
-
* releases repo (#799).
|
|
36
|
+
* releases repo (#799/#883): `<brand.id>-releases` under the brand's one
|
|
37
|
+
* `repo.org`, with no override key anywhere.
|
|
37
38
|
*
|
|
38
39
|
* Beside the hub sits `downloads` ([#620](https://github.com/Omega-JS-Stack/omega/issues/620)):
|
|
39
|
-
* one DIRECT-download URL per
|
|
40
|
-
* instead of a GitHub page.
|
|
41
|
-
* [
|
|
42
|
-
*
|
|
43
|
-
*
|
|
40
|
+
* one DIRECT-download URL per FORMAT, so a download button hands over a file
|
|
41
|
+
* instead of a GitHub page. Both the format keys and the filenames are
|
|
42
|
+
* [platforms.js](platforms.js)'s, the same table @omega.js/desktop packages
|
|
43
|
+
* under, and they carry no version, so releasing a desktop build never touches
|
|
44
|
+
* the website. A `store` format (the snap) has no file: its URL is the store
|
|
45
|
+
* page itself ([#867](https://github.com/Omega-JS-Stack/omega/issues/867)).
|
|
44
46
|
*/
|
|
45
47
|
|
|
46
|
-
const { desktopProductName, desktopArtifactNames } = require('./
|
|
48
|
+
const { enabledFormats, desktopProductName, desktopArtifactNames, sanitizeProductName } = require('./platforms.js');
|
|
47
49
|
const { releasesRepo } = require('./repo.js');
|
|
48
50
|
|
|
49
51
|
// Keys that are resolution machinery, not site content
|
|
@@ -56,11 +58,10 @@ const MACHINERY_KEYS = ['targets', 'enabled'];
|
|
|
56
58
|
* `releases.enabled: false` always suppresses. On the pipeline's second
|
|
57
59
|
* toSiteGlobal pass the raw block is gone, so a curated `releasesUrl` is
|
|
58
60
|
* itself the opt-in (idempotence).
|
|
59
|
-
* @param {object}
|
|
61
|
+
* @param {object} desktop - the desktop target's entry
|
|
60
62
|
* @returns {boolean}
|
|
61
63
|
*/
|
|
62
|
-
function desktopReleasesEnabled(
|
|
63
|
-
const desktop = targetEntry(config, 'desktop');
|
|
64
|
+
function desktopReleasesEnabled(desktop) {
|
|
64
65
|
if (!desktop) return false;
|
|
65
66
|
if (desktop.releases) return desktop.releases.enabled !== false;
|
|
66
67
|
|
|
@@ -72,92 +73,101 @@ function desktopReleasesEnabled(config) {
|
|
|
72
73
|
* public releases repo, whose address `@omega.js/config`'s `releasesRepo` owns
|
|
73
74
|
* (#799). Nothing is derived here; a repo the derivation cannot address (no
|
|
74
75
|
* owner, no brand.id) yields no URL.
|
|
76
|
+
* @param {object} desktop - the desktop target's entry
|
|
75
77
|
* @param {object} config - resolved config object
|
|
76
78
|
* @returns {string|undefined}
|
|
77
79
|
*/
|
|
78
|
-
function desktopReleasesUrl(config) {
|
|
79
|
-
const desktop = targetEntry(config, 'desktop');
|
|
80
|
-
|
|
80
|
+
function desktopReleasesUrl(desktop, config) {
|
|
81
81
|
// Idempotence: the build pipeline applies toSiteGlobal more than once
|
|
82
82
|
// (consumer.js loadSiteData, then engine.js configureOmega). On a second pass
|
|
83
83
|
// the raw releases block is gone, so the curated view's own URL is the
|
|
84
|
-
// authority: re-deriving
|
|
85
|
-
//
|
|
84
|
+
// authority: re-deriving off a config that no longer carries the block would
|
|
85
|
+
// drop the links a brand already has.
|
|
86
86
|
if (!desktop?.releases && desktop?.releasesUrl) return desktop.releasesUrl;
|
|
87
87
|
|
|
88
|
-
const
|
|
88
|
+
const releases = releasesRepo(config);
|
|
89
89
|
|
|
90
|
-
return
|
|
90
|
+
return releases ? `https://github.com/${releases.slug}/releases/latest` : undefined;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
* The
|
|
95
|
-
*
|
|
96
|
-
* release's asset by
|
|
94
|
+
* The download URL of every desktop FORMAT, keyed platform then format (#620,
|
|
95
|
+
* #867). An `asset` format links the file itself: GitHub's
|
|
96
|
+
* `/releases/latest/download/<asset>` serves the newest release's asset by
|
|
97
|
+
* name, which only holds because the names are versionless. A `store` format
|
|
98
|
+
* links the store page, because the store holds the file.
|
|
99
|
+
* @param {object} desktop - the desktop target's entry
|
|
97
100
|
* @param {object} config - resolved config object
|
|
98
101
|
* @param {string} releasesUrl - the derived releases hub URL
|
|
99
102
|
* @returns {object|undefined}
|
|
100
103
|
*/
|
|
101
|
-
function desktopDownloads(config, releasesUrl) {
|
|
102
|
-
const desktop = targetEntry(config, 'desktop');
|
|
103
|
-
|
|
104
|
+
function desktopDownloads(desktop, config, releasesUrl) {
|
|
104
105
|
// Idempotence, same trap as the URL above: the second toSiteGlobal pass sees
|
|
105
106
|
// the curated view, which carries no app block and no brand-derived product
|
|
106
|
-
// name
|
|
107
|
+
// name, so the curated URLs are the authority by then.
|
|
107
108
|
if (desktop?.downloads) return desktop.downloads;
|
|
108
109
|
|
|
109
|
-
// No product name (an invalid config
|
|
110
|
-
// nothing
|
|
111
|
-
const
|
|
110
|
+
// No product name (an invalid config: brand.name is required) derives
|
|
111
|
+
// nothing, because a guessed filename is a download button that 404s.
|
|
112
|
+
const productName = desktopProductName({ app: desktop?.app, brand: config?.brand });
|
|
113
|
+
const names = desktopArtifactNames(productName);
|
|
112
114
|
if (!Object.keys(names).length) return undefined;
|
|
113
115
|
|
|
116
|
+
// Only what this brand SHIPS gets a link: a dropped platform or format must
|
|
117
|
+
// not render a button that 404s (#867).
|
|
114
118
|
const downloads = {};
|
|
115
|
-
for (const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
for (const { platform, format } of enabledFormats(desktop, 'desktop')) {
|
|
120
|
+
// An asset links its file; the one store format (the snap) links the Snap
|
|
121
|
+
// Store listing, whose slug is the snap's own name, which electron-builder
|
|
122
|
+
// derives from the executable name: the sanitized product name, lowercased.
|
|
123
|
+
const filename = names[platform]?.[format];
|
|
124
|
+
const url = filename
|
|
125
|
+
? `${releasesUrl}/download/${filename}`
|
|
126
|
+
: `https://snapcraft.io/${sanitizeProductName(productName).toLowerCase()}`;
|
|
127
|
+
|
|
128
|
+
downloads[platform] = downloads[platform] || {};
|
|
129
|
+
downloads[platform][format] = url;
|
|
120
130
|
}
|
|
121
131
|
|
|
122
|
-
return downloads;
|
|
132
|
+
return Object.keys(downloads).length ? downloads : undefined;
|
|
123
133
|
}
|
|
124
134
|
|
|
125
135
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* @param {object}
|
|
131
|
-
* @param {string}
|
|
132
|
-
* @
|
|
136
|
+
* Whether an entry is a target of this TYPE (#886): the declared `type`, or,
|
|
137
|
+
* on the second toSiteGlobal pass, the curated fact only that type derives.
|
|
138
|
+
* The NAME is never the test, so a brand that names its desktop target `app`
|
|
139
|
+
* still gets `site.targets.app.downloads`.
|
|
140
|
+
* @param {object} entry - a raw or curated targets entry
|
|
141
|
+
* @param {string} type - 'desktop' | 'extension'
|
|
142
|
+
* @param {string} curatedKey - the curated key that type alone produces
|
|
143
|
+
* @returns {boolean}
|
|
133
144
|
*/
|
|
134
|
-
function
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return entry;
|
|
145
|
+
function isTargetOfType(entry, type, curatedKey) {
|
|
146
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;
|
|
147
|
+
return entry.type === type || entry[curatedKey] !== undefined;
|
|
138
148
|
}
|
|
139
149
|
|
|
140
150
|
/**
|
|
141
151
|
* Curate one target's display-safe view. Presence = enabled; per-type
|
|
142
152
|
* extras are allow-listed here, never spread from the raw entry.
|
|
143
|
-
* @param {
|
|
153
|
+
* @param {object} entry - the target's own config entry
|
|
144
154
|
* @param {object} config - resolved config object
|
|
145
155
|
* @returns {object}
|
|
146
156
|
*/
|
|
147
|
-
function curateTarget(
|
|
157
|
+
function curateTarget(entry, config) {
|
|
148
158
|
const view = { enabled: true };
|
|
149
159
|
|
|
150
|
-
if (
|
|
151
|
-
const releasesUrl = desktopReleasesUrl(config);
|
|
160
|
+
if (isTargetOfType(entry, 'desktop', 'releasesUrl') && desktopReleasesEnabled(entry)) {
|
|
161
|
+
const releasesUrl = desktopReleasesUrl(entry, config);
|
|
152
162
|
if (releasesUrl) {
|
|
153
163
|
view.releasesUrl = releasesUrl;
|
|
154
|
-
const downloads = desktopDownloads(config, releasesUrl);
|
|
164
|
+
const downloads = desktopDownloads(entry, config, releasesUrl);
|
|
155
165
|
if (downloads) view.downloads = downloads;
|
|
156
166
|
}
|
|
157
167
|
}
|
|
158
168
|
|
|
159
|
-
if (
|
|
160
|
-
const listings =
|
|
169
|
+
if (isTargetOfType(entry, 'extension', 'listings')) {
|
|
170
|
+
const { listings } = entry;
|
|
161
171
|
if (listings) {
|
|
162
172
|
const curated = {};
|
|
163
173
|
for (const [store, entry] of Object.entries(listings)) {
|
|
@@ -194,8 +204,8 @@ function toSiteGlobal(config) {
|
|
|
194
204
|
const targets = config?.targets;
|
|
195
205
|
if (targets && typeof targets === 'object') {
|
|
196
206
|
site.targets = {};
|
|
197
|
-
for (const name of Object.
|
|
198
|
-
site.targets[name] = curateTarget(
|
|
207
|
+
for (const [name, entry] of Object.entries(targets)) {
|
|
208
|
+
site.targets[name] = curateTarget(entry, config);
|
|
199
209
|
}
|
|
200
210
|
}
|
|
201
211
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `targets` map: every key is a target NAME, every entry declares its
|
|
3
|
+
* `type` ([#886](https://github.com/Omega-JS-Stack/omega/issues/886)).
|
|
4
|
+
*
|
|
5
|
+
* The name is the whole addressing story: it is the folder
|
|
6
|
+
* (`targets/<name>`), the `--target=<name>` word, and the derived-repo
|
|
7
|
+
* suffix. The type says WHICH framework runs there, so a brand runs two web
|
|
8
|
+
* targets by declaring two names:
|
|
9
|
+
*
|
|
10
|
+
* targets: {
|
|
11
|
+
* web: { type: 'web', url: 'https://somiibo.com' },
|
|
12
|
+
* community: { type: 'web', url: 'https://community.somiibo.com' },
|
|
13
|
+
* backend: { type: 'backend' },
|
|
14
|
+
* docs: { type: 'custom' },
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* There is no id, no folder key, and no name table: the key IS the answer, so
|
|
18
|
+
* the dir walk and the path derivation are each one line. This module is the
|
|
19
|
+
* ONE home of that derivation, imported by every framework and the manager,
|
|
20
|
+
* never copied.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const path = require('node:path');
|
|
24
|
+
|
|
25
|
+
const { TARGETS, CUSTOM_TARGET_TYPE } = require('./schema.js');
|
|
26
|
+
|
|
27
|
+
// A target name is a folder name, so it must be a dir-safe slug
|
|
28
|
+
const TARGET_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
29
|
+
|
|
30
|
+
// Every legal `type`: the framework targets plus the custom one whose verbs
|
|
31
|
+
// come from its own package.json scripts (#603)
|
|
32
|
+
const TARGET_TYPES = [...TARGETS, CUSTOM_TARGET_TYPE];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Every declared target as `{ name, type, ...entry }`, in config order.
|
|
36
|
+
* @param {object} config - A raw or resolved config carrying `targets`.
|
|
37
|
+
* @returns {Array<object>} The entries; [] when the config declares none.
|
|
38
|
+
* @throws {Error} When an entry carries no `type`, or a type no framework owns.
|
|
39
|
+
*/
|
|
40
|
+
function targetEntries(config) {
|
|
41
|
+
const targets = config && config.targets;
|
|
42
|
+
if (!targets || typeof targets !== 'object') return [];
|
|
43
|
+
|
|
44
|
+
return Object.keys(targets).map((name) => {
|
|
45
|
+
const entry = targets[name] || {};
|
|
46
|
+
|
|
47
|
+
if (!TARGET_TYPES.includes(entry.type)) {
|
|
48
|
+
throw new Error(`targets.${name} must declare a type: one of [${TARGET_TYPES.join(', ')}] (got ${entry.type === undefined ? 'nothing' : JSON.stringify(entry.type)})`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { ...entry, name, type: entry.type };
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The declared targets of one type, in config order.
|
|
57
|
+
* @param {object} config - A raw or resolved config carrying `targets`.
|
|
58
|
+
* @param {string} type - A value from TARGET_TYPES.
|
|
59
|
+
* @returns {Array<object>} The matching entries.
|
|
60
|
+
*/
|
|
61
|
+
function targetsOfType(config, type) {
|
|
62
|
+
return targetEntries(config).filter((entry) => entry.type === type);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Does this config declare a target of that TYPE? The ONE presence gate, for
|
|
67
|
+
* every "only when this brand runs a backend/desktop/mobile" check: a key
|
|
68
|
+
* lookup answers the wrong question now that a backend may be named `api`
|
|
69
|
+
* (#886). Raw, unvalidated configs answer too, so a missing or unknown `type`
|
|
70
|
+
* is simply "no", never a throw.
|
|
71
|
+
* @param {object} config - A raw or resolved config carrying `targets`.
|
|
72
|
+
* @param {string} type - A value from TARGET_TYPES.
|
|
73
|
+
* @returns {boolean} True when at least one entry declares that type.
|
|
74
|
+
*/
|
|
75
|
+
function hasTargetOfType(config, type) {
|
|
76
|
+
const targets = config && config.targets;
|
|
77
|
+
if (!targets || typeof targets !== 'object') return false;
|
|
78
|
+
|
|
79
|
+
return Object.values(targets).some((entry) => entry && entry.type === type);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A target's folder, relative to the brand root: the name IS the folder.
|
|
84
|
+
* @param {object} config - A raw or resolved config carrying `targets`.
|
|
85
|
+
* @param {string} name - The target name.
|
|
86
|
+
* @returns {string} `targets/<name>` (posix separators).
|
|
87
|
+
* @throws {Error} When the config declares no target of that name.
|
|
88
|
+
*/
|
|
89
|
+
function targetPath(config, name) {
|
|
90
|
+
const declared = config && config.targets ? Object.keys(config.targets) : [];
|
|
91
|
+
|
|
92
|
+
if (!declared.includes(name)) {
|
|
93
|
+
throw new Error(`No target "${name}" is declared: this brand declares [${declared.join(', ')}]`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return `targets/${name}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* WHICH target a project dir is: the target root's basename when the dir (or
|
|
101
|
+
* one of its TARGET_SUBDIRS, functions/ and dist/) sits inside a brand
|
|
102
|
+
* monorepo. A standalone project's dir name is arbitrary, so it names nothing.
|
|
103
|
+
* @param {string} projectDir - Target root, or one of its TARGET_SUBDIRS.
|
|
104
|
+
* @returns {string|null} The target name, or null outside a brand.
|
|
105
|
+
*/
|
|
106
|
+
function targetNameFromDir(projectDir) {
|
|
107
|
+
// Local require to avoid a load-time cycle (load.js requires this module)
|
|
108
|
+
const { findBrandRoot, TARGET_SUBDIRS } = require('./load.js');
|
|
109
|
+
|
|
110
|
+
let targetRoot = path.resolve(projectDir);
|
|
111
|
+
if (TARGET_SUBDIRS.includes(path.basename(targetRoot))) {
|
|
112
|
+
targetRoot = path.dirname(targetRoot);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!findBrandRoot(targetRoot)) return null;
|
|
116
|
+
return path.basename(targetRoot);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The host `brand.url` names, EXACTLY as it states it: a `www.` label is kept
|
|
121
|
+
* (a www brand is a www brand), any path is dropped, a port survives. A
|
|
122
|
+
* scheme-less value is read as https.
|
|
123
|
+
* @param {string} url - A brand URL.
|
|
124
|
+
* @returns {string} The host, or '' when the value is not a URL.
|
|
125
|
+
*/
|
|
126
|
+
function brandHost(url) {
|
|
127
|
+
try {
|
|
128
|
+
return new URL(url.includes('://') ? url : `https://${url}`).host;
|
|
129
|
+
} catch {
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The live URL of a target: its entry's `url`, a target-scoped `brand.url`
|
|
136
|
+
* override, then the derivation. A target NAMED for its type is the brand
|
|
137
|
+
* itself (`brand.url`); any other name is that name AS A SUBDOMAIN of the
|
|
138
|
+
* brand host (#588), which is what makes `community: { type: 'web' }` a
|
|
139
|
+
* complete declaration. Nothing is half-derived: no usable brand.url means
|
|
140
|
+
* null, never `https://community.`.
|
|
141
|
+
* @param {object} config - The (resolved) config carrying brand.url and targets.
|
|
142
|
+
* @param {string} name - The target name.
|
|
143
|
+
* @returns {string|null} The target's URL, or null when none is known.
|
|
144
|
+
*/
|
|
145
|
+
function targetUrl(config, name) {
|
|
146
|
+
const entry = config && config.targets ? config.targets[name] : null;
|
|
147
|
+
|
|
148
|
+
if (entry && entry.url) return entry.url;
|
|
149
|
+
if (entry && entry.brand && entry.brand.url) return entry.brand.url;
|
|
150
|
+
|
|
151
|
+
const brandUrl = config && config.brand ? config.brand.url : null;
|
|
152
|
+
if (!brandUrl) return null;
|
|
153
|
+
if (entry && name === entry.type) return brandUrl;
|
|
154
|
+
|
|
155
|
+
const host = brandHost(brandUrl);
|
|
156
|
+
return host ? `https://${name}.${host}` : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A target's deterministic dev-port offset: its position among the targets of
|
|
161
|
+
* ITS type, so N same-type targets get N side-by-side ports off the same
|
|
162
|
+
* classic base. Unknown names offset 0, and the N7 bump-if-taken allocator
|
|
163
|
+
* still guarantees a free port.
|
|
164
|
+
* @param {object} config - A raw or resolved config carrying `targets`.
|
|
165
|
+
* @param {string} name - The target name.
|
|
166
|
+
* @returns {number} Zero-based offset.
|
|
167
|
+
*/
|
|
168
|
+
function targetPortOffset(config, name) {
|
|
169
|
+
const entry = config && config.targets ? config.targets[name] : null;
|
|
170
|
+
if (!entry || !entry.type) return 0;
|
|
171
|
+
|
|
172
|
+
const index = targetsOfType(config, entry.type).findIndex((sibling) => sibling.name === name);
|
|
173
|
+
return index > 0 ? index : 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
TARGET_NAME_PATTERN,
|
|
178
|
+
TARGET_TYPES,
|
|
179
|
+
targetEntries,
|
|
180
|
+
targetsOfType,
|
|
181
|
+
hasTargetOfType,
|
|
182
|
+
targetPath,
|
|
183
|
+
targetNameFromDir,
|
|
184
|
+
targetUrl,
|
|
185
|
+
targetPortOffset,
|
|
186
|
+
brandHost,
|
|
187
|
+
};
|
|
@@ -3,19 +3,24 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The rule walker (runSchema) is electron-manager's proven validate-config
|
|
5
5
|
* engine ported verbatim: required/type/match/enum semantics, where match +
|
|
6
|
-
* enum
|
|
6
|
+
* enum (and itemEnum, the same check per member of an array value) only run on
|
|
7
|
+
* PRESENT values and a conditional `required` function
|
|
7
8
|
* receives the full config. See schema.js for the rule format.
|
|
8
9
|
*
|
|
9
10
|
* validateConfig() layers on top of the walker:
|
|
10
11
|
* - SHARED_SCHEMA always runs; TARGET_SCHEMAS[options.target] adds that
|
|
11
12
|
* target's refinements against the same resolved top-level namespace
|
|
12
|
-
* - `targets` sanity:
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* - `targets` sanity (#886): every key is a target NAME, so it must be a
|
|
14
|
+
* dir-safe slug, and every entry must be an object declaring a `type` the
|
|
15
|
+
* framework list knows (an ARRAY is the retired multi-instance form, whose
|
|
16
|
+
* replacement is a sibling key; >1 backend target is a WARNING)
|
|
17
|
+
* - the `repo` block (#883): one `provider` from REPO_PROVIDERS, and an `org`
|
|
18
|
+
* that is a real non-empty string whenever the block is there, because the
|
|
19
|
+
* org is the owner of every repo the brand's names derive under
|
|
20
|
+
* - `hosting` (#883) is a WEB target's key: a non-web target carrying it says
|
|
21
|
+
* something nothing serves, and its provider comes from HOSTING_PROVIDERS
|
|
17
22
|
* - `cloud.config.authDomain`, when set, must be the brand's OWN host (the
|
|
18
|
-
* resolved
|
|
23
|
+
* resolved target url, else brand.url): a firebaseapp.com value or a
|
|
19
24
|
* mismatch is an error, and demo-* (emulator-only) projects are exempt
|
|
20
25
|
* - retired keys are always errors (see retired-keys.js) — a name that was
|
|
21
26
|
* renamed outright reads as nothing at all, so it fails loudly instead of
|
|
@@ -27,11 +32,12 @@
|
|
|
27
32
|
* used to pass in silence
|
|
28
33
|
*/
|
|
29
34
|
|
|
30
|
-
const { TARGETS,
|
|
35
|
+
const { TARGETS, SHARED_SCHEMA, TARGET_SCHEMAS } = require('./schema.js');
|
|
31
36
|
const { findSecretKeys } = require('./secrets.js');
|
|
32
37
|
const { findRetiredKeys } = require('./retired-keys.js');
|
|
33
38
|
const { isPlainObject } = require('./merge.js');
|
|
34
|
-
const {
|
|
39
|
+
const { TARGET_NAME_PATTERN, TARGET_TYPES } = require('./targets.js');
|
|
40
|
+
const { REPO_PROVIDERS, HOSTING_PROVIDERS } = require('./repo.js');
|
|
35
41
|
const { isDemoProject } = require('./demo.js');
|
|
36
42
|
const { isCountedFeature } = require('../account/features.js');
|
|
37
43
|
|
|
@@ -142,6 +148,18 @@ function runSchema(config, schema) {
|
|
|
142
148
|
if (rule.enum && !rule.enum.includes(value)) {
|
|
143
149
|
errors.push(`config.${rule.path} "${value}" is not allowed — must be one of [${rule.enum.join(', ')}]`);
|
|
144
150
|
}
|
|
151
|
+
|
|
152
|
+
// ─── Item enum check (arrays) ───────────────────────────────────────────
|
|
153
|
+
// A LIST of allowed values (the extension's AMO categories): every member
|
|
154
|
+
// is named on its own line, so one bad slug in a good list is the one the
|
|
155
|
+
// error points at.
|
|
156
|
+
if (rule.itemEnum && Array.isArray(value)) {
|
|
157
|
+
for (const item of value) {
|
|
158
|
+
if (!rule.itemEnum.includes(item)) {
|
|
159
|
+
errors.push(`config.${rule.path} "${item}" is not allowed: must be one of [${rule.itemEnum.join(', ')}]`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
145
163
|
}
|
|
146
164
|
|
|
147
165
|
return errors;
|
|
@@ -149,13 +167,13 @@ function runSchema(config, schema) {
|
|
|
149
167
|
|
|
150
168
|
/**
|
|
151
169
|
* The host this (resolved) config's BRAND lives on, which is a brand-level
|
|
152
|
-
* fact and never
|
|
153
|
-
* one backend, one persona domain, shared by every
|
|
154
|
-
* `brand.url` answers first and the top-level `url` (the
|
|
155
|
-
* url, derived from its
|
|
170
|
+
* fact and never a per-target one (#588, Ian 2026-09-01): one Firebase project,
|
|
171
|
+
* one backend, one persona domain, shared by every target a brand runs. So
|
|
172
|
+
* `brand.url` answers first and the top-level `url` (the target's own public
|
|
173
|
+
* url, derived from its name or declared on its entry) is only the fallback for
|
|
156
174
|
* a config that carries no brand.url at all. Reading them the other way round
|
|
157
|
-
* made a `targets/
|
|
158
|
-
*
|
|
175
|
+
* made a `targets/admin` load fail its own brand's authDomain check and seeded
|
|
176
|
+
* personas at a different domain than the backend's.
|
|
159
177
|
* @param {object} config - The resolved config object.
|
|
160
178
|
* @returns {string} The lowercase hostname, or '' when no URL is known.
|
|
161
179
|
*/
|
|
@@ -173,6 +191,60 @@ function resolvedBrandHost(config) {
|
|
|
173
191
|
}
|
|
174
192
|
}
|
|
175
193
|
|
|
194
|
+
/**
|
|
195
|
+
* The repo block and the hosting key (#883).
|
|
196
|
+
*
|
|
197
|
+
* `repo` is `{ provider, org }` and nothing else: its PRESENCE enables the repo
|
|
198
|
+
* service, and every repo name derives from `<brand.id>-<role>`, so the org is
|
|
199
|
+
* the whole address. A block with no usable org is the failure this catches
|
|
200
|
+
* loudly, because every derivation would otherwise answer null and each reader
|
|
201
|
+
* would skip in its own quiet way.
|
|
202
|
+
*
|
|
203
|
+
* `hosting` says where a WEB target is served from. Only a web target has a
|
|
204
|
+
* built site to serve, so the key on any other type is a statement nothing
|
|
205
|
+
* reads. The targets map rides every resolved config, so this check answers the
|
|
206
|
+
* same on a raw config and on a target-resolved one.
|
|
207
|
+
*
|
|
208
|
+
* @param {object} config - The resolved config object.
|
|
209
|
+
* @returns {string[]} Errors; empty when the config passes.
|
|
210
|
+
*/
|
|
211
|
+
function validateRepo(config) {
|
|
212
|
+
const errors = [];
|
|
213
|
+
const repo = config ? config.repo : undefined;
|
|
214
|
+
|
|
215
|
+
if (isPlainObject(repo)) {
|
|
216
|
+
if (repo.provider !== undefined && !REPO_PROVIDERS.includes(repo.provider)) {
|
|
217
|
+
errors.push(`config.repo.provider ${JSON.stringify(repo.provider)} is not a host OMEGA builds for: one of [${REPO_PROVIDERS.join(', ')}]`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (typeof repo.org !== 'string' || !repo.org.trim()) {
|
|
221
|
+
errors.push(
|
|
222
|
+
'config.repo.org is required when the repo block is present: it is the org every repo the brand owns lives in '
|
|
223
|
+
+ '(`<brand.id>-omega`, `<brand.id>-releases`, `<brand.id>-<web target>`), and no repo name is configurable anywhere',
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const targets = config ? config.targets : undefined;
|
|
229
|
+
if (!isPlainObject(targets)) return errors;
|
|
230
|
+
|
|
231
|
+
Object.entries(targets).forEach(([name, entry]) => {
|
|
232
|
+
if (!isPlainObject(entry) || entry.hosting === undefined) return;
|
|
233
|
+
|
|
234
|
+
if (entry.type !== 'web') {
|
|
235
|
+
errors.push(`config.targets.${name}.hosting is a web-target key: a ${JSON.stringify(entry.type)} target has no built site to serve`);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const provider = isPlainObject(entry.hosting) ? entry.hosting.provider : undefined;
|
|
240
|
+
if (provider !== undefined && !HOSTING_PROVIDERS.includes(provider)) {
|
|
241
|
+
errors.push(`config.targets.${name}.hosting.provider ${JSON.stringify(provider)} is not a host OMEGA builds for: one of [${HOSTING_PROVIDERS.join(', ')}]`);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
return errors;
|
|
246
|
+
}
|
|
247
|
+
|
|
176
248
|
/**
|
|
177
249
|
* authDomain must be the brand's OWN host (cp268). Firebase's default
|
|
178
250
|
* `<project>.firebaseapp.com` is a third-party host: under browser storage
|
|
@@ -410,64 +482,52 @@ function validateConfig(config, options) {
|
|
|
410
482
|
// ─── targets sanity ────────────────────────────────────────────────────
|
|
411
483
|
const targets = config ? config.targets : undefined;
|
|
412
484
|
if (isPlainObject(targets)) {
|
|
485
|
+
const backends = [];
|
|
486
|
+
|
|
413
487
|
Object.keys(targets).forEach((key) => {
|
|
414
488
|
const entry = targets[key];
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
// inverse: they may never claim the custom type, since their verbs come
|
|
421
|
-
// from their framework, not from package.json scripts.
|
|
422
|
-
if (!framework && !isCustomTargetEntry(entry)) {
|
|
423
|
-
errors.push(
|
|
424
|
-
`config.targets.${key} is not a known target — must be one of [${TARGETS.join(', ')}], `
|
|
425
|
-
+ `or declare \`type: 'custom'\` (every instance of the array form) to run through its own package.json scripts`,
|
|
426
|
-
);
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
if (framework && isCustomTargetEntry(entry)) {
|
|
430
|
-
errors.push(`config.targets.${key} is a framework target — it cannot declare \`type: 'custom'\``);
|
|
489
|
+
|
|
490
|
+
// The key is a NAME, and the name is the folder (targets/<name>), so it
|
|
491
|
+
// has to be a dir-safe slug (#886).
|
|
492
|
+
if (!TARGET_NAME_PATTERN.test(key)) {
|
|
493
|
+
errors.push(`config.targets.${key} is not a usable target name (lowercase, starts with a letter, alnum/-): the name IS the folder targets/${key}`);
|
|
431
494
|
return;
|
|
432
495
|
}
|
|
433
496
|
|
|
434
|
-
// Multi-instance array form: every entry MUST carry a unique dir-safe
|
|
435
|
-
// id (it names the targets/<canonical>-<id> dir). Backend stays single-
|
|
436
|
-
// instance in practice — >1 is a warning, not an error (spec v1).
|
|
437
497
|
if (Array.isArray(entry)) {
|
|
438
|
-
|
|
439
|
-
errors.push(`config.targets.${key} instance array must not be empty — use {} for a single default instance`);
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const seen = new Set();
|
|
444
|
-
entry.forEach((instance, index) => {
|
|
445
|
-
if (!isPlainObject(instance)) {
|
|
446
|
-
errors.push(`config.targets.${key}[${index}] must be an instance object — got ${Array.isArray(instance) ? 'array' : typeof instance}`);
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
if (typeof instance.id !== 'string' || !INSTANCE_ID_PATTERN.test(instance.id)) {
|
|
450
|
-
errors.push(`config.targets.${key}[${index}] must carry a dir-safe id (lowercase, starts with a letter, alnum/-) — it names targets/<dir>-<id>`);
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
if (seen.has(instance.id)) {
|
|
454
|
-
errors.push(`config.targets.${key} instance id "${instance.id}" is not unique — ids must be unique per target type`);
|
|
455
|
-
}
|
|
456
|
-
seen.add(instance.id);
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
if (key === 'backend' && entry.length > 1) {
|
|
460
|
-
warnings.push(`config.targets.backend has ${entry.length} instances — multi-instance backend is unsupported for now (Cloud Functions = one project surface per brand)`);
|
|
461
|
-
}
|
|
498
|
+
errors.push(`config.targets.${key} is an array; a second instance is a sibling key, see docs/shared/breaking-changes.md 2026-09-11`);
|
|
462
499
|
return;
|
|
463
500
|
}
|
|
464
501
|
|
|
465
502
|
if (!isPlainObject(entry)) {
|
|
466
|
-
errors.push(`config.targets.${key} must be an object
|
|
503
|
+
errors.push(`config.targets.${key} must be an object declaring its type, got ${typeof entry}`);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// The TYPE says which framework runs there, and nothing else does: a
|
|
508
|
+
// name no framework owns is a deliberate brand choice, never a typo to
|
|
509
|
+
// guess at, so the type is what every reader keys off.
|
|
510
|
+
if (!TARGET_TYPES.includes(entry.type)) {
|
|
511
|
+
errors.push(
|
|
512
|
+
`config.targets.${key} must declare \`type\`, one of [${TARGET_TYPES.join(', ')}]`
|
|
513
|
+
+ `${entry.type === undefined ? '' : ` (got ${JSON.stringify(entry.type)})`}`,
|
|
514
|
+
);
|
|
515
|
+
return;
|
|
467
516
|
}
|
|
517
|
+
|
|
518
|
+
if (entry.type === 'backend') backends.push(key);
|
|
468
519
|
});
|
|
520
|
+
|
|
521
|
+
// Backend stays single-target in practice (one Cloud Functions surface per
|
|
522
|
+
// brand), so more than one is a warning, not an error.
|
|
523
|
+
if (backends.length > 1) {
|
|
524
|
+
warnings.push(`config.targets declares ${backends.length} backend targets (${backends.join(', ')}): multiple backends are unsupported for now (Cloud Functions = one project surface per brand)`);
|
|
525
|
+
}
|
|
469
526
|
}
|
|
470
527
|
|
|
528
|
+
// ─── the repo block, and where a web target is hosted (#883) ───────────
|
|
529
|
+
validateRepo(config).forEach((error) => errors.push(error));
|
|
530
|
+
|
|
471
531
|
// ─── authDomain is the brand's own host (cp268) ────────────────────────
|
|
472
532
|
validateAuthDomain(config).forEach((error) => errors.push(error));
|
|
473
533
|
|