@_linked/cli 1.18.0 → 1.19.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/CHANGELOG.md +87 -0
- package/README.md +119 -0
- package/lib/esm/app-release/app-assets-store.d.ts +23 -0
- package/lib/esm/app-release/app-assets-store.d.ts.map +1 -0
- package/lib/esm/app-release/app-assets-store.js +33 -0
- package/lib/esm/app-release/app-assets-store.js.map +1 -0
- package/lib/esm/app-release/create-release-manifest.d.ts +114 -0
- package/lib/esm/app-release/create-release-manifest.d.ts.map +1 -0
- package/lib/esm/app-release/create-release-manifest.js +238 -0
- package/lib/esm/app-release/create-release-manifest.js.map +1 -0
- package/lib/esm/app-release/paths.d.ts +5 -0
- package/lib/esm/app-release/paths.d.ts.map +1 -0
- package/lib/esm/app-release/paths.js +44 -0
- package/lib/esm/app-release/paths.js.map +1 -0
- package/lib/esm/app-release/publisher.d.ts +25 -0
- package/lib/esm/app-release/publisher.d.ts.map +1 -0
- package/lib/esm/app-release/publisher.js +238 -0
- package/lib/esm/app-release/publisher.js.map +1 -0
- package/lib/esm/app-release/resolve-build-target.d.ts +19 -0
- package/lib/esm/app-release/resolve-build-target.d.ts.map +1 -0
- package/lib/esm/app-release/resolve-build-target.js +30 -0
- package/lib/esm/app-release/resolve-build-target.js.map +1 -0
- package/lib/esm/app-release/static-assets.d.ts +13 -0
- package/lib/esm/app-release/static-assets.d.ts.map +1 -0
- package/lib/esm/app-release/static-assets.js +55 -0
- package/lib/esm/app-release/static-assets.js.map +1 -0
- package/lib/esm/app-release/types.d.ts +71 -0
- package/lib/esm/app-release/types.d.ts.map +1 -0
- package/lib/esm/app-release/types.js +2 -0
- package/lib/esm/app-release/types.js.map +1 -0
- package/lib/esm/cli.js +78 -16
- package/lib/esm/cli.js.map +1 -1
- package/lib/esm/commands/build-app.d.ts +55 -0
- package/lib/esm/commands/build-app.d.ts.map +1 -0
- package/lib/esm/commands/build-app.js +178 -0
- package/lib/esm/commands/build-app.js.map +1 -0
- package/lib/esm/commands/publish-app.d.ts +18 -0
- package/lib/esm/commands/publish-app.d.ts.map +1 -0
- package/lib/esm/commands/publish-app.js +124 -0
- package/lib/esm/commands/publish-app.js.map +1 -0
- package/lib/esm/commands/serve-app.d.ts +14 -0
- package/lib/esm/commands/serve-app.d.ts.map +1 -0
- package/lib/esm/commands/serve-app.js +49 -0
- package/lib/esm/commands/serve-app.js.map +1 -0
- package/lib/esm/index.d.ts +11 -0
- package/lib/esm/index.d.ts.map +1 -1
- package/lib/esm/index.js +9 -0
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/lifecycle.d.ts +1 -1
- package/lib/esm/lifecycle.d.ts.map +1 -1
- package/lib/esm/lifecycle.js +17 -8
- package/lib/esm/lifecycle.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,92 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.19.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#100](https://github.com/linked-fw/cli/pull/100) [`8ef0fd7`](https://github.com/linked-fw/cli/commit/8ef0fd7f810dfff26b95f5390565661b8c38c7ac) Thanks [@flyon](https://github.com/flyon)! - ## App release pipeline on the file-store API
|
|
8
|
+
|
|
9
|
+
A manifest-driven build/publish/serve flow for Linked apps, replacing the old recursive upload of
|
|
10
|
+
everything under `public/`.
|
|
11
|
+
|
|
12
|
+
### New commands
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
linked build-app --env <env> --target web # build + write public/bundles/linked-release.json
|
|
16
|
+
linked build-app --env <env> --publish # ...and upload it in the same run
|
|
17
|
+
linked build-app --env <env> --target capacitor # local, non-publishable manifest; never uploads
|
|
18
|
+
linked build-app --revision <sha> # identify the release outside a git checkout
|
|
19
|
+
linked build-app --allow-dirty # build uncommitted work as <sha>-dirty
|
|
20
|
+
linked publish-app --env <env> # dry run by default; pass --yes to upload
|
|
21
|
+
linked publish-app --manifest <path> # publish a manifest from another path
|
|
22
|
+
linked serve-app --env <env> # compiled runtime, no Vite/HMR
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`build-app` never uploads on its own — it writes a release manifest, and `publish-app` uploads
|
|
26
|
+
exactly the files that manifest lists.
|
|
27
|
+
|
|
28
|
+
### Publishing destination
|
|
29
|
+
|
|
30
|
+
Uploads go through `LinkedFileStorage.getStore(FileStorePurposes.appAssets)`. An app that dedicates a
|
|
31
|
+
bundle store configures `setStore(FileStorePurposes.appAssets, store)`; an app with a single store
|
|
32
|
+
configures only `setDefaultStore(store)` and publishing reaches it through the purpose fallback, with
|
|
33
|
+
no extra configuration and no error.
|
|
34
|
+
|
|
35
|
+
### Behaviour
|
|
36
|
+
- Object keys are `releases/<appVersion>-<revision>/<path>`, so releases no longer overwrite each
|
|
37
|
+
other. `publish.releasePrefix` in `linked.config.js` changes the base. A manifest may only upload
|
|
38
|
+
to its own prefix: each `objectKey` is recomputed from the prefix and source path before any
|
|
39
|
+
write, so an edited or foreign manifest cannot overwrite a different release, and source paths
|
|
40
|
+
that leave the app root (`../`, absolute, or through a symlink) are refused.
|
|
41
|
+
- The client bundle is built with `base = <accessURL>/<releasePrefix>/public/bundles/`, so the URLs
|
|
42
|
+
it emits for its own chunks and assets resolve under the release prefix on a plain static store
|
|
43
|
+
with no server rewrite. The value is recorded as `destination.baseURL`; the unused `publicRoot`
|
|
44
|
+
field is gone. The store is therefore resolved before Vite runs, and a release is built for one
|
|
45
|
+
store.
|
|
46
|
+
- The release revision comes from a clean `git rev-parse HEAD`. A dirty tree is refused (two builds
|
|
47
|
+
of the same uncommitted work would share a prefix and overwrite each other); `--allow-dirty`
|
|
48
|
+
builds as `<sha>-dirty`. `--revision <sha>`, `LINKED_RELEASE_REVISION` and `GITHUB_SHA` work
|
|
49
|
+
outside a Git checkout.
|
|
50
|
+
- Cache policy follows a file's origin, not its name: files in the Vite manifest are hashed by
|
|
51
|
+
construction and get `public, max-age=31536000, immutable`; declared static assets and the
|
|
52
|
+
release manifest get `public, max-age=60, must-revalidate`. The old filename heuristic gave
|
|
53
|
+
`og-image-1200x630.png` and `sw-v20260101.js` a year of immutable caching.
|
|
54
|
+
- `publish.staticAssets` globs are checked per match, not only per pattern, so brace expansion
|
|
55
|
+
(`public/{,../}secret/**`) and symlinks can no longer pull in files from outside `public/`.
|
|
56
|
+
- `APP_ENV` selects the Capacitor target only when it is literally `capacitor`. Any other value
|
|
57
|
+
(`production`, `staging`, …) builds web instead of silently producing an empty, non-publishable
|
|
58
|
+
manifest.
|
|
59
|
+
- Files are re-hashed locally against the manifest before upload. The post-upload check uses the
|
|
60
|
+
optional `IFileStore.statFile`; a store that lacks it, or reports no `sha256`, is warned about once
|
|
61
|
+
and skipped rather than failing. `etag` is never treated as a content hash.
|
|
62
|
+
- The manifest records the store's `accessURL` and publishing refuses a store that no longer matches.
|
|
63
|
+
- Objects are written with `preventDuplicates: false` and a store that reports a different location
|
|
64
|
+
than the key it was given fails the publish, rather than scattering renamed files.
|
|
65
|
+
- A failure partway through an upload names the file, the object key, the store and how many objects
|
|
66
|
+
were already uploaded, and says that re-running resumes the release.
|
|
67
|
+
- Standalone `publish-app` is a dry run unless `--yes`; only publishable `target: 'web'` manifests
|
|
68
|
+
upload; the manifest is read before a store is resolved, so a Capacitor manifest fails with a
|
|
69
|
+
manifest error rather than a storage error; nothing is ever deleted remotely; credentials are
|
|
70
|
+
redacted from publisher errors.
|
|
71
|
+
- `build-app`, `publish-app` and `serve-app` print a single error and exit 1 instead of surfacing an
|
|
72
|
+
unhandled rejection, and `--target`/`--publish`/`--revision`/`--allow-dirty` are rejected on the
|
|
73
|
+
legacy webpack path instead of being ignored.
|
|
74
|
+
|
|
75
|
+
### New exports
|
|
76
|
+
|
|
77
|
+
From `@_linked/cli`: `buildViteApp`, `hasViteConfig`, `publishApp`, `serveCompiledApp`,
|
|
78
|
+
`validateCompiledAppArtifacts`, `createReleaseManifest`, `serializeReleaseManifest`,
|
|
79
|
+
`writeReleaseManifest`, `buildReleasePrefix`, `releaseObjectKey`, `getCacheControl`,
|
|
80
|
+
`planReleasePublish`, `publishRelease`, `loadReleaseManifest`, `resolveAppAssetsStore`,
|
|
81
|
+
`normalizeAccessURL`, `resolveBuildTarget`, `resolveDeclaredStaticAssets`, `resolveReleaseIdentity`,
|
|
82
|
+
`resolveSourceRevision`, `releaseBaseURL`, `VITE_OUTPUT_DIR`, the path helpers, and the release
|
|
83
|
+
types.
|
|
84
|
+
|
|
85
|
+
Apps must build with `build.outDir: 'public/bundles'` and `build.manifest: true` (both already set
|
|
86
|
+
by `createViteConfig`) and must not set `base` themselves; see the README.
|
|
87
|
+
|
|
88
|
+
Requires `@_linked/core` 2.20.1 for the purpose-named file stores and `IFileStore.statFile`.
|
|
89
|
+
|
|
3
90
|
## 1.18.0
|
|
4
91
|
|
|
5
92
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -34,6 +34,8 @@ linked create-component <name> # add a React component file
|
|
|
34
34
|
```bash
|
|
35
35
|
linked build # build the current package (tsc + checks)
|
|
36
36
|
linked build-app # build frontend + backend for the current app
|
|
37
|
+
linked build-app --target web # web release: build and write public/bundles/linked-release.json
|
|
38
|
+
linked build-app --target capacitor # Capacitor/local native build: local manifest only, never published
|
|
37
39
|
linked build-workspace # build all linked packages in the workspace in dependency order
|
|
38
40
|
linked build-updated # incremental: only packages that changed since last build
|
|
39
41
|
linked build-package <file> # walk up from a file path to find its package and rebuild
|
|
@@ -43,6 +45,123 @@ linked build-package <file> # walk up from a file path to find its package
|
|
|
43
45
|
in a package without `"linkedPackage": true` (it used to exit 0 there). A build that finishes with warnings still
|
|
44
46
|
exits 0.
|
|
45
47
|
|
|
48
|
+
### App release / production runtime
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
linked build-app # build and write the release manifest (never uploads)
|
|
52
|
+
linked build-app --publish # ...and upload it in the same run
|
|
53
|
+
linked build-app --revision <sha> # identify the release without asking git
|
|
54
|
+
linked build-app --allow-dirty # allow uncommitted changes; revision gets a "-dirty" suffix
|
|
55
|
+
linked publish-app # dry-run the release manifest upload (pass --yes to write)
|
|
56
|
+
linked publish-app --yes # upload the release described by the manifest
|
|
57
|
+
linked publish-app --manifest <p> # publish a manifest other than the default path
|
|
58
|
+
linked serve-app # run the compiled backend without Vite/HMR (production runtime)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Required Vite config.** The release flow reads `vite build` output directly, and the paths are
|
|
62
|
+
hardcoded: the client bundle must land in `public/bundles` and Vite must write its manifest.
|
|
63
|
+
`createViteConfig()` from `@_linked/cli/vite` already sets both; a hand-written `vite.config.ts`
|
|
64
|
+
needs them explicitly:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
export default defineConfig({
|
|
68
|
+
build: {
|
|
69
|
+
outDir: 'public/bundles', // release object keys are <prefix>/public/bundles/...
|
|
70
|
+
manifest: true, // build-app reads public/bundles/.vite/manifest.json
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Without `manifest: true` the build fails with a missing `public/bundles/.vite/manifest.json`; with a
|
|
76
|
+
different `outDir` nothing is found to publish. Do not set `base` yourself — `build-app` passes it
|
|
77
|
+
(see "How a release is served").
|
|
78
|
+
|
|
79
|
+
**Building is not publishing.** `build-app` compiles the app and writes
|
|
80
|
+
`public/bundles/linked-release.json`, a manifest listing every file of the release with its sha256,
|
|
81
|
+
size, content type and cache policy. `publish-app` uploads exactly the files in that manifest —
|
|
82
|
+
nothing else in `public/` is touched. Pass `--publish` to `build-app` only when you want the two
|
|
83
|
+
chained in one command. `publish-app` is a dry run unless `--yes` is given, and `--manifest <path>`
|
|
84
|
+
publishes a manifest from somewhere other than `public/bundles/linked-release.json` (the path is
|
|
85
|
+
relative to the app root and must stay inside it).
|
|
86
|
+
|
|
87
|
+
**Where it publishes.** Uploads go to the store returned by
|
|
88
|
+
`LinkedFileStorage.getStore(FileStorePurposes.appAssets)`. Configure a dedicated bundle store with
|
|
89
|
+
`LinkedFileStorage.setStore(FileStorePurposes.appAssets, store)`; an app that configures only
|
|
90
|
+
`setDefaultStore(store)` publishes to that store through the purpose fallback, which needs no extra
|
|
91
|
+
configuration.
|
|
92
|
+
|
|
93
|
+
**Every release gets its own prefix.** Object keys are
|
|
94
|
+
`releases/<appVersion>-<revision>/<path>`, so publishing a new release cannot overwrite the
|
|
95
|
+
previous one and a rollback means pointing at an older prefix. Set `publish.releasePrefix` in
|
|
96
|
+
`linked.config.js` to change the `releases` base. A manifest may only upload to its own prefix: every
|
|
97
|
+
`objectKey` is recomputed from the prefix and the source path before anything is written, so an
|
|
98
|
+
edited or foreign manifest cannot reach into another release.
|
|
99
|
+
|
|
100
|
+
**How a release is served.** Because every release has its own key prefix, the bundle's own URLs have
|
|
101
|
+
to carry that prefix — nothing rewrites them at request time. `build-app` therefore resolves the
|
|
102
|
+
store and the release prefix *before* running Vite and passes
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
base = <store accessURL>/<releasePrefix>/public/bundles/
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
so the chunk and asset URLs the bundle emits are absolute URLs into the release prefix. This works on
|
|
109
|
+
a plain static store or CDN with no rewrite rules, and two releases can be served side by side. The
|
|
110
|
+
value is recorded as `destination.baseURL` in the release manifest.
|
|
111
|
+
|
|
112
|
+
The HTML entry tags are not part of the bundle — `@_linked/server` renders them as
|
|
113
|
+
`${STATIC_ACCESS_URL}/public/bundles/<file>`. So serving a particular release is one deployment
|
|
114
|
+
variable, which `build-app` prints when it finishes:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
STATIC_ACCESS_URL=<store accessURL>/releases/<appVersion>-<revision>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Set that on the app server (or roll back by setting it to an older release prefix) and the entry
|
|
121
|
+
tags and everything the bundle loads resolve to the same release. Note the consequence of baking the
|
|
122
|
+
base in: a release is built for one store, and pointing it at another means rebuilding.
|
|
123
|
+
|
|
124
|
+
**Which revision identifies a release.** By default the short `git rev-parse HEAD` of a **clean**
|
|
125
|
+
tree. A dirty tree is refused, because two builds of the same uncommitted work would share a release
|
|
126
|
+
ID and prefix and silently overwrite each other while removed files linger; pass `--allow-dirty` to
|
|
127
|
+
build anyway as `<sha>-dirty`. Outside a Git checkout (an exported tarball, some CI images) pass
|
|
128
|
+
`--revision <sha>`, or set `LINKED_RELEASE_REVISION` or `GITHUB_SHA`; those win over Git and skip
|
|
129
|
+
the clean-tree check.
|
|
130
|
+
|
|
131
|
+
**Verification.** Files are re-hashed from disk and compared against the manifest immediately before
|
|
132
|
+
upload, so a build that changed since the manifest was written is refused. After upload the store is
|
|
133
|
+
asked for `statFile`; that method is optional and may report no `sha256`, in which case the remote
|
|
134
|
+
check is skipped with a single warning rather than a failure. An `etag` is never used as a content
|
|
135
|
+
hash. The manifest also records the store's `accessURL`, and publishing refuses to run against a
|
|
136
|
+
store that now writes somewhere else. A failure partway through names the file, the object key, the
|
|
137
|
+
store and how many objects were already uploaded; the manifest is uploaded last, so an incomplete
|
|
138
|
+
release is never marked complete, and re-running `publish-app` resumes it.
|
|
139
|
+
|
|
140
|
+
**The store must keep keys verbatim.** A release object has to be stored under exactly the key it
|
|
141
|
+
was given, or the URLs baked into the bundle point at nothing, so publishing passes
|
|
142
|
+
`preventDuplicates: false` and fails with a clear message if the store reports a different location.
|
|
143
|
+
`@_linked/server`'s `LocalFileStore` lowercases the keys it is handed, so an app publishing to it
|
|
144
|
+
needs lowercase asset filenames (`rollupOptions.output.hashCharacters: 'hex'` in `vite.config`).
|
|
145
|
+
|
|
146
|
+
**Cache policy follows the origin, not the filename.** Files listed in the Vite manifest are
|
|
147
|
+
content-hashed by construction and get `public, max-age=31536000, immutable`. Everything else —
|
|
148
|
+
declared `publish.staticAssets` and the release manifest — gets
|
|
149
|
+
`public, max-age=60, must-revalidate`. (Guessing from the filename was tried and dropped:
|
|
150
|
+
`og-image-1200x630.png` and `sw-v20260101.js` read as hashed, and a year of immutable caching on
|
|
151
|
+
those cannot be undone without renaming the file.)
|
|
152
|
+
|
|
153
|
+
**Extra static assets.** `publish.staticAssets` in `linked.config.js` lists globs of files under
|
|
154
|
+
`public/` to ship besides the bundle. Both the pattern and every match must resolve inside
|
|
155
|
+
`public/`, so brace expansion and symlinks cannot pull in files from elsewhere in the repo.
|
|
156
|
+
|
|
157
|
+
**Capacitor.** `--target capacitor` (or `APP_ENV=capacitor`, matched literally) writes a local,
|
|
158
|
+
non-publishable manifest for inspection. Its assets ship inside the native app, so no file store is
|
|
159
|
+
resolved, no `base` is set and nothing is uploaded, even with `--publish`. Any other `APP_ENV` value
|
|
160
|
+
(`production`, `staging`, …) is ignored by the target choice and builds web.
|
|
161
|
+
|
|
162
|
+
Use `linked start` for development; use `linked serve-app` after `linked build-app` for
|
|
163
|
+
staging/production.
|
|
164
|
+
|
|
46
165
|
### Publishing / release
|
|
47
166
|
|
|
48
167
|
```bash
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { IFileStore } from '@_linked/core/interfaces/IFileStore';
|
|
2
|
+
export interface ResolveAppAssetsStoreDependencies {
|
|
3
|
+
/**
|
|
4
|
+
* Loads the app's storage bootstrap file. Importing it is what registers the
|
|
5
|
+
* stores on `LinkedFileStorage`; the module's own exports are not inspected.
|
|
6
|
+
*/
|
|
7
|
+
loadStorageConfig?: () => Promise<unknown>;
|
|
8
|
+
/** Injection point for tests. Defaults to `LinkedFileStorage.getStore`. */
|
|
9
|
+
getStore?: (purpose: string) => IFileStore;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the store that release artifacts are published to.
|
|
13
|
+
*
|
|
14
|
+
* Apps that dedicate a CDN bucket to bundles configure
|
|
15
|
+
* `LinkedFileStorage.setStore(FileStorePurposes.appAssets, …)`. Apps with a
|
|
16
|
+
* single store configure only a default store and `getStore` falls back to it
|
|
17
|
+
* — that is a supported setup, not an error, so there is no check for a
|
|
18
|
+
* dedicated store here.
|
|
19
|
+
*/
|
|
20
|
+
export declare const resolveAppAssetsStore: (dependencies?: ResolveAppAssetsStoreDependencies) => Promise<IFileStore>;
|
|
21
|
+
/** `accessURL` values are compared ignoring a trailing slash. */
|
|
22
|
+
export declare const normalizeAccessURL: (accessURL: string) => string;
|
|
23
|
+
//# sourceMappingURL=app-assets-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-assets-store.d.ts","sourceRoot":"","sources":["../../../src/app-release/app-assets-store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,qCAAqC,CAAC;AAEpE,MAAM,WAAW,iCAAiC;IAChD;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,CAAC;CAC5C;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,GAChC,eAAc,iCAAsC,KACnD,OAAO,CAAC,UAAU,CAapB,CAAC;AAEF,iEAAiE;AACjE,eAAO,MAAM,kBAAkB,GAAI,WAAW,MAAM,KAAG,MACT,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { FileStorePurposes, LinkedFileStorage, } from '@_linked/core/utils/LinkedFileStorage';
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the store that release artifacts are published to.
|
|
13
|
+
*
|
|
14
|
+
* Apps that dedicate a CDN bucket to bundles configure
|
|
15
|
+
* `LinkedFileStorage.setStore(FileStorePurposes.appAssets, …)`. Apps with a
|
|
16
|
+
* single store configure only a default store and `getStore` falls back to it
|
|
17
|
+
* — that is a supported setup, not an error, so there is no check for a
|
|
18
|
+
* dedicated store here.
|
|
19
|
+
*/
|
|
20
|
+
export const resolveAppAssetsStore = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (dependencies = {}) {
|
|
21
|
+
const loadStorageConfig = dependencies.loadStorageConfig ||
|
|
22
|
+
(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
23
|
+
const { loadBackendStorageConfig } = yield import('../lifecycle.js');
|
|
24
|
+
return loadBackendStorageConfig();
|
|
25
|
+
}));
|
|
26
|
+
yield loadStorageConfig();
|
|
27
|
+
const getStore = dependencies.getStore ||
|
|
28
|
+
((purpose) => LinkedFileStorage.getStore(purpose));
|
|
29
|
+
return getStore(FileStorePurposes.appAssets);
|
|
30
|
+
});
|
|
31
|
+
/** `accessURL` values are compared ignoring a trailing slash. */
|
|
32
|
+
export const normalizeAccessURL = (accessURL) => (accessURL || '').trim().replace(/\/+$/, '');
|
|
33
|
+
//# sourceMappingURL=app-assets-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-assets-store.js","sourceRoot":"","sources":["../../../src/app-release/app-assets-store.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EACL,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,uCAAuC,CAAC;AAa/C;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,YAEd,EAAE,mDADvB,eAAkD,EAAE;IAEpD,MAAM,iBAAiB,GACrB,YAAY,CAAC,iBAAiB;QAC9B,CAAC,GAAS,EAAE;YACV,MAAM,EAAC,wBAAwB,EAAC,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC;YACnE,OAAO,wBAAwB,EAAE,CAAC;QACpC,CAAC,CAAA,CAAC,CAAC;IACL,MAAM,iBAAiB,EAAE,CAAC;IAE1B,MAAM,QAAQ,GACZ,YAAY,CAAC,QAAQ;QACrB,CAAC,CAAC,OAAe,EAAE,EAAE,CAAC,iBAAiB,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;IAC7D,OAAO,QAAQ,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;AAC/C,CAAC,CAAA,CAAC;AAEF,iEAAiE;AACjE,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,SAAiB,EAAU,EAAE,CAC9D,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { AppBuildTarget, LinkedAppReleaseManifest } from './types.js';
|
|
2
|
+
export interface CreateReleaseManifestOptions {
|
|
3
|
+
appRoot: string;
|
|
4
|
+
environmentNames: string[];
|
|
5
|
+
target: AppBuildTarget;
|
|
6
|
+
/**
|
|
7
|
+
* `accessURL` of the store this release will be published to. Omitted (or
|
|
8
|
+
* null) for a Capacitor build, which produces a local, non-publishable
|
|
9
|
+
* manifest and never touches a file store.
|
|
10
|
+
*/
|
|
11
|
+
accessURL?: string | null;
|
|
12
|
+
staticAssets?: string[];
|
|
13
|
+
/** Base key releases are nested under. Defaults to `releases`. */
|
|
14
|
+
releasePrefix?: string;
|
|
15
|
+
builtAt?: string;
|
|
16
|
+
sourceRevision?: string;
|
|
17
|
+
/** Allow a revision derived from a working tree with uncommitted changes. */
|
|
18
|
+
allowDirty?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Public URL every asset of this release is served from — the release
|
|
21
|
+
* prefix resolved against the store's `accessURL`, and the value the client
|
|
22
|
+
* bundle was built with as Vite's `base`. Empty for a Capacitor build.
|
|
23
|
+
*/
|
|
24
|
+
baseURL?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare const DEFAULT_RELEASE_PREFIX = "releases";
|
|
27
|
+
export declare const MANIFEST_FILE_NAME = "linked-release.json";
|
|
28
|
+
export declare const MANIFEST_SOURCE_PATH = "public/bundles/linked-release.json";
|
|
29
|
+
export declare const MANIFEST_CONTENT_TYPE = "application/json; charset=utf-8";
|
|
30
|
+
/**
|
|
31
|
+
* Cache-Control rule, decided by where a file came from rather than by what it
|
|
32
|
+
* is called.
|
|
33
|
+
*
|
|
34
|
+
* Files listed in the Vite manifest are content-hashed by construction — Vite
|
|
35
|
+
* emitted the name, so the bytes behind that key can never change and the
|
|
36
|
+
* object is immutable for a year. Everything else (declared static assets, the
|
|
37
|
+
* release manifest) is revalidated after a minute, so a re-published release is
|
|
38
|
+
* picked up quickly. Guessing "is this hashed?" from the filename was tried and
|
|
39
|
+
* removed: `og-image-1200x630.png`, `sw-v20260101.js` and `icon-FacebookRound.svg`
|
|
40
|
+
* all read as hashed, and a year of immutable caching on those cannot be undone
|
|
41
|
+
* without renaming the file.
|
|
42
|
+
*/
|
|
43
|
+
export declare const IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
44
|
+
export declare const ENTRY_CACHE_CONTROL = "public, max-age=60, must-revalidate";
|
|
45
|
+
/** Where a release file came from; this is what decides its cache policy. */
|
|
46
|
+
export type ReleaseFileOrigin = 'vite-bundle' | 'static-asset';
|
|
47
|
+
export declare const getCacheControl: (origin: ReleaseFileOrigin) => string;
|
|
48
|
+
export interface ResolveSourceRevisionOptions {
|
|
49
|
+
appRoot: string;
|
|
50
|
+
/** `--revision`, or a caller-supplied value. Used verbatim when given. */
|
|
51
|
+
revision?: string;
|
|
52
|
+
/** `--allow-dirty`: build from a dirty tree, tagging the revision `-dirty`. */
|
|
53
|
+
allowDirty?: boolean;
|
|
54
|
+
env?: NodeJS.ProcessEnv;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Determine the revision that identifies this release.
|
|
58
|
+
*
|
|
59
|
+
* An explicit `--revision`, then `LINKED_RELEASE_REVISION`, then `GITHUB_SHA`
|
|
60
|
+
* win over Git, so a release can be built from an exported tarball or a CI
|
|
61
|
+
* checkout that is not a working repository. Falling back to `git rev-parse`
|
|
62
|
+
* requires a clean tree: two builds of the same uncommitted work would
|
|
63
|
+
* otherwise share a release ID and a prefix, silently overwriting each other
|
|
64
|
+
* while files removed in between linger under the prefix.
|
|
65
|
+
*/
|
|
66
|
+
export declare const resolveSourceRevision: ({ appRoot, revision, allowDirty, env, }: ResolveSourceRevisionOptions) => string;
|
|
67
|
+
/**
|
|
68
|
+
* Build the key prefix for one release. Every object of a release lives under
|
|
69
|
+
* it, so publishing a new release cannot overwrite the previous one and a
|
|
70
|
+
* rollback is a matter of pointing at the older prefix.
|
|
71
|
+
*/
|
|
72
|
+
export declare const buildReleasePrefix: (releaseId: string, basePrefix?: string) => string;
|
|
73
|
+
export declare const releaseObjectKey: (releasePrefix: string, sourcePath: string) => string;
|
|
74
|
+
/** Directory `vite build` writes the client bundle to. See the README. */
|
|
75
|
+
export declare const VITE_OUTPUT_DIR = "public/bundles";
|
|
76
|
+
/**
|
|
77
|
+
* Public URL the client bundle of a release is served from. Object keys are
|
|
78
|
+
* `<releasePrefix>/public/bundles/…` under the store's `accessURL`, and this is
|
|
79
|
+
* the value the build passes to Vite as `base`, so the root-absolute URLs the
|
|
80
|
+
* bundle emits for its own chunks and assets resolve under the release prefix
|
|
81
|
+
* without any server rewrite.
|
|
82
|
+
*/
|
|
83
|
+
export declare const releaseBaseURL: (accessURL: string, releasePrefix: string) => string;
|
|
84
|
+
/**
|
|
85
|
+
* The release's root URL: `accessURL` plus the release prefix, without the
|
|
86
|
+
* bundle directory. `@_linked/server` builds the HTML entry tags as
|
|
87
|
+
* `${STATIC_ACCESS_URL}/public/bundles/<file>`, so pointing `STATIC_ACCESS_URL`
|
|
88
|
+
* at this value is what makes a running app serve one particular release —
|
|
89
|
+
* a value to deploy, not server code to write or a rewrite rule to configure.
|
|
90
|
+
*/
|
|
91
|
+
export declare const releaseStaticAccessURL: (accessURL: string, releasePrefix: string) => string;
|
|
92
|
+
export interface ReleaseIdentity {
|
|
93
|
+
appName: string;
|
|
94
|
+
appVersion: string;
|
|
95
|
+
sourceRevision: string;
|
|
96
|
+
releaseId: string;
|
|
97
|
+
releasePrefix: string;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Work out the release ID and prefix before anything is built. The client
|
|
101
|
+
* bundle has to be built with the release prefix as its `base`, so the prefix
|
|
102
|
+
* cannot wait until the manifest is written.
|
|
103
|
+
*/
|
|
104
|
+
export declare const resolveReleaseIdentity: (options: {
|
|
105
|
+
appRoot: string;
|
|
106
|
+
releasePrefix?: string;
|
|
107
|
+
sourceRevision?: string;
|
|
108
|
+
allowDirty?: boolean;
|
|
109
|
+
env?: NodeJS.ProcessEnv;
|
|
110
|
+
}) => ReleaseIdentity;
|
|
111
|
+
export declare const createReleaseManifest: (options: CreateReleaseManifestOptions) => LinkedAppReleaseManifest;
|
|
112
|
+
export declare const serializeReleaseManifest: (manifest: LinkedAppReleaseManifest) => string;
|
|
113
|
+
export declare const writeReleaseManifest: (appRoot: string, manifest: LinkedAppReleaseManifest) => string;
|
|
114
|
+
//# sourceMappingURL=create-release-manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-release-manifest.d.ts","sourceRoot":"","sources":["../../../src/app-release/create-release-manifest.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,cAAc,EAEd,wBAAwB,EACzB,MAAM,YAAY,CAAC;AAYpB,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,MAAM,EAAE,cAAc,CAAC;IACvB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,kEAAkE;IAClE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAmBD,eAAO,MAAM,sBAAsB,aAAa,CAAC;AACjD,eAAO,MAAM,kBAAkB,wBAAwB,CAAC;AACxD,eAAO,MAAM,oBAAoB,uCAAyC,CAAC;AAC3E,eAAO,MAAM,qBAAqB,oCAAoC,CAAC;AAEvE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,wCAAwC,CAAC;AAC7E,eAAO,MAAM,mBAAmB,wCAAwC,CAAC;AAEzE,6EAA6E;AAC7E,MAAM,MAAM,iBAAiB,GAAG,aAAa,GAAG,cAAc,CAAC;AAK/D,eAAO,MAAM,eAAe,GAAI,QAAQ,iBAAiB,KAAG,MACc,CAAC;AAY3E,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,GAAI,yCAKnC,4BAA4B,KAAG,MA2CjC,CAAC;AA8BF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAC7B,WAAW,MAAM,EACjB,aAAY,MAA+B,KAC1C,MAA8C,CAAC;AAElD,eAAO,MAAM,gBAAgB,GAC3B,eAAe,MAAM,EACrB,YAAY,MAAM,KACjB,MAAkD,CAAC;AAEtD,0EAA0E;AAC1E,eAAO,MAAM,eAAe,mBAAmB,CAAC;AAEhD;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GACzB,WAAW,MAAM,EACjB,eAAe,MAAM,KACpB,MACwE,CAAC;AAE5E;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,GACjC,WAAW,MAAM,EACjB,eAAe,MAAM,KACpB,MACiE,CAAC;AAErE,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,GAAI,SAAS;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB,KAAG,eAkBH,CAAC;AAsBF,eAAO,MAAM,qBAAqB,GAChC,SAAS,4BAA4B,KACpC,wBAoEF,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,UAAU,wBAAwB,KACjC,MAAkD,CAAC;AAEtD,eAAO,MAAM,oBAAoB,GAC/B,SAAS,MAAM,EACf,UAAU,wBAAwB,KACjC,MAIF,CAAC"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { execFileSync } from 'child_process';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { joinObjectKey, normalizeReleasePath, resolveExistingPathWithinRoot, } from './paths.js';
|
|
6
|
+
import { resolveDeclaredStaticAssets } from './static-assets.js';
|
|
7
|
+
const MIME_TYPES = {
|
|
8
|
+
'.css': 'text/css; charset=utf-8',
|
|
9
|
+
'.gif': 'image/gif',
|
|
10
|
+
'.html': 'text/html; charset=utf-8',
|
|
11
|
+
'.ico': 'image/x-icon',
|
|
12
|
+
'.jpeg': 'image/jpeg',
|
|
13
|
+
'.jpg': 'image/jpeg',
|
|
14
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
15
|
+
'.json': 'application/json; charset=utf-8',
|
|
16
|
+
'.map': 'application/json; charset=utf-8',
|
|
17
|
+
'.png': 'image/png',
|
|
18
|
+
'.svg': 'image/svg+xml',
|
|
19
|
+
'.webp': 'image/webp',
|
|
20
|
+
'.woff': 'font/woff',
|
|
21
|
+
'.woff2': 'font/woff2',
|
|
22
|
+
};
|
|
23
|
+
export const DEFAULT_RELEASE_PREFIX = 'releases';
|
|
24
|
+
export const MANIFEST_FILE_NAME = 'linked-release.json';
|
|
25
|
+
export const MANIFEST_SOURCE_PATH = `public/bundles/${MANIFEST_FILE_NAME}`;
|
|
26
|
+
export const MANIFEST_CONTENT_TYPE = 'application/json; charset=utf-8';
|
|
27
|
+
/**
|
|
28
|
+
* Cache-Control rule, decided by where a file came from rather than by what it
|
|
29
|
+
* is called.
|
|
30
|
+
*
|
|
31
|
+
* Files listed in the Vite manifest are content-hashed by construction — Vite
|
|
32
|
+
* emitted the name, so the bytes behind that key can never change and the
|
|
33
|
+
* object is immutable for a year. Everything else (declared static assets, the
|
|
34
|
+
* release manifest) is revalidated after a minute, so a re-published release is
|
|
35
|
+
* picked up quickly. Guessing "is this hashed?" from the filename was tried and
|
|
36
|
+
* removed: `og-image-1200x630.png`, `sw-v20260101.js` and `icon-FacebookRound.svg`
|
|
37
|
+
* all read as hashed, and a year of immutable caching on those cannot be undone
|
|
38
|
+
* without renaming the file.
|
|
39
|
+
*/
|
|
40
|
+
export const IMMUTABLE_CACHE_CONTROL = 'public, max-age=31536000, immutable';
|
|
41
|
+
export const ENTRY_CACHE_CONTROL = 'public, max-age=60, must-revalidate';
|
|
42
|
+
const getContentType = (filePath) => MIME_TYPES[path.extname(filePath).toLowerCase()] || 'application/octet-stream';
|
|
43
|
+
export const getCacheControl = (origin) => origin === 'vite-bundle' ? IMMUTABLE_CACHE_CONTROL : ENTRY_CACHE_CONTROL;
|
|
44
|
+
/** A revision has to be safe to embed in an object key and a URL. */
|
|
45
|
+
const SAFE_REVISION = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
46
|
+
const runGit = (appRoot, args) => execFileSync('git', args, {
|
|
47
|
+
cwd: appRoot,
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
50
|
+
});
|
|
51
|
+
/**
|
|
52
|
+
* Determine the revision that identifies this release.
|
|
53
|
+
*
|
|
54
|
+
* An explicit `--revision`, then `LINKED_RELEASE_REVISION`, then `GITHUB_SHA`
|
|
55
|
+
* win over Git, so a release can be built from an exported tarball or a CI
|
|
56
|
+
* checkout that is not a working repository. Falling back to `git rev-parse`
|
|
57
|
+
* requires a clean tree: two builds of the same uncommitted work would
|
|
58
|
+
* otherwise share a release ID and a prefix, silently overwriting each other
|
|
59
|
+
* while files removed in between linger under the prefix.
|
|
60
|
+
*/
|
|
61
|
+
export const resolveSourceRevision = ({ appRoot, revision, allowDirty, env = process.env, }) => {
|
|
62
|
+
const explicit = (revision ||
|
|
63
|
+
env.LINKED_RELEASE_REVISION ||
|
|
64
|
+
env.GITHUB_SHA ||
|
|
65
|
+
'').trim();
|
|
66
|
+
if (explicit) {
|
|
67
|
+
if (!SAFE_REVISION.test(explicit)) {
|
|
68
|
+
throw new Error(`Invalid release revision "${explicit}". Use letters, digits, ".", "-" or "_".`);
|
|
69
|
+
}
|
|
70
|
+
return explicit;
|
|
71
|
+
}
|
|
72
|
+
let head;
|
|
73
|
+
try {
|
|
74
|
+
head = runGit(appRoot, ['rev-parse', '--short=12', 'HEAD']).trim();
|
|
75
|
+
}
|
|
76
|
+
catch (_a) {
|
|
77
|
+
throw new Error('Cannot determine the Git revision for this release. Build from a Git checkout, ' +
|
|
78
|
+
'or pass --revision <sha> (or set LINKED_RELEASE_REVISION / GITHUB_SHA).');
|
|
79
|
+
}
|
|
80
|
+
let dirty = false;
|
|
81
|
+
try {
|
|
82
|
+
dirty = runGit(appRoot, ['status', '--porcelain']).trim().length > 0;
|
|
83
|
+
}
|
|
84
|
+
catch (_b) {
|
|
85
|
+
// `rev-parse` worked, so this is a repository; treat an unreadable status
|
|
86
|
+
// as clean rather than blocking the build on a Git quirk.
|
|
87
|
+
}
|
|
88
|
+
if (!dirty)
|
|
89
|
+
return head;
|
|
90
|
+
if (!allowDirty) {
|
|
91
|
+
throw new Error(`The working tree has uncommitted changes, so release ${head} would not identify ` +
|
|
92
|
+
'what was built: a second build of the same edits reuses the prefix and overwrites it. ' +
|
|
93
|
+
'Commit or stash the changes, pass --allow-dirty to publish as ' +
|
|
94
|
+
`"${head}-dirty", or pass --revision <sha>.`);
|
|
95
|
+
}
|
|
96
|
+
return `${head}-dirty`;
|
|
97
|
+
};
|
|
98
|
+
const collectViteOutputPaths = (manifest) => {
|
|
99
|
+
const files = new Set();
|
|
100
|
+
for (const [entryName, entry] of Object.entries(manifest)) {
|
|
101
|
+
if (!entry || typeof entry.file !== 'string') {
|
|
102
|
+
throw new Error(`Invalid Vite manifest entry: ${entryName}`);
|
|
103
|
+
}
|
|
104
|
+
files.add(joinObjectKey('public/bundles', entry.file));
|
|
105
|
+
for (const relativePath of [...(entry.css || []), ...(entry.assets || [])]) {
|
|
106
|
+
files.add(joinObjectKey('public/bundles', relativePath));
|
|
107
|
+
}
|
|
108
|
+
for (const importedEntry of [
|
|
109
|
+
...(entry.imports || []),
|
|
110
|
+
...(entry.dynamicImports || []),
|
|
111
|
+
]) {
|
|
112
|
+
if (!manifest[importedEntry]) {
|
|
113
|
+
throw new Error(`Vite manifest entry ${entryName} references missing import ${importedEntry}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return [...files];
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Build the key prefix for one release. Every object of a release lives under
|
|
121
|
+
* it, so publishing a new release cannot overwrite the previous one and a
|
|
122
|
+
* rollback is a matter of pointing at the older prefix.
|
|
123
|
+
*/
|
|
124
|
+
export const buildReleasePrefix = (releaseId, basePrefix = DEFAULT_RELEASE_PREFIX) => joinObjectKey(basePrefix, releaseId);
|
|
125
|
+
export const releaseObjectKey = (releasePrefix, sourcePath) => joinObjectKey(releasePrefix, sourcePath);
|
|
126
|
+
/** Directory `vite build` writes the client bundle to. See the README. */
|
|
127
|
+
export const VITE_OUTPUT_DIR = 'public/bundles';
|
|
128
|
+
/**
|
|
129
|
+
* Public URL the client bundle of a release is served from. Object keys are
|
|
130
|
+
* `<releasePrefix>/public/bundles/…` under the store's `accessURL`, and this is
|
|
131
|
+
* the value the build passes to Vite as `base`, so the root-absolute URLs the
|
|
132
|
+
* bundle emits for its own chunks and assets resolve under the release prefix
|
|
133
|
+
* without any server rewrite.
|
|
134
|
+
*/
|
|
135
|
+
export const releaseBaseURL = (accessURL, releasePrefix) => `${releaseStaticAccessURL(accessURL, releasePrefix)}/${VITE_OUTPUT_DIR}/`;
|
|
136
|
+
/**
|
|
137
|
+
* The release's root URL: `accessURL` plus the release prefix, without the
|
|
138
|
+
* bundle directory. `@_linked/server` builds the HTML entry tags as
|
|
139
|
+
* `${STATIC_ACCESS_URL}/public/bundles/<file>`, so pointing `STATIC_ACCESS_URL`
|
|
140
|
+
* at this value is what makes a running app serve one particular release —
|
|
141
|
+
* a value to deploy, not server code to write or a rewrite rule to configure.
|
|
142
|
+
*/
|
|
143
|
+
export const releaseStaticAccessURL = (accessURL, releasePrefix) => `${(accessURL || '').trim().replace(/\/+$/, '')}/${releasePrefix}`;
|
|
144
|
+
/**
|
|
145
|
+
* Work out the release ID and prefix before anything is built. The client
|
|
146
|
+
* bundle has to be built with the release prefix as its `base`, so the prefix
|
|
147
|
+
* cannot wait until the manifest is written.
|
|
148
|
+
*/
|
|
149
|
+
export const resolveReleaseIdentity = (options) => {
|
|
150
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(options.appRoot, 'package.json'), 'utf8'));
|
|
151
|
+
const sourceRevision = resolveSourceRevision({
|
|
152
|
+
appRoot: options.appRoot,
|
|
153
|
+
revision: options.sourceRevision,
|
|
154
|
+
allowDirty: options.allowDirty,
|
|
155
|
+
env: options.env,
|
|
156
|
+
});
|
|
157
|
+
const releaseId = `${packageJson.version}-${sourceRevision}`;
|
|
158
|
+
return {
|
|
159
|
+
appName: packageJson.name,
|
|
160
|
+
appVersion: packageJson.version,
|
|
161
|
+
sourceRevision,
|
|
162
|
+
releaseId,
|
|
163
|
+
releasePrefix: buildReleasePrefix(releaseId, options.releasePrefix),
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
const describeReleaseFile = (appRoot, releasePrefix, sourcePath, origin) => {
|
|
167
|
+
const normalizedSourcePath = normalizeReleasePath(sourcePath, 'source path');
|
|
168
|
+
const absolutePath = resolveExistingPathWithinRoot(appRoot, normalizedSourcePath);
|
|
169
|
+
const content = fs.readFileSync(absolutePath);
|
|
170
|
+
return {
|
|
171
|
+
sourcePath: normalizedSourcePath,
|
|
172
|
+
objectKey: releaseObjectKey(releasePrefix, normalizedSourcePath),
|
|
173
|
+
sha256: crypto.createHash('sha256').update(content).digest('hex'),
|
|
174
|
+
size: content.byteLength,
|
|
175
|
+
contentType: getContentType(normalizedSourcePath),
|
|
176
|
+
cacheControl: getCacheControl(origin),
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
export const createReleaseManifest = (options) => {
|
|
180
|
+
var _a;
|
|
181
|
+
const identity = resolveReleaseIdentity({
|
|
182
|
+
appRoot: options.appRoot,
|
|
183
|
+
releasePrefix: options.releasePrefix,
|
|
184
|
+
sourceRevision: options.sourceRevision,
|
|
185
|
+
allowDirty: options.allowDirty,
|
|
186
|
+
});
|
|
187
|
+
const { releasePrefix } = identity;
|
|
188
|
+
// A Capacitor build ships its assets inside the native app, so it produces a
|
|
189
|
+
// manifest for local inspection only and is never uploaded. That is why it
|
|
190
|
+
// needs no destination and is marked unpublishable.
|
|
191
|
+
const publishable = options.target === 'web';
|
|
192
|
+
if (publishable && options.accessURL === undefined) {
|
|
193
|
+
throw new Error('A web release manifest requires the accessURL of the store it will be published to');
|
|
194
|
+
}
|
|
195
|
+
// Origin decides the cache policy: Vite named these files after their own
|
|
196
|
+
// content, so they are immutable; declared static assets keep their name
|
|
197
|
+
// across releases and must be revalidated.
|
|
198
|
+
const origins = new Map();
|
|
199
|
+
if (options.target === 'web') {
|
|
200
|
+
const viteManifestPath = path.join(options.appRoot, `${VITE_OUTPUT_DIR}/.vite/manifest.json`);
|
|
201
|
+
const viteManifest = JSON.parse(fs.readFileSync(viteManifestPath, 'utf8'));
|
|
202
|
+
for (const sourcePath of collectViteOutputPaths(viteManifest)) {
|
|
203
|
+
origins.set(sourcePath, 'vite-bundle');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const sourcePath of resolveDeclaredStaticAssets(options.appRoot, options.staticAssets)) {
|
|
207
|
+
if (!origins.has(sourcePath))
|
|
208
|
+
origins.set(sourcePath, 'static-asset');
|
|
209
|
+
}
|
|
210
|
+
const files = [...origins.entries()]
|
|
211
|
+
.map(([sourcePath, origin]) => describeReleaseFile(options.appRoot, releasePrefix, sourcePath, origin))
|
|
212
|
+
.sort((a, b) => a.objectKey.localeCompare(b.objectKey));
|
|
213
|
+
return {
|
|
214
|
+
schemaVersion: 1,
|
|
215
|
+
appName: identity.appName,
|
|
216
|
+
appVersion: identity.appVersion,
|
|
217
|
+
releaseId: identity.releaseId,
|
|
218
|
+
target: options.target,
|
|
219
|
+
publishable,
|
|
220
|
+
builtAt: options.builtAt || new Date().toISOString(),
|
|
221
|
+
environmentNames: [...options.environmentNames],
|
|
222
|
+
destination: {
|
|
223
|
+
accessURL: options.accessURL || '',
|
|
224
|
+
releasePrefix,
|
|
225
|
+
baseURL: (_a = options.baseURL) !== null && _a !== void 0 ? _a : (options.accessURL
|
|
226
|
+
? releaseBaseURL(options.accessURL, releasePrefix)
|
|
227
|
+
: ''),
|
|
228
|
+
},
|
|
229
|
+
files,
|
|
230
|
+
};
|
|
231
|
+
};
|
|
232
|
+
export const serializeReleaseManifest = (manifest) => `${JSON.stringify(manifest, null, 2)}\n`;
|
|
233
|
+
export const writeReleaseManifest = (appRoot, manifest) => {
|
|
234
|
+
const outputPath = path.join(appRoot, MANIFEST_SOURCE_PATH);
|
|
235
|
+
fs.writeFileSync(outputPath, serializeReleaseManifest(manifest));
|
|
236
|
+
return outputPath;
|
|
237
|
+
};
|
|
238
|
+
//# sourceMappingURL=create-release-manifest.js.map
|