unhead 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,429 @@
1
+ 'use strict';
2
+
3
+ const hookable = require('hookable');
4
+
5
+ function normaliseTag$1(tagName, input, options = {}) {
6
+ const tag = { tag: tagName, props: {} };
7
+ if (tagName === "title")
8
+ tag.children = String(input);
9
+ else
10
+ tag.props = normaliseProps({ ...input });
11
+ ["children", ...options?.childrenKeys || []].forEach((key) => {
12
+ if (typeof tag.props[key] !== "undefined") {
13
+ tag.children = tag.props[key];
14
+ delete tag.props[key];
15
+ }
16
+ });
17
+ return tag;
18
+ }
19
+ function normaliseProps(props) {
20
+ for (const k in props) {
21
+ if (String(props[k]) === "true") {
22
+ props[k] = "";
23
+ } else if (String(props[k]) === "false") {
24
+ delete props[k];
25
+ }
26
+ }
27
+ return props;
28
+ }
29
+ const HasElementTags = [
30
+ "base",
31
+ "meta",
32
+ "link",
33
+ "style",
34
+ "script",
35
+ "noscript"
36
+ ];
37
+ const ValidHeadTags = [
38
+ "title",
39
+ "titleTemplate",
40
+ "base",
41
+ "htmlAttrs",
42
+ "bodyAttrs",
43
+ "meta",
44
+ "link",
45
+ "style",
46
+ "script",
47
+ "noscript"
48
+ ];
49
+
50
+ const sortCriticalTags = (aTag, bTag) => {
51
+ const tagWeight = (tag) => {
52
+ switch (tag.tag) {
53
+ case "base":
54
+ return -1;
55
+ case "title":
56
+ return 1;
57
+ case "meta":
58
+ if (tag.props.charset)
59
+ return -2;
60
+ if (tag.props["http-equiv"] === "content-security-policy")
61
+ return 0;
62
+ return 10;
63
+ default:
64
+ return 10;
65
+ }
66
+ };
67
+ return tagWeight(aTag) - tagWeight(bTag);
68
+ };
69
+
70
+ function tagDedupeKey(tag) {
71
+ const { props, tag: tagName } = tag;
72
+ if (["base", "title", "titleTemplate", "bodyAttrs", "htmlAttrs"].includes(tagName))
73
+ return tagName;
74
+ if (tagName === "link" && props.rel === "canonical")
75
+ return "canonical";
76
+ if (props.charset)
77
+ return "charset";
78
+ const name = ["id"];
79
+ if (tagName === "meta")
80
+ name.push(...["name", "property", "http-equiv"]);
81
+ for (const n of name) {
82
+ if (typeof props[n] !== "undefined") {
83
+ return `${tagName}:${n}:${props[n]}`;
84
+ }
85
+ }
86
+ return false;
87
+ }
88
+
89
+ const renderTitleTemplate = (template, title) => {
90
+ if (template == null)
91
+ return title || null;
92
+ if (typeof template === "function")
93
+ return template(title);
94
+ return template.replace("%s", title ?? "");
95
+ };
96
+ function resolveTitleTemplateFromTags(tags) {
97
+ const titleTemplateIdx = tags.findIndex((i) => i.tag === "titleTemplate");
98
+ const titleIdx = tags.findIndex((i) => i.tag === "title");
99
+ if (titleIdx !== -1 && titleTemplateIdx !== -1) {
100
+ const newTitle = renderTitleTemplate(
101
+ tags[titleTemplateIdx].children,
102
+ tags[titleIdx].children
103
+ );
104
+ if (newTitle !== null) {
105
+ tags[titleIdx].children = newTitle || tags[titleIdx].children;
106
+ } else {
107
+ tags = tags.filter((_, i) => i !== titleIdx);
108
+ }
109
+ } else if (titleTemplateIdx !== -1) {
110
+ const newTitle = renderTitleTemplate(
111
+ tags[titleTemplateIdx].children
112
+ );
113
+ if (newTitle !== null) {
114
+ tags[titleTemplateIdx].children = newTitle;
115
+ tags[titleTemplateIdx].tag = "title";
116
+ }
117
+ }
118
+ if (titleTemplateIdx !== -1)
119
+ tags = tags.filter((_, i) => i !== titleTemplateIdx);
120
+ return tags;
121
+ }
122
+
123
+ const DedupesTagsPlugin = (options) => {
124
+ options = options || {};
125
+ const dedupeKeys = options.dedupeKeys || ["hid", "vmid", "key"];
126
+ return defineHeadPlugin({
127
+ hooks: {
128
+ "tag:normalise": function({ tag }) {
129
+ dedupeKeys.forEach((key) => {
130
+ if (tag.props[key]) {
131
+ tag.key = tag.props[key];
132
+ delete tag.props[key];
133
+ }
134
+ });
135
+ const dedupe = tag.key ? `${tag.tag}:${tag.key}` : tagDedupeKey(tag);
136
+ if (dedupe)
137
+ tag._d = dedupe;
138
+ },
139
+ "tags:resolve": function(ctx) {
140
+ const deduping = {};
141
+ ctx.tags.forEach((tag, i) => {
142
+ let dedupeKey = tag._d || tag._p || i;
143
+ const dupedTag = deduping[dedupeKey];
144
+ if (dupedTag) {
145
+ let strategy = tag?.tagDuplicateStrategy;
146
+ if (!strategy && (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs"))
147
+ strategy = "merge";
148
+ if (strategy === "merge") {
149
+ const oldProps = dupedTag.props;
150
+ ["class", "style"].forEach((key) => {
151
+ if (tag.props[key] && oldProps[key])
152
+ tag.props[key] = `${oldProps[key]} ${tag.props[key]}`;
153
+ });
154
+ deduping[dedupeKey].props = {
155
+ ...oldProps,
156
+ ...tag.props
157
+ };
158
+ return;
159
+ } else if (tag._e === dupedTag._e) {
160
+ dedupeKey = `${dedupeKey}:entry(${tag._e}:${tag._p})`;
161
+ tag._d = dedupeKey;
162
+ } else {
163
+ tag._p = dupedTag._p;
164
+ }
165
+ if (Object.keys(tag.props).length === 0 && !tag.children) {
166
+ delete deduping[dedupeKey];
167
+ return;
168
+ }
169
+ }
170
+ deduping[dedupeKey] = tag;
171
+ });
172
+ ctx.tags = Object.values(deduping);
173
+ }
174
+ }
175
+ });
176
+ };
177
+
178
+ const SortTagsPlugin = () => {
179
+ return defineHeadPlugin({
180
+ hooks: {
181
+ "tags:resolve": (ctx) => {
182
+ const tagIndexForKey = (key) => ctx.tags.find((tag) => tag._d === key)?._p;
183
+ for (const tag of ctx.tags) {
184
+ if (!tag?.tagPriority)
185
+ continue;
186
+ if (typeof tag.tagPriority === "number") {
187
+ tag._p = tag.tagPriority;
188
+ continue;
189
+ }
190
+ const modifiers = [{ prefix: "before:", offset: -1 }, { prefix: "after:", offset: 1 }];
191
+ for (const { prefix, offset } of modifiers) {
192
+ if (tag.tagPriority.startsWith(prefix)) {
193
+ const key = tag.tagPriority.replace(prefix, "");
194
+ const index = tagIndexForKey(key);
195
+ if (typeof index !== "undefined")
196
+ tag._p = index + offset;
197
+ }
198
+ }
199
+ }
200
+ ctx.tags.sort((a, b) => a._p - b._p).sort(sortCriticalTags);
201
+ }
202
+ }
203
+ });
204
+ };
205
+
206
+ const TitleTemplatePlugin = () => {
207
+ return defineHeadPlugin({
208
+ hooks: {
209
+ "tags:resolve": (ctx) => {
210
+ ctx.tags = resolveTitleTemplateFromTags(ctx.tags);
211
+ }
212
+ }
213
+ });
214
+ };
215
+
216
+ function hashCode(s) {
217
+ let h = 9;
218
+ for (let i = 0; i < s.length; )
219
+ h = Math.imul(h ^ s.charCodeAt(i++), 9 ** 9);
220
+ return ((h ^ h >>> 9) + 65536).toString(16).substring(1, 7).toLowerCase();
221
+ }
222
+
223
+ const HydratesStatePlugin = () => {
224
+ return defineHeadPlugin({
225
+ hooks: {
226
+ "tag:normalise": (ctx) => {
227
+ const { tag, entry } = ctx;
228
+ if (!HasElementTags.includes(tag.tag))
229
+ return;
230
+ if (typeof tag._d === "undefined" && entry._m === "server")
231
+ return;
232
+ const hasChildren = tag.children && tag.children.length > 0;
233
+ tag._s = `data-h-${hashCode(tag._d || tag.tag + (hasChildren ? tag.children : JSON.stringify(tag.props)))}`;
234
+ tag.props[tag._s] = "";
235
+ }
236
+ }
237
+ });
238
+ };
239
+
240
+ const IsClient = typeof window !== "undefined";
241
+
242
+ exports.activeHead = void 0;
243
+ const setActiveHead = (head) => exports.activeHead = head;
244
+ const getActiveHead = () => exports.activeHead;
245
+
246
+ function useHead(input, options = {}) {
247
+ if (options.mode === "server" && IsClient || options.mode === "client" && !IsClient)
248
+ return;
249
+ const head = getActiveHead();
250
+ head.push(input, options);
251
+ }
252
+ function useServerHead(input, options = {}) {
253
+ useHead(input, { ...options, mode: "server" });
254
+ }
255
+ const useTitle = (title) => {
256
+ useHead({ title });
257
+ };
258
+ const useMeta = (meta) => {
259
+ useHead({ meta: [meta] });
260
+ };
261
+ const useLink = (link) => {
262
+ useHead({ link: [link] });
263
+ };
264
+ const useScript = (script) => {
265
+ useHead({ script: [script] });
266
+ };
267
+ const useStyle = (style) => {
268
+ useHead({ style: [style] });
269
+ };
270
+ const useBase = (base) => {
271
+ useHead({ base });
272
+ };
273
+ const useHtmlAttrs = (attrs) => {
274
+ useHead({ htmlAttrs: attrs });
275
+ };
276
+ const useBodyAttrs = (attrs) => {
277
+ useHead({ bodyAttrs: attrs });
278
+ };
279
+ const useTitleTemplate = (titleTemplate) => {
280
+ useHead({ titleTemplate });
281
+ };
282
+ const useNoscript = (noscript) => {
283
+ useHead({ noscript: [noscript] });
284
+ };
285
+
286
+ function asArray(value) {
287
+ return Array.isArray(value) ? value : [value];
288
+ }
289
+ const TagConfigKeys = ["tagPosition", "tagPriority", "tagDuplicateStrategy"];
290
+
291
+ function normaliseTag(tagName, input, entry) {
292
+ const tag = normaliseTag$1(tagName, input, { childrenKeys: ["innerHTML", "textContent"] });
293
+ tag._e = entry._i;
294
+ Object.keys(tag.props).filter((k) => TagConfigKeys.includes(k)).forEach((k) => {
295
+ tag[k] = tag.props[k];
296
+ delete tag.props[k];
297
+ });
298
+ if (typeof tag.props.class === "object" && !Array.isArray(tag.props.class)) {
299
+ tag.props.class = Object.keys(tag.props.class).filter((k) => tag.props.class[k]);
300
+ }
301
+ if (Array.isArray(tag.props.class))
302
+ tag.props.class = tag.props.class.join(" ");
303
+ if (tag.props.content && Array.isArray(tag.props.content)) {
304
+ return tag.props.content.map((v, i) => {
305
+ const newTag = { ...tag, props: { ...tag.props } };
306
+ newTag.props.content = v;
307
+ newTag.key = `${tag.props.name || tag.props.property}:${i}`;
308
+ return newTag;
309
+ });
310
+ }
311
+ return tag;
312
+ }
313
+ function normaliseEntryTags(e) {
314
+ return Object.entries(e.input).filter(([k, v]) => typeof v !== "undefined" && ValidHeadTags.includes(k)).map(
315
+ ([k, value]) => asArray(value).map((props) => asArray(normaliseTag(k, props, e)))
316
+ ).flat(3).map((t, i) => {
317
+ t._p = (e._i << 8) + i++;
318
+ return t;
319
+ });
320
+ }
321
+
322
+ async function createHead(options = {}) {
323
+ let entries = [];
324
+ let _sde = {};
325
+ let entryId = 0;
326
+ const hooks = hookable.createHooks();
327
+ if (options.hooks)
328
+ hooks.addHooks(options.hooks);
329
+ const plugins = [
330
+ DedupesTagsPlugin(),
331
+ SortTagsPlugin(),
332
+ TitleTemplatePlugin()
333
+ ];
334
+ plugins.push(...options.plugins || []);
335
+ plugins.forEach((plugin) => hooks.addHooks(plugin.hooks || {}));
336
+ const head = {
337
+ _removeQueuedSideEffect(key) {
338
+ delete _sde[key];
339
+ },
340
+ _flushQueuedSideEffects() {
341
+ Object.values(_sde).forEach((fn) => fn());
342
+ _sde = {};
343
+ },
344
+ headEntries() {
345
+ return entries;
346
+ },
347
+ get hooks() {
348
+ return hooks;
349
+ },
350
+ push(input, options2) {
351
+ const _i = entryId++;
352
+ entries.push({
353
+ _i,
354
+ input,
355
+ _sde: {},
356
+ ...options2
357
+ });
358
+ hooks.callHook("entries:updated", head);
359
+ return {
360
+ dispose() {
361
+ entries = entries.filter((e) => {
362
+ if (e._i !== _i)
363
+ return true;
364
+ _sde = { ..._sde, ...e._sde || {} };
365
+ e._sde = {};
366
+ return false;
367
+ });
368
+ hooks.callHook("entries:updated", head);
369
+ },
370
+ patch(input2) {
371
+ entries = entries.map((e) => {
372
+ if (e._i === _i) {
373
+ _sde = { ..._sde, ...e._sde || {} };
374
+ e._sde = {};
375
+ e.input = e._i === _i ? input2 : e.input;
376
+ }
377
+ return e;
378
+ });
379
+ hooks.callHook("entries:updated", head);
380
+ }
381
+ };
382
+ },
383
+ async resolveTags() {
384
+ const resolveCtx = { tags: [], entries: [...entries] };
385
+ await hooks.callHook("entries:resolve", resolveCtx);
386
+ for (const entry of resolveCtx.entries) {
387
+ for (const tag of normaliseEntryTags(entry)) {
388
+ const tagCtx = { tag, entry };
389
+ await hooks.callHook("tag:normalise", tagCtx);
390
+ resolveCtx.tags.push(tagCtx.tag);
391
+ }
392
+ }
393
+ await hooks.callHook("tags:resolve", resolveCtx);
394
+ return resolveCtx.tags;
395
+ }
396
+ };
397
+ await head.hooks.callHook("init", head);
398
+ setActiveHead(head);
399
+ return head;
400
+ }
401
+
402
+ function defineHeadPlugin(plugin) {
403
+ return plugin;
404
+ }
405
+
406
+ exports.DedupesTagsPlugin = DedupesTagsPlugin;
407
+ exports.HydratesStatePlugin = HydratesStatePlugin;
408
+ exports.SortTagsPlugin = SortTagsPlugin;
409
+ exports.TagConfigKeys = TagConfigKeys;
410
+ exports.TitleTemplatePlugin = TitleTemplatePlugin;
411
+ exports.asArray = asArray;
412
+ exports.createHead = createHead;
413
+ exports.defineHeadPlugin = defineHeadPlugin;
414
+ exports.getActiveHead = getActiveHead;
415
+ exports.normaliseEntryTags = normaliseEntryTags;
416
+ exports.normaliseTag = normaliseTag;
417
+ exports.setActiveHead = setActiveHead;
418
+ exports.useBase = useBase;
419
+ exports.useBodyAttrs = useBodyAttrs;
420
+ exports.useHead = useHead;
421
+ exports.useHtmlAttrs = useHtmlAttrs;
422
+ exports.useLink = useLink;
423
+ exports.useMeta = useMeta;
424
+ exports.useNoscript = useNoscript;
425
+ exports.useScript = useScript;
426
+ exports.useServerHead = useServerHead;
427
+ exports.useStyle = useStyle;
428
+ exports.useTitle = useTitle;
429
+ exports.useTitleTemplate = useTitleTemplate;
package/dist/index.d.ts CHANGED
@@ -29,7 +29,7 @@ declare let activeHead: HeadClient<any> | undefined;
29
29
  declare const setActiveHead: <T extends HeadClient<_unhead_schema.Head<_unhead_schema.SchemaAugmentations>>>(head: T | undefined) => T | undefined;
30
30
  declare const getActiveHead: <T extends HeadClient<_unhead_schema.Head<_unhead_schema.SchemaAugmentations>>>() => T;
31
31
 
32
- declare function createHead<T extends {} = Head>(options?: CreateHeadOptions): HeadClient<T>;
32
+ declare function createHead<T extends {} = Head>(options?: CreateHeadOptions): Promise<HeadClient<T>>;
33
33
 
34
34
  declare function defineHeadPlugin(plugin: HeadPlugin): HeadPlugin;
35
35
 
package/dist/index.mjs CHANGED
@@ -317,7 +317,7 @@ function normaliseEntryTags(e) {
317
317
  });
318
318
  }
