@hollware/vue 0.0.0 → 1.0.0-rc.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 +78 -1
- package/dist/index.d.mts +70 -0
- package/dist/index.mjs +110 -0
- package/package.json +44 -4
package/README.md
CHANGED
|
@@ -1,3 +1,80 @@
|
|
|
1
1
|
# @hollware/vue
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Read [Hollware](https://hollware.com) from a Vue 3 app that is not Nuxt (for Nuxt, use
|
|
4
|
+
`@hollware/nuxt`): `<HollwareContent>` renders an entry's Comark document with your own Vue
|
|
5
|
+
components, on the server and in the browser, and composables read published content from a
|
|
6
|
+
browser-only app. It imports nothing of Cloudflare.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install @hollware/vue
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## A browser-only app (a Vite SPA)
|
|
13
|
+
|
|
14
|
+
The composables read over HTTP with a **read token** (`hw_read_`, from `hollware token create --read`).
|
|
15
|
+
The token is in the page, so anyone can read it: it can only read published content, and nothing
|
|
16
|
+
else. Hollware accepts it from a page whose origin is in the site's `cors.origins`, so add the
|
|
17
|
+
origin of the app there (`http://localhost:5173` in development).
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// main.ts
|
|
21
|
+
import { createHollwarePlugin } from '@hollware/vue'
|
|
22
|
+
createApp(App)
|
|
23
|
+
.use(
|
|
24
|
+
createHollwarePlugin({ site: 'my-site', url: 'https://cms.example.com', token: 'hw_read_…' }),
|
|
25
|
+
)
|
|
26
|
+
.mount('#app')
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```vue
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
import { HollwareContent, useHollwareEntry } from '@hollware/vue'
|
|
32
|
+
const props = defineProps<{ slug: string }>()
|
|
33
|
+
const { data, error, pending } = useHollwareEntry('blog', 'en', () => props.slug)
|
|
34
|
+
</script>
|
|
35
|
+
<template>
|
|
36
|
+
<p v-if="pending">Loading…</p>
|
|
37
|
+
<p v-else-if="error">{{ error.message }}</p>
|
|
38
|
+
<HollwareContent v-else-if="data" :entry="data" :components="{ callout: Callout }" />
|
|
39
|
+
<p v-else>Not found</p>
|
|
40
|
+
</template>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| What | Does |
|
|
44
|
+
| -------------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
45
|
+
| `createHollwarePlugin({ site, url, token })` | The plugin that provides Hollware to the app; `provideHollware(app, options)` is the same |
|
|
46
|
+
| `useHollwareEntry(collection, locale, slug)` | `{ data, error, pending }` for one published entry; each argument may be a ref or a getter |
|
|
47
|
+
| `useHollwareEntries(collection, options?)` | `{ data, error, pending }` for a page of entries, newest first (`locale`, `tag`, `limit`, `cursor`) |
|
|
48
|
+
|
|
49
|
+
`data` is `undefined` while the first read loads and when the entry is not published (then
|
|
50
|
+
`pending` is `false` and `error` is `undefined`). A read that fails sets `error` and clears `data`.
|
|
51
|
+
When an argument changes, the composable reads again, keeps the previous `data` meanwhile, and
|
|
52
|
+
drops the answer of a read that a newer one replaced.
|
|
53
|
+
|
|
54
|
+
There is **no preview in the browser**: "Preview on the site" exchanges its link for a cookie that is
|
|
55
|
+
`HttpOnly` on the site's server, so a page in the browser never holds the token. Previews need a
|
|
56
|
+
server that renders the page.
|
|
57
|
+
|
|
58
|
+
## A server that renders Vue (Vue SSR with Vite, Vike, any server)
|
|
59
|
+
|
|
60
|
+
The server helpers are the same as `@hollware/react`'s: they are `@hollware/client`'s, re-exported.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { createHollware, previewHeaders, previewRedirect } from '@hollware/vue'
|
|
64
|
+
const hollware = createHollware({ site: 'my-site', url: HOLLWARE_URL, token: HOLLWARE_TOKEN })
|
|
65
|
+
const redirect = previewRedirect(request) // the 303 that exchanges a preview link, or null
|
|
66
|
+
const answer = await hollware.getEntry('blog', 'en', slug, { request }) // { entry, preview } | undefined
|
|
67
|
+
const headers = previewHeaders(request) // `no-store` for a visitor who previews
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Render `answer.entry` with `<HollwareContent :entry="answer.entry" />` and send the same entry to the
|
|
71
|
+
browser to hydrate. `createHollware({ site, reader: () => hollwareBinding(env.HOLLWARE) })` reads over
|
|
72
|
+
a Cloudflare service binding instead. See the README of `@hollware/react` for `listEntries`,
|
|
73
|
+
`getTranslations` and the previews in full: they work the same.
|
|
74
|
+
|
|
75
|
+
## Components
|
|
76
|
+
|
|
77
|
+
`<HollwareContent :entry :components>` renders the Comark document Hollware stored when the entry
|
|
78
|
+
was written, with `@comark/vue`. `components` maps the names of the components in the entry
|
|
79
|
+
(`::callout` is `callout`) to your own Vue components; an element with no mapping renders as the
|
|
80
|
+
plain tag. The page needs no Markdown parser: the document is already parsed.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { EntryAnswer, EntryPage, EntryPage as EntryPage$1, GetEntryOptions, Hollware, HollwareOptions, HollwareOptions as HollwareOptions$1, HollwareReader, ListEntriesOptions, ListEntriesOptions as ListEntriesOptions$1, PREVIEW_COOKIE, PageDocument, PageEntry, PageEntry as PageEntry$1, PageSummary, ReaderOptions, Translation, createHollware, hollwareBinding, isPreviewRequest, previewCookieClear, previewHeaders, previewRedirect } from "@hollware/client";
|
|
2
|
+
import { App, MaybeRefOrGetter, Plugin, PropType, Ref } from "vue";
|
|
3
|
+
//#region src/content.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The body of an entry: the Comark document Hollware stored when it was written, rendered with
|
|
6
|
+
* `@comark/vue`, imported from its own subpath so no Markdown parser reaches the bundle. It
|
|
7
|
+
* renders on the server and hydrates in the browser from the same document. `components` maps
|
|
8
|
+
* the names of the components in the entry (`::callout` is `Callout`) to the site's own Vue
|
|
9
|
+
* components; an element with no mapping renders as the plain tag.
|
|
10
|
+
*/
|
|
11
|
+
export declare const HollwareContent: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
12
|
+
/** An entry as `getEntry` or `useHollwareEntry` gives it, with its `document`. */
|
|
13
|
+
entry: {
|
|
14
|
+
type: PropType<{
|
|
15
|
+
readonly document?: unknown;
|
|
16
|
+
} | null | undefined>;
|
|
17
|
+
default: null;
|
|
18
|
+
};
|
|
19
|
+
components: {
|
|
20
|
+
type: PropType<Record<string, unknown>>;
|
|
21
|
+
default: () => {};
|
|
22
|
+
};
|
|
23
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
24
|
+
[key: string]: any;
|
|
25
|
+
}> | null, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
26
|
+
/** An entry as `getEntry` or `useHollwareEntry` gives it, with its `document`. */
|
|
27
|
+
entry: {
|
|
28
|
+
type: PropType<{
|
|
29
|
+
readonly document?: unknown;
|
|
30
|
+
} | null | undefined>;
|
|
31
|
+
default: null;
|
|
32
|
+
};
|
|
33
|
+
components: {
|
|
34
|
+
type: PropType<Record<string, unknown>>;
|
|
35
|
+
default: () => {};
|
|
36
|
+
};
|
|
37
|
+
}>> & Readonly<{}>, {
|
|
38
|
+
entry: {
|
|
39
|
+
readonly document?: unknown;
|
|
40
|
+
} | null | undefined;
|
|
41
|
+
components: Record<string, unknown>;
|
|
42
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/composables.d.ts
|
|
45
|
+
/** Makes `app` read Hollware for the composables below. */
|
|
46
|
+
export declare function provideHollware(app: App, options: HollwareOptions$1): void;
|
|
47
|
+
/** `provideHollware` as a plugin: `createApp(App).use(createHollwarePlugin({ site, url, token }))`. */
|
|
48
|
+
export declare function createHollwarePlugin(options: HollwareOptions$1): Plugin;
|
|
49
|
+
/**
|
|
50
|
+
* What a composable gives: the answer, the error of the last read, and whether a read is under
|
|
51
|
+
* way. `data` keeps the previous answer while the next one loads, and is `undefined` after an
|
|
52
|
+
* error.
|
|
53
|
+
*/
|
|
54
|
+
interface HollwareState<T> {
|
|
55
|
+
readonly data: Readonly<Ref<T | undefined>>;
|
|
56
|
+
readonly error: Readonly<Ref<Error | undefined>>;
|
|
57
|
+
readonly pending: Readonly<Ref<boolean>>;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A published entry with its Comark document, for `<HollwareContent>`. `data` is `undefined`
|
|
61
|
+
* while it loads and when the entry is not published (`pending` is `false` then, and `error` too).
|
|
62
|
+
*/
|
|
63
|
+
export declare function useHollwareEntry(collection: MaybeRefOrGetter<string>, locale: MaybeRefOrGetter<string>, slug: MaybeRefOrGetter<string>): HollwareState<PageEntry$1>;
|
|
64
|
+
/** A page of the published entries of a collection, newest first. */
|
|
65
|
+
export declare function useHollwareEntries(collection: MaybeRefOrGetter<string>, options?: MaybeRefOrGetter<ListEntriesOptions$1>): HollwareState<EntryPage$1>;
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/index.d.ts
|
|
68
|
+
export declare const name = "@hollware/vue";
|
|
69
|
+
//#endregion
|
|
70
|
+
export { type EntryAnswer, type EntryPage, type GetEntryOptions, type Hollware, type HollwareOptions, type HollwareReader, type HollwareState, type ListEntriesOptions, PREVIEW_COOKIE, type PageDocument, type PageEntry, type PageSummary, type ReaderOptions, type Translation, createHollware, hollwareBinding, isPreviewRequest, previewCookieClear, previewHeaders, previewRedirect };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { PREVIEW_COOKIE, createHollware, createHollware as createHollware$1, hollwareBinding, isPreviewRequest, previewCookieClear, previewHeaders, previewRedirect } from "@hollware/client";
|
|
2
|
+
import { MarkdownDocument } from "@comark/vue/components/MarkdownDocument";
|
|
3
|
+
import { defineComponent, h, inject, ref, shallowReadonly, shallowRef, toValue, watchEffect } from "vue";
|
|
4
|
+
//#region src/content.ts
|
|
5
|
+
/**
|
|
6
|
+
* The body of an entry: the Comark document Hollware stored when it was written, rendered with
|
|
7
|
+
* `@comark/vue`, imported from its own subpath so no Markdown parser reaches the bundle. It
|
|
8
|
+
* renders on the server and hydrates in the browser from the same document. `components` maps
|
|
9
|
+
* the names of the components in the entry (`::callout` is `Callout`) to the site's own Vue
|
|
10
|
+
* components; an element with no mapping renders as the plain tag.
|
|
11
|
+
*/
|
|
12
|
+
const HollwareContent = defineComponent({
|
|
13
|
+
name: "HollwareContent",
|
|
14
|
+
props: {
|
|
15
|
+
/** An entry as `getEntry` or `useHollwareEntry` gives it, with its `document`. */
|
|
16
|
+
entry: {
|
|
17
|
+
type: Object,
|
|
18
|
+
default: null
|
|
19
|
+
},
|
|
20
|
+
components: {
|
|
21
|
+
type: Object,
|
|
22
|
+
default: () => ({})
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
setup(props) {
|
|
26
|
+
return () => {
|
|
27
|
+
const document = props.entry?.document;
|
|
28
|
+
if (typeof document !== "object" || document === null || !Array.isArray(Reflect.get(document, "nodes"))) return null;
|
|
29
|
+
return h(MarkdownDocument, {
|
|
30
|
+
value: document,
|
|
31
|
+
components: props.components
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/composables.ts
|
|
38
|
+
/**
|
|
39
|
+
* Composables for a browser-only Vue app (a Vite SPA). They read published content over HTTP
|
|
40
|
+
* with a read token (`hw_read_`), which the browser holds: Hollware allows it from a page whose
|
|
41
|
+
* origin is in the site's `cors.origins`. A token in a page is public, so it must be a read
|
|
42
|
+
* token, never a personal one. There is no preview here: the preview cookie is `HttpOnly` on the
|
|
43
|
+
* site's server, so a browser never has the token to send. A site that renders on a server reads
|
|
44
|
+
* with `createHollware` there.
|
|
45
|
+
*/
|
|
46
|
+
const HOLLWARE = Symbol("hollware");
|
|
47
|
+
/** Makes `app` read Hollware for the composables below. */
|
|
48
|
+
function provideHollware(app, options) {
|
|
49
|
+
app.provide(HOLLWARE, createHollware$1(options));
|
|
50
|
+
}
|
|
51
|
+
/** `provideHollware` as a plugin: `createApp(App).use(createHollwarePlugin({ site, url, token }))`. */
|
|
52
|
+
function createHollwarePlugin(options) {
|
|
53
|
+
return { install: (app) => provideHollware(app, options) };
|
|
54
|
+
}
|
|
55
|
+
function hollwareOf() {
|
|
56
|
+
const hollware = inject(HOLLWARE, void 0);
|
|
57
|
+
if (hollware === void 0) throw new Error("Hollware: nothing provides it to this app: add `app.use(createHollwarePlugin({ site, url, token }))`.");
|
|
58
|
+
return hollware;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Reads again whenever a ref that `read` reads changes, and drops the answer of a read that a
|
|
62
|
+
* newer one has replaced. `read` runs synchronously, so the refs it reads are the ones tracked.
|
|
63
|
+
*/
|
|
64
|
+
function useRead(read) {
|
|
65
|
+
const hollware = hollwareOf();
|
|
66
|
+
const data = shallowRef();
|
|
67
|
+
const error = shallowRef();
|
|
68
|
+
const pending = ref(true);
|
|
69
|
+
watchEffect((onCleanup) => {
|
|
70
|
+
let stale = false;
|
|
71
|
+
onCleanup(() => {
|
|
72
|
+
stale = true;
|
|
73
|
+
});
|
|
74
|
+
pending.value = true;
|
|
75
|
+
new Promise((resolve) => resolve(read(hollware))).then((value) => {
|
|
76
|
+
if (stale) return;
|
|
77
|
+
data.value = value;
|
|
78
|
+
error.value = void 0;
|
|
79
|
+
pending.value = false;
|
|
80
|
+
}, (cause) => {
|
|
81
|
+
if (stale) return;
|
|
82
|
+
data.value = void 0;
|
|
83
|
+
error.value = cause instanceof Error ? cause : new Error(String(cause));
|
|
84
|
+
pending.value = false;
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
data: shallowReadonly(data),
|
|
89
|
+
error: shallowReadonly(error),
|
|
90
|
+
pending: shallowReadonly(pending)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A published entry with its Comark document, for `<HollwareContent>`. `data` is `undefined`
|
|
95
|
+
* while it loads and when the entry is not published (`pending` is `false` then, and `error` too).
|
|
96
|
+
*/
|
|
97
|
+
function useHollwareEntry(collection, locale, slug) {
|
|
98
|
+
return useRead(async (hollware) => {
|
|
99
|
+
return (await hollware.getEntry(toValue(collection), toValue(locale), toValue(slug)))?.entry;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** A page of the published entries of a collection, newest first. */
|
|
103
|
+
function useHollwareEntries(collection, options = {}) {
|
|
104
|
+
return useRead((hollware) => hollware.listEntries(toValue(collection), toValue(options)));
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/index.ts
|
|
108
|
+
const name = "@hollware/vue";
|
|
109
|
+
//#endregion
|
|
110
|
+
export { HollwareContent, PREVIEW_COOKIE, createHollware, createHollwarePlugin, hollwareBinding, isPreviewRequest, name, previewCookieClear, previewHeaders, previewRedirect, provideHollware, useHollwareEntries, useHollwareEntry };
|
package/package.json
CHANGED
|
@@ -1,13 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hollware/vue",
|
|
3
|
-
"version": "0.0.0",
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0-rc.0",
|
|
4
|
+
"description": "Read Hollware from any Vue 3 app that is not Nuxt: a component that renders an entry's Comark document, server helpers, and composables for a browser-only app.",
|
|
5
5
|
"homepage": "https://hollware.com",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/hollware/hollware/issues"
|
|
8
|
+
},
|
|
6
9
|
"license": "MIT",
|
|
7
10
|
"repository": {
|
|
8
11
|
"type": "git",
|
|
9
12
|
"url": "git+https://github.com/hollware/hollware.git",
|
|
10
13
|
"directory": "packages/vue"
|
|
11
14
|
},
|
|
12
|
-
"
|
|
13
|
-
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.mts",
|
|
23
|
+
"default": "./dist/index.mjs"
|
|
24
|
+
},
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public",
|
|
29
|
+
"provenance": true
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@comark/vue": "0.7.0",
|
|
33
|
+
"@hollware/client": "1.0.0-rc.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "24.19.2",
|
|
37
|
+
"publint": "0.3.25",
|
|
38
|
+
"typescript": "7.0.2",
|
|
39
|
+
"vite-plus": "1.2.0",
|
|
40
|
+
"vue": "3.5.44"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"vue": "^3.5.0"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=24.11.0"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "vp pack",
|
|
50
|
+
"test": "vp test",
|
|
51
|
+
"publint": "publint --strict"
|
|
52
|
+
}
|
|
53
|
+
}
|