handoff-app 2.0.0-0 → 2.0.0-1
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/dist/app-builder/paths.js +25 -1
- package/dist/app-builder/static-export.js +25 -0
- package/dist/artifacts/index.d.ts +1 -1
- package/dist/artifacts/index.js +4 -1
- package/dist/artifacts/url.d.ts +18 -0
- package/dist/artifacts/url.js +31 -1
- package/dist/types/config.d.ts +6 -6
- package/package.json +1 -1
- package/src/app/components/Foundations/DisplayLogo.tsx +8 -2
- package/src/app/pages/assets/logos.tsx +4 -1
- package/src/app/pages/foundations/icons/[name]/index.tsx +2 -2
- package/src/app/pages/foundations/icons/index.tsx +5 -3
- package/src/app/pages/foundations/logo.tsx +5 -4
- package/src/app-builder/paths.ts +27 -1
- package/src/app-builder/static-export.ts +27 -0
- package/src/artifacts/index.ts +8 -1
- package/src/artifacts/url.ts +32 -0
- package/src/types/config.ts +6 -6
|
@@ -50,14 +50,35 @@ const mirrorDirectory = (sourcePath, destinationPath) => __awaiter(void 0, void
|
|
|
50
50
|
yield fs_extra_1.default.ensureDir(path_1.default.dirname(destinationPath));
|
|
51
51
|
yield fs_extra_1.default.copy(sourcePath, destinationPath, { overwrite: true });
|
|
52
52
|
});
|
|
53
|
+
/**
|
|
54
|
+
* Stage the fetch-produced asset download bundles (`exported/<id>/{collection}.zip`) into the app
|
|
55
|
+
* public root, where the docs read API's workspace asset store serves them at
|
|
56
|
+
* `/api/docs/assets/{collection}/{collection}.zip` (the canonical download URL). `fetch` writes these
|
|
57
|
+
* bundles, but a plain `build`/`start` cleans and re-stages `.handoff` without carrying them, so the
|
|
58
|
+
* download links would 404 until a fetch happened to run. Copying the committed export bundles here
|
|
59
|
+
* makes the route resolve deterministically in workspace dev and static builds. Absent bundles (a
|
|
60
|
+
* project with no logos/icons) are skipped.
|
|
61
|
+
*/
|
|
62
|
+
const stageAssetDownloadBundles = (handoff, destinationPublicPath) => __awaiter(void 0, void 0, void 0, function* () {
|
|
63
|
+
const bundles = [
|
|
64
|
+
{ src: handoff.getLogosZipFilePath(), name: 'logos.zip' },
|
|
65
|
+
{ src: handoff.getIconsZipFilePath(), name: 'icons.zip' },
|
|
66
|
+
];
|
|
67
|
+
for (const { src, name } of bundles) {
|
|
68
|
+
if (fs_extra_1.default.existsSync(src) && fs_extra_1.default.statSync(src).isFile()) {
|
|
69
|
+
yield fs_extra_1.default.ensureDir(destinationPublicPath);
|
|
70
|
+
yield fs_extra_1.default.copy(src, path_1.default.resolve(destinationPublicPath, name), { overwrite: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
53
74
|
/**
|
|
54
75
|
* Copy the public dir from the working dir to the module dir.
|
|
55
76
|
*/
|
|
56
77
|
const syncPublicFiles = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
|
|
57
78
|
const appPath = (0, exports.getAppPath)(handoff);
|
|
79
|
+
const destinationPublicPath = path_1.default.resolve(appPath, 'public');
|
|
58
80
|
const workingPublicPath = (0, exports.getWorkingPublicPath)(handoff);
|
|
59
81
|
if (workingPublicPath) {
|
|
60
|
-
const destinationPublicPath = path_1.default.resolve(appPath, 'public');
|
|
61
82
|
const sourceApiPath = path_1.default.resolve(workingPublicPath, 'api');
|
|
62
83
|
const destinationApiPath = path_1.default.resolve(destinationPublicPath, 'api');
|
|
63
84
|
yield fs_extra_1.default.copy(workingPublicPath, destinationPublicPath, {
|
|
@@ -69,5 +90,8 @@ const syncPublicFiles = (handoff) => __awaiter(void 0, void 0, void 0, function*
|
|
|
69
90
|
});
|
|
70
91
|
yield mirrorDirectory(sourceApiPath, destinationApiPath);
|
|
71
92
|
}
|
|
93
|
+
// Stage the asset download bundles regardless of whether the workspace has a `public/` dir — they
|
|
94
|
+
// come from the fetch output, not the workspace public tree.
|
|
95
|
+
yield stageAssetDownloadBundles(handoff, destinationPublicPath);
|
|
72
96
|
});
|
|
73
97
|
exports.syncPublicFiles = syncPublicFiles;
|
|
@@ -176,6 +176,31 @@ const materializeDocsReadModel = (handoff, outDir) => __awaiter(void 0, void 0,
|
|
|
176
176
|
const detail = Object.assign(Object.assign({}, pattern), { build: { status: deriveBuildStatus(artifactRoot, path_1.default.join('pattern', `${pattern.id}.json`)) } });
|
|
177
177
|
yield fs_extra_1.default.writeJson(path_1.default.join(docsRoot, 'patterns', `${pattern.id}.json`), detail);
|
|
178
178
|
}
|
|
179
|
+
yield materializeAssetDownloads(handoff, docsRoot);
|
|
179
180
|
logger_1.Logger.success(`Materialized docs read model (${components.length} component(s), ${patterns.length} pattern(s)) for static export.`);
|
|
180
181
|
});
|
|
181
182
|
exports.materializeDocsReadModel = materializeDocsReadModel;
|
|
183
|
+
/**
|
|
184
|
+
* Materialize the whole-collection asset download bundles at the canonical asset route
|
|
185
|
+
* (`api/docs/assets/{collection}/{collection}.zip`) — the same URL the docs read API serves in
|
|
186
|
+
* registry mode, so the "Download" links resolve identically on a static host. The bundles come from
|
|
187
|
+
* the fetch output (`exported/<id>/{collection}.zip`); `next build` never re-creates them, and the
|
|
188
|
+
* fetch-time copy into the app `public/` is wiped by the build's clean+restage, so they are copied
|
|
189
|
+
* here. Absent bundles (a project with no logos/icons) are skipped, not an error.
|
|
190
|
+
*/
|
|
191
|
+
const materializeAssetDownloads = (handoff, docsRoot) => __awaiter(void 0, void 0, void 0, function* () {
|
|
192
|
+
const bundles = [
|
|
193
|
+
{ collection: 'logos', src: handoff.getLogosZipFilePath() },
|
|
194
|
+
{ collection: 'icons', src: handoff.getIconsZipFilePath() },
|
|
195
|
+
];
|
|
196
|
+
let copied = 0;
|
|
197
|
+
for (const { collection, src } of bundles) {
|
|
198
|
+
if (fs_extra_1.default.existsSync(src) && fs_extra_1.default.statSync(src).isFile()) {
|
|
199
|
+
yield fs_extra_1.default.copy(src, path_1.default.join(docsRoot, 'assets', collection, `${collection}.zip`), { overwrite: true });
|
|
200
|
+
copied += 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (copied > 0) {
|
|
204
|
+
logger_1.Logger.info(`Materialized ${copied} asset download bundle(s) for static export.`);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { ARTIFACT_METADATA_PATH, REACT_PREVIEW_FORMAT_VERSION, SHARED_MAIN_CSS_ARTIFACT_PATH, SHARED_MAIN_JS_ARTIFACT_PATH, SHARED_STYLES_CSS_ARTIFACT_PATH, type ArtifactBuildMetadata, type ArtifactBuildStatus, type ArtifactDescriptor, type ArtifactKind, type ArtifactOwnerKind, type ArtifactReference, type ArtifactReferenceKind, type ArtifactSizeDiagnostics, } from './types';
|
|
2
|
-
export { ARTIFACTS_ROUTE_SEGMENT, buildArtifactUrl, normalizeBasePath } from './url';
|
|
2
|
+
export { ARTIFACTS_ROUTE_SEGMENT, ASSETS_ROUTE_SEGMENT, buildArtifactUrl, buildAssetDownloadUrl, buildAssetUrl, normalizeBasePath, } from './url';
|
package/dist/artifacts/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalizeBasePath = exports.buildArtifactUrl = exports.ARTIFACTS_ROUTE_SEGMENT = exports.SHARED_STYLES_CSS_ARTIFACT_PATH = exports.SHARED_MAIN_JS_ARTIFACT_PATH = exports.SHARED_MAIN_CSS_ARTIFACT_PATH = exports.REACT_PREVIEW_FORMAT_VERSION = exports.ARTIFACT_METADATA_PATH = void 0;
|
|
3
|
+
exports.normalizeBasePath = exports.buildAssetUrl = exports.buildAssetDownloadUrl = exports.buildArtifactUrl = exports.ASSETS_ROUTE_SEGMENT = exports.ARTIFACTS_ROUTE_SEGMENT = exports.SHARED_STYLES_CSS_ARTIFACT_PATH = exports.SHARED_MAIN_JS_ARTIFACT_PATH = exports.SHARED_MAIN_CSS_ARTIFACT_PATH = exports.REACT_PREVIEW_FORMAT_VERSION = exports.ARTIFACT_METADATA_PATH = void 0;
|
|
4
4
|
var types_1 = require("./types");
|
|
5
5
|
Object.defineProperty(exports, "ARTIFACT_METADATA_PATH", { enumerable: true, get: function () { return types_1.ARTIFACT_METADATA_PATH; } });
|
|
6
6
|
Object.defineProperty(exports, "REACT_PREVIEW_FORMAT_VERSION", { enumerable: true, get: function () { return types_1.REACT_PREVIEW_FORMAT_VERSION; } });
|
|
@@ -9,5 +9,8 @@ Object.defineProperty(exports, "SHARED_MAIN_JS_ARTIFACT_PATH", { enumerable: tru
|
|
|
9
9
|
Object.defineProperty(exports, "SHARED_STYLES_CSS_ARTIFACT_PATH", { enumerable: true, get: function () { return types_1.SHARED_STYLES_CSS_ARTIFACT_PATH; } });
|
|
10
10
|
var url_1 = require("./url");
|
|
11
11
|
Object.defineProperty(exports, "ARTIFACTS_ROUTE_SEGMENT", { enumerable: true, get: function () { return url_1.ARTIFACTS_ROUTE_SEGMENT; } });
|
|
12
|
+
Object.defineProperty(exports, "ASSETS_ROUTE_SEGMENT", { enumerable: true, get: function () { return url_1.ASSETS_ROUTE_SEGMENT; } });
|
|
12
13
|
Object.defineProperty(exports, "buildArtifactUrl", { enumerable: true, get: function () { return url_1.buildArtifactUrl; } });
|
|
14
|
+
Object.defineProperty(exports, "buildAssetDownloadUrl", { enumerable: true, get: function () { return url_1.buildAssetDownloadUrl; } });
|
|
15
|
+
Object.defineProperty(exports, "buildAssetUrl", { enumerable: true, get: function () { return url_1.buildAssetUrl; } });
|
|
13
16
|
Object.defineProperty(exports, "normalizeBasePath", { enumerable: true, get: function () { return url_1.normalizeBasePath; } });
|
package/dist/artifacts/url.d.ts
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
export declare const ARTIFACTS_ROUTE_SEGMENT = "api/docs/artifacts";
|
|
18
18
|
/** Route prefix for docs read API metadata (`.json`) reads. */
|
|
19
19
|
export declare const DOCS_ROUTE_SEGMENT = "api/docs";
|
|
20
|
+
/** Canonical route prefix all asset content URLs are served under (mode-independent). */
|
|
21
|
+
export declare const ASSETS_ROUTE_SEGMENT = "api/docs/assets";
|
|
20
22
|
/**
|
|
21
23
|
* Normalize a configured basePath to either an empty string or a single-leading-slash,
|
|
22
24
|
* no-trailing-slash form. Mirrors the Next app's `resolveBasePath` so builder output composes
|
|
@@ -47,3 +49,19 @@ export declare const buildComponentDetailUrl: (id: string, basePath?: string | n
|
|
|
47
49
|
* (`{basePath}/api/docs/patterns/{id}.json`). The id is URL-encoded; `basePath` is preserved.
|
|
48
50
|
*/
|
|
49
51
|
export declare const buildPatternDetailUrl: (id: string, basePath?: string | null) => string;
|
|
52
|
+
/**
|
|
53
|
+
* Build the canonical, basePath-aware content URL for one asset within a collection
|
|
54
|
+
* (`{basePath}/api/docs/assets/{collection}/{logicalPath}`). This is the same route the docs read API
|
|
55
|
+
* serves in every runtime mode: from the database in registry mode, and from statically materialized
|
|
56
|
+
* files in a static export. Each segment is individually encoded and traversal segments are rejected,
|
|
57
|
+
* mirroring {@link buildArtifactUrl}.
|
|
58
|
+
*
|
|
59
|
+
* @throws If the collection or logical path has no usable segments, or contains a `.`/`..` segment.
|
|
60
|
+
*/
|
|
61
|
+
export declare const buildAssetUrl: (collection: string, logicalPath: string, basePath?: string | null) => string;
|
|
62
|
+
/**
|
|
63
|
+
* Build the canonical download URL for a collection's whole-collection zip bundle
|
|
64
|
+
* (`{basePath}/api/docs/assets/{collection}/{collection}.zip`) — the default target for the docs
|
|
65
|
+
* "Download" links, overridable via `assets_zip_links`.
|
|
66
|
+
*/
|
|
67
|
+
export declare const buildAssetDownloadUrl: (collection: string, basePath?: string | null) => string;
|
package/dist/artifacts/url.js
CHANGED
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
* Next app can both import it.
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.buildPatternDetailUrl = exports.buildComponentDetailUrl = exports.buildArtifactUrl = exports.normalizeBasePath = exports.DOCS_ROUTE_SEGMENT = exports.ARTIFACTS_ROUTE_SEGMENT = void 0;
|
|
18
|
+
exports.buildAssetDownloadUrl = exports.buildAssetUrl = exports.buildPatternDetailUrl = exports.buildComponentDetailUrl = exports.buildArtifactUrl = exports.normalizeBasePath = exports.ASSETS_ROUTE_SEGMENT = exports.DOCS_ROUTE_SEGMENT = exports.ARTIFACTS_ROUTE_SEGMENT = void 0;
|
|
19
19
|
/** Canonical route prefix all artifact URLs are served under. */
|
|
20
20
|
exports.ARTIFACTS_ROUTE_SEGMENT = 'api/docs/artifacts';
|
|
21
21
|
/** Route prefix for docs read API metadata (`.json`) reads. */
|
|
22
22
|
exports.DOCS_ROUTE_SEGMENT = 'api/docs';
|
|
23
|
+
/** Canonical route prefix all asset content URLs are served under (mode-independent). */
|
|
24
|
+
exports.ASSETS_ROUTE_SEGMENT = 'api/docs/assets';
|
|
23
25
|
/**
|
|
24
26
|
* Normalize a configured basePath to either an empty string or a single-leading-slash,
|
|
25
27
|
* no-trailing-slash form. Mirrors the Next app's `resolveBasePath` so builder output composes
|
|
@@ -84,3 +86,31 @@ exports.buildComponentDetailUrl = buildComponentDetailUrl;
|
|
|
84
86
|
*/
|
|
85
87
|
const buildPatternDetailUrl = (id, basePath) => `${(0, exports.normalizeBasePath)(basePath)}/${exports.DOCS_ROUTE_SEGMENT}/patterns/${encodeURIComponent(id)}.json`;
|
|
86
88
|
exports.buildPatternDetailUrl = buildPatternDetailUrl;
|
|
89
|
+
/**
|
|
90
|
+
* Build the canonical, basePath-aware content URL for one asset within a collection
|
|
91
|
+
* (`{basePath}/api/docs/assets/{collection}/{logicalPath}`). This is the same route the docs read API
|
|
92
|
+
* serves in every runtime mode: from the database in registry mode, and from statically materialized
|
|
93
|
+
* files in a static export. Each segment is individually encoded and traversal segments are rejected,
|
|
94
|
+
* mirroring {@link buildArtifactUrl}.
|
|
95
|
+
*
|
|
96
|
+
* @throws If the collection or logical path has no usable segments, or contains a `.`/`..` segment.
|
|
97
|
+
*/
|
|
98
|
+
const buildAssetUrl = (collection, logicalPath, basePath) => {
|
|
99
|
+
const segments = [...toArtifactSegments(collection), ...toArtifactSegments(logicalPath)];
|
|
100
|
+
if (segments.length < 2) {
|
|
101
|
+
throw new Error(`Cannot build asset URL: empty collection ("${collection}") or path ("${logicalPath}").`);
|
|
102
|
+
}
|
|
103
|
+
if (segments.some(isTraversalSegment)) {
|
|
104
|
+
throw new Error(`Cannot build asset URL: path traversal is not allowed in "${collection}/${logicalPath}".`);
|
|
105
|
+
}
|
|
106
|
+
const encoded = segments.map((segment) => encodeURIComponent(segment)).join('/');
|
|
107
|
+
return `${(0, exports.normalizeBasePath)(basePath)}/${exports.ASSETS_ROUTE_SEGMENT}/${encoded}`;
|
|
108
|
+
};
|
|
109
|
+
exports.buildAssetUrl = buildAssetUrl;
|
|
110
|
+
/**
|
|
111
|
+
* Build the canonical download URL for a collection's whole-collection zip bundle
|
|
112
|
+
* (`{basePath}/api/docs/assets/{collection}/{collection}.zip`) — the default target for the docs
|
|
113
|
+
* "Download" links, overridable via `assets_zip_links`.
|
|
114
|
+
*/
|
|
115
|
+
const buildAssetDownloadUrl = (collection, basePath) => (0, exports.buildAssetUrl)(collection, `${collection}.zip`, basePath);
|
|
116
|
+
exports.buildAssetDownloadUrl = buildAssetDownloadUrl;
|
package/dist/types/config.d.ts
CHANGED
|
@@ -214,18 +214,18 @@ export interface Config {
|
|
|
214
214
|
*/
|
|
215
215
|
entries?: ConfigEntries;
|
|
216
216
|
/**
|
|
217
|
-
*
|
|
218
|
-
*
|
|
217
|
+
* Override URLs for the asset zip download links. When unset, each link defaults to the canonical,
|
|
218
|
+
* basePath-aware asset route `{basePath}/api/docs/assets/{collection}/{collection}.zip`, served by
|
|
219
|
+
* the docs read API in every runtime mode (from the DB in registry mode, from statically
|
|
220
|
+
* materialized files in a static export).
|
|
219
221
|
*/
|
|
220
222
|
assets_zip_links?: {
|
|
221
223
|
/**
|
|
222
|
-
*
|
|
223
|
-
* @default "/icons.zip"
|
|
224
|
+
* Override URL for the icons zip. Defaults to `/api/docs/assets/icons/icons.zip` (basePath-aware).
|
|
224
225
|
*/
|
|
225
226
|
icons?: string;
|
|
226
227
|
/**
|
|
227
|
-
*
|
|
228
|
-
* @default "/logos.zip"
|
|
228
|
+
* Override URL for the logos zip. Defaults to `/api/docs/assets/logos/logos.zip` (basePath-aware).
|
|
229
229
|
*/
|
|
230
230
|
logos?: string;
|
|
231
231
|
};
|
package/package.json
CHANGED
|
@@ -2,11 +2,17 @@ import React from 'react';
|
|
|
2
2
|
import type { DisplayableAsset } from '../util/useCollectionAssets';
|
|
3
3
|
|
|
4
4
|
export const DisplayLogo: React.FC<{ logo: DisplayableAsset; content?: string; correct?: boolean }> = ({ logo, content }) => {
|
|
5
|
-
//
|
|
5
|
+
// Workspace/static ships the SVG body inline in tokens.json (`data`) — render it directly so the
|
|
6
|
+
// logo has no dependency on the fetch-only `/api/assets/logos/*` files (absent on a static host).
|
|
7
|
+
// Registry has no inline body; it falls back to the individual asset content URL (`src`).
|
|
6
8
|
const src = logo.src ?? `${process.env.HANDOFF_APP_BASE_PATH ?? ''}/api/assets/logos/${logo.path}`;
|
|
7
9
|
return (
|
|
8
10
|
<div className="flex flex-col gap-5">
|
|
9
|
-
|
|
11
|
+
{logo.data ? (
|
|
12
|
+
<div className="rounded-3xl" dangerouslySetInnerHTML={{ __html: logo.data }} />
|
|
13
|
+
) : (
|
|
14
|
+
<img src={src} alt={logo.name} className="rounded-3xl" />
|
|
15
|
+
)}
|
|
10
16
|
<div className="flex flex-row gap-4">
|
|
11
17
|
<img src={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/assets/images/check-circle.svg`} alt="Do" className="h-6 w-6" />
|
|
12
18
|
<p className="leading-normal text-gray-500">
|
|
@@ -6,6 +6,7 @@ import rehypeRaw from 'rehype-raw';
|
|
|
6
6
|
import remarkGfm from 'remark-gfm';
|
|
7
7
|
import Footer from '../../components/Footer';
|
|
8
8
|
import { MarkdownComponents, remarkCodeMeta } from '../../components/Markdown/MarkdownComponents';
|
|
9
|
+
import { buildAssetDownloadUrl } from '@handoff/artifacts/url';
|
|
9
10
|
import Header from '../../components/old/Header';
|
|
10
11
|
import { DocumentationProps, fetchDocPageMarkdown, getClientRuntimeConfig } from '../../components/util';
|
|
11
12
|
|
|
@@ -52,7 +53,9 @@ const AssetsLogosPage = ({ content, menu, metadata, config }: DocumentationProps
|
|
|
52
53
|
<h4>{config?.app?.client} Logo</h4>
|
|
53
54
|
<p>Vector files of approved {config?.app?.client} logos.</p>
|
|
54
55
|
<p>
|
|
55
|
-
<a href={config?.assets_zip_links?.logos ?? '
|
|
56
|
+
<a href={config?.assets_zip_links?.logos ?? buildAssetDownloadUrl('logos', process.env.HANDOFF_APP_BASE_PATH)} download>
|
|
57
|
+
Download Logos
|
|
58
|
+
</a>
|
|
56
59
|
</p>
|
|
57
60
|
</div>
|
|
58
61
|
</div>
|
|
@@ -124,13 +124,13 @@ export default function SingleIcon({ menu, metadata, current, config, assets }:
|
|
|
124
124
|
Copy SVG <Code strokeWidth={1.5} />
|
|
125
125
|
</Link>
|
|
126
126
|
|
|
127
|
-
<
|
|
127
|
+
<a
|
|
128
128
|
href={icon.src ?? 'data:text/plain;charset=utf-8,' + encodeURIComponent(icon.data ?? '')}
|
|
129
129
|
download={icon.name}
|
|
130
130
|
className={buttonVariants({ variant: 'outline', size: 'sm' }) + ' font-normal [&_svg]:size-3!'}
|
|
131
131
|
>
|
|
132
132
|
Download SVG <Download strokeWidth={1.5} />
|
|
133
|
-
</
|
|
133
|
+
</a>
|
|
134
134
|
</div>
|
|
135
135
|
</div>
|
|
136
136
|
|
|
@@ -12,6 +12,7 @@ import { MarkdownComponents, remarkCodeMeta } from '../../../components/Markdown
|
|
|
12
12
|
import HeadersType from '../../../components/Typography/Headers';
|
|
13
13
|
import { buttonVariants } from '../../../components/ui/button';
|
|
14
14
|
import { Input } from '../../../components/ui/input';
|
|
15
|
+
import { buildAssetDownloadUrl } from '@handoff/artifacts/url';
|
|
15
16
|
import { AssetDocumentationProps, buildTimeAssets, fetchDocPageMarkdown, getClientRuntimeConfig } from '../../../components/util';
|
|
16
17
|
import { useCollectionAssets, type DisplayableAsset } from '../../../components/util/useCollectionAssets';
|
|
17
18
|
|
|
@@ -94,12 +95,13 @@ const IconsPage = ({ content, menu, metadata, current, config, assets }: AssetDo
|
|
|
94
95
|
<HeadersType.H1>{metadata.title}</HeadersType.H1>
|
|
95
96
|
<p className="max-w-[800px] text-lg font-light text-gray-500 dark:text-gray-300">{metadata.description}</p>
|
|
96
97
|
<div className="mt-3 flex flex-row gap-3">
|
|
97
|
-
<
|
|
98
|
+
<a
|
|
98
99
|
className={buttonVariants({ variant: 'outline', size: 'sm' }) + ' font-normal [&_svg]:size-3!'}
|
|
99
|
-
href={config?.assets_zip_links?.icons ?? '
|
|
100
|
+
href={config?.assets_zip_links?.icons ?? buildAssetDownloadUrl('icons', process.env.HANDOFF_APP_BASE_PATH)}
|
|
101
|
+
download
|
|
100
102
|
>
|
|
101
103
|
Download Icons <Download strokeWidth={1.5} />
|
|
102
|
-
</
|
|
104
|
+
</a>
|
|
103
105
|
</div>
|
|
104
106
|
</div>
|
|
105
107
|
<hr className="mb-10" />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { GetStaticProps } from 'next';
|
|
2
|
-
import Link from 'next/link';
|
|
3
2
|
|
|
3
|
+
import { buildAssetDownloadUrl } from '@handoff/artifacts/url';
|
|
4
4
|
import { AssetDocumentationProps, buildTimeAssets, fetchDocPageMarkdown, getClientRuntimeConfig } from '../../components/util';
|
|
5
5
|
import { useCollectionAssets } from '../../components/util/useCollectionAssets';
|
|
6
6
|
|
|
@@ -56,12 +56,13 @@ const LogoPage = ({ content, menu, metadata, current, config, assets }: AssetDoc
|
|
|
56
56
|
<HeadersType.H1>{metadata.title}</HeadersType.H1>
|
|
57
57
|
<p className="text-lg leading-relaxed text-gray-600 dark:text-gray-300">{metadata.description}</p>
|
|
58
58
|
<div className="mt-3 flex flex-row gap-3">
|
|
59
|
-
<
|
|
59
|
+
<a
|
|
60
60
|
className={buttonVariants({ variant: 'outline', size: 'sm' }) + ' font-normal [&_svg]:size-3!'}
|
|
61
|
-
href={config?.assets_zip_links?.logos ?? '
|
|
61
|
+
href={config?.assets_zip_links?.logos ?? buildAssetDownloadUrl('logos', process.env.HANDOFF_APP_BASE_PATH)}
|
|
62
|
+
download
|
|
62
63
|
>
|
|
63
64
|
Download Logos <Download strokeWidth={1.5} />
|
|
64
|
-
</
|
|
65
|
+
</a>
|
|
65
66
|
</div>
|
|
66
67
|
</div>
|
|
67
68
|
|
package/src/app-builder/paths.ts
CHANGED
|
@@ -41,14 +41,36 @@ const mirrorDirectory = async (sourcePath: string, destinationPath: string): Pro
|
|
|
41
41
|
await fs.copy(sourcePath, destinationPath, { overwrite: true });
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Stage the fetch-produced asset download bundles (`exported/<id>/{collection}.zip`) into the app
|
|
46
|
+
* public root, where the docs read API's workspace asset store serves them at
|
|
47
|
+
* `/api/docs/assets/{collection}/{collection}.zip` (the canonical download URL). `fetch` writes these
|
|
48
|
+
* bundles, but a plain `build`/`start` cleans and re-stages `.handoff` without carrying them, so the
|
|
49
|
+
* download links would 404 until a fetch happened to run. Copying the committed export bundles here
|
|
50
|
+
* makes the route resolve deterministically in workspace dev and static builds. Absent bundles (a
|
|
51
|
+
* project with no logos/icons) are skipped.
|
|
52
|
+
*/
|
|
53
|
+
const stageAssetDownloadBundles = async (handoff: Handoff, destinationPublicPath: string): Promise<void> => {
|
|
54
|
+
const bundles: Array<{ src: string; name: string }> = [
|
|
55
|
+
{ src: handoff.getLogosZipFilePath(), name: 'logos.zip' },
|
|
56
|
+
{ src: handoff.getIconsZipFilePath(), name: 'icons.zip' },
|
|
57
|
+
];
|
|
58
|
+
for (const { src, name } of bundles) {
|
|
59
|
+
if (fs.existsSync(src) && fs.statSync(src).isFile()) {
|
|
60
|
+
await fs.ensureDir(destinationPublicPath);
|
|
61
|
+
await fs.copy(src, path.resolve(destinationPublicPath, name), { overwrite: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
44
66
|
/**
|
|
45
67
|
* Copy the public dir from the working dir to the module dir.
|
|
46
68
|
*/
|
|
47
69
|
export const syncPublicFiles = async (handoff: Handoff): Promise<void> => {
|
|
48
70
|
const appPath = getAppPath(handoff);
|
|
71
|
+
const destinationPublicPath = path.resolve(appPath, 'public');
|
|
49
72
|
const workingPublicPath = getWorkingPublicPath(handoff);
|
|
50
73
|
if (workingPublicPath) {
|
|
51
|
-
const destinationPublicPath = path.resolve(appPath, 'public');
|
|
52
74
|
const sourceApiPath = path.resolve(workingPublicPath, 'api');
|
|
53
75
|
const destinationApiPath = path.resolve(destinationPublicPath, 'api');
|
|
54
76
|
|
|
@@ -62,4 +84,8 @@ export const syncPublicFiles = async (handoff: Handoff): Promise<void> => {
|
|
|
62
84
|
|
|
63
85
|
await mirrorDirectory(sourceApiPath, destinationApiPath);
|
|
64
86
|
}
|
|
87
|
+
|
|
88
|
+
// Stage the asset download bundles regardless of whether the workspace has a `public/` dir — they
|
|
89
|
+
// come from the fetch output, not the workspace public tree.
|
|
90
|
+
await stageAssetDownloadBundles(handoff, destinationPublicPath);
|
|
65
91
|
};
|
|
@@ -209,7 +209,34 @@ export const materializeDocsReadModel = async (handoff: Handoff, outDir: string)
|
|
|
209
209
|
await fs.writeJson(path.join(docsRoot, 'patterns', `${pattern.id}.json`), detail);
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
await materializeAssetDownloads(handoff, docsRoot);
|
|
213
|
+
|
|
212
214
|
Logger.success(
|
|
213
215
|
`Materialized docs read model (${components.length} component(s), ${patterns.length} pattern(s)) for static export.`
|
|
214
216
|
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Materialize the whole-collection asset download bundles at the canonical asset route
|
|
221
|
+
* (`api/docs/assets/{collection}/{collection}.zip`) — the same URL the docs read API serves in
|
|
222
|
+
* registry mode, so the "Download" links resolve identically on a static host. The bundles come from
|
|
223
|
+
* the fetch output (`exported/<id>/{collection}.zip`); `next build` never re-creates them, and the
|
|
224
|
+
* fetch-time copy into the app `public/` is wiped by the build's clean+restage, so they are copied
|
|
225
|
+
* here. Absent bundles (a project with no logos/icons) are skipped, not an error.
|
|
226
|
+
*/
|
|
227
|
+
const materializeAssetDownloads = async (handoff: Handoff, docsRoot: string): Promise<void> => {
|
|
228
|
+
const bundles: Array<{ collection: string; src: string }> = [
|
|
229
|
+
{ collection: 'logos', src: handoff.getLogosZipFilePath() },
|
|
230
|
+
{ collection: 'icons', src: handoff.getIconsZipFilePath() },
|
|
231
|
+
];
|
|
232
|
+
let copied = 0;
|
|
233
|
+
for (const { collection, src } of bundles) {
|
|
234
|
+
if (fs.existsSync(src) && fs.statSync(src).isFile()) {
|
|
235
|
+
await fs.copy(src, path.join(docsRoot, 'assets', collection, `${collection}.zip`), { overwrite: true });
|
|
236
|
+
copied += 1;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (copied > 0) {
|
|
240
|
+
Logger.info(`Materialized ${copied} asset download bundle(s) for static export.`);
|
|
241
|
+
}
|
|
215
242
|
};
|
package/src/artifacts/index.ts
CHANGED
|
@@ -14,4 +14,11 @@ export {
|
|
|
14
14
|
type ArtifactSizeDiagnostics,
|
|
15
15
|
} from './types';
|
|
16
16
|
|
|
17
|
-
export {
|
|
17
|
+
export {
|
|
18
|
+
ARTIFACTS_ROUTE_SEGMENT,
|
|
19
|
+
ASSETS_ROUTE_SEGMENT,
|
|
20
|
+
buildArtifactUrl,
|
|
21
|
+
buildAssetDownloadUrl,
|
|
22
|
+
buildAssetUrl,
|
|
23
|
+
normalizeBasePath,
|
|
24
|
+
} from './url';
|
package/src/artifacts/url.ts
CHANGED
|
@@ -20,6 +20,9 @@ export const ARTIFACTS_ROUTE_SEGMENT = 'api/docs/artifacts';
|
|
|
20
20
|
/** Route prefix for docs read API metadata (`.json`) reads. */
|
|
21
21
|
export const DOCS_ROUTE_SEGMENT = 'api/docs';
|
|
22
22
|
|
|
23
|
+
/** Canonical route prefix all asset content URLs are served under (mode-independent). */
|
|
24
|
+
export const ASSETS_ROUTE_SEGMENT = 'api/docs/assets';
|
|
25
|
+
|
|
23
26
|
/**
|
|
24
27
|
* Normalize a configured basePath to either an empty string or a single-leading-slash,
|
|
25
28
|
* no-trailing-slash form. Mirrors the Next app's `resolveBasePath` so builder output composes
|
|
@@ -88,3 +91,32 @@ export const buildComponentDetailUrl = (id: string, basePath?: string | null): s
|
|
|
88
91
|
*/
|
|
89
92
|
export const buildPatternDetailUrl = (id: string, basePath?: string | null): string =>
|
|
90
93
|
`${normalizeBasePath(basePath)}/${DOCS_ROUTE_SEGMENT}/patterns/${encodeURIComponent(id)}.json`;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Build the canonical, basePath-aware content URL for one asset within a collection
|
|
97
|
+
* (`{basePath}/api/docs/assets/{collection}/{logicalPath}`). This is the same route the docs read API
|
|
98
|
+
* serves in every runtime mode: from the database in registry mode, and from statically materialized
|
|
99
|
+
* files in a static export. Each segment is individually encoded and traversal segments are rejected,
|
|
100
|
+
* mirroring {@link buildArtifactUrl}.
|
|
101
|
+
*
|
|
102
|
+
* @throws If the collection or logical path has no usable segments, or contains a `.`/`..` segment.
|
|
103
|
+
*/
|
|
104
|
+
export const buildAssetUrl = (collection: string, logicalPath: string, basePath?: string | null): string => {
|
|
105
|
+
const segments = [...toArtifactSegments(collection), ...toArtifactSegments(logicalPath)];
|
|
106
|
+
if (segments.length < 2) {
|
|
107
|
+
throw new Error(`Cannot build asset URL: empty collection ("${collection}") or path ("${logicalPath}").`);
|
|
108
|
+
}
|
|
109
|
+
if (segments.some(isTraversalSegment)) {
|
|
110
|
+
throw new Error(`Cannot build asset URL: path traversal is not allowed in "${collection}/${logicalPath}".`);
|
|
111
|
+
}
|
|
112
|
+
const encoded = segments.map((segment) => encodeURIComponent(segment)).join('/');
|
|
113
|
+
return `${normalizeBasePath(basePath)}/${ASSETS_ROUTE_SEGMENT}/${encoded}`;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build the canonical download URL for a collection's whole-collection zip bundle
|
|
118
|
+
* (`{basePath}/api/docs/assets/{collection}/{collection}.zip`) — the default target for the docs
|
|
119
|
+
* "Download" links, overridable via `assets_zip_links`.
|
|
120
|
+
*/
|
|
121
|
+
export const buildAssetDownloadUrl = (collection: string, basePath?: string | null): string =>
|
|
122
|
+
buildAssetUrl(collection, `${collection}.zip`, basePath);
|
package/src/types/config.ts
CHANGED
|
@@ -214,18 +214,18 @@ export interface Config {
|
|
|
214
214
|
*/
|
|
215
215
|
entries?: ConfigEntries;
|
|
216
216
|
/**
|
|
217
|
-
*
|
|
218
|
-
*
|
|
217
|
+
* Override URLs for the asset zip download links. When unset, each link defaults to the canonical,
|
|
218
|
+
* basePath-aware asset route `{basePath}/api/docs/assets/{collection}/{collection}.zip`, served by
|
|
219
|
+
* the docs read API in every runtime mode (from the DB in registry mode, from statically
|
|
220
|
+
* materialized files in a static export).
|
|
219
221
|
*/
|
|
220
222
|
assets_zip_links?: {
|
|
221
223
|
/**
|
|
222
|
-
*
|
|
223
|
-
* @default "/icons.zip"
|
|
224
|
+
* Override URL for the icons zip. Defaults to `/api/docs/assets/icons/icons.zip` (basePath-aware).
|
|
224
225
|
*/
|
|
225
226
|
icons?: string;
|
|
226
227
|
/**
|
|
227
|
-
*
|
|
228
|
-
* @default "/logos.zip"
|
|
228
|
+
* Override URL for the logos zip. Defaults to `/api/docs/assets/logos/logos.zip` (basePath-aware).
|
|
229
229
|
*/
|
|
230
230
|
logos?: string;
|
|
231
231
|
};
|