319
319
 
320
- function createHead(options = {}) {
320
+ async function createHead(options = {}) {
321
321
  let entries = [];
322
322
  let _sde = {};
323
323
  let entryId = 0;
@@ -353,6 +353,7 @@ function createHead(options = {}) {
353
353
  _sde: {},
354
354
  ...options2
355
355
  });
356
+ hooks.callHook("entries:updated", head);
356
357
  return {
357
358
  dispose() {
358
359
  entries = entries.filter((e) => {
@@ -362,6 +363,7 @@ function createHead(options = {}) {
362
363
  e._sde = {};
363
364
  return false;
364
365
  });
366
+ hooks.callHook("entries:updated", head);
365
367
  },
366
368
  patch(input2) {
367
369
  entries = entries.map((e) => {
@@ -372,6 +374,7 @@ function createHead(options = {}) {
372
374
  }
373
375
  return e;
374
376
  });
377
+ hooks.callHook("entries:updated", head);
375
378
  }
376
379
  };
377
380
  },
@@ -389,6 +392,7 @@ function createHead(options = {}) {
389
392
  return resolveCtx.tags;
390
393
  }
391
394
  };
395
+ await head.hooks.callHook("init", head);
392
396
  setActiveHead(head);
393
397
  return head;
394
398
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "unhead",
3
3
  "type": "module",
4
- "version": "0.1.1",
4
+ "version": "0.1.2",
5
5
  "packageManager": "pnpm@7.14.0",
6
6
  "author": "Harlan Wilton <harlan@harlanzw.com>",
7
7
  "license": "MIT",
@@ -28,7 +28,7 @@
28
28
  "dist"
29
29
  ],
30
30
  "dependencies": {
31
- "@unhead/schema": "0.1.1",
31
+ "@unhead/schema": "0.1.2",
32
32
  "hookable": "^5.4.1"
33
33
  },
34
34
  "devDependencies": {