diff --git a/CHANGELOG.md b/CHANGELOG.md index c1604259..a64a77a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,29 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [3.0.0-alpha.0](https://github.com/nuxt/vue-meta/compare/v2.3.3...v3.0.0-alpha.0) (2021-01-25) + + +### Features + +* add amp-boilerplate as boolean attribute (resolves: [#530](https://github.com/nuxt/vue-meta/issues/530)) ([#531](https://github.com/nuxt/vue-meta/issues/531)) ([bb45319](https://github.com/nuxt/vue-meta/commit/bb453195747058d90862d2db20d6a538ef04811f)) +* add deepest resolver (wip) ([bb04dc0](https://github.com/nuxt/vue-meta/commit/bb04dc068dbcf1871bdd08eddd3bb7997b122f04)) +* add poc vue-compiler ([6d25ff2](https://github.com/nuxt/vue-meta/commit/6d25ff2f372f20af682389d2a8f85eacdc3423ed)) +* add support for attributes (wip) ([5eaa0ab](https://github.com/nuxt/vue-meta/commit/5eaa0ab5b63000a56e0f1d4460700cc6a10d3b79)) +* continued progress ([642a62c](https://github.com/nuxt/vue-meta/commit/642a62c56126f5dfdc094282f6bc179e07f022eb)) +* convert to ts (wip) ([28d3fc1](https://github.com/nuxt/vue-meta/commit/28d3fc192363b9caf0a8b25a357684e6e7ae337f)) +* first work on vue v3 composition metainfo app ([5d0eb1a](https://github.com/nuxt/vue-meta/commit/5d0eb1ab60ce476ed8a97e97d4d409e74284df9b)) +* implement first useApi basics ([b0edfbe](https://github.com/nuxt/vue-meta/commit/b0edfbe6bd638ae3404739ec881d6a0ef598e43c)) +* improve useApi ([303eae1](https://github.com/nuxt/vue-meta/commit/303eae1603a5f15611a9d66457a56b417784da8c)) +* make attributes part of the metainfo object ([5add8bf](https://github.com/nuxt/vue-meta/commit/5add8bf83f597e9576b3a4502d6d7a0f1a76014c)) +* make ssr work ([9cfde5b](https://github.com/nuxt/vue-meta/commit/9cfde5b5509e7cd0899ea450801bed753ec64075)) +* refactor of object merge & make vue-router example work ([e68b535](https://github.com/nuxt/vue-meta/commit/e68b53573e60969a0f616c053e58a90fe87ceee2)) + + +### Bug Fixes + +* resolving arrays (collections still wip) ([5c4ee7a](https://github.com/nuxt/vue-meta/commit/5c4ee7a54720ce54cd94217c2e663b350873f4f2)) + ### [2.3.3](https://github.com/nuxt/vue-meta/compare/v2.3.2...v2.3.3) (2020-02-26) diff --git a/dist/src/Metainfo.d.ts b/dist/src/Metainfo.d.ts new file mode 100644 index 00000000..3c308155 --- /dev/null +++ b/dist/src/Metainfo.d.ts @@ -0,0 +1,12 @@ +import { VNodeProps } from 'vue'; +import { MetainfoActive } from './types'; +export interface MetainfoProps { + metainfo: MetainfoActive; +} +export declare const MetainfoImpl: import("vue").DefineComponent<{}, () => import("vue").VNode[] | undefined, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, import("vue").EmitsOptions, string, VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<{} & {}>, {}>; +export declare const Metainfo: new () => { + $props: VNodeProps & MetainfoProps; +}; +//# sourceMappingURL=Metainfo.d.ts.map \ No newline at end of file diff --git a/dist/src/Metainfo.d.ts.map b/dist/src/Metainfo.d.ts.map new file mode 100644 index 00000000..2476e430 --- /dev/null +++ b/dist/src/Metainfo.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"Metainfo.d.ts","sourceRoot":"","sources":["../../src/Metainfo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,UAAU,EAAE,MAAM,KAAK,CAAA;AAEjD,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAExC,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,cAAc,CAAA;CACzB;AAED,eAAO,MAAM,YAAY;;yPAavB,CAAA;AAEF,eAAO,MAAM,QAAQ,YACX;IACN,MAAM,EAAE,UAAU,GAAG,aAAa,CAAA;CAErC,CAAA"} \ No newline at end of file diff --git a/dist/src/config/default.d.ts b/dist/src/config/default.d.ts new file mode 100644 index 00000000..755b4e2a --- /dev/null +++ b/dist/src/config/default.d.ts @@ -0,0 +1,3 @@ +import { Config } from '../types'; +export declare const defaultConfig: Config; +//# sourceMappingURL=default.d.ts.map \ No newline at end of file diff --git a/dist/src/config/default.d.ts.map b/dist/src/config/default.d.ts.map new file mode 100644 index 00000000..9ae2c7b7 --- /dev/null +++ b/dist/src/config/default.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"default.d.ts","sourceRoot":"","sources":["../../../src/config/default.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAEjC,eAAO,MAAM,aAAa,EAAE,MAoC3B,CAAA"} \ No newline at end of file diff --git a/dist/src/config/helpers.d.ts b/dist/src/config/helpers.d.ts new file mode 100644 index 00000000..b1266a12 --- /dev/null +++ b/dist/src/config/helpers.d.ts @@ -0,0 +1,4 @@ +import { Config } from '../types'; +export declare function hasConfig(name: string, config: Config): boolean; +export declare function getConfigByKey(tagOrName: string | Array, key: string, config: Config): any; +//# sourceMappingURL=helpers.d.ts.map \ No newline at end of file diff --git a/dist/src/config/helpers.d.ts.map b/dist/src/config/helpers.d.ts.map new file mode 100644 index 00000000..a5fd8221 --- /dev/null +++ b/dist/src/config/helpers.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/config/helpers.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAGjC,wBAAgB,SAAS,CAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAEhE;AAED,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,EACjC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,GACb,GAAG,CAmBL"} \ No newline at end of file diff --git a/dist/src/config/index.d.ts b/dist/src/config/index.d.ts new file mode 100644 index 00000000..f01bba67 --- /dev/null +++ b/dist/src/config/index.d.ts @@ -0,0 +1,4 @@ +export * from './default'; +export * from './helpers'; +export * from './tags'; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/src/config/index.d.ts.map b/dist/src/config/index.d.ts.map new file mode 100644 index 00000000..c7c87d93 --- /dev/null +++ b/dist/src/config/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/config/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,QAAQ,CAAA"} \ No newline at end of file diff --git a/dist/src/config/tags.d.ts b/dist/src/config/tags.d.ts new file mode 100644 index 00000000..187cff36 --- /dev/null +++ b/dist/src/config/tags.d.ts @@ -0,0 +1,11 @@ +export interface TagConfig { + keyAttribute?: string; + contentAsAttribute?: boolean | string; + attributes: boolean | Array; + [key: string]: any; +} +declare const tags: { + [key: string]: TagConfig; +}; +export { tags }; +//# sourceMappingURL=tags.d.ts.map \ No newline at end of file diff --git a/dist/src/config/tags.d.ts.map b/dist/src/config/tags.d.ts.map new file mode 100644 index 00000000..c11d5adb --- /dev/null +++ b/dist/src/config/tags.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"tags.d.ts","sourceRoot":"","sources":["../../../src/config/tags.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,SAAS;IACxB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,kBAAkB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IACrC,UAAU,EAAE,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAA;IACnC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CACnB;AAED,QAAA,MAAM,IAAI,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAiDrC,CAAA;AAED,OAAO,EAAE,IAAI,EAAE,CAAA"} \ No newline at end of file diff --git a/dist/src/index.d.ts b/dist/src/index.d.ts new file mode 100644 index 00000000..5cc73857 --- /dev/null +++ b/dist/src/index.d.ts @@ -0,0 +1,9 @@ +import * as deepestResolver from './resolvers/deepest'; +export { defaultConfig } from './config'; +export { createMetaManager } from './manager'; +export { resolveOption } from './resolvers'; +export * from './ssr'; +export * from './types'; +export * from './useApi'; +export { deepestResolver }; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/src/index.d.ts.map b/dist/src/index.d.ts.map new file mode 100644 index 00000000..2d0dfaa0 --- /dev/null +++ b/dist/src/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,eAAe,MAAM,qBAAqB,CAAA;AAEtD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3C,cAAc,OAAO,CAAA;AACrB,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AAExB,OAAO,EACL,eAAe,EAChB,CAAA"} \ No newline at end of file diff --git a/dist/src/manager.d.ts b/dist/src/manager.d.ts new file mode 100644 index 00000000..ff8a6a26 --- /dev/null +++ b/dist/src/manager.d.ts @@ -0,0 +1,8 @@ +import { VNode } from 'vue'; +import type { ResolveMethod } from './object-merge'; +import type { Manager, Config, Resolver, MetainfoActive } from './types'; +export declare const ssrAttribute = "data-vm-ssr"; +export declare const active: MetainfoActive; +export declare function addVnode(teleports: any, to: string, _vnodes: VNode | Array): void; +export declare function createMetaManager(config: Config, resolver: Resolver | ResolveMethod): Manager; +//# sourceMappingURL=manager.d.ts.map \ No newline at end of file diff --git a/dist/src/manager.d.ts.map b/dist/src/manager.d.ts.map new file mode 100644 index 00000000..27b7d4f1 --- /dev/null +++ b/dist/src/manager.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsC,KAAK,EAAW,MAAM,KAAK,CAAA;AAMxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAe,cAAc,EAAE,MAAM,SAAS,CAAA;AAErF,eAAO,MAAM,YAAY,gBAAgB,CAAA;AAEzC,eAAO,MAAM,MAAM,EAAE,cAA6B,CAAA;AAElD,wBAAgB,QAAQ,CAAE,SAAS,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,QA2BlF;AAED,wBAAgB,iBAAiB,CAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,aAAa,GAAG,OAAO,CAsG9F"} \ No newline at end of file diff --git a/dist/src/object-merge/constants.d.ts b/dist/src/object-merge/constants.d.ts new file mode 100644 index 00000000..ad399872 --- /dev/null +++ b/dist/src/object-merge/constants.d.ts @@ -0,0 +1,5 @@ +export declare const IS_PROXY: string; +export declare const PROXY_SOURCES: string; +export declare const PROXY_TARGET: string; +export declare const RESOLVE_CONTEXT: string; +//# sourceMappingURL=constants.d.ts.map \ No newline at end of file diff --git a/dist/src/object-merge/constants.d.ts.map b/dist/src/object-merge/constants.d.ts.map new file mode 100644 index 00000000..493f449a --- /dev/null +++ b/dist/src/object-merge/constants.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/object-merge/constants.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,QAAQ,QAA0C,CAAA;AAC/D,eAAO,MAAM,aAAa,QAA+C,CAAA;AACzE,eAAO,MAAM,YAAY,QAA8C,CAAA;AACvE,eAAO,MAAM,eAAe,QAAiD,CAAA"} \ No newline at end of file diff --git a/dist/src/object-merge/index.d.ts b/dist/src/object-merge/index.d.ts new file mode 100644 index 00000000..7f489825 --- /dev/null +++ b/dist/src/object-merge/index.d.ts @@ -0,0 +1,25 @@ +export declare type MergeSource = { + [key: string]: any; +}; +export declare type MergedObjectValue = boolean | number | string | MergedObject | any; +export declare type MergedObject = { + [key: string]: MergedObjectValue; +}; +export declare type PathSegments = Array; +export declare type ResolveContext = {}; +export declare type ResolveMethod = (options: Array, contexts: Array, active: MergedObjectValue, key: string | number | symbol, pathSegments: PathSegments) => MergedObjectValue; +export declare type MergeContext = { + resolve: ResolveMethod; + active: MergedObject; + sources: Array; +}; +export declare const createMergedObject: (resolve: ResolveMethod, active?: MergedObject) => { + context: MergeContext; + active: MergedObject; + resolve: ResolveMethod; + sources: MergeSource[]; + addSource: (source: MergeSource, resolveContext: ResolveContext | undefined, recompute?: Boolean) => any; + delSource: (sourceOrProxy: MergeSource, recompute?: boolean) => boolean; + compute: () => void; +}; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/src/object-merge/index.d.ts.map b/dist/src/object-merge/index.d.ts.map new file mode 100644 index 00000000..51b375a0 --- /dev/null +++ b/dist/src/object-merge/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/object-merge/index.ts"],"names":[],"mappings":"AAIA,oBAAY,WAAW,GAAG;IACxB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CACnB,CAAA;AAGD,oBAAY,iBAAiB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,YAAY,GAAG,GAAG,CAAA;AAE9E,oBAAY,YAAY,GAAG;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAAA;CACjC,CAAA;AAED,oBAAY,YAAY,GAAG,KAAK,CAAC,MAAM,CAAC,CAAA;AAExC,oBAAY,cAAc,GAAG,EAAE,CAAA;AAE/B,oBAAY,aAAa,GAAG,CAC1B,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,EACnB,QAAQ,EAAE,KAAK,CAAC,cAAc,CAAC,EAC/B,MAAM,EAAE,iBAAiB,EACzB,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAC7B,YAAY,EAAE,YAAY,KACvB,iBAAiB,CAAA;AAEtB,oBAAY,YAAY,GAAG;IACzB,OAAO,EAAE,aAAa,CAAA;IACtB,MAAM,EAAE,YAAY,CAAA;IACpB,OAAO,EAAE,KAAK,CAAC,WAAW,CAAC,CAAA;CAC5B,CAAA;AAED,eAAO,MAAM,kBAAkB,YAAa,aAAa,WAAU,YAAY;;;;;wBAelD,WAAW,kBAAkB,cAAc,GAAG,SAAS,cAAa,OAAO;+BAUpE,WAAW,cAAa,OAAO,KAAU,OAAO;;CAyBnF,CAAA"} \ No newline at end of file diff --git a/dist/src/object-merge/proxy.d.ts b/dist/src/object-merge/proxy.d.ts new file mode 100644 index 00000000..7bbeae42 --- /dev/null +++ b/dist/src/object-merge/proxy.d.ts @@ -0,0 +1,4 @@ +import type { MergeContext, MergeSource, PathSegments, ResolveContext } from '.'; +export declare const createProxy: (context: MergeContext, target: MergeSource, resolveContext: ResolveContext, pathSegments?: PathSegments) => any; +export declare const createHandler: (context: MergeContext, resolveContext: ResolveContext, pathSegments: PathSegments) => ProxyHandler; +//# sourceMappingURL=proxy.d.ts.map \ No newline at end of file diff --git a/dist/src/object-merge/proxy.d.ts.map b/dist/src/object-merge/proxy.d.ts.map new file mode 100644 index 00000000..6c7746b7 --- /dev/null +++ b/dist/src/object-merge/proxy.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../../src/object-merge/proxy.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAqB,YAAY,EAAE,cAAc,EAAE,MAAM,GAAG,CAAA;AAEnG,eAAO,MAAM,WAAW,YAAa,YAAY,UAAU,WAAW,kBAAkB,cAAc,qCASrG,CAAA;AAED,eAAO,MAAM,aAAa,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,KAAK,YAAY,CAAC,GAAG,CA2KjI,CAAA"} \ No newline at end of file diff --git a/dist/src/object-merge/recompute.d.ts b/dist/src/object-merge/recompute.d.ts new file mode 100644 index 00000000..1356acdb --- /dev/null +++ b/dist/src/object-merge/recompute.d.ts @@ -0,0 +1,4 @@ +import type { MergeContext, MergeSource, MergedObject, PathSegments } from '.'; +export declare const allKeys: (source?: MergeSource | undefined, ...sources: Array) => Array; +export declare const recompute: (context: MergeContext, sources?: MergeSource[] | undefined, target?: MergedObject | undefined, path?: PathSegments) => void; +//# sourceMappingURL=recompute.d.ts.map \ No newline at end of file diff --git a/dist/src/object-merge/recompute.d.ts.map b/dist/src/object-merge/recompute.d.ts.map new file mode 100644 index 00000000..b6f437b0 --- /dev/null +++ b/dist/src/object-merge/recompute.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"recompute.d.ts","sourceRoot":"","sources":["../../../src/object-merge/recompute.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAkB,MAAM,GAAG,CAAA;AAE9F,eAAO,MAAM,OAAO,iDAAsC,MAAM,WAAW,CAAC,KAAG,MAAM,MAAM,CAoB1F,CAAA;AAED,eAAO,MAAM,SAAS,YAAa,YAAY,kGAAiF,IAkE/H,CAAA"} \ No newline at end of file diff --git a/dist/src/render.d.ts b/dist/src/render.d.ts new file mode 100644 index 00000000..8a83e863 --- /dev/null +++ b/dist/src/render.d.ts @@ -0,0 +1,29 @@ +import { VNode } from 'vue'; +import { TODO } from './types'; +export interface RenderContext { + slots: any; + [key: string]: TODO; +} +export interface GroupConfig { + group: string; + data: Array | TODO; + tagNamespace?: string; + fullName?: string; + slotName?: string; +} +export interface SlotScopeProperties { + content: any; + metainfo: any; + [key: string]: any; +} +export declare type RenderedMetainfoNode = { + vnode: VNode; + to?: string; +}; +export declare type RenderedMetainfo = Array; +export declare function renderMeta(context: RenderContext, key: string, data: TODO, config: TODO): void | RenderedMetainfo | RenderedMetainfoNode; +export declare function renderGroup(context: RenderContext, key: string, data: TODO, config: TODO): RenderedMetainfo | RenderedMetainfoNode; +export declare function renderTag(context: RenderContext, key: string, data: TODO, config?: TODO, groupConfig?: GroupConfig): RenderedMetainfo | RenderedMetainfoNode; +export declare function renderAttributes(context: RenderContext, key: string, data: TODO, config?: TODO): RenderedMetainfoNode | void; +export declare function getSlotContent({ metainfo, slots }: RenderContext, slotName: string, content: any, groupConfig?: GroupConfig): TODO; +//# sourceMappingURL=render.d.ts.map \ No newline at end of file diff --git a/dist/src/render.d.ts.map b/dist/src/render.d.ts.map new file mode 100644 index 00000000..d5bc022c --- /dev/null +++ b/dist/src/render.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAK,KAAK,EAAE,MAAM,KAAK,CAAA;AAG9B,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAS9B,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,GAAG,CAAA;IACV,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IACxB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,GAAG,CAAA;IACZ,QAAQ,EAAE,GAAG,CAAA;IACb,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CACnB;AAED,oBAAY,oBAAoB,GAAG;IACjC,KAAK,EAAE,KAAK,CAAA;IACZ,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,oBAAY,gBAAgB,GAAG,KAAK,CAAC,oBAAoB,CAAC,CAAA;AAE1D,wBAAgB,UAAU,CACxB,OAAO,EAAE,aAAa,EACtB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,IAAI,GACX,IAAI,GAAG,gBAAgB,GAAG,oBAAoB,CAYhD;AAED,wBAAgB,WAAW,CACzB,OAAO,EAAE,aAAa,EACtB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,IAAI,GACX,gBAAgB,GAAG,oBAAoB,CAgCzC;AAED,wBAAgB,SAAS,CACvB,OAAO,EAAE,aAAa,EACtB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,IAAI,EACV,MAAM,GAAE,IAAS,EACjB,WAAW,CAAC,EAAE,WAAW,GACxB,gBAAgB,GAAG,oBAAoB,CA8HzC;AAED,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,aAAa,EACtB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,IAAI,EACV,MAAM,GAAE,IAAS,GAChB,oBAAoB,GAAG,IAAI,CAkD7B;AAED,wBAAgB,cAAc,CAC5B,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,aAAa,EAClC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,GAAG,EACZ,WAAW,CAAC,EAAE,WAAW,GACxB,IAAI,CAqBN"} \ No newline at end of file diff --git a/dist/src/resolvers/deepest.d.ts b/dist/src/resolvers/deepest.d.ts new file mode 100644 index 00000000..072bf06c --- /dev/null +++ b/dist/src/resolvers/deepest.d.ts @@ -0,0 +1,9 @@ +import { ResolveMethod } from '../object-merge'; +import { MetaContext } from '../types'; +declare type MergeContextDeepest = MetaContext & { + depth: number; +}; +export declare function setup(context: MergeContextDeepest): void; +export declare const resolve: ResolveMethod; +export {}; +//# sourceMappingURL=deepest.d.ts.map \ No newline at end of file diff --git a/dist/src/resolvers/deepest.d.ts.map b/dist/src/resolvers/deepest.d.ts.map new file mode 100644 index 00000000..0046b244 --- /dev/null +++ b/dist/src/resolvers/deepest.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"deepest.d.ts","sourceRoot":"","sources":["../../../src/resolvers/deepest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAGtC,aAAK,mBAAmB,GAAG,WAAW,GAAG;IACvC,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,wBAAgB,KAAK,CAAE,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAgBzD;AAED,eAAO,MAAM,OAAO,EAAE,aAKpB,CAAA"} \ No newline at end of file diff --git a/dist/src/resolvers/index.d.ts b/dist/src/resolvers/index.d.ts new file mode 100644 index 00000000..dacfffb4 --- /dev/null +++ b/dist/src/resolvers/index.d.ts @@ -0,0 +1,4 @@ +import { ResolveContext, ResolveMethod } from '../object-merge'; +export declare type ResolveOptionReducer = (accumulator: any, context: ResolveContext) => ResolveMethod; +export declare const resolveOption: (predicament: ResolveOptionReducer) => ResolveMethod; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/src/resolvers/index.d.ts.map b/dist/src/resolvers/index.d.ts.map new file mode 100644 index 00000000..03ef0c00 --- /dev/null +++ b/dist/src/resolvers/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/resolvers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAE/D,oBAAY,oBAAoB,GAAG,CAAC,WAAW,EAAE,GAAG,EAAE,OAAO,EAAE,cAAc,KAAK,aAAa,CAAA;AAE/F,eAAO,MAAM,aAAa,EAAE,CAAC,WAAW,EAAE,oBAAoB,KAAK,aAiBlE,CAAA"} \ No newline at end of file diff --git a/dist/src/ssr.d.ts b/dist/src/ssr.d.ts new file mode 100644 index 00000000..4eccc4ea --- /dev/null +++ b/dist/src/ssr.d.ts @@ -0,0 +1,4 @@ +import type { App } from 'vue'; +import type { SSRContext } from '@vue/server-renderer'; +export declare function renderToStringWithMeta(app: App): Promise<[string, SSRContext]>; +//# sourceMappingURL=ssr.d.ts.map \ No newline at end of file diff --git a/dist/src/ssr.d.ts.map b/dist/src/ssr.d.ts.map new file mode 100644 index 00000000..c1b55115 --- /dev/null +++ b/dist/src/ssr.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"ssr.d.ts","sourceRoot":"","sources":["../../src/ssr.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAC9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAKtD,wBAAsB,sBAAsB,CAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAsBrF"} \ No newline at end of file diff --git a/dist/src/symbols.d.ts b/dist/src/symbols.d.ts new file mode 100644 index 00000000..4841feba --- /dev/null +++ b/dist/src/symbols.d.ts @@ -0,0 +1,6 @@ +import { InjectionKey } from 'vue'; +import { MetainfoActive } from './types'; +export declare const hasSymbol: boolean; +export declare const PolySymbol: (name: string) => string | symbol; +export declare const metaInfoKey: InjectionKey; +//# sourceMappingURL=symbols.d.ts.map \ No newline at end of file diff --git a/dist/src/symbols.d.ts.map b/dist/src/symbols.d.ts.map new file mode 100644 index 00000000..ce5ebe56 --- /dev/null +++ b/dist/src/symbols.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"symbols.d.ts","sourceRoot":"","sources":["../../src/symbols.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,KAAK,CAAA;AAClC,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAExC,eAAO,MAAM,SAAS,SACkD,CAAA;AAExE,eAAO,MAAM,UAAU,SAAU,MAAM,oBAIM,CAAA;AAE7C,eAAO,MAAM,WAAW,8BAES,CAAA"} \ No newline at end of file diff --git a/dist/src/types/index.d.ts b/dist/src/types/index.d.ts new file mode 100644 index 00000000..40853f3a --- /dev/null +++ b/dist/src/types/index.d.ts @@ -0,0 +1,59 @@ +import type { App, VNode, ComponentInternalInstance } from 'vue'; +import type { MergedObject, ResolveContext, ResolveMethod } from '../object-merge'; +export declare type TODO = any; +export declare type MetainfoInput = { + [key: string]: TODO; +}; +export declare type MetaContext = ResolveContext & { + vm: ComponentInternalInstance | undefined; +}; +export interface ConfigOption { + tag?: string; + to?: string; + group?: boolean; + keyAttribute?: string; + valueAttribute?: string; + nameless?: boolean; + namespaced?: boolean; + namespacedAttribute?: boolean; + attributesFor?: string; +} +export interface Config { + [key: string]: ConfigOption; +} +export interface MetainfoProxy extends MergedObject { +} +export interface MetainfoActive { + [key: string]: TODO; +} +export declare type MetaProxy = { + meta: MetainfoProxy; + unmount: TODO; +}; +export declare type ResolveSetup = (context: MetaContext) => void; +export declare type Resolver = { + setup?: ResolveSetup; + resolve: ResolveMethod; +}; +export declare type Manager = { + readonly config: Config; + install(app: App): void; + addMeta(obj: MetainfoInput, vm?: ComponentInternalInstance): MetaProxy; + render(ctx: { + slots?: any; + }): Array; +}; +declare module '@vue/runtime-core' { + interface ComponentInternalInstance { + $metaManager: Manager; + } +} +declare global { + namespace NodeJS { + interface Process { + client: boolean; + server: boolean; + } + } +} +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/src/types/index.d.ts.map b/dist/src/types/index.d.ts.map new file mode 100644 index 00000000..aca35cb5 --- /dev/null +++ b/dist/src/types/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,yBAAyB,EAAE,MAAM,KAAK,CAAA;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAElF,oBAAY,IAAI,GAAG,GAAG,CAAA;AAEtB,oBAAY,aAAa,GAAG;IAC1B,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB,CAAA;AAED,oBAAY,WAAW,GAAG,cAAc,GAAG;IACzC,EAAE,EAAE,yBAAyB,GAAG,SAAS,CAAA;CAC1C,CAAA;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,MAAM;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAAA;CAC5B;AAED,MAAM,WAAW,aAAc,SAAQ,YAAY;CAElD;AAED,MAAM,WAAW,cAAc;IAC7B,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB;AAED,oBAAY,SAAS,GAAG;IACtB,IAAI,EAAE,aAAa,CAAA;IACnB,OAAO,EAAE,IAAI,CAAA;CACd,CAAA;AAED,oBAAY,YAAY,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAA;AAEzD,oBAAY,QAAQ,GAAG;IACrB,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB,OAAO,EAAE,aAAa,CAAA;CACvB,CAAA;AAED,oBAAY,OAAO,GAAG;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IAEvB,OAAO,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAAA;IACvB,OAAO,CAAC,GAAG,EAAE,aAAa,EAAE,EAAE,CAAC,EAAE,yBAAyB,GAAG,SAAS,CAAA;IAEtE,MAAM,CAAC,GAAG,EAAE;QAAE,KAAK,CAAC,EAAE,GAAG,CAAA;KAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAA;CAC3C,CAAA;AAED,OAAO,QAAQ,mBAAmB,CAAC;IACjC,UAAU,yBAAyB;QACjC,YAAY,EAAE,OAAO,CAAA;KACtB;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM,CAAC;QACf,UAAU,OAAO;YACf,MAAM,EAAE,OAAO,CAAA;YACf,MAAM,EAAE,OAAO,CAAA;SAChB;KACF;CACF"} \ No newline at end of file diff --git a/dist/src/useApi.d.ts b/dist/src/useApi.d.ts new file mode 100644 index 00000000..518dad59 --- /dev/null +++ b/dist/src/useApi.d.ts @@ -0,0 +1,6 @@ +import { ComponentInternalInstance } from 'vue'; +import type { Manager, MetainfoActive, MetainfoInput, MetaProxy } from './types'; +export declare function getCurrentManager(vm?: ComponentInternalInstance): Manager; +export declare function useMeta(obj: MetainfoInput, manager?: Manager): MetaProxy; +export declare function useMetainfo(): MetainfoActive; +//# sourceMappingURL=useApi.d.ts.map \ No newline at end of file diff --git a/dist/src/useApi.d.ts.map b/dist/src/useApi.d.ts.map new file mode 100644 index 00000000..d56817e2 --- /dev/null +++ b/dist/src/useApi.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"useApi.d.ts","sourceRoot":"","sources":["../../src/useApi.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,yBAAyB,EAAE,MAAM,KAAK,CAAA;AAE3E,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAEhF,wBAAgB,iBAAiB,CAAE,EAAE,CAAC,EAAE,yBAAyB,GAAG,OAAO,CAM1E;AAED,wBAAgB,OAAO,CAAE,GAAG,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAazE;AAED,wBAAgB,WAAW,IAAK,cAAc,CAE7C"} \ No newline at end of file diff --git a/dist/src/utils/clone.d.ts b/dist/src/utils/clone.d.ts new file mode 100644 index 00000000..63a7155b --- /dev/null +++ b/dist/src/utils/clone.d.ts @@ -0,0 +1,2 @@ +export declare function clone(v: any): any; +//# sourceMappingURL=clone.d.ts.map \ No newline at end of file diff --git a/dist/src/utils/clone.d.ts.map b/dist/src/utils/clone.d.ts.map new file mode 100644 index 00000000..c49ae05a --- /dev/null +++ b/dist/src/utils/clone.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"clone.d.ts","sourceRoot":"","sources":["../../../src/utils/clone.ts"],"names":[],"mappings":"AAGA,wBAAgB,KAAK,CAAE,CAAC,EAAE,GAAG,GAAG,GAAG,CAoBlC"} \ No newline at end of file diff --git a/dist/src/utils/collection.d.ts b/dist/src/utils/collection.d.ts new file mode 100644 index 00000000..dd0241d2 --- /dev/null +++ b/dist/src/utils/collection.d.ts @@ -0,0 +1,2 @@ +export declare const pluck: (collection: Array, key: string, callback?: ((row: any) => void) | undefined) => any[]; +//# sourceMappingURL=collection.d.ts.map \ No newline at end of file diff --git a/dist/src/utils/collection.d.ts.map b/dist/src/utils/collection.d.ts.map new file mode 100644 index 00000000..d5acf454 --- /dev/null +++ b/dist/src/utils/collection.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../../../src/utils/collection.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,KAAK,eAAgB,MAAM,GAAG,CAAC,OAAO,MAAM,oBAAmB,GAAG,KAAK,IAAI,uBAcvF,CAAA"} \ No newline at end of file diff --git a/dist/src/utils/debug.d.ts b/dist/src/utils/debug.d.ts new file mode 100644 index 00000000..957d0393 --- /dev/null +++ b/dist/src/utils/debug.d.ts @@ -0,0 +1,13 @@ +interface DebugInterface { + (...args: any[]): void; + warn: Function; + error: Function; +} +export declare const debugFn: (logFn: Function, setChildFns?: boolean) => { + (...args: any[]): void; + warn: any; + error: any; +}; +export declare const debug: DebugInterface; +export {}; +//# sourceMappingURL=debug.d.ts.map \ No newline at end of file diff --git a/dist/src/utils/debug.d.ts.map b/dist/src/utils/debug.d.ts.map new file mode 100644 index 00000000..7dd18865 --- /dev/null +++ b/dist/src/utils/debug.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"debug.d.ts","sourceRoot":"","sources":["../../../src/utils/debug.ts"],"names":[],"mappings":"AACA,UAAU,cAAc;IACtB,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;IACtB,IAAI,EAAE,QAAQ,CAAA;IACd,KAAK,EAAE,QAAQ,CAAA;CAChB;AAED,eAAO,MAAM,OAAO,UAAW,QAAQ,gBAAe,OAAO;cACtC,GAAG,EAAE;;;CAc3B,CAAA;AAED,eAAO,MAAM,KAAK,EAAE,cAA2C,CAAA"} \ No newline at end of file diff --git a/dist/src/utils/index.d.ts b/dist/src/utils/index.d.ts new file mode 100644 index 00000000..e2a85577 --- /dev/null +++ b/dist/src/utils/index.d.ts @@ -0,0 +1,4 @@ +export * from './clone'; +export * from './collection'; +export * from './debug'; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/src/utils/index.d.ts.map b/dist/src/utils/index.d.ts.map new file mode 100644 index 00000000..ecfa749b --- /dev/null +++ b/dist/src/utils/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAA;AACvB,cAAc,cAAc,CAAA;AAC5B,cAAc,SAAS,CAAA"} \ No newline at end of file diff --git a/dist/vue-meta.cjs.js b/dist/vue-meta.cjs.js new file mode 100644 index 00000000..3fd46f08 --- /dev/null +++ b/dist/vue-meta.cjs.js @@ -0,0 +1,801 @@ +/** + * vue-meta v3.0.0-alpha.0 + * (c) 2021 + * - Pim (@pimlie) + * - All the amazing contributors + * @license MIT + */ + +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +var vue = require('vue'); + +const resolveOption = predicament => (options, contexts) => { + let resolvedIndex = -1; + contexts.reduce((acc, context, index) => { + const retval = predicament(acc, context); + if (retval !== acc) { + resolvedIndex = index; + return retval; + } + return acc; + }, undefined); + if (resolvedIndex > -1) { + return options[resolvedIndex]; + } +}; + +function setup(context) { + let depth = 0; + if (context.vm) { + let { vm } = context; + do { + if (vm.parent) { + depth++; + vm = vm.parent; + } + } while (vm && vm.parent && vm !== vm.root); + } + context.depth = depth; +} +const resolve = resolveOption((acc, context) => { + const { depth } = context; + if (!acc || depth > acc) { + return acc; + } +}); + +var deepest = /*#__PURE__*/Object.freeze({ + __proto__: null, + setup: setup, + resolve: resolve +}); + +const defaultConfig = { + body: { + tag: 'script', + to: 'body' + }, + base: { + valueAttribute: 'href' + }, + charset: { + tag: 'meta', + nameless: true, + valueAttribute: 'charset' + }, + description: { + tag: 'meta' + }, + og: { + group: true, + namespacedAttribute: true, + tag: 'meta', + keyAttribute: 'property' + }, + twitter: { + group: true, + namespacedAttribute: true, + tag: 'meta' + }, + htmlAttrs: { + attributesFor: 'html' + }, + headAttrs: { + attributesFor: 'head' + }, + bodyAttrs: { + attributesFor: 'body' + } +}; + +/** + * Make a map and return a function for checking if a key + * is in that map. + * IMPORTANT: all calls of this function must be prefixed with + * \/\*#\_\_PURE\_\_\*\/ + * So that rollup can tree-shake them if necessary. + */ +(process.env.NODE_ENV !== 'production') + ? Object.freeze({}) + : {}; +(process.env.NODE_ENV !== 'production') ? Object.freeze([]) : []; +const isArray = Array.isArray; +const isFunction = (val) => typeof val === 'function'; +const isString = (val) => typeof val === 'string'; +const isObject = (val) => val !== null && typeof val === 'object'; +const objectToString = Object.prototype.toString; +const toTypeString = (value) => objectToString.call(value); +const isPlainObject = (val) => toTypeString(val) === '[object Object]'; + +/* + * This is a fixed config for real HTML tags + * + * TODO: we probably dont need all attributes + */ +const tags = { + title: { + attributes: false + }, + base: { + contentAsAttribute: true, + attributes: ['href', 'target'] + }, + meta: { + contentAsAttribute: true, + keyAttribute: 'name', + attributes: ['content', 'name', 'http-equiv', 'charset'] + }, + link: { + contentAsAttribute: true, + attributes: [ + 'href', + 'crossorigin', + 'rel', + 'media', + 'integrity', + 'hreflang', + 'type', + 'referrerpolicy', + 'sizes', + 'imagesrcset', + 'imagesizes', + 'as', + 'color' + ] + }, + style: { + attributes: ['media'] + }, + script: { + attributes: [ + 'src', + 'type', + 'nomodule', + 'async', + 'defer', + 'crossorigin', + 'integrity', + 'referrerpolicy' + ] + }, + noscript: { + attributes: false + } +}; + +function getConfigByKey(tagOrName, key, config) { + if (config && key in config) { + return config[key]; + } + if (isArray(tagOrName)) { + for (const name of tagOrName) { + if (name && name in tags) { + return tags[name][key]; + } + } + return; + } + if (tagOrName in tags) { + const tag = tags[tagOrName]; + return tag[key]; + } +} + +// https://github.com/microsoft/TypeScript/issues/1863 +const IS_PROXY = Symbol('kIsProxy'); +const PROXY_SOURCES = Symbol('kProxySources'); +const PROXY_TARGET = Symbol('kProxyTarget'); +const RESOLVE_CONTEXT = Symbol('kResolveContext'); + +// See: https://github.com/vuejs/vue-next/blob/08b4e8815da4e8911058ccbab986bea6365c3352/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts +function clone(v) { + if (isArray(v)) { + return v.map(clone); + } + if (isObject(v)) { + const res = {}; + for (const key in v) { + // never clone the context + if (key === 'context') { + res[key] = v[key]; + } + else { + res[key] = clone(v[key]); + } + } + return res; + } + return v; +} + +const pluck = (collection, key, callback) => { + const plucked = []; + for (const row of collection) { + if (key in row) { + plucked.push(row[key]); + if (callback) { + callback(row); + } + } + } + return plucked; +}; + +const allKeys = (source, ...sources) => { + const keys = source ? Object.keys(source) : []; + if (sources) { + for (const source of sources) { + if (!source || !isObject(source)) { + continue; + } + for (const key in source) { + if (!keys.includes(key)) { + keys.push(key); + } + } + } + } + // TODO: add check for consistent types for each key (dev only) + return keys; +}; +const recompute = (context, sources, target, path = []) => { + if (!path.length) { + if (!target) { + target = context.active; + } + if (!sources) { + sources = context.sources; + } + } + if (!target || !sources) { + return; + } + const keys = allKeys(...sources); + // Clean up properties that dont exists anymore + const targetKeys = Object.keys(target); + for (const key of targetKeys) { + if (!keys.includes(key)) { + delete target[key]; + } + } + for (const key of keys) { + // This assumes consistent types usages for keys across sources + if (isPlainObject(sources[0][key])) { + if (!target[key]) { + target[key] = {}; + } + const keySources = []; + for (const source of sources) { + if (key in source) { + keySources.push(source[key]); + } + } + recompute(context, keySources, target[key], [...path, key]); + continue; + } + // Ensure the target is an array if source is an array and target is empty + if (!target[key] && isArray(sources[0][key])) { + target[key] = []; + } + const keyContexts = []; + const keySources = pluck(sources, key, source => keyContexts.push(source[RESOLVE_CONTEXT])); + let resolved = context.resolve(keySources, keyContexts, target[key], key, path); + if (isPlainObject(resolved)) { + resolved = clone(resolved); + } + // console.log('RESOLVED', key, resolved, 'was', target[key]) + target[key] = resolved; + } +}; + +const createProxy = (context, target, resolveContext, pathSegments = []) => { + const handler = createHandler(context, resolveContext, pathSegments); + const proxy = vue.markRaw(new Proxy(target, handler)); + if (!pathSegments.length && context.sources) { + context.sources.push(proxy); + } + return proxy; +}; +const createHandler = (context, resolveContext, pathSegments = []) => ({ + get: (target, key, receiver) => { + if (key === IS_PROXY) { + return true; + } + if (key === PROXY_SOURCES) { + return context.sources; + } + if (key === PROXY_TARGET) { + return target; + } + if (key === RESOLVE_CONTEXT) { + return resolveContext; + } + let value = Reflect.get(target, key, receiver); + if (!isObject(value)) { + return value; + } + if (!value[IS_PROXY]) { + const keyPath = [...pathSegments, key]; + value = createProxy(context, value, resolveContext, keyPath); + target[key] = value; + } + return value; + }, + set: (target, key, value) => { + const success = Reflect.set(target, key, value); + // console.warn(success, 'PROXY SET\nkey:', key, '\npath:', pathSegments, '\ntarget:', isArray(target), target, '\ncontext:\n', context) + if (success) { + const isArrayItem = isArray(target); + let hasArrayParent = false; + let { sources: proxies, active } = context; + let activeSegmentKey; + let index = 0; + for (const segment of pathSegments) { + proxies = pluck(proxies, segment); + if (isArrayItem && index === pathSegments.length - 1) { + activeSegmentKey = segment; + break; + } + if (isArray(active)) { + hasArrayParent = true; + } + active = active[segment]; + index++; + } + if (hasArrayParent) { + // TODO: fix that we dont have to recompute the full merged object + // we should only have to recompute the branch that has changed + // but there is an issue here with supporting both arrays of strings + // as collections (parent vs parent of parent we need to trigger the + // update from) + recompute(context); + return success; + } + let keyContexts = []; + let keySources; + if (isArrayItem) { + keySources = proxies; + keyContexts = proxies.map(proxy => proxy[RESOLVE_CONTEXT]); + } + else { + keySources = pluck(proxies, key, proxy => keyContexts.push(proxy[RESOLVE_CONTEXT])); + } + let resolved = context.resolve(keySources, keyContexts, active, key, pathSegments); + // Ensure to clone if value is an object, cause sources is an array of + // the sourceProxies not the sources so we could trigger an endless loop when + // updating a prop on an obj as the prop on the active object refers to + // a prop on a proxy + if (isPlainObject(resolved)) { + resolved = clone(resolved); + } + // console.log('SET VALUE', isArrayItem, key, '\nresolved:\n', resolved, '\nsources:\n', context.sources, '\nactive:\n', active, Object.keys(active)) + if (isArrayItem && activeSegmentKey) { + active[activeSegmentKey] = resolved; + } + else { + active[key] = resolved; + } + } + // console.log('CONTEXT.ACTIVE', context.active, '\nparent:\n', target) + return success; + }, + deleteProperty: (target, key) => { + const success = Reflect.deleteProperty(target, key); + // console.warn('PROXY DELETE\nkey:', key, '\npath:', pathSegments, '\nparent:', isArray(target), target) + if (success) { + const isArrayItem = isArray(target); + let activeSegmentKey; + let proxies = context.sources; + let active = context.active; + let index = 0; + for (const segment of pathSegments) { + proxies = proxies.map(proxy => proxy[segment]); + if (isArrayItem && index === pathSegments.length - 1) { + activeSegmentKey = segment; + break; + } + active = active[segment]; + index++; + } + // Check if the key still exists in one of the sourceProxies, + // if so resolve the new value, if not remove the key + if (proxies.some(proxy => (key in proxy))) { + let keyContexts = []; + let keySources; + if (isArrayItem) { + keySources = proxies; + keyContexts = proxies.map(proxy => proxy[RESOLVE_CONTEXT]); + } + else { + keySources = pluck(proxies, key, proxy => keyContexts.push(proxy[RESOLVE_CONTEXT])); + } + let resolved = context.resolve(keySources, keyContexts, active, key, pathSegments); + if (isPlainObject(resolved)) { + resolved = clone(resolved); + } + // console.log('SET VALUE', resolved) + if (isArrayItem && activeSegmentKey) { + active[activeSegmentKey] = resolved; + } + else { + active[key] = resolved; + } + } + else { + delete active[key]; + } + } + return success; + } +}); + +const createMergedObject = (resolve, active = {}) => { + const sources = []; + if (!active) { + active = {}; + } + const context = { + active, + resolve, + sources + }; + const compute = () => recompute(context); + const addSource = (source, resolveContext, recompute = false) => { + const proxy = createProxy(context, source, resolveContext || {}); + if (recompute) { + compute(); + } + return proxy; + }; + const delSource = (sourceOrProxy, recompute = true) => { + const index = sources.findIndex(src => src === sourceOrProxy || src[PROXY_TARGET] === sourceOrProxy); + if (index > -1) { + sources.splice(index, 1); + if (recompute) { + compute(); + } + return true; + } + return false; + }; + return { + context, + active, + resolve, + sources, + addSource, + delSource, + compute + }; +}; + +function renderMeta(context, key, data, config) { + // console.info('renderMeta', key, data, config) + if (config.attributesFor) { + return renderAttributes(context, key, data, config); + } + if (config.group) { + return renderGroup(context, key, data, config); + } + return renderTag(context, key, data, config); +} +function renderGroup(context, key, data, config) { + // console.info('renderGroup', key, data, config) + if (isArray(data)) { + { + // eslint-disable-next-line no-console + console.warn('Specifying an array for group properties isnt supported mostly as we didnt found a use-case for this yet. If you have one, please create an issue on the vue-meta repo'); + } + // config.attributes = getConfigKey([key, config.tag], 'attributes', config) + return []; + } + return Object.keys(data) + .map((childKey) => { + const groupConfig = { + group: key, + data + }; + if (config.namespaced) { + groupConfig.tagNamespace = config.namespaced === true ? key : config.namespaced; + } + else if (config.namespacedAttribute) { + const namespace = config.namespacedAttribute === true ? key : config.namespacedAttribute; + groupConfig.fullName = `${namespace}:${childKey}`; + groupConfig.slotName = `${namespace}(${childKey})`; + } + return renderTag(context, key, data[childKey], config, groupConfig); + }) + .flat(); +} +function renderTag(context, key, data, config = {}, groupConfig) { + // console.info('renderTag', key, data, config, groupConfig) + const contentAttributes = ['content', 'json', 'rawContent']; + const getConfig = (key) => getConfigByKey([tag, config.tag], key, config); + if (isArray(data)) { + return data + .map((child) => { + return renderTag(context, key, child, config, groupConfig); + }) + .flat(); + } + const { tag = config.tag || key } = data; + let content; + let hasChilds = false; + let isRaw = false; + if (isString(data)) { + content = data; + } + else if (data.children && isArray(data.children)) { + hasChilds = true; + content = data.children.map((child) => { + const data = renderTag(context, key, child, config, groupConfig); + if (isArray(data)) { + return data.map(({ vnode }) => vnode); + } + return data.vnode; + }); + } + else { + let i = 0; + for (const contentAttribute of contentAttributes) { + if (!content && data[contentAttribute]) { + if (i === 1) { + content = JSON.stringify(data[contentAttribute]); + } + else { + content = data[contentAttribute]; + } + isRaw = i > 1; + break; + } + i++; + } + } + const fullName = (groupConfig && groupConfig.fullName) || key; + const slotName = (groupConfig && groupConfig.slotName) || key; + let { attrs: attributes } = data; + if (!attributes && typeof data === 'object') { + attributes = { ...data }; + delete attributes.tag; + delete attributes.children; + delete attributes.to; + // cleanup all content attributes + for (const attr of contentAttributes) { + delete attributes[attr]; + } + } + else if (!attributes) { + attributes = {}; + } + if (hasChilds) { + content = getSlotContent(context, slotName, content, data); + } + else { + const contentAsAttribute = getConfig('contentAsAttribute'); + let valueAttribute = config.valueAttribute; + if (!valueAttribute && contentAsAttribute) { + const tagAttributes = getConfig('attributes'); + valueAttribute = isString(contentAsAttribute) ? contentAsAttribute : tagAttributes[0]; + } + if (!valueAttribute) { + content = getSlotContent(context, slotName, content, data); + } + else { + if (!config.nameless) { + const keyAttribute = getConfig('keyAttribute'); + if (keyAttribute) { + attributes[keyAttribute] = fullName; + } + } + attributes[valueAttribute] = getSlotContent(context, slotName, attributes[valueAttribute] || content, groupConfig); + content = undefined; + } + } + const finalTag = groupConfig && groupConfig.tagNamespace + ? `${groupConfig.tagNamespace}:${tag}` + : tag; + // console.info('FINAL TAG', finalTag) + // console.log(' ATTRIBUTES', attributes) + // console.log(' CONTENT', content) + // // console.log(data, attributes, config) + let vnode; + if (isRaw) { + attributes.innerHTML = content; + vnode = vue.h(finalTag, attributes); + } + else { + vnode = vue.h(finalTag, attributes, content); + } + return { + to: data.to, + vnode + }; +} +function renderAttributes(context, key, data, config = {}) { + // console.info('renderAttributes', key, data, config) + const { attributesFor } = config; + { + // render attributes in a placeholder vnode so Vue + // will render the string for us + return { + to: '', + vnode: vue.h(`ssr-${attributesFor}`, data) + }; + } +} +function getSlotContent({ metainfo, slots }, slotName, content, groupConfig) { + if (!slots || !slots[slotName]) { + return content; + } + const slotProps = { + content, + metainfo + }; + if (groupConfig && groupConfig.group) { + slotProps[groupConfig.group] = groupConfig.data; + } + const slotContent = slots[slotName](slotProps); + if (slotContent && slotContent.length) { + return slotContent[0].children; + } + return content; +} + +const hasSymbol = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol'; +const PolySymbol = (name) => +// vm = vue meta +hasSymbol + ? Symbol( '[vue-meta]: ' + name ) + : ( '[vue-meta]: ' ) + name; +const metaInfoKey = PolySymbol( 'metainfo' ); + +function getCurrentManager(vm) { + if (!vm) { + vm = vue.getCurrentInstance(); + } + return vm.appContext.config.globalProperties.$metaManager; +} +function useMeta(obj, manager) { + const vm = vue.getCurrentInstance(); + if (!manager && vm) { + manager = getCurrentManager(vm); + } + if (!manager) { + // oopsydoopsy + throw new Error('No manager or current instance'); + } + return manager.addMeta(obj, vm || undefined); +} +function useMetainfo() { + return vue.inject(metaInfoKey); +} + +const MetainfoImpl = vue.defineComponent({ + name: 'Metainfo', + inheritAttrs: false, + setup(_, { slots }) { + return () => { + const manager = getCurrentManager(); + if (!manager) { + return; + } + return manager.render({ slots }); + }; + } +}); +const Metainfo = MetainfoImpl; + +const ssrAttribute = 'data-vm-ssr'; +const active = vue.reactive({}); +function addVnode(teleports, to, _vnodes) { + const vnodes = (isArray(_vnodes) ? _vnodes : [_vnodes]); + { + // dont add ssrAttribute for attribute vnode placeholder + if (!to.endsWith('Attrs')) { + vnodes.forEach((vnode) => { + if (!vnode.props) { + vnode.props = {}; + } + vnode.props[ssrAttribute] = true; + }); + } + } + if (!teleports[to]) { + teleports[to] = []; + } + teleports[to].push(...vnodes); +} +function createMetaManager(config, resolver) { + const resolve = (options, contexts, active, key, pathSegments) => { + if (isFunction(resolver)) { + return resolver(options, contexts, active, key, pathSegments); + } + return resolver.resolve(options, contexts, active, key, pathSegments); + }; + const { addSource, delSource } = createMergedObject(resolve, active); + // TODO: validate resolver + const manager = { + config, + install(app) { + app.component('Metainfo', Metainfo); + app.config.globalProperties.$metaManager = manager; + app.provide(metaInfoKey, active); + }, + addMeta(metaObj, vm) { + const resolveContext = { vm }; + if (resolver && 'setup' in resolver && isFunction(resolver.setup)) { + resolver.setup(resolveContext); + } + // TODO: optimize initial compute + const meta = addSource(metaObj, resolveContext, true); + const unmount = () => delSource(meta); + if (vm) { + vue.onUnmounted(unmount); + } + return { + meta, + unmount + }; + }, + render({ slots } = {}) { + const teleports = {}; + for (const key in active) { + const config = this.config[key] || {}; + const vnode = renderMeta({ metainfo: active, slots }, key, active[key], config); + if (!vnode) { + continue; + } + const vnodes = isArray(vnode) ? vnode : [vnode]; + const defaultTo = (key !== 'base' && active[key].to) || config.to || (config.attributesFor ? key : 'head'); + for (const { to, vnode } of vnodes) { + addVnode(teleports, to || defaultTo, vnode); + } + } + if (slots) { + for (const tag in slots) { + const slotFn = slots[tag]; + if (isFunction(slotFn)) { + addVnode(teleports, tag === 'default' ? 'head' : tag, slotFn({ metainfo: active })); + } + } + } + return Object.keys(teleports).map((to) => { + return vue.h(vue.Teleport, { to }, teleports[to]); + }); + } + }; + return manager; +} + +// rollup doesnt like an import, cant find export so use require +const { renderToString } = require('@vue/server-renderer'); +async function renderToStringWithMeta(app) { + const ctx = {}; + const html = await renderToString(app, ctx); + // TODO: better way of determining whether meta was rendered with the component or not + if (!ctx.teleports || !ctx.teleports.head) { + const teleports = app.config.globalProperties.$metaManager.render(); + await Promise.all(teleports.map((teleport) => renderToString(teleport, ctx))); + } + const { teleports } = ctx; + for (const target in teleports) { + if (target.endsWith('Attrs')) { + const str = teleports[target]; + // match from first space to first >, these should be all rendered attributes + teleports[target] = str.slice(str.indexOf(' ') + 1, str.indexOf('>')); + } + } + return [html, ctx]; +} + +exports.createMetaManager = createMetaManager; +exports.deepestResolver = deepest; +exports.defaultConfig = defaultConfig; +exports.getCurrentManager = getCurrentManager; +exports.renderToStringWithMeta = renderToStringWithMeta; +exports.resolveOption = resolveOption; +exports.useMeta = useMeta; +exports.useMetainfo = useMetainfo; diff --git a/dist/vue-meta.cjs.prod.js b/dist/vue-meta.cjs.prod.js new file mode 100644 index 00000000..52f30e09 --- /dev/null +++ b/dist/vue-meta.cjs.prod.js @@ -0,0 +1,797 @@ +/** + * vue-meta v3.0.0-alpha.0 + * (c) 2021 + * - Pim (@pimlie) + * - All the amazing contributors + * @license MIT + */ + +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +var vue = require('vue'); + +const resolveOption = predicament => (options, contexts) => { + let resolvedIndex = -1; + contexts.reduce((acc, context, index) => { + const retval = predicament(acc, context); + if (retval !== acc) { + resolvedIndex = index; + return retval; + } + return acc; + }, undefined); + if (resolvedIndex > -1) { + return options[resolvedIndex]; + } +}; + +function setup(context) { + let depth = 0; + if (context.vm) { + let { vm } = context; + do { + if (vm.parent) { + depth++; + vm = vm.parent; + } + } while (vm && vm.parent && vm !== vm.root); + } + context.depth = depth; +} +const resolve = resolveOption((acc, context) => { + const { depth } = context; + if (!acc || depth > acc) { + return acc; + } +}); + +var deepest = /*#__PURE__*/Object.freeze({ + __proto__: null, + setup: setup, + resolve: resolve +}); + +const defaultConfig = { + body: { + tag: 'script', + to: 'body' + }, + base: { + valueAttribute: 'href' + }, + charset: { + tag: 'meta', + nameless: true, + valueAttribute: 'charset' + }, + description: { + tag: 'meta' + }, + og: { + group: true, + namespacedAttribute: true, + tag: 'meta', + keyAttribute: 'property' + }, + twitter: { + group: true, + namespacedAttribute: true, + tag: 'meta' + }, + htmlAttrs: { + attributesFor: 'html' + }, + headAttrs: { + attributesFor: 'head' + }, + bodyAttrs: { + attributesFor: 'body' + } +}; + +/** + * Make a map and return a function for checking if a key + * is in that map. + * IMPORTANT: all calls of this function must be prefixed with + * \/\*#\_\_PURE\_\_\*\/ + * So that rollup can tree-shake them if necessary. + */ +(process.env.NODE_ENV !== 'production') + ? Object.freeze({}) + : {}; +(process.env.NODE_ENV !== 'production') ? Object.freeze([]) : []; +const isArray = Array.isArray; +const isFunction = (val) => typeof val === 'function'; +const isString = (val) => typeof val === 'string'; +const isObject = (val) => val !== null && typeof val === 'object'; +const objectToString = Object.prototype.toString; +const toTypeString = (value) => objectToString.call(value); +const isPlainObject = (val) => toTypeString(val) === '[object Object]'; + +/* + * This is a fixed config for real HTML tags + * + * TODO: we probably dont need all attributes + */ +const tags = { + title: { + attributes: false + }, + base: { + contentAsAttribute: true, + attributes: ['href', 'target'] + }, + meta: { + contentAsAttribute: true, + keyAttribute: 'name', + attributes: ['content', 'name', 'http-equiv', 'charset'] + }, + link: { + contentAsAttribute: true, + attributes: [ + 'href', + 'crossorigin', + 'rel', + 'media', + 'integrity', + 'hreflang', + 'type', + 'referrerpolicy', + 'sizes', + 'imagesrcset', + 'imagesizes', + 'as', + 'color' + ] + }, + style: { + attributes: ['media'] + }, + script: { + attributes: [ + 'src', + 'type', + 'nomodule', + 'async', + 'defer', + 'crossorigin', + 'integrity', + 'referrerpolicy' + ] + }, + noscript: { + attributes: false + } +}; + +function getConfigByKey(tagOrName, key, config) { + if (config && key in config) { + return config[key]; + } + if (isArray(tagOrName)) { + for (const name of tagOrName) { + if (name && name in tags) { + return tags[name][key]; + } + } + return; + } + if (tagOrName in tags) { + const tag = tags[tagOrName]; + return tag[key]; + } +} + +// https://github.com/microsoft/TypeScript/issues/1863 +const IS_PROXY = Symbol('kIsProxy'); +const PROXY_SOURCES = Symbol('kProxySources'); +const PROXY_TARGET = Symbol('kProxyTarget'); +const RESOLVE_CONTEXT = Symbol('kResolveContext'); + +// See: https://github.com/vuejs/vue-next/blob/08b4e8815da4e8911058ccbab986bea6365c3352/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts +function clone(v) { + if (isArray(v)) { + return v.map(clone); + } + if (isObject(v)) { + const res = {}; + for (const key in v) { + // never clone the context + if (key === 'context') { + res[key] = v[key]; + } + else { + res[key] = clone(v[key]); + } + } + return res; + } + return v; +} + +const pluck = (collection, key, callback) => { + const plucked = []; + for (const row of collection) { + if (key in row) { + plucked.push(row[key]); + if (callback) { + callback(row); + } + } + } + return plucked; +}; + +const allKeys = (source, ...sources) => { + const keys = source ? Object.keys(source) : []; + if (sources) { + for (const source of sources) { + if (!source || !isObject(source)) { + continue; + } + for (const key in source) { + if (!keys.includes(key)) { + keys.push(key); + } + } + } + } + // TODO: add check for consistent types for each key (dev only) + return keys; +}; +const recompute = (context, sources, target, path = []) => { + if (!path.length) { + if (!target) { + target = context.active; + } + if (!sources) { + sources = context.sources; + } + } + if (!target || !sources) { + return; + } + const keys = allKeys(...sources); + // Clean up properties that dont exists anymore + const targetKeys = Object.keys(target); + for (const key of targetKeys) { + if (!keys.includes(key)) { + delete target[key]; + } + } + for (const key of keys) { + // This assumes consistent types usages for keys across sources + if (isPlainObject(sources[0][key])) { + if (!target[key]) { + target[key] = {}; + } + const keySources = []; + for (const source of sources) { + if (key in source) { + keySources.push(source[key]); + } + } + recompute(context, keySources, target[key], [...path, key]); + continue; + } + // Ensure the target is an array if source is an array and target is empty + if (!target[key] && isArray(sources[0][key])) { + target[key] = []; + } + const keyContexts = []; + const keySources = pluck(sources, key, source => keyContexts.push(source[RESOLVE_CONTEXT])); + let resolved = context.resolve(keySources, keyContexts, target[key], key, path); + if (isPlainObject(resolved)) { + resolved = clone(resolved); + } + // console.log('RESOLVED', key, resolved, 'was', target[key]) + target[key] = resolved; + } +}; + +const createProxy = (context, target, resolveContext, pathSegments = []) => { + const handler = createHandler(context, resolveContext, pathSegments); + const proxy = vue.markRaw(new Proxy(target, handler)); + if (!pathSegments.length && context.sources) { + context.sources.push(proxy); + } + return proxy; +}; +const createHandler = (context, resolveContext, pathSegments = []) => ({ + get: (target, key, receiver) => { + if (key === IS_PROXY) { + return true; + } + if (key === PROXY_SOURCES) { + return context.sources; + } + if (key === PROXY_TARGET) { + return target; + } + if (key === RESOLVE_CONTEXT) { + return resolveContext; + } + let value = Reflect.get(target, key, receiver); + if (!isObject(value)) { + return value; + } + if (!value[IS_PROXY]) { + const keyPath = [...pathSegments, key]; + value = createProxy(context, value, resolveContext, keyPath); + target[key] = value; + } + return value; + }, + set: (target, key, value) => { + const success = Reflect.set(target, key, value); + // console.warn(success, 'PROXY SET\nkey:', key, '\npath:', pathSegments, '\ntarget:', isArray(target), target, '\ncontext:\n', context) + if (success) { + const isArrayItem = isArray(target); + let hasArrayParent = false; + let { sources: proxies, active } = context; + let activeSegmentKey; + let index = 0; + for (const segment of pathSegments) { + proxies = pluck(proxies, segment); + if (isArrayItem && index === pathSegments.length - 1) { + activeSegmentKey = segment; + break; + } + if (isArray(active)) { + hasArrayParent = true; + } + active = active[segment]; + index++; + } + if (hasArrayParent) { + // TODO: fix that we dont have to recompute the full merged object + // we should only have to recompute the branch that has changed + // but there is an issue here with supporting both arrays of strings + // as collections (parent vs parent of parent we need to trigger the + // update from) + recompute(context); + return success; + } + let keyContexts = []; + let keySources; + if (isArrayItem) { + keySources = proxies; + keyContexts = proxies.map(proxy => proxy[RESOLVE_CONTEXT]); + } + else { + keySources = pluck(proxies, key, proxy => keyContexts.push(proxy[RESOLVE_CONTEXT])); + } + let resolved = context.resolve(keySources, keyContexts, active, key, pathSegments); + // Ensure to clone if value is an object, cause sources is an array of + // the sourceProxies not the sources so we could trigger an endless loop when + // updating a prop on an obj as the prop on the active object refers to + // a prop on a proxy + if (isPlainObject(resolved)) { + resolved = clone(resolved); + } + // console.log('SET VALUE', isArrayItem, key, '\nresolved:\n', resolved, '\nsources:\n', context.sources, '\nactive:\n', active, Object.keys(active)) + if (isArrayItem && activeSegmentKey) { + active[activeSegmentKey] = resolved; + } + else { + active[key] = resolved; + } + } + // console.log('CONTEXT.ACTIVE', context.active, '\nparent:\n', target) + return success; + }, + deleteProperty: (target, key) => { + const success = Reflect.deleteProperty(target, key); + // console.warn('PROXY DELETE\nkey:', key, '\npath:', pathSegments, '\nparent:', isArray(target), target) + if (success) { + const isArrayItem = isArray(target); + let activeSegmentKey; + let proxies = context.sources; + let active = context.active; + let index = 0; + for (const segment of pathSegments) { + proxies = proxies.map(proxy => proxy[segment]); + if (isArrayItem && index === pathSegments.length - 1) { + activeSegmentKey = segment; + break; + } + active = active[segment]; + index++; + } + // Check if the key still exists in one of the sourceProxies, + // if so resolve the new value, if not remove the key + if (proxies.some(proxy => (key in proxy))) { + let keyContexts = []; + let keySources; + if (isArrayItem) { + keySources = proxies; + keyContexts = proxies.map(proxy => proxy[RESOLVE_CONTEXT]); + } + else { + keySources = pluck(proxies, key, proxy => keyContexts.push(proxy[RESOLVE_CONTEXT])); + } + let resolved = context.resolve(keySources, keyContexts, active, key, pathSegments); + if (isPlainObject(resolved)) { + resolved = clone(resolved); + } + // console.log('SET VALUE', resolved) + if (isArrayItem && activeSegmentKey) { + active[activeSegmentKey] = resolved; + } + else { + active[key] = resolved; + } + } + else { + delete active[key]; + } + } + return success; + } +}); + +const createMergedObject = (resolve, active = {}) => { + const sources = []; + if (!active) { + active = {}; + } + const context = { + active, + resolve, + sources + }; + const compute = () => recompute(context); + const addSource = (source, resolveContext, recompute = false) => { + const proxy = createProxy(context, source, resolveContext || {}); + if (recompute) { + compute(); + } + return proxy; + }; + const delSource = (sourceOrProxy, recompute = true) => { + const index = sources.findIndex(src => src === sourceOrProxy || src[PROXY_TARGET] === sourceOrProxy); + if (index > -1) { + sources.splice(index, 1); + if (recompute) { + compute(); + } + return true; + } + return false; + }; + return { + context, + active, + resolve, + sources, + addSource, + delSource, + compute + }; +}; + +function renderMeta(context, key, data, config) { + // console.info('renderMeta', key, data, config) + if (config.attributesFor) { + return renderAttributes(context, key, data, config); + } + if (config.group) { + return renderGroup(context, key, data, config); + } + return renderTag(context, key, data, config); +} +function renderGroup(context, key, data, config) { + // console.info('renderGroup', key, data, config) + if (isArray(data)) { + // config.attributes = getConfigKey([key, config.tag], 'attributes', config) + return []; + } + return Object.keys(data) + .map((childKey) => { + const groupConfig = { + group: key, + data + }; + if (config.namespaced) { + groupConfig.tagNamespace = config.namespaced === true ? key : config.namespaced; + } + else if (config.namespacedAttribute) { + const namespace = config.namespacedAttribute === true ? key : config.namespacedAttribute; + groupConfig.fullName = `${namespace}:${childKey}`; + groupConfig.slotName = `${namespace}(${childKey})`; + } + return renderTag(context, key, data[childKey], config, groupConfig); + }) + .flat(); +} +function renderTag(context, key, data, config = {}, groupConfig) { + // console.info('renderTag', key, data, config, groupConfig) + const contentAttributes = ['content', 'json', 'rawContent']; + const getConfig = (key) => getConfigByKey([tag, config.tag], key, config); + if (isArray(data)) { + return data + .map((child) => { + return renderTag(context, key, child, config, groupConfig); + }) + .flat(); + } + const { tag = config.tag || key } = data; + let content; + let hasChilds = false; + let isRaw = false; + if (isString(data)) { + content = data; + } + else if (data.children && isArray(data.children)) { + hasChilds = true; + content = data.children.map((child) => { + const data = renderTag(context, key, child, config, groupConfig); + if (isArray(data)) { + return data.map(({ vnode }) => vnode); + } + return data.vnode; + }); + } + else { + let i = 0; + for (const contentAttribute of contentAttributes) { + if (!content && data[contentAttribute]) { + if (i === 1) { + content = JSON.stringify(data[contentAttribute]); + } + else { + content = data[contentAttribute]; + } + isRaw = i > 1; + break; + } + i++; + } + } + const fullName = (groupConfig && groupConfig.fullName) || key; + const slotName = (groupConfig && groupConfig.slotName) || key; + let { attrs: attributes } = data; + if (!attributes && typeof data === 'object') { + attributes = { ...data }; + delete attributes.tag; + delete attributes.children; + delete attributes.to; + // cleanup all content attributes + for (const attr of contentAttributes) { + delete attributes[attr]; + } + } + else if (!attributes) { + attributes = {}; + } + if (hasChilds) { + content = getSlotContent(context, slotName, content, data); + } + else { + const contentAsAttribute = getConfig('contentAsAttribute'); + let valueAttribute = config.valueAttribute; + if (!valueAttribute && contentAsAttribute) { + const tagAttributes = getConfig('attributes'); + valueAttribute = isString(contentAsAttribute) ? contentAsAttribute : tagAttributes[0]; + } + if (!valueAttribute) { + content = getSlotContent(context, slotName, content, data); + } + else { + if (!config.nameless) { + const keyAttribute = getConfig('keyAttribute'); + if (keyAttribute) { + attributes[keyAttribute] = fullName; + } + } + attributes[valueAttribute] = getSlotContent(context, slotName, attributes[valueAttribute] || content, groupConfig); + content = undefined; + } + } + const finalTag = groupConfig && groupConfig.tagNamespace + ? `${groupConfig.tagNamespace}:${tag}` + : tag; + // console.info('FINAL TAG', finalTag) + // console.log(' ATTRIBUTES', attributes) + // console.log(' CONTENT', content) + // // console.log(data, attributes, config) + let vnode; + if (isRaw) { + attributes.innerHTML = content; + vnode = vue.h(finalTag, attributes); + } + else { + vnode = vue.h(finalTag, attributes, content); + } + return { + to: data.to, + vnode + }; +} +function renderAttributes(context, key, data, config = {}) { + // console.info('renderAttributes', key, data, config) + const { attributesFor } = config; + { + // render attributes in a placeholder vnode so Vue + // will render the string for us + return { + to: '', + vnode: vue.h(`ssr-${attributesFor}`, data) + }; + } +} +function getSlotContent({ metainfo, slots }, slotName, content, groupConfig) { + if (!slots || !slots[slotName]) { + return content; + } + const slotProps = { + content, + metainfo + }; + if (groupConfig && groupConfig.group) { + slotProps[groupConfig.group] = groupConfig.data; + } + const slotContent = slots[slotName](slotProps); + if (slotContent && slotContent.length) { + return slotContent[0].children; + } + return content; +} + +const hasSymbol = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol'; +const PolySymbol = (name) => +// vm = vue meta +hasSymbol + ? Symbol( name) + : ( '_vm_') + name; +const metaInfoKey = PolySymbol( 'mi'); + +function getCurrentManager(vm) { + if (!vm) { + vm = vue.getCurrentInstance(); + } + return vm.appContext.config.globalProperties.$metaManager; +} +function useMeta(obj, manager) { + const vm = vue.getCurrentInstance(); + if (!manager && vm) { + manager = getCurrentManager(vm); + } + if (!manager) { + // oopsydoopsy + throw new Error('No manager or current instance'); + } + return manager.addMeta(obj, vm || undefined); +} +function useMetainfo() { + return vue.inject(metaInfoKey); +} + +const MetainfoImpl = vue.defineComponent({ + name: 'Metainfo', + inheritAttrs: false, + setup(_, { slots }) { + return () => { + const manager = getCurrentManager(); + if (!manager) { + return; + } + return manager.render({ slots }); + }; + } +}); +const Metainfo = MetainfoImpl; + +const ssrAttribute = 'data-vm-ssr'; +const active = vue.reactive({}); +function addVnode(teleports, to, _vnodes) { + const vnodes = (isArray(_vnodes) ? _vnodes : [_vnodes]); + { + // dont add ssrAttribute for attribute vnode placeholder + if (!to.endsWith('Attrs')) { + vnodes.forEach((vnode) => { + if (!vnode.props) { + vnode.props = {}; + } + vnode.props[ssrAttribute] = true; + }); + } + } + if (!teleports[to]) { + teleports[to] = []; + } + teleports[to].push(...vnodes); +} +function createMetaManager(config, resolver) { + const resolve = (options, contexts, active, key, pathSegments) => { + if (isFunction(resolver)) { + return resolver(options, contexts, active, key, pathSegments); + } + return resolver.resolve(options, contexts, active, key, pathSegments); + }; + const { addSource, delSource } = createMergedObject(resolve, active); + // TODO: validate resolver + const manager = { + config, + install(app) { + app.component('Metainfo', Metainfo); + app.config.globalProperties.$metaManager = manager; + app.provide(metaInfoKey, active); + }, + addMeta(metaObj, vm) { + const resolveContext = { vm }; + if (resolver && 'setup' in resolver && isFunction(resolver.setup)) { + resolver.setup(resolveContext); + } + // TODO: optimize initial compute + const meta = addSource(metaObj, resolveContext, true); + const unmount = () => delSource(meta); + if (vm) { + vue.onUnmounted(unmount); + } + return { + meta, + unmount + }; + }, + render({ slots } = {}) { + const teleports = {}; + for (const key in active) { + const config = this.config[key] || {}; + const vnode = renderMeta({ metainfo: active, slots }, key, active[key], config); + if (!vnode) { + continue; + } + const vnodes = isArray(vnode) ? vnode : [vnode]; + const defaultTo = (key !== 'base' && active[key].to) || config.to || (config.attributesFor ? key : 'head'); + for (const { to, vnode } of vnodes) { + addVnode(teleports, to || defaultTo, vnode); + } + } + if (slots) { + for (const tag in slots) { + const slotFn = slots[tag]; + if (isFunction(slotFn)) { + addVnode(teleports, tag === 'default' ? 'head' : tag, slotFn({ metainfo: active })); + } + } + } + return Object.keys(teleports).map((to) => { + return vue.h(vue.Teleport, { to }, teleports[to]); + }); + } + }; + return manager; +} + +// rollup doesnt like an import, cant find export so use require +const { renderToString } = require('@vue/server-renderer'); +async function renderToStringWithMeta(app) { + const ctx = {}; + const html = await renderToString(app, ctx); + // TODO: better way of determining whether meta was rendered with the component or not + if (!ctx.teleports || !ctx.teleports.head) { + const teleports = app.config.globalProperties.$metaManager.render(); + await Promise.all(teleports.map((teleport) => renderToString(teleport, ctx))); + } + const { teleports } = ctx; + for (const target in teleports) { + if (target.endsWith('Attrs')) { + const str = teleports[target]; + // match from first space to first >, these should be all rendered attributes + teleports[target] = str.slice(str.indexOf(' ') + 1, str.indexOf('>')); + } + } + return [html, ctx]; +} + +exports.createMetaManager = createMetaManager; +exports.deepestResolver = deepest; +exports.defaultConfig = defaultConfig; +exports.getCurrentManager = getCurrentManager; +exports.renderToStringWithMeta = renderToStringWithMeta; +exports.resolveOption = resolveOption; +exports.useMeta = useMeta; +exports.useMetainfo = useMetainfo; diff --git a/dist/vue-meta.esm-browser.js b/dist/vue-meta.esm-browser.js new file mode 100644 index 00000000..cfbaf714 --- /dev/null +++ b/dist/vue-meta.esm-browser.js @@ -0,0 +1,801 @@ +/** + * vue-meta v3.0.0-alpha.0 + * (c) 2021 + * - Pim (@pimlie) + * - All the amazing contributors + * @license MIT + */ + +import { markRaw, h, getCurrentInstance, inject, defineComponent, reactive, onUnmounted, Teleport, Comment } from 'vue'; + +const resolveOption = predicament => (options, contexts) => { + let resolvedIndex = -1; + contexts.reduce((acc, context, index) => { + const retval = predicament(acc, context); + if (retval !== acc) { + resolvedIndex = index; + return retval; + } + return acc; + }, undefined); + if (resolvedIndex > -1) { + return options[resolvedIndex]; + } +}; + +function setup(context) { + let depth = 0; + if (context.vm) { + let { vm } = context; + do { + if (vm.parent) { + depth++; + vm = vm.parent; + } + } while (vm && vm.parent && vm !== vm.root); + } + context.depth = depth; +} +const resolve = resolveOption((acc, context) => { + const { depth } = context; + if (!acc || depth > acc) { + return acc; + } +}); + +var deepest = /*#__PURE__*/Object.freeze({ + __proto__: null, + setup: setup, + resolve: resolve +}); + +const defaultConfig = { + body: { + tag: 'script', + to: 'body' + }, + base: { + valueAttribute: 'href' + }, + charset: { + tag: 'meta', + nameless: true, + valueAttribute: 'charset' + }, + description: { + tag: 'meta' + }, + og: { + group: true, + namespacedAttribute: true, + tag: 'meta', + keyAttribute: 'property' + }, + twitter: { + group: true, + namespacedAttribute: true, + tag: 'meta' + }, + htmlAttrs: { + attributesFor: 'html' + }, + headAttrs: { + attributesFor: 'head' + }, + bodyAttrs: { + attributesFor: 'body' + } +}; + +/** + * Make a map and return a function for checking if a key + * is in that map. + * IMPORTANT: all calls of this function must be prefixed with + * \/\*#\_\_PURE\_\_\*\/ + * So that rollup can tree-shake them if necessary. + */ +(process.env.NODE_ENV !== 'production') + ? Object.freeze({}) + : {}; +(process.env.NODE_ENV !== 'production') ? Object.freeze([]) : []; +const isArray = Array.isArray; +const isFunction = (val) => typeof val === 'function'; +const isString = (val) => typeof val === 'string'; +const isObject = (val) => val !== null && typeof val === 'object'; +const objectToString = Object.prototype.toString; +const toTypeString = (value) => objectToString.call(value); +const isPlainObject = (val) => toTypeString(val) === '[object Object]'; + +/* + * This is a fixed config for real HTML tags + * + * TODO: we probably dont need all attributes + */ +const tags = { + title: { + attributes: false + }, + base: { + contentAsAttribute: true, + attributes: ['href', 'target'] + }, + meta: { + contentAsAttribute: true, + keyAttribute: 'name', + attributes: ['content', 'name', 'http-equiv', 'charset'] + }, + link: { + contentAsAttribute: true, + attributes: [ + 'href', + 'crossorigin', + 'rel', + 'media', + 'integrity', + 'hreflang', + 'type', + 'referrerpolicy', + 'sizes', + 'imagesrcset', + 'imagesizes', + 'as', + 'color' + ] + }, + style: { + attributes: ['media'] + }, + script: { + attributes: [ + 'src', + 'type', + 'nomodule', + 'async', + 'defer', + 'crossorigin', + 'integrity', + 'referrerpolicy' + ] + }, + noscript: { + attributes: false + } +}; + +function getConfigByKey(tagOrName, key, config) { + if (config && key in config) { + return config[key]; + } + if (isArray(tagOrName)) { + for (const name of tagOrName) { + if (name && name in tags) { + return tags[name][key]; + } + } + return; + } + if (tagOrName in tags) { + const tag = tags[tagOrName]; + return tag[key]; + } +} + +// https://github.com/microsoft/TypeScript/issues/1863 +const IS_PROXY = Symbol('kIsProxy'); +const PROXY_SOURCES = Symbol('kProxySources'); +const PROXY_TARGET = Symbol('kProxyTarget'); +const RESOLVE_CONTEXT = Symbol('kResolveContext'); + +// See: https://github.com/vuejs/vue-next/blob/08b4e8815da4e8911058ccbab986bea6365c3352/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts +function clone(v) { + if (isArray(v)) { + return v.map(clone); + } + if (isObject(v)) { + const res = {}; + for (const key in v) { + // never clone the context + if (key === 'context') { + res[key] = v[key]; + } + else { + res[key] = clone(v[key]); + } + } + return res; + } + return v; +} + +const pluck = (collection, key, callback) => { + const plucked = []; + for (const row of collection) { + if (key in row) { + plucked.push(row[key]); + if (callback) { + callback(row); + } + } + } + return plucked; +}; + +const allKeys = (source, ...sources) => { + const keys = source ? Object.keys(source) : []; + if (sources) { + for (const source of sources) { + if (!source || !isObject(source)) { + continue; + } + for (const key in source) { + if (!keys.includes(key)) { + keys.push(key); + } + } + } + } + // TODO: add check for consistent types for each key (dev only) + return keys; +}; +const recompute = (context, sources, target, path = []) => { + if (!path.length) { + if (!target) { + target = context.active; + } + if (!sources) { + sources = context.sources; + } + } + if (!target || !sources) { + return; + } + const keys = allKeys(...sources); + // Clean up properties that dont exists anymore + const targetKeys = Object.keys(target); + for (const key of targetKeys) { + if (!keys.includes(key)) { + delete target[key]; + } + } + for (const key of keys) { + // This assumes consistent types usages for keys across sources + if (isPlainObject(sources[0][key])) { + if (!target[key]) { + target[key] = {}; + } + const keySources = []; + for (const source of sources) { + if (key in source) { + keySources.push(source[key]); + } + } + recompute(context, keySources, target[key], [...path, key]); + continue; + } + // Ensure the target is an array if source is an array and target is empty + if (!target[key] && isArray(sources[0][key])) { + target[key] = []; + } + const keyContexts = []; + const keySources = pluck(sources, key, source => keyContexts.push(source[RESOLVE_CONTEXT])); + let resolved = context.resolve(keySources, keyContexts, target[key], key, path); + if (isPlainObject(resolved)) { + resolved = clone(resolved); + } + // console.log('RESOLVED', key, resolved, 'was', target[key]) + target[key] = resolved; + } +}; + +const createProxy = (context, target, resolveContext, pathSegments = []) => { + const handler = createHandler(context, resolveContext, pathSegments); + const proxy = markRaw(new Proxy(target, handler)); + if (!pathSegments.length && context.sources) { + context.sources.push(proxy); + } + return proxy; +}; +const createHandler = (context, resolveContext, pathSegments = []) => ({ + get: (target, key, receiver) => { + if (key === IS_PROXY) { + return true; + } + if (key === PROXY_SOURCES) { + return context.sources; + } + if (key === PROXY_TARGET) { + return target; + } + if (key === RESOLVE_CONTEXT) { + return resolveContext; + } + let value = Reflect.get(target, key, receiver); + if (!isObject(value)) { + return value; + } + if (!value[IS_PROXY]) { + const keyPath = [...pathSegments, key]; + value = createProxy(context, value, resolveContext, keyPath); + target[key] = value; + } + return value; + }, + set: (target, key, value) => { + const success = Reflect.set(target, key, value); + // console.warn(success, 'PROXY SET\nkey:', key, '\npath:', pathSegments, '\ntarget:', isArray(target), target, '\ncontext:\n', context) + if (success) { + const isArrayItem = isArray(target); + let hasArrayParent = false; + let { sources: proxies, active } = context; + let activeSegmentKey; + let index = 0; + for (const segment of pathSegments) { + proxies = pluck(proxies, segment); + if (isArrayItem && index === pathSegments.length - 1) { + activeSegmentKey = segment; + break; + } + if (isArray(active)) { + hasArrayParent = true; + } + active = active[segment]; + index++; + } + if (hasArrayParent) { + // TODO: fix that we dont have to recompute the full merged object + // we should only have to recompute the branch that has changed + // but there is an issue here with supporting both arrays of strings + // as collections (parent vs parent of parent we need to trigger the + // update from) + recompute(context); + return success; + } + let keyContexts = []; + let keySources; + if (isArrayItem) { + keySources = proxies; + keyContexts = proxies.map(proxy => proxy[RESOLVE_CONTEXT]); + } + else { + keySources = pluck(proxies, key, proxy => keyContexts.push(proxy[RESOLVE_CONTEXT])); + } + let resolved = context.resolve(keySources, keyContexts, active, key, pathSegments); + // Ensure to clone if value is an object, cause sources is an array of + // the sourceProxies not the sources so we could trigger an endless loop when + // updating a prop on an obj as the prop on the active object refers to + // a prop on a proxy + if (isPlainObject(resolved)) { + resolved = clone(resolved); + } + // console.log('SET VALUE', isArrayItem, key, '\nresolved:\n', resolved, '\nsources:\n', context.sources, '\nactive:\n', active, Object.keys(active)) + if (isArrayItem && activeSegmentKey) { + active[activeSegmentKey] = resolved; + } + else { + active[key] = resolved; + } + } + // console.log('CONTEXT.ACTIVE', context.active, '\nparent:\n', target) + return success; + }, + deleteProperty: (target, key) => { + const success = Reflect.deleteProperty(target, key); + // console.warn('PROXY DELETE\nkey:', key, '\npath:', pathSegments, '\nparent:', isArray(target), target) + if (success) { + const isArrayItem = isArray(target); + let activeSegmentKey; + let proxies = context.sources; + let active = context.active; + let index = 0; + for (const segment of pathSegments) { + proxies = proxies.map(proxy => proxy[segment]); + if (isArrayItem && index === pathSegments.length - 1) { + activeSegmentKey = segment; + break; + } + active = active[segment]; + index++; + } + // Check if the key still exists in one of the sourceProxies, + // if so resolve the new value, if not remove the key + if (proxies.some(proxy => (key in proxy))) { + let keyContexts = []; + let keySources; + if (isArrayItem) { + keySources = proxies; + keyContexts = proxies.map(proxy => proxy[RESOLVE_CONTEXT]); + } + else { + keySources = pluck(proxies, key, proxy => keyContexts.push(proxy[RESOLVE_CONTEXT])); + } + let resolved = context.resolve(keySources, keyContexts, active, key, pathSegments); + if (isPlainObject(resolved)) { + resolved = clone(resolved); + } + // console.log('SET VALUE', resolved) + if (isArrayItem && activeSegmentKey) { + active[activeSegmentKey] = resolved; + } + else { + active[key] = resolved; + } + } + else { + delete active[key]; + } + } + return success; + } +}); + +const createMergedObject = (resolve, active = {}) => { + const sources = []; + if (!active) { + active = {}; + } + const context = { + active, + resolve, + sources + }; + const compute = () => recompute(context); + const addSource = (source, resolveContext, recompute = false) => { + const proxy = createProxy(context, source, resolveContext || {}); + if (recompute) { + compute(); + } + return proxy; + }; + const delSource = (sourceOrProxy, recompute = true) => { + const index = sources.findIndex(src => src === sourceOrProxy || src[PROXY_TARGET] === sourceOrProxy); + if (index > -1) { + sources.splice(index, 1); + if (recompute) { + compute(); + } + return true; + } + return false; + }; + return { + context, + active, + resolve, + sources, + addSource, + delSource, + compute + }; +}; + +const cachedElements = {}; +function renderMeta(context, key, data, config) { + // console.info('renderMeta', key, data, config) + if (config.attributesFor) { + return renderAttributes(context, key, data, config); + } + if (config.group) { + return renderGroup(context, key, data, config); + } + return renderTag(context, key, data, config); +} +function renderGroup(context, key, data, config) { + // console.info('renderGroup', key, data, config) + if (isArray(data)) { + { + // eslint-disable-next-line no-console + console.warn('Specifying an array for group properties isnt supported mostly as we didnt found a use-case for this yet. If you have one, please create an issue on the vue-meta repo'); + } + // config.attributes = getConfigKey([key, config.tag], 'attributes', config) + return []; + } + return Object.keys(data) + .map((childKey) => { + const groupConfig = { + group: key, + data + }; + if (config.namespaced) { + groupConfig.tagNamespace = config.namespaced === true ? key : config.namespaced; + } + else if (config.namespacedAttribute) { + const namespace = config.namespacedAttribute === true ? key : config.namespacedAttribute; + groupConfig.fullName = `${namespace}:${childKey}`; + groupConfig.slotName = `${namespace}(${childKey})`; + } + return renderTag(context, key, data[childKey], config, groupConfig); + }) + .flat(); +} +function renderTag(context, key, data, config = {}, groupConfig) { + // console.info('renderTag', key, data, config, groupConfig) + const contentAttributes = ['content', 'json', 'rawContent']; + const getConfig = (key) => getConfigByKey([tag, config.tag], key, config); + if (isArray(data)) { + return data + .map((child) => { + return renderTag(context, key, child, config, groupConfig); + }) + .flat(); + } + const { tag = config.tag || key } = data; + let content; + let hasChilds = false; + let isRaw = false; + if (isString(data)) { + content = data; + } + else if (data.children && isArray(data.children)) { + hasChilds = true; + content = data.children.map((child) => { + const data = renderTag(context, key, child, config, groupConfig); + if (isArray(data)) { + return data.map(({ vnode }) => vnode); + } + return data.vnode; + }); + } + else { + let i = 0; + for (const contentAttribute of contentAttributes) { + if (!content && data[contentAttribute]) { + if (i === 1) { + content = JSON.stringify(data[contentAttribute]); + } + else { + content = data[contentAttribute]; + } + isRaw = i > 1; + break; + } + i++; + } + } + const fullName = (groupConfig && groupConfig.fullName) || key; + const slotName = (groupConfig && groupConfig.slotName) || key; + let { attrs: attributes } = data; + if (!attributes && typeof data === 'object') { + attributes = { ...data }; + delete attributes.tag; + delete attributes.children; + delete attributes.to; + // cleanup all content attributes + for (const attr of contentAttributes) { + delete attributes[attr]; + } + } + else if (!attributes) { + attributes = {}; + } + if (hasChilds) { + content = getSlotContent(context, slotName, content, data); + } + else { + const contentAsAttribute = getConfig('contentAsAttribute'); + let valueAttribute = config.valueAttribute; + if (!valueAttribute && contentAsAttribute) { + const tagAttributes = getConfig('attributes'); + valueAttribute = isString(contentAsAttribute) ? contentAsAttribute : tagAttributes[0]; + } + if (!valueAttribute) { + content = getSlotContent(context, slotName, content, data); + } + else { + if (!config.nameless) { + const keyAttribute = getConfig('keyAttribute'); + if (keyAttribute) { + attributes[keyAttribute] = fullName; + } + } + attributes[valueAttribute] = getSlotContent(context, slotName, attributes[valueAttribute] || content, groupConfig); + content = undefined; + } + } + const finalTag = groupConfig && groupConfig.tagNamespace + ? `${groupConfig.tagNamespace}:${tag}` + : tag; + // console.info('FINAL TAG', finalTag) + // console.log(' ATTRIBUTES', attributes) + // console.log(' CONTENT', content) + // // console.log(data, attributes, config) + let vnode; + if (isRaw) { + attributes.innerHTML = content; + vnode = h(finalTag, attributes); + } + else { + vnode = h(finalTag, attributes, content); + } + return { + to: data.to, + vnode + }; +} +function renderAttributes(context, key, data, config = {}) { + // console.info('renderAttributes', key, data, config) + const { attributesFor } = config; + if (!cachedElements[attributesFor]) { + const [el, el2] = Array.from(document.querySelectorAll(attributesFor)); + if ( !el) { + // eslint-disable-next-line no-console + console.error('Could not find element with selector', attributesFor, ', won\'t render attributes'); + return; + } + if ( el2) { + // eslint-disable-next-line no-console + console.warn('Found multiple elements with selector', attributesFor); + } + cachedElements[attributesFor] = { + el, + attrs: [] + }; + } + const { el, attrs } = cachedElements[attributesFor]; + for (const attr in data) { + const content = getSlotContent(context, `${key}(${attr})`, data[attr], data); + el.setAttribute(attr, `${content || ''}`); + if (!attrs.includes(attr)) { + attrs.push(attr); + } + } + const attrsToRemove = attrs.filter(attr => !data[attr]); + for (const attr of attrsToRemove) { + el.removeAttribute(attr); + } +} +function getSlotContent({ metainfo, slots }, slotName, content, groupConfig) { + if (!slots || !slots[slotName]) { + return content; + } + const slotProps = { + content, + metainfo + }; + if (groupConfig && groupConfig.group) { + slotProps[groupConfig.group] = groupConfig.data; + } + const slotContent = slots[slotName](slotProps); + if (slotContent && slotContent.length) { + return slotContent[0].children; + } + return content; +} + +const hasSymbol = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol'; +const PolySymbol = (name) => +// vm = vue meta +hasSymbol + ? Symbol( '[vue-meta]: ' + name ) + : ( '[vue-meta]: ' ) + name; +const metaInfoKey = PolySymbol( 'metainfo' ); + +function getCurrentManager(vm) { + if (!vm) { + vm = getCurrentInstance(); + } + return vm.appContext.config.globalProperties.$metaManager; +} +function useMeta(obj, manager) { + const vm = getCurrentInstance(); + if (!manager && vm) { + manager = getCurrentManager(vm); + } + if (!manager) { + // oopsydoopsy + throw new Error('No manager or current instance'); + } + return manager.addMeta(obj, vm || undefined); +} +function useMetainfo() { + return inject(metaInfoKey); +} + +const MetainfoImpl = defineComponent({ + name: 'Metainfo', + inheritAttrs: false, + setup(_, { slots }) { + return () => { + const manager = getCurrentManager(); + if (!manager) { + return; + } + return manager.render({ slots }); + }; + } +}); +const Metainfo = MetainfoImpl; + +const ssrAttribute = 'data-vm-ssr'; +const active = reactive({}); +function addVnode(teleports, to, _vnodes) { + const vnodes = (isArray(_vnodes) ? _vnodes : [_vnodes]); + { + // Comments shouldnt have any use on the client as they are not reactive anyway + vnodes.forEach((vnode, idx) => { + if (vnode.type === Comment) { + vnodes.splice(idx, 1); + } + }); + } + if (!teleports[to]) { + teleports[to] = []; + } + teleports[to].push(...vnodes); +} +function createMetaManager(config, resolver) { + const resolve = (options, contexts, active, key, pathSegments) => { + if (isFunction(resolver)) { + return resolver(options, contexts, active, key, pathSegments); + } + return resolver.resolve(options, contexts, active, key, pathSegments); + }; + const { addSource, delSource } = createMergedObject(resolve, active); + let cleanedUpSsr = false; + // TODO: validate resolver + const manager = { + config, + install(app) { + app.component('Metainfo', Metainfo); + app.config.globalProperties.$metaManager = manager; + app.provide(metaInfoKey, active); + }, + addMeta(metaObj, vm) { + const resolveContext = { vm }; + if (resolver && 'setup' in resolver && isFunction(resolver.setup)) { + resolver.setup(resolveContext); + } + // TODO: optimize initial compute + const meta = addSource(metaObj, resolveContext, true); + const unmount = () => delSource(meta); + if (vm) { + onUnmounted(unmount); + } + return { + meta, + unmount + }; + }, + render({ slots } = {}) { + // cleanup ssr tags if not yet done + if ( !cleanedUpSsr) { + cleanedUpSsr = true; + // Listen for DOM loaded because tags in the body couldnt be loaded + // yet once the manager does it first render + // (preferable there should only be one render on hydration) + window.addEventListener('DOMContentLoaded', () => { + const ssrTags = document.querySelectorAll(`[${ssrAttribute}]`); + if (ssrTags && ssrTags.length) { + Array.from(ssrTags).forEach(el => el.parentNode && el.parentNode.removeChild(el)); + } + }); + } + const teleports = {}; + for (const key in active) { + const config = this.config[key] || {}; + const vnode = renderMeta({ metainfo: active, slots }, key, active[key], config); + if (!vnode) { + continue; + } + const vnodes = isArray(vnode) ? vnode : [vnode]; + const defaultTo = (key !== 'base' && active[key].to) || config.to || (config.attributesFor ? key : 'head'); + for (const { to, vnode } of vnodes) { + addVnode(teleports, to || defaultTo, vnode); + } + } + if (slots) { + for (const tag in slots) { + const slotFn = slots[tag]; + if (isFunction(slotFn)) { + addVnode(teleports, tag === 'default' ? 'head' : tag, slotFn({ metainfo: active })); + } + } + } + return Object.keys(teleports).map((to) => { + return h(Teleport, { to }, teleports[to]); + }); + } + }; + return manager; +} + +export { createMetaManager, deepest as deepestResolver, defaultConfig, getCurrentManager, resolveOption, useMeta, useMetainfo }; diff --git a/dist/vue-meta.esm-browser.min.js b/dist/vue-meta.esm-browser.min.js new file mode 100644 index 00000000..bb57e94e --- /dev/null +++ b/dist/vue-meta.esm-browser.min.js @@ -0,0 +1,8 @@ +/** + * vue-meta v3.0.0-alpha.0 + * (c) 2021 + * - Pim (@pimlie) + * - All the amazing contributors + * @license MIT + */ +import{markRaw as t,h as e,getCurrentInstance as o,inject as r,defineComponent as n,reactive as s,onUnmounted as c,Teleport as i,Comment as a}from"vue";const u=t=>(e,o)=>{let r=-1;if(o.reduce(((e,o,n)=>{const s=t(e,o);return s!==e?(r=n,s):e}),void 0),r>-1)return e[r]};const f=u(((t,e)=>{const{depth:o}=e;if(!t||o>t)return t}));var l=Object.freeze({__proto__:null,setup:function(t){let e=0;if(t.vm){let{vm:o}=t;do{o.parent&&(e++,o=o.parent)}while(o&&o.parent&&o!==o.root)}t.depth=e},resolve:f});const p={body:{tag:"script",to:"body"},base:{valueAttribute:"href"},charset:{tag:"meta",nameless:!0,valueAttribute:"charset"},description:{tag:"meta"},og:{group:!0,namespacedAttribute:!0,tag:"meta",keyAttribute:"property"},twitter:{group:!0,namespacedAttribute:!0,tag:"meta"},htmlAttrs:{attributesFor:"html"},headAttrs:{attributesFor:"head"},bodyAttrs:{attributesFor:"body"}};"production"===process.env.NODE_ENV||Object.freeze({}),"production"===process.env.NODE_ENV||Object.freeze([]);const d=Array.isArray,m=t=>"function"==typeof t,b=t=>"string"==typeof t,y=t=>null!==t&&"object"==typeof t,g=Object.prototype.toString,h=t=>"[object Object]"===g.call(t),v={title:{attributes:!1},base:{contentAsAttribute:!0,attributes:["href","target"]},meta:{contentAsAttribute:!0,keyAttribute:"name",attributes:["content","name","http-equiv","charset"]},link:{contentAsAttribute:!0,attributes:["href","crossorigin","rel","media","integrity","hreflang","type","referrerpolicy","sizes","imagesrcset","imagesizes","as","color"]},style:{attributes:["media"]},script:{attributes:["src","type","nomodule","async","defer","crossorigin","integrity","referrerpolicy"]},noscript:{attributes:!1}};const A=Symbol("kIsProxy"),S=Symbol("kProxySources"),k=Symbol("kProxyTarget"),N=Symbol("kResolveContext");function w(t){if(d(t))return t.map(w);if(y(t)){const e={};for(const o in t)e[o]="context"===o?t[o]:w(t[o]);return e}return t}const j=(t,e,o)=>{const r=[];for(const n of t)e in n&&(r.push(n[e]),o&&o(n));return r},O=(t,e,o,r=[])=>{if(r.length||(o||(o=t.active),e||(e=t.sources)),!o||!e)return;const n=((t,...e)=>{const o=t?Object.keys(t):[];if(e)for(const t of e)if(t&&y(t))for(const e in t)o.includes(e)||o.push(e);return o})(...e),s=Object.keys(o);for(const t of s)n.includes(t)||delete o[t];for(const s of n){if(h(e[0][s])){o[s]||(o[s]={});const n=[];for(const t of e)s in t&&n.push(t[s]);O(t,n,o[s],[...r,s]);continue}!o[s]&&d(e[0][s])&&(o[s]=[]);const n=[],c=j(e,s,(t=>n.push(t[N])));let i=t.resolve(c,n,o[s],s,r);h(i)&&(i=w(i)),o[s]=i}},$=(e,o,r,n=[])=>{const s=x(e,r,n),c=t(new Proxy(o,s));return!n.length&&e.sources&&e.sources.push(c),c},x=(t,e,o=[])=>({get:(r,n,s)=>{if(n===A)return!0;if(n===S)return t.sources;if(n===k)return r;if(n===N)return e;let c=Reflect.get(r,n,s);if(!y(c))return c;if(!c[A]){const s=[...o,n];c=$(t,c,e,s),r[n]=c}return c},set:(e,r,n)=>{const s=Reflect.set(e,r,n);if(s){const n=d(e);let c,i=!1,{sources:a,active:u}=t,f=0;for(const t of o){if(a=j(a,t),n&&f===o.length-1){c=t;break}d(u)&&(i=!0),u=u[t],f++}if(i)return O(t),s;let l,p=[];n?(l=a,p=a.map((t=>t[N]))):l=j(a,r,(t=>p.push(t[N])));let m=t.resolve(l,p,u,r,o);h(m)&&(m=w(m)),n&&c?u[c]=m:u[r]=m}return s},deleteProperty:(e,r)=>{const n=Reflect.deleteProperty(e,r);if(n){const n=d(e);let s,c=t.sources,i=t.active,a=0;for(const t of o){if(c=c.map((e=>e[t])),n&&a===o.length-1){s=t;break}i=i[t],a++}if(c.some((t=>r in t))){let e,a=[];n?(e=c,a=c.map((t=>t[N]))):e=j(c,r,(t=>a.push(t[N])));let u=t.resolve(e,a,i,r,o);h(u)&&(u=w(u)),n&&s?i[s]=u:i[r]=u}else delete i[r]}return n}}),E={};function M(t,e,o,r){return r.attributesFor?function(t,e,o,r={}){const{attributesFor:n}=r;if(!E[n]){const[t,e]=Array.from(document.querySelectorAll(n));if(!t)return void console.error("Could not find element with selector",n,", won't render attributes");e&&console.warn("Found multiple elements with selector",n),E[n]={el:t,attrs:[]}}const{el:s,attrs:c}=E[n];for(const r in o){const n=F(t,`${e}(${r})`,o[r],o);s.setAttribute(r,`${n||""}`),c.includes(r)||c.push(r)}const i=c.filter((t=>!o[t]));for(const t of i)s.removeAttribute(t)}(t,e,o,r):r.group?function(t,e,o,r){if(d(o))return console.warn("Specifying an array for group properties isnt supported mostly as we didnt found a use-case for this yet. If you have one, please create an issue on the vue-meta repo"),[];return Object.keys(o).map((n=>{const s={group:e,data:o};if(r.namespaced)s.tagNamespace=!0===r.namespaced?e:r.namespaced;else if(r.namespacedAttribute){const t=!0===r.namespacedAttribute?e:r.namespacedAttribute;s.fullName=`${t}:${n}`,s.slotName=`${t}(${n})`}return P(t,e,o[n],r,s)})).flat()}(t,e,o,r):P(t,e,o,r)}function P(t,o,r,n={},s){const c=["content","json","rawContent"],i=t=>function(t,e,o){if(o&&e in o)return o[e];if(d(t)){for(const o of t)if(o&&o in v)return v[o][e]}else if(t in v)return v[t][e]}([a,n.tag],t,n);if(d(r))return r.map((e=>P(t,o,e,n,s))).flat();const{tag:a=n.tag||o}=r;let u,f=!1,l=!1;if(b(r))u=r;else if(r.children&&d(r.children))f=!0,u=r.children.map((e=>{const r=P(t,o,e,n,s);return d(r)?r.map((({vnode:t})=>t)):r.vnode}));else{let t=0;for(const e of c){if(!u&&r[e]){u=1===t?JSON.stringify(r[e]):r[e],l=t>1;break}t++}}const p=s&&s.fullName||o,m=s&&s.slotName||o;let{attrs:y}=r;if(y||"object"!=typeof r)y||(y={});else{y={...r},delete y.tag,delete y.children,delete y.to;for(const t of c)delete y[t]}if(f)u=F(t,m,u,r);else{const e=i("contentAsAttribute");let o=n.valueAttribute;if(!o&&e){const t=i("attributes");o=b(e)?e:t[0]}if(o){if(!n.nameless){const t=i("keyAttribute");t&&(y[t]=p)}y[o]=F(t,m,y[o]||u,s),u=void 0}else u=F(t,m,u,r)}const g=s&&s.tagNamespace?`${s.tagNamespace}:${a}`:a;let h;return l?(y.innerHTML=u,h=e(g,y)):h=e(g,y,u),{to:r.to,vnode:h}}function F({metainfo:t,slots:e},o,r,n){if(!e||!e[o])return r;const s={content:r,metainfo:t};n&&n.group&&(s[n.group]=n.data);const c=e[o](s);return c&&c.length?c[0].children:r}const C="function"==typeof Symbol&&"symbol"==typeof Symbol.toStringTag,_=(z="metainfo",C?Symbol("[vue-meta]: "+z):"[vue-meta]: "+z);var z;function R(t){return t||(t=o()),t.appContext.config.globalProperties.$metaManager}function q(t,e){const r=o();if(!e&&r&&(e=R(r)),!e)throw new Error("No manager or current instance");return e.addMeta(t,r||void 0)}function D(){return r(_)}const I=n({name:"Metainfo",inheritAttrs:!1,setup:(t,{slots:e})=>()=>{const t=R();if(t)return t.render({slots:e})}}),L=s({});function T(t,e,o){const r=d(o)?o:[o];r.forEach(((t,e)=>{t.type===a&&r.splice(e,1)})),t[e]||(t[e]=[]),t[e].push(...r)}function V(t,o){const{addSource:r,delSource:n}=((t,e={})=>{const o=[];e||(e={});const r={active:e,resolve:t,sources:o},n=()=>O(r);return{context:r,active:e,resolve:t,sources:o,addSource:(t,e,o=!1)=>{const s=$(r,t,e||{});return o&&n(),s},delSource:(t,e=!0)=>{const r=o.findIndex((e=>e===t||e[k]===t));return r>-1&&(o.splice(r,1),e&&n(),!0)},compute:n}})(((t,e,r,n,s)=>m(o)?o(t,e,r,n,s):o.resolve(t,e,r,n,s)),L);let s=!1;const a={config:t,install(t){t.component("Metainfo",I),t.config.globalProperties.$metaManager=a,t.provide(_,L)},addMeta(t,e){const s={vm:e};o&&"setup"in o&&m(o.setup)&&o.setup(s);const i=r(t,s,!0),a=()=>n(i);return e&&c(a),{meta:i,unmount:a}},render({slots:t}={}){s||(s=!0,window.addEventListener("DOMContentLoaded",(()=>{const t=document.querySelectorAll("[data-vm-ssr]");t&&t.length&&Array.from(t).forEach((t=>t.parentNode&&t.parentNode.removeChild(t)))})));const o={};for(const e in L){const r=this.config[e]||{},n=M({metainfo:L,slots:t},e,L[e],r);if(!n)continue;const s=d(n)?n:[n],c="base"!==e&&L[e].to||r.to||(r.attributesFor?e:"head");for(const{to:t,vnode:e}of s)T(o,t||c,e)}if(t)for(const e in t){const r=t[e];m(r)&&T(o,"default"===e?"head":e,r({metainfo:L}))}return Object.keys(o).map((t=>e(i,{to:t},o[t])))}};return a}export{V as createMetaManager,l as deepestResolver,p as defaultConfig,R as getCurrentManager,u as resolveOption,q as useMeta,D as useMetainfo}; diff --git a/dist/vue-meta.esm-bundler.js b/dist/vue-meta.esm-bundler.js new file mode 100644 index 00000000..c0d0a7d8 --- /dev/null +++ b/dist/vue-meta.esm-bundler.js @@ -0,0 +1,790 @@ +/** + * vue-meta v3.0.0-alpha.0 + * (c) 2021 + * - Pim (@pimlie) + * - All the amazing contributors + * @license MIT + */ + +import { markRaw, h, getCurrentInstance, inject, defineComponent, reactive, onUnmounted, Teleport } from 'vue'; + +const resolveOption = predicament => (options, contexts) => { + let resolvedIndex = -1; + contexts.reduce((acc, context, index) => { + const retval = predicament(acc, context); + if (retval !== acc) { + resolvedIndex = index; + return retval; + } + return acc; + }, undefined); + if (resolvedIndex > -1) { + return options[resolvedIndex]; + } +}; + +function setup(context) { + let depth = 0; + if (context.vm) { + let { vm } = context; + do { + if (vm.parent) { + depth++; + vm = vm.parent; + } + } while (vm && vm.parent && vm !== vm.root); + } + context.depth = depth; +} +const resolve = resolveOption((acc, context) => { + const { depth } = context; + if (!acc || depth > acc) { + return acc; + } +}); + +var deepest = /*#__PURE__*/Object.freeze({ + __proto__: null, + setup: setup, + resolve: resolve +}); + +const defaultConfig = { + body: { + tag: 'script', + to: 'body' + }, + base: { + valueAttribute: 'href' + }, + charset: { + tag: 'meta', + nameless: true, + valueAttribute: 'charset' + }, + description: { + tag: 'meta' + }, + og: { + group: true, + namespacedAttribute: true, + tag: 'meta', + keyAttribute: 'property' + }, + twitter: { + group: true, + namespacedAttribute: true, + tag: 'meta' + }, + htmlAttrs: { + attributesFor: 'html' + }, + headAttrs: { + attributesFor: 'head' + }, + bodyAttrs: { + attributesFor: 'body' + } +}; + +/** + * Make a map and return a function for checking if a key + * is in that map. + * IMPORTANT: all calls of this function must be prefixed with + * \/\*#\_\_PURE\_\_\*\/ + * So that rollup can tree-shake them if necessary. + */ +(process.env.NODE_ENV !== 'production') + ? Object.freeze({}) + : {}; +(process.env.NODE_ENV !== 'production') ? Object.freeze([]) : []; +const isArray = Array.isArray; +const isFunction = (val) => typeof val === 'function'; +const isString = (val) => typeof val === 'string'; +const isObject = (val) => val !== null && typeof val === 'object'; +const objectToString = Object.prototype.toString; +const toTypeString = (value) => objectToString.call(value); +const isPlainObject = (val) => toTypeString(val) === '[object Object]'; + +/* + * This is a fixed config for real HTML tags + * + * TODO: we probably dont need all attributes + */ +const tags = { + title: { + attributes: false + }, + base: { + contentAsAttribute: true, + attributes: ['href', 'target'] + }, + meta: { + contentAsAttribute: true, + keyAttribute: 'name', + attributes: ['content', 'name', 'http-equiv', 'charset'] + }, + link: { + contentAsAttribute: true, + attributes: [ + 'href', + 'crossorigin', + 'rel', + 'media', + 'integrity', + 'hreflang', + 'type', + 'referrerpolicy', + 'sizes', + 'imagesrcset', + 'imagesizes', + 'as', + 'color' + ] + }, + style: { + attributes: ['media'] + }, + script: { + attributes: [ + 'src', + 'type', + 'nomodule', + 'async', + 'defer', + 'crossorigin', + 'integrity', + 'referrerpolicy' + ] + }, + noscript: { + attributes: false + } +}; + +function getConfigByKey(tagOrName, key, config) { + if (config && key in config) { + return config[key]; + } + if (isArray(tagOrName)) { + for (const name of tagOrName) { + if (name && name in tags) { + return tags[name][key]; + } + } + return; + } + if (tagOrName in tags) { + const tag = tags[tagOrName]; + return tag[key]; + } +} + +// https://github.com/microsoft/TypeScript/issues/1863 +const IS_PROXY = Symbol('kIsProxy'); +const PROXY_SOURCES = Symbol('kProxySources'); +const PROXY_TARGET = Symbol('kProxyTarget'); +const RESOLVE_CONTEXT = Symbol('kResolveContext'); + +// See: https://github.com/vuejs/vue-next/blob/08b4e8815da4e8911058ccbab986bea6365c3352/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts +function clone(v) { + if (isArray(v)) { + return v.map(clone); + } + if (isObject(v)) { + const res = {}; + for (const key in v) { + // never clone the context + if (key === 'context') { + res[key] = v[key]; + } + else { + res[key] = clone(v[key]); + } + } + return res; + } + return v; +} + +const pluck = (collection, key, callback) => { + const plucked = []; + for (const row of collection) { + if (key in row) { + plucked.push(row[key]); + if (callback) { + callback(row); + } + } + } + return plucked; +}; + +const allKeys = (source, ...sources) => { + const keys = source ? Object.keys(source) : []; + if (sources) { + for (const source of sources) { + if (!source || !isObject(source)) { + continue; + } + for (const key in source) { + if (!keys.includes(key)) { + keys.push(key); + } + } + } + } + // TODO: add check for consistent types for each key (dev only) + return keys; +}; +const recompute = (context, sources, target, path = []) => { + if (!path.length) { + if (!target) { + target = context.active; + } + if (!sources) { + sources = context.sources; + } + } + if (!target || !sources) { + return; + } + const keys = allKeys(...sources); + // Clean up properties that dont exists anymore + const targetKeys = Object.keys(target); + for (const key of targetKeys) { + if (!keys.includes(key)) { + delete target[key]; + } + } + for (const key of keys) { + // This assumes consistent types usages for keys across sources + if (isPlainObject(sources[0][key])) { + if (!target[key]) { + target[key] = {}; + } + const keySources = []; + for (const source of sources) { + if (key in source) { + keySources.push(source[key]); + } + } + recompute(context, keySources, target[key], [...path, key]); + continue; + } + // Ensure the target is an array if source is an array and target is empty + if (!target[key] && isArray(sources[0][key])) { + target[key] = []; + } + const keyContexts = []; + const keySources = pluck(sources, key, source => keyContexts.push(source[RESOLVE_CONTEXT])); + let resolved = context.resolve(keySources, keyContexts, target[key], key, path); + if (isPlainObject(resolved)) { + resolved = clone(resolved); + } + // console.log('RESOLVED', key, resolved, 'was', target[key]) + target[key] = resolved; + } +}; + +const createProxy = (context, target, resolveContext, pathSegments = []) => { + const handler = createHandler(context, resolveContext, pathSegments); + const proxy = markRaw(new Proxy(target, handler)); + if (!pathSegments.length && context.sources) { + context.sources.push(proxy); + } + return proxy; +}; +const createHandler = (context, resolveContext, pathSegments = []) => ({ + get: (target, key, receiver) => { + if (key === IS_PROXY) { + return true; + } + if (key === PROXY_SOURCES) { + return context.sources; + } + if (key === PROXY_TARGET) { + return target; + } + if (key === RESOLVE_CONTEXT) { + return resolveContext; + } + let value = Reflect.get(target, key, receiver); + if (!isObject(value)) { + return value; + } + if (!value[IS_PROXY]) { + const keyPath = [...pathSegments, key]; + value = createProxy(context, value, resolveContext, keyPath); + target[key] = value; + } + return value; + }, + set: (target, key, value) => { + const success = Reflect.set(target, key, value); + // console.warn(success, 'PROXY SET\nkey:', key, '\npath:', pathSegments, '\ntarget:', isArray(target), target, '\ncontext:\n', context) + if (success) { + const isArrayItem = isArray(target); + let hasArrayParent = false; + let { sources: proxies, active } = context; + let activeSegmentKey; + let index = 0; + for (const segment of pathSegments) { + proxies = pluck(proxies, segment); + if (isArrayItem && index === pathSegments.length - 1) { + activeSegmentKey = segment; + break; + } + if (isArray(active)) { + hasArrayParent = true; + } + active = active[segment]; + index++; + } + if (hasArrayParent) { + // TODO: fix that we dont have to recompute the full merged object + // we should only have to recompute the branch that has changed + // but there is an issue here with supporting both arrays of strings + // as collections (parent vs parent of parent we need to trigger the + // update from) + recompute(context); + return success; + } + let keyContexts = []; + let keySources; + if (isArrayItem) { + keySources = proxies; + keyContexts = proxies.map(proxy => proxy[RESOLVE_CONTEXT]); + } + else { + keySources = pluck(proxies, key, proxy => keyContexts.push(proxy[RESOLVE_CONTEXT])); + } + let resolved = context.resolve(keySources, keyContexts, active, key, pathSegments); + // Ensure to clone if value is an object, cause sources is an array of + // the sourceProxies not the sources so we could trigger an endless loop when + // updating a prop on an obj as the prop on the active object refers to + // a prop on a proxy + if (isPlainObject(resolved)) { + resolved = clone(resolved); + } + // console.log('SET VALUE', isArrayItem, key, '\nresolved:\n', resolved, '\nsources:\n', context.sources, '\nactive:\n', active, Object.keys(active)) + if (isArrayItem && activeSegmentKey) { + active[activeSegmentKey] = resolved; + } + else { + active[key] = resolved; + } + } + // console.log('CONTEXT.ACTIVE', context.active, '\nparent:\n', target) + return success; + }, + deleteProperty: (target, key) => { + const success = Reflect.deleteProperty(target, key); + // console.warn('PROXY DELETE\nkey:', key, '\npath:', pathSegments, '\nparent:', isArray(target), target) + if (success) { + const isArrayItem = isArray(target); + let activeSegmentKey; + let proxies = context.sources; + let active = context.active; + let index = 0; + for (const segment of pathSegments) { + proxies = proxies.map(proxy => proxy[segment]); + if (isArrayItem && index === pathSegments.length - 1) { + activeSegmentKey = segment; + break; + } + active = active[segment]; + index++; + } + // Check if the key still exists in one of the sourceProxies, + // if so resolve the new value, if not remove the key + if (proxies.some(proxy => (key in proxy))) { + let keyContexts = []; + let keySources; + if (isArrayItem) { + keySources = proxies; + keyContexts = proxies.map(proxy => proxy[RESOLVE_CONTEXT]); + } + else { + keySources = pluck(proxies, key, proxy => keyContexts.push(proxy[RESOLVE_CONTEXT])); + } + let resolved = context.resolve(keySources, keyContexts, active, key, pathSegments); + if (isPlainObject(resolved)) { + resolved = clone(resolved); + } + // console.log('SET VALUE', resolved) + if (isArrayItem && activeSegmentKey) { + active[activeSegmentKey] = resolved; + } + else { + active[key] = resolved; + } + } + else { + delete active[key]; + } + } + return success; + } +}); + +const createMergedObject = (resolve, active = {}) => { + const sources = []; + if (!active) { + active = {}; + } + const context = { + active, + resolve, + sources + }; + const compute = () => recompute(context); + const addSource = (source, resolveContext, recompute = false) => { + const proxy = createProxy(context, source, resolveContext || {}); + if (recompute) { + compute(); + } + return proxy; + }; + const delSource = (sourceOrProxy, recompute = true) => { + const index = sources.findIndex(src => src === sourceOrProxy || src[PROXY_TARGET] === sourceOrProxy); + if (index > -1) { + sources.splice(index, 1); + if (recompute) { + compute(); + } + return true; + } + return false; + }; + return { + context, + active, + resolve, + sources, + addSource, + delSource, + compute + }; +}; + +function renderMeta(context, key, data, config) { + // console.info('renderMeta', key, data, config) + if (config.attributesFor) { + return renderAttributes(context, key, data, config); + } + if (config.group) { + return renderGroup(context, key, data, config); + } + return renderTag(context, key, data, config); +} +function renderGroup(context, key, data, config) { + // console.info('renderGroup', key, data, config) + if (isArray(data)) { + if ((process.env.NODE_ENV !== 'production')) { + // eslint-disable-next-line no-console + console.warn('Specifying an array for group properties isnt supported mostly as we didnt found a use-case for this yet. If you have one, please create an issue on the vue-meta repo'); + } + // config.attributes = getConfigKey([key, config.tag], 'attributes', config) + return []; + } + return Object.keys(data) + .map((childKey) => { + const groupConfig = { + group: key, + data + }; + if (config.namespaced) { + groupConfig.tagNamespace = config.namespaced === true ? key : config.namespaced; + } + else if (config.namespacedAttribute) { + const namespace = config.namespacedAttribute === true ? key : config.namespacedAttribute; + groupConfig.fullName = `${namespace}:${childKey}`; + groupConfig.slotName = `${namespace}(${childKey})`; + } + return renderTag(context, key, data[childKey], config, groupConfig); + }) + .flat(); +} +function renderTag(context, key, data, config = {}, groupConfig) { + // console.info('renderTag', key, data, config, groupConfig) + const contentAttributes = ['content', 'json', 'rawContent']; + const getConfig = (key) => getConfigByKey([tag, config.tag], key, config); + if (isArray(data)) { + return data + .map((child) => { + return renderTag(context, key, child, config, groupConfig); + }) + .flat(); + } + const { tag = config.tag || key } = data; + let content; + let hasChilds = false; + let isRaw = false; + if (isString(data)) { + content = data; + } + else if (data.children && isArray(data.children)) { + hasChilds = true; + content = data.children.map((child) => { + const data = renderTag(context, key, child, config, groupConfig); + if (isArray(data)) { + return data.map(({ vnode }) => vnode); + } + return data.vnode; + }); + } + else { + let i = 0; + for (const contentAttribute of contentAttributes) { + if (!content && data[contentAttribute]) { + if (i === 1) { + content = JSON.stringify(data[contentAttribute]); + } + else { + content = data[contentAttribute]; + } + isRaw = i > 1; + break; + } + i++; + } + } + const fullName = (groupConfig && groupConfig.fullName) || key; + const slotName = (groupConfig && groupConfig.slotName) || key; + let { attrs: attributes } = data; + if (!attributes && typeof data === 'object') { + attributes = { ...data }; + delete attributes.tag; + delete attributes.children; + delete attributes.to; + // cleanup all content attributes + for (const attr of contentAttributes) { + delete attributes[attr]; + } + } + else if (!attributes) { + attributes = {}; + } + if (hasChilds) { + content = getSlotContent(context, slotName, content, data); + } + else { + const contentAsAttribute = getConfig('contentAsAttribute'); + let valueAttribute = config.valueAttribute; + if (!valueAttribute && contentAsAttribute) { + const tagAttributes = getConfig('attributes'); + valueAttribute = isString(contentAsAttribute) ? contentAsAttribute : tagAttributes[0]; + } + if (!valueAttribute) { + content = getSlotContent(context, slotName, content, data); + } + else { + if (!config.nameless) { + const keyAttribute = getConfig('keyAttribute'); + if (keyAttribute) { + attributes[keyAttribute] = fullName; + } + } + attributes[valueAttribute] = getSlotContent(context, slotName, attributes[valueAttribute] || content, groupConfig); + content = undefined; + } + } + const finalTag = groupConfig && groupConfig.tagNamespace + ? `${groupConfig.tagNamespace}:${tag}` + : tag; + // console.info('FINAL TAG', finalTag) + // console.log(' ATTRIBUTES', attributes) + // console.log(' CONTENT', content) + // // console.log(data, attributes, config) + let vnode; + if (isRaw) { + attributes.innerHTML = content; + vnode = h(finalTag, attributes); + } + else { + vnode = h(finalTag, attributes, content); + } + return { + to: data.to, + vnode + }; +} +function renderAttributes(context, key, data, config = {}) { + // console.info('renderAttributes', key, data, config) + const { attributesFor } = config; + { + // render attributes in a placeholder vnode so Vue + // will render the string for us + return { + to: '', + vnode: h(`ssr-${attributesFor}`, data) + }; + } +} +function getSlotContent({ metainfo, slots }, slotName, content, groupConfig) { + if (!slots || !slots[slotName]) { + return content; + } + const slotProps = { + content, + metainfo + }; + if (groupConfig && groupConfig.group) { + slotProps[groupConfig.group] = groupConfig.data; + } + const slotContent = slots[slotName](slotProps); + if (slotContent && slotContent.length) { + return slotContent[0].children; + } + return content; +} + +const hasSymbol = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol'; +const PolySymbol = (name) => +// vm = vue meta +hasSymbol + ? Symbol((process.env.NODE_ENV !== 'production') ? '[vue-meta]: ' + name : name) + : ((process.env.NODE_ENV !== 'production') ? '[vue-meta]: ' : '_vm_') + name; +const metaInfoKey = PolySymbol((process.env.NODE_ENV !== 'production') ? 'metainfo' : 'mi'); + +function getCurrentManager(vm) { + if (!vm) { + vm = getCurrentInstance(); + } + return vm.appContext.config.globalProperties.$metaManager; +} +function useMeta(obj, manager) { + const vm = getCurrentInstance(); + if (!manager && vm) { + manager = getCurrentManager(vm); + } + if (!manager) { + // oopsydoopsy + throw new Error('No manager or current instance'); + } + return manager.addMeta(obj, vm || undefined); +} +function useMetainfo() { + return inject(metaInfoKey); +} + +const MetainfoImpl = defineComponent({ + name: 'Metainfo', + inheritAttrs: false, + setup(_, { slots }) { + return () => { + const manager = getCurrentManager(); + if (!manager) { + return; + } + return manager.render({ slots }); + }; + } +}); +const Metainfo = MetainfoImpl; + +const ssrAttribute = 'data-vm-ssr'; +const active = reactive({}); +function addVnode(teleports, to, _vnodes) { + const vnodes = (isArray(_vnodes) ? _vnodes : [_vnodes]); + { + // dont add ssrAttribute for attribute vnode placeholder + if (!to.endsWith('Attrs')) { + vnodes.forEach((vnode) => { + if (!vnode.props) { + vnode.props = {}; + } + vnode.props[ssrAttribute] = true; + }); + } + } + if (!teleports[to]) { + teleports[to] = []; + } + teleports[to].push(...vnodes); +} +function createMetaManager(config, resolver) { + const resolve = (options, contexts, active, key, pathSegments) => { + if (isFunction(resolver)) { + return resolver(options, contexts, active, key, pathSegments); + } + return resolver.resolve(options, contexts, active, key, pathSegments); + }; + const { addSource, delSource } = createMergedObject(resolve, active); + // TODO: validate resolver + const manager = { + config, + install(app) { + app.component('Metainfo', Metainfo); + app.config.globalProperties.$metaManager = manager; + app.provide(metaInfoKey, active); + }, + addMeta(metaObj, vm) { + const resolveContext = { vm }; + if (resolver && 'setup' in resolver && isFunction(resolver.setup)) { + resolver.setup(resolveContext); + } + // TODO: optimize initial compute + const meta = addSource(metaObj, resolveContext, true); + const unmount = () => delSource(meta); + if (vm) { + onUnmounted(unmount); + } + return { + meta, + unmount + }; + }, + render({ slots } = {}) { + const teleports = {}; + for (const key in active) { + const config = this.config[key] || {}; + const vnode = renderMeta({ metainfo: active, slots }, key, active[key], config); + if (!vnode) { + continue; + } + const vnodes = isArray(vnode) ? vnode : [vnode]; + const defaultTo = (key !== 'base' && active[key].to) || config.to || (config.attributesFor ? key : 'head'); + for (const { to, vnode } of vnodes) { + addVnode(teleports, to || defaultTo, vnode); + } + } + if (slots) { + for (const tag in slots) { + const slotFn = slots[tag]; + if (isFunction(slotFn)) { + addVnode(teleports, tag === 'default' ? 'head' : tag, slotFn({ metainfo: active })); + } + } + } + return Object.keys(teleports).map((to) => { + return h(Teleport, { to }, teleports[to]); + }); + } + }; + return manager; +} + +// rollup doesnt like an import, cant find export so use require +const { renderToString } = require('@vue/server-renderer'); +async function renderToStringWithMeta(app) { + const ctx = {}; + const html = await renderToString(app, ctx); + // TODO: better way of determining whether meta was rendered with the component or not + if (!ctx.teleports || !ctx.teleports.head) { + const teleports = app.config.globalProperties.$metaManager.render(); + await Promise.all(teleports.map((teleport) => renderToString(teleport, ctx))); + } + const { teleports } = ctx; + for (const target in teleports) { + if (target.endsWith('Attrs')) { + const str = teleports[target]; + // match from first space to first >, these should be all rendered attributes + teleports[target] = str.slice(str.indexOf(' ') + 1, str.indexOf('>')); + } + } + return [html, ctx]; +} + +export { createMetaManager, deepest as deepestResolver, defaultConfig, getCurrentManager, renderToStringWithMeta, resolveOption, useMeta, useMetainfo }; diff --git a/dist/vue-meta.global.js b/dist/vue-meta.global.js new file mode 100644 index 00000000..674ce59b --- /dev/null +++ b/dist/vue-meta.global.js @@ -0,0 +1,814 @@ +/** + * vue-meta v3.0.0-alpha.0 + * (c) 2021 + * - Pim (@pimlie) + * - All the amazing contributors + * @license MIT + */ + +var VueMeta = (function (exports, vue) { + 'use strict'; + + const resolveOption = predicament => (options, contexts) => { + let resolvedIndex = -1; + contexts.reduce((acc, context, index) => { + const retval = predicament(acc, context); + if (retval !== acc) { + resolvedIndex = index; + return retval; + } + return acc; + }, undefined); + if (resolvedIndex > -1) { + return options[resolvedIndex]; + } + }; + + function setup(context) { + let depth = 0; + if (context.vm) { + let { vm } = context; + do { + if (vm.parent) { + depth++; + vm = vm.parent; + } + } while (vm && vm.parent && vm !== vm.root); + } + context.depth = depth; + } + const resolve = resolveOption((acc, context) => { + const { depth } = context; + if (!acc || depth > acc) { + return acc; + } + }); + + var deepest = /*#__PURE__*/Object.freeze({ + __proto__: null, + setup: setup, + resolve: resolve + }); + + const defaultConfig = { + body: { + tag: 'script', + to: 'body' + }, + base: { + valueAttribute: 'href' + }, + charset: { + tag: 'meta', + nameless: true, + valueAttribute: 'charset' + }, + description: { + tag: 'meta' + }, + og: { + group: true, + namespacedAttribute: true, + tag: 'meta', + keyAttribute: 'property' + }, + twitter: { + group: true, + namespacedAttribute: true, + tag: 'meta' + }, + htmlAttrs: { + attributesFor: 'html' + }, + headAttrs: { + attributesFor: 'head' + }, + bodyAttrs: { + attributesFor: 'body' + } + }; + + /** + * Make a map and return a function for checking if a key + * is in that map. + * IMPORTANT: all calls of this function must be prefixed with + * \/\*#\_\_PURE\_\_\*\/ + * So that rollup can tree-shake them if necessary. + */ + (process.env.NODE_ENV !== 'production') + ? Object.freeze({}) + : {}; + (process.env.NODE_ENV !== 'production') ? Object.freeze([]) : []; + const isArray = Array.isArray; + const isFunction = (val) => typeof val === 'function'; + const isString = (val) => typeof val === 'string'; + const isObject = (val) => val !== null && typeof val === 'object'; + const objectToString = Object.prototype.toString; + const toTypeString = (value) => objectToString.call(value); + const isPlainObject = (val) => toTypeString(val) === '[object Object]'; + + /* + * This is a fixed config for real HTML tags + * + * TODO: we probably dont need all attributes + */ + const tags = { + title: { + attributes: false + }, + base: { + contentAsAttribute: true, + attributes: ['href', 'target'] + }, + meta: { + contentAsAttribute: true, + keyAttribute: 'name', + attributes: ['content', 'name', 'http-equiv', 'charset'] + }, + link: { + contentAsAttribute: true, + attributes: [ + 'href', + 'crossorigin', + 'rel', + 'media', + 'integrity', + 'hreflang', + 'type', + 'referrerpolicy', + 'sizes', + 'imagesrcset', + 'imagesizes', + 'as', + 'color' + ] + }, + style: { + attributes: ['media'] + }, + script: { + attributes: [ + 'src', + 'type', + 'nomodule', + 'async', + 'defer', + 'crossorigin', + 'integrity', + 'referrerpolicy' + ] + }, + noscript: { + attributes: false + } + }; + + function getConfigByKey(tagOrName, key, config) { + if (config && key in config) { + return config[key]; + } + if (isArray(tagOrName)) { + for (const name of tagOrName) { + if (name && name in tags) { + return tags[name][key]; + } + } + return; + } + if (tagOrName in tags) { + const tag = tags[tagOrName]; + return tag[key]; + } + } + + // https://github.com/microsoft/TypeScript/issues/1863 + const IS_PROXY = Symbol('kIsProxy'); + const PROXY_SOURCES = Symbol('kProxySources'); + const PROXY_TARGET = Symbol('kProxyTarget'); + const RESOLVE_CONTEXT = Symbol('kResolveContext'); + + // See: https://github.com/vuejs/vue-next/blob/08b4e8815da4e8911058ccbab986bea6365c3352/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts + function clone(v) { + if (isArray(v)) { + return v.map(clone); + } + if (isObject(v)) { + const res = {}; + for (const key in v) { + // never clone the context + if (key === 'context') { + res[key] = v[key]; + } + else { + res[key] = clone(v[key]); + } + } + return res; + } + return v; + } + + const pluck = (collection, key, callback) => { + const plucked = []; + for (const row of collection) { + if (key in row) { + plucked.push(row[key]); + if (callback) { + callback(row); + } + } + } + return plucked; + }; + + const allKeys = (source, ...sources) => { + const keys = source ? Object.keys(source) : []; + if (sources) { + for (const source of sources) { + if (!source || !isObject(source)) { + continue; + } + for (const key in source) { + if (!keys.includes(key)) { + keys.push(key); + } + } + } + } + // TODO: add check for consistent types for each key (dev only) + return keys; + }; + const recompute = (context, sources, target, path = []) => { + if (!path.length) { + if (!target) { + target = context.active; + } + if (!sources) { + sources = context.sources; + } + } + if (!target || !sources) { + return; + } + const keys = allKeys(...sources); + // Clean up properties that dont exists anymore + const targetKeys = Object.keys(target); + for (const key of targetKeys) { + if (!keys.includes(key)) { + delete target[key]; + } + } + for (const key of keys) { + // This assumes consistent types usages for keys across sources + if (isPlainObject(sources[0][key])) { + if (!target[key]) { + target[key] = {}; + } + const keySources = []; + for (const source of sources) { + if (key in source) { + keySources.push(source[key]); + } + } + recompute(context, keySources, target[key], [...path, key]); + continue; + } + // Ensure the target is an array if source is an array and target is empty + if (!target[key] && isArray(sources[0][key])) { + target[key] = []; + } + const keyContexts = []; + const keySources = pluck(sources, key, source => keyContexts.push(source[RESOLVE_CONTEXT])); + let resolved = context.resolve(keySources, keyContexts, target[key], key, path); + if (isPlainObject(resolved)) { + resolved = clone(resolved); + } + // console.log('RESOLVED', key, resolved, 'was', target[key]) + target[key] = resolved; + } + }; + + const createProxy = (context, target, resolveContext, pathSegments = []) => { + const handler = createHandler(context, resolveContext, pathSegments); + const proxy = vue.markRaw(new Proxy(target, handler)); + if (!pathSegments.length && context.sources) { + context.sources.push(proxy); + } + return proxy; + }; + const createHandler = (context, resolveContext, pathSegments = []) => ({ + get: (target, key, receiver) => { + if (key === IS_PROXY) { + return true; + } + if (key === PROXY_SOURCES) { + return context.sources; + } + if (key === PROXY_TARGET) { + return target; + } + if (key === RESOLVE_CONTEXT) { + return resolveContext; + } + let value = Reflect.get(target, key, receiver); + if (!isObject(value)) { + return value; + } + if (!value[IS_PROXY]) { + const keyPath = [...pathSegments, key]; + value = createProxy(context, value, resolveContext, keyPath); + target[key] = value; + } + return value; + }, + set: (target, key, value) => { + const success = Reflect.set(target, key, value); + // console.warn(success, 'PROXY SET\nkey:', key, '\npath:', pathSegments, '\ntarget:', isArray(target), target, '\ncontext:\n', context) + if (success) { + const isArrayItem = isArray(target); + let hasArrayParent = false; + let { sources: proxies, active } = context; + let activeSegmentKey; + let index = 0; + for (const segment of pathSegments) { + proxies = pluck(proxies, segment); + if (isArrayItem && index === pathSegments.length - 1) { + activeSegmentKey = segment; + break; + } + if (isArray(active)) { + hasArrayParent = true; + } + active = active[segment]; + index++; + } + if (hasArrayParent) { + // TODO: fix that we dont have to recompute the full merged object + // we should only have to recompute the branch that has changed + // but there is an issue here with supporting both arrays of strings + // as collections (parent vs parent of parent we need to trigger the + // update from) + recompute(context); + return success; + } + let keyContexts = []; + let keySources; + if (isArrayItem) { + keySources = proxies; + keyContexts = proxies.map(proxy => proxy[RESOLVE_CONTEXT]); + } + else { + keySources = pluck(proxies, key, proxy => keyContexts.push(proxy[RESOLVE_CONTEXT])); + } + let resolved = context.resolve(keySources, keyContexts, active, key, pathSegments); + // Ensure to clone if value is an object, cause sources is an array of + // the sourceProxies not the sources so we could trigger an endless loop when + // updating a prop on an obj as the prop on the active object refers to + // a prop on a proxy + if (isPlainObject(resolved)) { + resolved = clone(resolved); + } + // console.log('SET VALUE', isArrayItem, key, '\nresolved:\n', resolved, '\nsources:\n', context.sources, '\nactive:\n', active, Object.keys(active)) + if (isArrayItem && activeSegmentKey) { + active[activeSegmentKey] = resolved; + } + else { + active[key] = resolved; + } + } + // console.log('CONTEXT.ACTIVE', context.active, '\nparent:\n', target) + return success; + }, + deleteProperty: (target, key) => { + const success = Reflect.deleteProperty(target, key); + // console.warn('PROXY DELETE\nkey:', key, '\npath:', pathSegments, '\nparent:', isArray(target), target) + if (success) { + const isArrayItem = isArray(target); + let activeSegmentKey; + let proxies = context.sources; + let active = context.active; + let index = 0; + for (const segment of pathSegments) { + proxies = proxies.map(proxy => proxy[segment]); + if (isArrayItem && index === pathSegments.length - 1) { + activeSegmentKey = segment; + break; + } + active = active[segment]; + index++; + } + // Check if the key still exists in one of the sourceProxies, + // if so resolve the new value, if not remove the key + if (proxies.some(proxy => (key in proxy))) { + let keyContexts = []; + let keySources; + if (isArrayItem) { + keySources = proxies; + keyContexts = proxies.map(proxy => proxy[RESOLVE_CONTEXT]); + } + else { + keySources = pluck(proxies, key, proxy => keyContexts.push(proxy[RESOLVE_CONTEXT])); + } + let resolved = context.resolve(keySources, keyContexts, active, key, pathSegments); + if (isPlainObject(resolved)) { + resolved = clone(resolved); + } + // console.log('SET VALUE', resolved) + if (isArrayItem && activeSegmentKey) { + active[activeSegmentKey] = resolved; + } + else { + active[key] = resolved; + } + } + else { + delete active[key]; + } + } + return success; + } + }); + + const createMergedObject = (resolve, active = {}) => { + const sources = []; + if (!active) { + active = {}; + } + const context = { + active, + resolve, + sources + }; + const compute = () => recompute(context); + const addSource = (source, resolveContext, recompute = false) => { + const proxy = createProxy(context, source, resolveContext || {}); + if (recompute) { + compute(); + } + return proxy; + }; + const delSource = (sourceOrProxy, recompute = true) => { + const index = sources.findIndex(src => src === sourceOrProxy || src[PROXY_TARGET] === sourceOrProxy); + if (index > -1) { + sources.splice(index, 1); + if (recompute) { + compute(); + } + return true; + } + return false; + }; + return { + context, + active, + resolve, + sources, + addSource, + delSource, + compute + }; + }; + + const cachedElements = {}; + function renderMeta(context, key, data, config) { + // console.info('renderMeta', key, data, config) + if (config.attributesFor) { + return renderAttributes(context, key, data, config); + } + if (config.group) { + return renderGroup(context, key, data, config); + } + return renderTag(context, key, data, config); + } + function renderGroup(context, key, data, config) { + // console.info('renderGroup', key, data, config) + if (isArray(data)) { + { + // eslint-disable-next-line no-console + console.warn('Specifying an array for group properties isnt supported mostly as we didnt found a use-case for this yet. If you have one, please create an issue on the vue-meta repo'); + } + // config.attributes = getConfigKey([key, config.tag], 'attributes', config) + return []; + } + return Object.keys(data) + .map((childKey) => { + const groupConfig = { + group: key, + data + }; + if (config.namespaced) { + groupConfig.tagNamespace = config.namespaced === true ? key : config.namespaced; + } + else if (config.namespacedAttribute) { + const namespace = config.namespacedAttribute === true ? key : config.namespacedAttribute; + groupConfig.fullName = `${namespace}:${childKey}`; + groupConfig.slotName = `${namespace}(${childKey})`; + } + return renderTag(context, key, data[childKey], config, groupConfig); + }) + .flat(); + } + function renderTag(context, key, data, config = {}, groupConfig) { + // console.info('renderTag', key, data, config, groupConfig) + const contentAttributes = ['content', 'json', 'rawContent']; + const getConfig = (key) => getConfigByKey([tag, config.tag], key, config); + if (isArray(data)) { + return data + .map((child) => { + return renderTag(context, key, child, config, groupConfig); + }) + .flat(); + } + const { tag = config.tag || key } = data; + let content; + let hasChilds = false; + let isRaw = false; + if (isString(data)) { + content = data; + } + else if (data.children && isArray(data.children)) { + hasChilds = true; + content = data.children.map((child) => { + const data = renderTag(context, key, child, config, groupConfig); + if (isArray(data)) { + return data.map(({ vnode }) => vnode); + } + return data.vnode; + }); + } + else { + let i = 0; + for (const contentAttribute of contentAttributes) { + if (!content && data[contentAttribute]) { + if (i === 1) { + content = JSON.stringify(data[contentAttribute]); + } + else { + content = data[contentAttribute]; + } + isRaw = i > 1; + break; + } + i++; + } + } + const fullName = (groupConfig && groupConfig.fullName) || key; + const slotName = (groupConfig && groupConfig.slotName) || key; + let { attrs: attributes } = data; + if (!attributes && typeof data === 'object') { + attributes = { ...data }; + delete attributes.tag; + delete attributes.children; + delete attributes.to; + // cleanup all content attributes + for (const attr of contentAttributes) { + delete attributes[attr]; + } + } + else if (!attributes) { + attributes = {}; + } + if (hasChilds) { + content = getSlotContent(context, slotName, content, data); + } + else { + const contentAsAttribute = getConfig('contentAsAttribute'); + let valueAttribute = config.valueAttribute; + if (!valueAttribute && contentAsAttribute) { + const tagAttributes = getConfig('attributes'); + valueAttribute = isString(contentAsAttribute) ? contentAsAttribute : tagAttributes[0]; + } + if (!valueAttribute) { + content = getSlotContent(context, slotName, content, data); + } + else { + if (!config.nameless) { + const keyAttribute = getConfig('keyAttribute'); + if (keyAttribute) { + attributes[keyAttribute] = fullName; + } + } + attributes[valueAttribute] = getSlotContent(context, slotName, attributes[valueAttribute] || content, groupConfig); + content = undefined; + } + } + const finalTag = groupConfig && groupConfig.tagNamespace + ? `${groupConfig.tagNamespace}:${tag}` + : tag; + // console.info('FINAL TAG', finalTag) + // console.log(' ATTRIBUTES', attributes) + // console.log(' CONTENT', content) + // // console.log(data, attributes, config) + let vnode; + if (isRaw) { + attributes.innerHTML = content; + vnode = vue.h(finalTag, attributes); + } + else { + vnode = vue.h(finalTag, attributes, content); + } + return { + to: data.to, + vnode + }; + } + function renderAttributes(context, key, data, config = {}) { + // console.info('renderAttributes', key, data, config) + const { attributesFor } = config; + if (!cachedElements[attributesFor]) { + const [el, el2] = Array.from(document.querySelectorAll(attributesFor)); + if ( !el) { + // eslint-disable-next-line no-console + console.error('Could not find element with selector', attributesFor, ', won\'t render attributes'); + return; + } + if ( el2) { + // eslint-disable-next-line no-console + console.warn('Found multiple elements with selector', attributesFor); + } + cachedElements[attributesFor] = { + el, + attrs: [] + }; + } + const { el, attrs } = cachedElements[attributesFor]; + for (const attr in data) { + const content = getSlotContent(context, `${key}(${attr})`, data[attr], data); + el.setAttribute(attr, `${content || ''}`); + if (!attrs.includes(attr)) { + attrs.push(attr); + } + } + const attrsToRemove = attrs.filter(attr => !data[attr]); + for (const attr of attrsToRemove) { + el.removeAttribute(attr); + } + } + function getSlotContent({ metainfo, slots }, slotName, content, groupConfig) { + if (!slots || !slots[slotName]) { + return content; + } + const slotProps = { + content, + metainfo + }; + if (groupConfig && groupConfig.group) { + slotProps[groupConfig.group] = groupConfig.data; + } + const slotContent = slots[slotName](slotProps); + if (slotContent && slotContent.length) { + return slotContent[0].children; + } + return content; + } + + const hasSymbol = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol'; + const PolySymbol = (name) => + // vm = vue meta + hasSymbol + ? Symbol( '[vue-meta]: ' + name ) + : ( '[vue-meta]: ' ) + name; + const metaInfoKey = PolySymbol( 'metainfo' ); + + function getCurrentManager(vm) { + if (!vm) { + vm = vue.getCurrentInstance(); + } + return vm.appContext.config.globalProperties.$metaManager; + } + function useMeta(obj, manager) { + const vm = vue.getCurrentInstance(); + if (!manager && vm) { + manager = getCurrentManager(vm); + } + if (!manager) { + // oopsydoopsy + throw new Error('No manager or current instance'); + } + return manager.addMeta(obj, vm || undefined); + } + function useMetainfo() { + return vue.inject(metaInfoKey); + } + + const MetainfoImpl = vue.defineComponent({ + name: 'Metainfo', + inheritAttrs: false, + setup(_, { slots }) { + return () => { + const manager = getCurrentManager(); + if (!manager) { + return; + } + return manager.render({ slots }); + }; + } + }); + const Metainfo = MetainfoImpl; + + const ssrAttribute = 'data-vm-ssr'; + const active = vue.reactive({}); + function addVnode(teleports, to, _vnodes) { + const vnodes = (isArray(_vnodes) ? _vnodes : [_vnodes]); + { + // Comments shouldnt have any use on the client as they are not reactive anyway + vnodes.forEach((vnode, idx) => { + if (vnode.type === vue.Comment) { + vnodes.splice(idx, 1); + } + }); + } + if (!teleports[to]) { + teleports[to] = []; + } + teleports[to].push(...vnodes); + } + function createMetaManager(config, resolver) { + const resolve = (options, contexts, active, key, pathSegments) => { + if (isFunction(resolver)) { + return resolver(options, contexts, active, key, pathSegments); + } + return resolver.resolve(options, contexts, active, key, pathSegments); + }; + const { addSource, delSource } = createMergedObject(resolve, active); + let cleanedUpSsr = false; + // TODO: validate resolver + const manager = { + config, + install(app) { + app.component('Metainfo', Metainfo); + app.config.globalProperties.$metaManager = manager; + app.provide(metaInfoKey, active); + }, + addMeta(metaObj, vm) { + const resolveContext = { vm }; + if (resolver && 'setup' in resolver && isFunction(resolver.setup)) { + resolver.setup(resolveContext); + } + // TODO: optimize initial compute + const meta = addSource(metaObj, resolveContext, true); + const unmount = () => delSource(meta); + if (vm) { + vue.onUnmounted(unmount); + } + return { + meta, + unmount + }; + }, + render({ slots } = {}) { + // cleanup ssr tags if not yet done + if ( !cleanedUpSsr) { + cleanedUpSsr = true; + // Listen for DOM loaded because tags in the body couldnt be loaded + // yet once the manager does it first render + // (preferable there should only be one render on hydration) + window.addEventListener('DOMContentLoaded', () => { + const ssrTags = document.querySelectorAll(`[${ssrAttribute}]`); + if (ssrTags && ssrTags.length) { + Array.from(ssrTags).forEach(el => el.parentNode && el.parentNode.removeChild(el)); + } + }); + } + const teleports = {}; + for (const key in active) { + const config = this.config[key] || {}; + const vnode = renderMeta({ metainfo: active, slots }, key, active[key], config); + if (!vnode) { + continue; + } + const vnodes = isArray(vnode) ? vnode : [vnode]; + const defaultTo = (key !== 'base' && active[key].to) || config.to || (config.attributesFor ? key : 'head'); + for (const { to, vnode } of vnodes) { + addVnode(teleports, to || defaultTo, vnode); + } + } + if (slots) { + for (const tag in slots) { + const slotFn = slots[tag]; + if (isFunction(slotFn)) { + addVnode(teleports, tag === 'default' ? 'head' : tag, slotFn({ metainfo: active })); + } + } + } + return Object.keys(teleports).map((to) => { + return vue.h(vue.Teleport, { to }, teleports[to]); + }); + } + }; + return manager; + } + + exports.createMetaManager = createMetaManager; + exports.deepestResolver = deepest; + exports.defaultConfig = defaultConfig; + exports.getCurrentManager = getCurrentManager; + exports.resolveOption = resolveOption; + exports.useMeta = useMeta; + exports.useMetainfo = useMetainfo; + + Object.defineProperty(exports, '__esModule', { value: true }); + + return exports; + +}({}, Vue)); diff --git a/dist/vue-meta.global.min.js b/dist/vue-meta.global.min.js new file mode 100644 index 00000000..316508db --- /dev/null +++ b/dist/vue-meta.global.min.js @@ -0,0 +1,8 @@ +/** + * vue-meta v3.0.0-alpha.0 + * (c) 2021 + * - Pim (@pimlie) + * - All the amazing contributors + * @license MIT + */ +var VueMeta=function(e,t){"use strict";const r=e=>(t,r)=>{let o=-1;if(r.reduce(((t,r,n)=>{const s=e(t,r);return s!==t?(o=n,s):t}),void 0),o>-1)return t[o]};const o=r(((e,t)=>{const{depth:r}=t;if(!e||r>e)return e}));var n=Object.freeze({__proto__:null,setup:function(e){let t=0;if(e.vm){let{vm:r}=e;do{r.parent&&(t++,r=r.parent)}while(r&&r.parent&&r!==r.root)}e.depth=t},resolve:o});"production"===process.env.NODE_ENV||Object.freeze({}),"production"===process.env.NODE_ENV||Object.freeze([]);const s=Array.isArray,i=e=>"function"==typeof e,c=e=>"string"==typeof e,a=e=>null!==e&&"object"==typeof e,u=Object.prototype.toString,f=e=>"[object Object]"===u.call(e),l={title:{attributes:!1},base:{contentAsAttribute:!0,attributes:["href","target"]},meta:{contentAsAttribute:!0,keyAttribute:"name",attributes:["content","name","http-equiv","charset"]},link:{contentAsAttribute:!0,attributes:["href","crossorigin","rel","media","integrity","hreflang","type","referrerpolicy","sizes","imagesrcset","imagesizes","as","color"]},style:{attributes:["media"]},script:{attributes:["src","type","nomodule","async","defer","crossorigin","integrity","referrerpolicy"]},noscript:{attributes:!1}};const p=Symbol("kIsProxy"),d=Symbol("kProxySources"),m=Symbol("kProxyTarget"),b=Symbol("kResolveContext");function g(e){if(s(e))return e.map(g);if(a(e)){const t={};for(const r in e)t[r]="context"===r?e[r]:g(e[r]);return t}return e}const y=(e,t,r)=>{const o=[];for(const n of e)t in n&&(o.push(n[t]),r&&r(n));return o},h=(e,t,r,o=[])=>{if(o.length||(r||(r=e.active),t||(t=e.sources)),!r||!t)return;const n=((e,...t)=>{const r=e?Object.keys(e):[];if(t)for(const e of t)if(e&&a(e))for(const t in e)r.includes(t)||r.push(t);return r})(...t),i=Object.keys(r);for(const e of i)n.includes(e)||delete r[e];for(const i of n){if(f(t[0][i])){r[i]||(r[i]={});const n=[];for(const e of t)i in e&&n.push(e[i]);h(e,n,r[i],[...o,i]);continue}!r[i]&&s(t[0][i])&&(r[i]=[]);const n=[],c=y(t,i,(e=>n.push(e[b])));let a=e.resolve(c,n,r[i],i,o);f(a)&&(a=g(a)),r[i]=a}},v=(e,r,o,n=[])=>{const s=A(e,o,n),i=t.markRaw(new Proxy(r,s));return!n.length&&e.sources&&e.sources.push(i),i},A=(e,t,r=[])=>({get:(o,n,s)=>{if(n===p)return!0;if(n===d)return e.sources;if(n===m)return o;if(n===b)return t;let i=Reflect.get(o,n,s);if(!a(i))return i;if(!i[p]){const s=[...r,n];i=v(e,i,t,s),o[n]=i}return i},set:(t,o,n)=>{const i=Reflect.set(t,o,n);if(i){const n=s(t);let c,a=!1,{sources:u,active:l}=e,p=0;for(const e of r){if(u=y(u,e),n&&p===r.length-1){c=e;break}s(l)&&(a=!0),l=l[e],p++}if(a)return h(e),i;let d,m=[];n?(d=u,m=u.map((e=>e[b]))):d=y(u,o,(e=>m.push(e[b])));let v=e.resolve(d,m,l,o,r);f(v)&&(v=g(v)),n&&c?l[c]=v:l[o]=v}return i},deleteProperty:(t,o)=>{const n=Reflect.deleteProperty(t,o);if(n){const n=s(t);let i,c=e.sources,a=e.active,u=0;for(const e of r){if(c=c.map((t=>t[e])),n&&u===r.length-1){i=e;break}a=a[e],u++}if(c.some((e=>o in e))){let t,s=[];n?(t=c,s=c.map((e=>e[b]))):t=y(c,o,(e=>s.push(e[b])));let u=e.resolve(t,s,a,o,r);f(u)&&(u=g(u)),n&&i?a[i]=u:a[o]=u}else delete a[o]}return n}}),S={};function k(e,t,r,o){return o.attributesFor?function(e,t,r,o={}){const{attributesFor:n}=o;if(!S[n]){const[e,t]=Array.from(document.querySelectorAll(n));if(!e)return void console.error("Could not find element with selector",n,", won't render attributes");t&&console.warn("Found multiple elements with selector",n),S[n]={el:e,attrs:[]}}const{el:s,attrs:i}=S[n];for(const o in r){const n=w(e,`${t}(${o})`,r[o],r);s.setAttribute(o,`${n||""}`),i.includes(o)||i.push(o)}const c=i.filter((e=>!r[e]));for(const e of c)s.removeAttribute(e)}(e,t,r,o):o.group?function(e,t,r,o){if(s(r))return console.warn("Specifying an array for group properties isnt supported mostly as we didnt found a use-case for this yet. If you have one, please create an issue on the vue-meta repo"),[];return Object.keys(r).map((n=>{const s={group:t,data:r};if(o.namespaced)s.tagNamespace=!0===o.namespaced?t:o.namespaced;else if(o.namespacedAttribute){const e=!0===o.namespacedAttribute?t:o.namespacedAttribute;s.fullName=`${e}:${n}`,s.slotName=`${e}(${n})`}return j(e,t,r[n],o,s)})).flat()}(e,t,r,o):j(e,t,r,o)}function j(e,r,o,n={},i){const a=["content","json","rawContent"],u=e=>function(e,t,r){if(r&&t in r)return r[t];if(s(e)){for(const r of e)if(r&&r in l)return l[r][t]}else if(e in l)return l[e][t]}([f,n.tag],e,n);if(s(o))return o.map((t=>j(e,r,t,n,i))).flat();const{tag:f=n.tag||r}=o;let p,d=!1,m=!1;if(c(o))p=o;else if(o.children&&s(o.children))d=!0,p=o.children.map((t=>{const o=j(e,r,t,n,i);return s(o)?o.map((({vnode:e})=>e)):o.vnode}));else{let e=0;for(const t of a){if(!p&&o[t]){p=1===e?JSON.stringify(o[t]):o[t],m=e>1;break}e++}}const b=i&&i.fullName||r,g=i&&i.slotName||r;let{attrs:y}=o;if(y||"object"!=typeof o)y||(y={});else{y={...o},delete y.tag,delete y.children,delete y.to;for(const e of a)delete y[e]}if(d)p=w(e,g,p,o);else{const t=u("contentAsAttribute");let r=n.valueAttribute;if(!r&&t){const e=u("attributes");r=c(t)?t:e[0]}if(r){if(!n.nameless){const e=u("keyAttribute");e&&(y[e]=b)}y[r]=w(e,g,y[r]||p,i),p=void 0}else p=w(e,g,p,o)}const h=i&&i.tagNamespace?`${i.tagNamespace}:${f}`:f;let v;return m?(y.innerHTML=p,v=t.h(h,y)):v=t.h(h,y,p),{to:o.to,vnode:v}}function w({metainfo:e,slots:t},r,o,n){if(!t||!t[r])return o;const s={content:o,metainfo:e};n&&n.group&&(s[n.group]=n.data);const i=t[r](s);return i&&i.length?i[0].children:o}const M="function"==typeof Symbol&&"symbol"==typeof Symbol.toStringTag,N=(O="metainfo",M?Symbol("[vue-meta]: "+O):"[vue-meta]: "+O);var O;function C(e){return e||(e=t.getCurrentInstance()),e.appContext.config.globalProperties.$metaManager}const $=t.defineComponent({name:"Metainfo",inheritAttrs:!1,setup:(e,{slots:t})=>()=>{const e=C();if(e)return e.render({slots:t})}}),x=t.reactive({});function P(e,r,o){const n=s(o)?o:[o];n.forEach(((e,r)=>{e.type===t.Comment&&n.splice(r,1)})),e[r]||(e[r]=[]),e[r].push(...n)}return e.createMetaManager=function(e,r){const{addSource:o,delSource:n}=((e,t={})=>{const r=[];t||(t={});const o={active:t,resolve:e,sources:r},n=()=>h(o);return{context:o,active:t,resolve:e,sources:r,addSource:(e,t,r=!1)=>{const s=v(o,e,t||{});return r&&n(),s},delSource:(e,t=!0)=>{const o=r.findIndex((t=>t===e||t[m]===e));return o>-1&&(r.splice(o,1),t&&n(),!0)},compute:n}})(((e,t,o,n,s)=>i(r)?r(e,t,o,n,s):r.resolve(e,t,o,n,s)),x);let c=!1;const a={config:e,install(e){e.component("Metainfo",$),e.config.globalProperties.$metaManager=a,e.provide(N,x)},addMeta(e,s){const c={vm:s};r&&"setup"in r&&i(r.setup)&&r.setup(c);const a=o(e,c,!0),u=()=>n(a);return s&&t.onUnmounted(u),{meta:a,unmount:u}},render({slots:e}={}){c||(c=!0,window.addEventListener("DOMContentLoaded",(()=>{const e=document.querySelectorAll("[data-vm-ssr]");e&&e.length&&Array.from(e).forEach((e=>e.parentNode&&e.parentNode.removeChild(e)))})));const r={};for(const t in x){const o=this.config[t]||{},n=k({metainfo:x,slots:e},t,x[t],o);if(!n)continue;const i=s(n)?n:[n],c="base"!==t&&x[t].to||o.to||(o.attributesFor?t:"head");for(const{to:e,vnode:t}of i)P(r,e||c,t)}if(e)for(const t in e){const o=e[t];i(o)&&P(r,"default"===t?"head":t,o({metainfo:x}))}return Object.keys(r).map((e=>t.h(t.Teleport,{to:e},r[e])))}};return a},e.deepestResolver=n,e.defaultConfig={body:{tag:"script",to:"body"},base:{valueAttribute:"href"},charset:{tag:"meta",nameless:!0,valueAttribute:"charset"},description:{tag:"meta"},og:{group:!0,namespacedAttribute:!0,tag:"meta",keyAttribute:"property"},twitter:{group:!0,namespacedAttribute:!0,tag:"meta"},htmlAttrs:{attributesFor:"html"},headAttrs:{attributesFor:"head"},bodyAttrs:{attributesFor:"body"}},e.getCurrentManager=C,e.resolveOption=r,e.useMeta=function(e,r){const o=t.getCurrentInstance();if(!r&&o&&(r=C(o)),!r)throw new Error("No manager or current instance");return r.addMeta(e,o||void 0)},e.useMetainfo=function(){return t.inject(N)},Object.defineProperty(e,"__esModule",{value:!0}),e}({},Vue); diff --git a/package.json b/package.json index 5899335d..ec2d7a2c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-meta", - "version": "2.3.3", + "version": "3.0.0-alpha.0", "description": "Manage HTML metadata in Vue.js components with SSR support", "main": "dist/vue-meta.cjs.js", "unpkg": "dist/vue-meta.global.js",