@ressjs/assets 0.6.0-experimental.0 → 0.6.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +69 -0
- package/dist/catalog.js +4 -1
- package/dist/client/entry.d.mts +2 -2
- package/dist/client/entry.mjs +179 -2
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.js +2 -1
- package/dist/components.d.ts +31 -3
- package/dist/components.js +197 -0
- package/dist/head.d.ts +11 -0
- package/dist/head.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +7 -1
- package/package.json +16 -7
package/LICENSE
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
ress.js Proprietary License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 gabrielspisso. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This license applies to the ress.js packages (the "Software"), published on npm
|
|
6
|
+
under the @ressjs and @gzzy scopes, in every version released from 0.6.0-rc.0
|
|
7
|
+
onwards. Earlier versions were released under the MIT License and remain
|
|
8
|
+
available under those terms.
|
|
9
|
+
|
|
10
|
+
1. GRANT
|
|
11
|
+
Subject to this license, the Licensor grants you a worldwide, non-exclusive,
|
|
12
|
+
non-transferable, non-sublicensable, royalty-free license to install and use
|
|
13
|
+
the Software, as distributed by the Licensor, to build, run and deploy your
|
|
14
|
+
own applications and services, including commercial ones, and to charge your
|
|
15
|
+
own customers for them.
|
|
16
|
+
|
|
17
|
+
2. RESTRICTIONS
|
|
18
|
+
You may not:
|
|
19
|
+
a) fork, copy, or redistribute the Software, in whole or in part, except for
|
|
20
|
+
the portions that are necessarily included in the build output of your own
|
|
21
|
+
application in order to run it;
|
|
22
|
+
b) publish, distribute or make available a modified version of the Software
|
|
23
|
+
or any work derived from it. Modifications for your own internal use are
|
|
24
|
+
allowed, but they remain subject to this license;
|
|
25
|
+
c) use the Software, or any part of it, to build, offer or support a
|
|
26
|
+
framework, library, tool or service that competes with the Software;
|
|
27
|
+
d) sell, sublicense, rent or host the Software itself as a product or service
|
|
28
|
+
for third parties, as opposed to hosting your own application built with
|
|
29
|
+
it;
|
|
30
|
+
e) remove or alter copyright, license or attribution notices;
|
|
31
|
+
f) use the name "ress.js" or the Licensor's names or marks to endorse or
|
|
32
|
+
promote your products without written permission.
|
|
33
|
+
|
|
34
|
+
3. SCAFFOLDING OUTPUT
|
|
35
|
+
Files that @ressjs/create-ress-app copies into your project (templates and
|
|
36
|
+
generated configuration) are yours: you may use, modify and distribute them
|
|
37
|
+
without restriction. This does not extend to the Software packages that the
|
|
38
|
+
generated project installs as dependencies.
|
|
39
|
+
|
|
40
|
+
4. SUPPORT
|
|
41
|
+
The Software is provided without any obligation of support, maintenance or
|
|
42
|
+
updates. The Licensor may offer support, maintenance, training or consulting
|
|
43
|
+
under a separate paid agreement; that agreement does not change this license
|
|
44
|
+
unless it says so explicitly and is signed by the Licensor.
|
|
45
|
+
|
|
46
|
+
5. OWNERSHIP
|
|
47
|
+
The Software is licensed, not sold. The Licensor retains all rights not
|
|
48
|
+
expressly granted here. Feedback and suggestions you send may be used by the
|
|
49
|
+
Licensor without obligation to you.
|
|
50
|
+
|
|
51
|
+
6. TERMINATION
|
|
52
|
+
This license ends automatically if you breach it. On termination you must
|
|
53
|
+
stop using the Software and delete your copies. Sections 4 to 8 survive
|
|
54
|
+
termination.
|
|
55
|
+
|
|
56
|
+
7. NO WARRANTY
|
|
57
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
58
|
+
IMPLIED, INCLUDING THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
59
|
+
PARTICULAR PURPOSE AND NON-INFRINGEMENT.
|
|
60
|
+
|
|
61
|
+
8. LIMITATION OF LIABILITY
|
|
62
|
+
TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE LICENSOR WILL NOT BE LIABLE FOR
|
|
63
|
+
ANY INDIRECT, INCIDENTAL, SPECIAL OR CONSEQUENTIAL DAMAGES, OR FOR LOSS OF
|
|
64
|
+
PROFITS, DATA OR BUSINESS, ARISING FROM THE USE OF OR INABILITY TO USE THE
|
|
65
|
+
SOFTWARE. THE LICENSOR'S TOTAL LIABILITY IS LIMITED TO THE AMOUNT YOU PAID
|
|
66
|
+
FOR THE SOFTWARE, WHICH MAY BE ZERO.
|
|
67
|
+
|
|
68
|
+
For other uses, including exceptions to section 2, contact the Licensor to
|
|
69
|
+
agree a separate license.
|
package/dist/catalog.js
CHANGED
|
@@ -21,7 +21,10 @@ exports.SUPPORTED_STYLE_EXTENSIONS = Object.freeze(['css', 'scss', 'sass', 'less
|
|
|
21
21
|
* (CSS antes que SCSS). Así el resultado no depende del orden del filesystem.
|
|
22
22
|
*/
|
|
23
23
|
async function discoverStyleVariants(base, registry = platform_1.defaultPlatformRegistry) {
|
|
24
|
-
|
|
24
|
+
// `base` es una ruta, no un patrón: `(shop)` o `[...slug]` son nombres de
|
|
25
|
+
// página y se escapan para que el glob los lea literalmente.
|
|
26
|
+
const pattern = fast_glob_1.default.escapePath(base.replace(/\\/g, '/'));
|
|
27
|
+
const files = await (0, fast_glob_1.default)(exports.SUPPORTED_STYLE_EXTENSIONS.flatMap((ext) => [`${pattern}.${ext}`, `${pattern}.*.${ext}`]), { absolute: false });
|
|
25
28
|
const priority = new Map(exports.SUPPORTED_STYLE_EXTENSIONS.map((ext, index) => [ext, index]));
|
|
26
29
|
const ordered = files.sort((a, b) => {
|
|
27
30
|
const extA = extensionOf(a);
|
package/dist/client/entry.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
export { Script, Style } from '../components.js';
|
|
1
|
+
export { Head, Script, Style } from '../components.js';
|
|
3
2
|
export type { ScriptProps, StyleProps } from '../components.js';
|
|
3
|
+
export type { HeadProps } from '../head.js';
|
package/dist/client/entry.mjs
CHANGED
|
@@ -1,2 +1,179 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// src/components.ts
|
|
2
|
+
import {
|
|
3
|
+
Children,
|
|
4
|
+
Fragment,
|
|
5
|
+
createContext,
|
|
6
|
+
createElement,
|
|
7
|
+
isValidElement,
|
|
8
|
+
useContext,
|
|
9
|
+
useEffect
|
|
10
|
+
} from "react";
|
|
11
|
+
var VALID_HEAD_TAGS = /* @__PURE__ */ new Set(["title", "meta", "link", "script", "style", "base", "noscript"]);
|
|
12
|
+
var HEAD_CONTEXT_KEY = /* @__PURE__ */ Symbol.for("@ressjs/assets/head-context");
|
|
13
|
+
var sharedGlobal = globalThis;
|
|
14
|
+
var HeadCollectorContext = sharedGlobal[HEAD_CONTEXT_KEY] ??= createContext(null);
|
|
15
|
+
function HeadCollectorProvider(props) {
|
|
16
|
+
return createElement(HeadCollectorContext.Provider, { value: props.collector }, props.children);
|
|
17
|
+
}
|
|
18
|
+
function Head({ children }) {
|
|
19
|
+
const collector = useContext(HeadCollectorContext);
|
|
20
|
+
const tags = collectHeadChildren(children);
|
|
21
|
+
if (collector) collector.push(...tags);
|
|
22
|
+
const signature = collector ? "" : headTagsSignature(tags);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (collector || typeof document === "undefined") return;
|
|
25
|
+
return applyHeadToDocument(tags);
|
|
26
|
+
}, [collector, signature]);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
function headTagsSignature(tags) {
|
|
30
|
+
return JSON.stringify(tags);
|
|
31
|
+
}
|
|
32
|
+
function collectHeadChildren(children) {
|
|
33
|
+
const tags = [];
|
|
34
|
+
Children.forEach(children, (child) => {
|
|
35
|
+
if (!isValidElement(child)) return;
|
|
36
|
+
if (child.type === Fragment) {
|
|
37
|
+
tags.push(...collectHeadChildren(child.props.children));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (typeof child.type !== "string" || !VALID_HEAD_TAGS.has(child.type)) return;
|
|
41
|
+
const raw = child.props;
|
|
42
|
+
const attrs = {};
|
|
43
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
44
|
+
if (name === "children" || name === "dangerouslySetInnerHTML") continue;
|
|
45
|
+
attrs[htmlAttributeName(name)] = value;
|
|
46
|
+
}
|
|
47
|
+
const content = textContent(raw.children) ?? innerHtml(raw.dangerouslySetInnerHTML);
|
|
48
|
+
tags.push({
|
|
49
|
+
type: child.type,
|
|
50
|
+
attrs,
|
|
51
|
+
...content !== void 0 ? { content } : {}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
return tags;
|
|
55
|
+
}
|
|
56
|
+
function clientSafeHeadTags(tags) {
|
|
57
|
+
return tags.filter((tag) => tag.type !== "script" || isJsonLdScript(tag));
|
|
58
|
+
}
|
|
59
|
+
function isJsonLdScript(tag) {
|
|
60
|
+
return String(tag.attrs.type ?? "").toLowerCase().trim() === "application/ld+json";
|
|
61
|
+
}
|
|
62
|
+
function applyHeadToDocument(tags) {
|
|
63
|
+
const inserted = [];
|
|
64
|
+
const displaced = [];
|
|
65
|
+
for (const tag of clientSafeHeadTags(tags)) {
|
|
66
|
+
const existing = findExistingHeadTag(tag);
|
|
67
|
+
if (existing) {
|
|
68
|
+
if (elementMatchesTag(existing, tag)) continue;
|
|
69
|
+
displaced.push({ node: existing, next: existing.nextSibling });
|
|
70
|
+
existing.remove();
|
|
71
|
+
}
|
|
72
|
+
const element = document.createElement(tag.type);
|
|
73
|
+
for (const [name, value] of Object.entries(tag.attrs)) {
|
|
74
|
+
if (value == null || value === false) continue;
|
|
75
|
+
if (value === true) element.setAttribute(name, "");
|
|
76
|
+
else element.setAttribute(name, String(value));
|
|
77
|
+
}
|
|
78
|
+
if (tag.content !== void 0) element.textContent = tag.content;
|
|
79
|
+
document.head.appendChild(element);
|
|
80
|
+
inserted.push(element);
|
|
81
|
+
}
|
|
82
|
+
return () => {
|
|
83
|
+
for (const element of inserted) element.remove();
|
|
84
|
+
for (const { node, next } of displaced.reverse()) document.head.insertBefore(node, next);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function elementMatchesTag(element, tag) {
|
|
88
|
+
if (element.localName !== tag.type) return false;
|
|
89
|
+
if ((tag.content ?? "") !== (element.textContent ?? "")) return false;
|
|
90
|
+
const expectedNames = Object.entries(tag.attrs).filter(([, value]) => value != null && value !== false).map(([name]) => name);
|
|
91
|
+
const actualNames = element.getAttributeNames?.() ?? [];
|
|
92
|
+
if (expectedNames.length !== actualNames.length) return false;
|
|
93
|
+
return expectedNames.every((name) => {
|
|
94
|
+
const value = tag.attrs[name];
|
|
95
|
+
const expected = value === true ? "" : String(value);
|
|
96
|
+
return element.getAttribute(name) === expected;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function textContent(value) {
|
|
100
|
+
if (value == null) return void 0;
|
|
101
|
+
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
102
|
+
if (Array.isArray(value)) {
|
|
103
|
+
const parts = value.map(textContent);
|
|
104
|
+
return parts.every((part) => part !== void 0) ? parts.join("") : void 0;
|
|
105
|
+
}
|
|
106
|
+
return void 0;
|
|
107
|
+
}
|
|
108
|
+
function innerHtml(value) {
|
|
109
|
+
if (!value || typeof value !== "object") return void 0;
|
|
110
|
+
const html = value.__html;
|
|
111
|
+
return html == null ? void 0 : String(html);
|
|
112
|
+
}
|
|
113
|
+
function htmlAttributeName(name) {
|
|
114
|
+
const known = {
|
|
115
|
+
charSet: "charset",
|
|
116
|
+
className: "class",
|
|
117
|
+
crossOrigin: "crossorigin",
|
|
118
|
+
httpEquiv: "http-equiv",
|
|
119
|
+
noModule: "nomodule"
|
|
120
|
+
};
|
|
121
|
+
return known[name] ?? name;
|
|
122
|
+
}
|
|
123
|
+
function findExistingHeadTag(tag) {
|
|
124
|
+
const candidates = Array.from(document.head.children);
|
|
125
|
+
if (tag.type === "title" || tag.type === "base") {
|
|
126
|
+
return candidates.find((element) => element.localName === tag.type) ?? null;
|
|
127
|
+
}
|
|
128
|
+
if (tag.type === "meta") {
|
|
129
|
+
if (tag.attrs.name != null) return findByAttribute(candidates, "meta", "name", tag.attrs.name);
|
|
130
|
+
if (tag.attrs.property != null) return findByAttribute(candidates, "meta", "property", tag.attrs.property);
|
|
131
|
+
if (tag.attrs.charset != null) {
|
|
132
|
+
return candidates.find((element) => element.localName === "meta" && element.hasAttribute("charset")) ?? null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (tag.type === "script" && tag.attrs.src != null) {
|
|
136
|
+
return findByAttribute(candidates, "script", "src", tag.attrs.src);
|
|
137
|
+
}
|
|
138
|
+
if (tag.type === "link" && String(tag.attrs.rel).toLowerCase() === "canonical") {
|
|
139
|
+
return candidates.find(
|
|
140
|
+
(element) => element.localName === "link" && element.getAttribute("rel")?.toLowerCase() === "canonical"
|
|
141
|
+
) ?? null;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
function findByAttribute(elements, tag, name, value) {
|
|
146
|
+
return elements.find(
|
|
147
|
+
(element) => element.localName === tag && element.getAttribute(name) === String(value)
|
|
148
|
+
) ?? null;
|
|
149
|
+
}
|
|
150
|
+
function Style({ children, media, nonce, title, id }) {
|
|
151
|
+
return createElement("style", {
|
|
152
|
+
...media ? { media } : {},
|
|
153
|
+
...nonce ? { nonce } : {},
|
|
154
|
+
...title ? { title } : {},
|
|
155
|
+
...id ? { id } : {},
|
|
156
|
+
"data-ress-style": "inline"
|
|
157
|
+
}, children);
|
|
158
|
+
}
|
|
159
|
+
function Script({ children, src, type, nonce, id, async, defer }) {
|
|
160
|
+
return createElement("script", {
|
|
161
|
+
...src ? { src } : {},
|
|
162
|
+
...type ? { type } : {},
|
|
163
|
+
...nonce ? { nonce } : {},
|
|
164
|
+
...id ? { id } : {},
|
|
165
|
+
...async ? { async: true } : {},
|
|
166
|
+
...defer ? { defer: true } : {},
|
|
167
|
+
"data-ress-script": children != null ? "inline" : "external"
|
|
168
|
+
}, children);
|
|
169
|
+
}
|
|
170
|
+
export {
|
|
171
|
+
Head,
|
|
172
|
+
HeadCollectorProvider,
|
|
173
|
+
Script,
|
|
174
|
+
Style,
|
|
175
|
+
applyHeadToDocument,
|
|
176
|
+
clientSafeHeadTags,
|
|
177
|
+
collectHeadChildren,
|
|
178
|
+
headTagsSignature
|
|
179
|
+
};
|
package/dist/client/index.d.ts
CHANGED
package/dist/client/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Style = exports.Script = void 0;
|
|
3
|
+
exports.Style = exports.Script = exports.Head = void 0;
|
|
4
4
|
var components_1 = require("../components");
|
|
5
|
+
Object.defineProperty(exports, "Head", { enumerable: true, get: function () { return components_1.Head; } });
|
|
5
6
|
Object.defineProperty(exports, "Script", { enumerable: true, get: function () { return components_1.Script; } });
|
|
6
7
|
Object.defineProperty(exports, "Style", { enumerable: true, get: function () { return components_1.Style; } });
|
package/dist/components.d.ts
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
1
|
+
import React, { type ReactNode } from 'react';
|
|
2
|
+
import type { CollectedHeadTag, HeadProps } from './head';
|
|
3
|
+
/** Internal provider used by the server renderer to isolate every request. */
|
|
4
|
+
export declare function HeadCollectorProvider(props: {
|
|
5
|
+
collector: CollectedHeadTag[];
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
}): React.FunctionComponentElement<React.ProviderProps<CollectedHeadTag[] | null>>;
|
|
8
|
+
/** Declares document-head elements next to the component that owns them. */
|
|
9
|
+
export declare function Head({ children }: HeadProps): null;
|
|
10
|
+
/**
|
|
11
|
+
* Firma estable del contenido de los tags. Se usa como dependencia del efecto
|
|
12
|
+
* de arriba en lugar de `children`, que es un objeto nuevo en cada render.
|
|
13
|
+
*/
|
|
14
|
+
export declare function headTagsSignature(tags: readonly CollectedHeadTag[]): string;
|
|
15
|
+
export declare function collectHeadChildren(children: ReactNode): CollectedHeadTag[];
|
|
16
|
+
/**
|
|
17
|
+
* Qué tags de un `<Head>` del cliente pueden aplicarse tal cual llegan.
|
|
18
|
+
*
|
|
19
|
+
* El servidor filtra `<script>` según `security.head.allowScriptTags` (ver
|
|
20
|
+
* `renderHeadTags` en `@ressjs/vite-router`), salvo JSON-LD, que no ejecuta
|
|
21
|
+
* nada. El `<Head>` del cliente no conoce esa configuración —no le llega al
|
|
22
|
+
* bundle del navegador—, así que en vez de asumir un default cualquiera toma
|
|
23
|
+
* la opción segura: nunca crea un `<script>` ejecutable. Si el SSR ya insertó
|
|
24
|
+
* uno legítimo, queda intacto porque `applyHeadToDocument` no lo toca; lo que
|
|
25
|
+
* se evita es que el propio cliente cree uno nuevo (o lo recree) al hidratar
|
|
26
|
+
* o re-renderizar.
|
|
27
|
+
*/
|
|
28
|
+
export declare function clientSafeHeadTags(tags: readonly CollectedHeadTag[]): CollectedHeadTag[];
|
|
29
|
+
export declare function applyHeadToDocument(tags: readonly CollectedHeadTag[]): () => void;
|
|
2
30
|
export interface StyleProps {
|
|
3
31
|
children?: ReactNode;
|
|
4
32
|
media?: string;
|
|
@@ -6,7 +34,7 @@ export interface StyleProps {
|
|
|
6
34
|
title?: string;
|
|
7
35
|
id?: string;
|
|
8
36
|
}
|
|
9
|
-
export declare function Style({ children, media, nonce, title, id }: StyleProps):
|
|
37
|
+
export declare function Style({ children, media, nonce, title, id }: StyleProps): React.DetailedReactHTMLElement<{
|
|
10
38
|
'data-ress-style': string;
|
|
11
39
|
id?: string | undefined;
|
|
12
40
|
title?: string | undefined;
|
|
@@ -22,7 +50,7 @@ export interface ScriptProps {
|
|
|
22
50
|
async?: boolean;
|
|
23
51
|
defer?: boolean;
|
|
24
52
|
}
|
|
25
|
-
export declare function Script({ children, src, type, nonce, id, async, defer }: ScriptProps):
|
|
53
|
+
export declare function Script({ children, src, type, nonce, id, async, defer }: ScriptProps): React.DetailedReactHTMLElement<{
|
|
26
54
|
'data-ress-script': string;
|
|
27
55
|
defer?: boolean | undefined;
|
|
28
56
|
async?: boolean | undefined;
|
package/dist/components.js
CHANGED
|
@@ -1,8 +1,205 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HeadCollectorProvider = HeadCollectorProvider;
|
|
4
|
+
exports.Head = Head;
|
|
5
|
+
exports.headTagsSignature = headTagsSignature;
|
|
6
|
+
exports.collectHeadChildren = collectHeadChildren;
|
|
7
|
+
exports.clientSafeHeadTags = clientSafeHeadTags;
|
|
8
|
+
exports.applyHeadToDocument = applyHeadToDocument;
|
|
3
9
|
exports.Style = Style;
|
|
4
10
|
exports.Script = Script;
|
|
5
11
|
const react_1 = require("react");
|
|
12
|
+
const VALID_HEAD_TAGS = new Set(['title', 'meta', 'link', 'script', 'style', 'base', 'noscript']);
|
|
13
|
+
const HEAD_CONTEXT_KEY = Symbol.for('@ressjs/assets/head-context');
|
|
14
|
+
const sharedGlobal = globalThis;
|
|
15
|
+
// Vite can bundle the page and router separately during SSR. A global symbol
|
|
16
|
+
// lets both module instances find the collector for the current React tree.
|
|
17
|
+
const HeadCollectorContext = sharedGlobal[HEAD_CONTEXT_KEY] ??=
|
|
18
|
+
(0, react_1.createContext)(null);
|
|
19
|
+
/** Internal provider used by the server renderer to isolate every request. */
|
|
20
|
+
function HeadCollectorProvider(props) {
|
|
21
|
+
return (0, react_1.createElement)(HeadCollectorContext.Provider, { value: props.collector }, props.children);
|
|
22
|
+
}
|
|
23
|
+
/** Declares document-head elements next to the component that owns them. */
|
|
24
|
+
function Head({ children }) {
|
|
25
|
+
const collector = (0, react_1.useContext)(HeadCollectorContext);
|
|
26
|
+
const tags = collectHeadChildren(children);
|
|
27
|
+
if (collector)
|
|
28
|
+
collector.push(...tags);
|
|
29
|
+
// `children` es un objeto nuevo en cada render aunque su contenido sea el
|
|
30
|
+
// mismo. Si el efecto dependiera de `children` (o de `tags`, otro array
|
|
31
|
+
// nuevo por render), un <Head> dentro de un componente con estado limpiaría
|
|
32
|
+
// y volvería a insertar sus tags en CADA re-render — y un <script> creado
|
|
33
|
+
// con `document.createElement` se ejecuta de nuevo cada vez que se inserta.
|
|
34
|
+
// La firma serializada del contenido sí es estable mientras no cambie nada.
|
|
35
|
+
const signature = collector ? '' : headTagsSignature(tags);
|
|
36
|
+
(0, react_1.useEffect)(() => {
|
|
37
|
+
if (collector || typeof document === 'undefined')
|
|
38
|
+
return;
|
|
39
|
+
return applyHeadToDocument(tags);
|
|
40
|
+
// La firma reemplaza a `children`/`tags` a propósito: sólo importa si el
|
|
41
|
+
// CONTENIDO cambió, no la identidad del array que arma cada render.
|
|
42
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
|
+
}, [collector, signature]);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Firma estable del contenido de los tags. Se usa como dependencia del efecto
|
|
48
|
+
* de arriba en lugar de `children`, que es un objeto nuevo en cada render.
|
|
49
|
+
*/
|
|
50
|
+
function headTagsSignature(tags) {
|
|
51
|
+
return JSON.stringify(tags);
|
|
52
|
+
}
|
|
53
|
+
function collectHeadChildren(children) {
|
|
54
|
+
const tags = [];
|
|
55
|
+
react_1.Children.forEach(children, (child) => {
|
|
56
|
+
if (!(0, react_1.isValidElement)(child))
|
|
57
|
+
return;
|
|
58
|
+
if (child.type === react_1.Fragment) {
|
|
59
|
+
tags.push(...collectHeadChildren(child.props.children));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (typeof child.type !== 'string' || !VALID_HEAD_TAGS.has(child.type))
|
|
63
|
+
return;
|
|
64
|
+
const raw = child.props;
|
|
65
|
+
const attrs = {};
|
|
66
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
67
|
+
if (name === 'children' || name === 'dangerouslySetInnerHTML')
|
|
68
|
+
continue;
|
|
69
|
+
attrs[htmlAttributeName(name)] = value;
|
|
70
|
+
}
|
|
71
|
+
const content = textContent(raw.children) ?? innerHtml(raw.dangerouslySetInnerHTML);
|
|
72
|
+
tags.push({
|
|
73
|
+
type: child.type,
|
|
74
|
+
attrs,
|
|
75
|
+
...(content !== undefined ? { content } : {}),
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
return tags;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Qué tags de un `<Head>` del cliente pueden aplicarse tal cual llegan.
|
|
82
|
+
*
|
|
83
|
+
* El servidor filtra `<script>` según `security.head.allowScriptTags` (ver
|
|
84
|
+
* `renderHeadTags` en `@ressjs/vite-router`), salvo JSON-LD, que no ejecuta
|
|
85
|
+
* nada. El `<Head>` del cliente no conoce esa configuración —no le llega al
|
|
86
|
+
* bundle del navegador—, así que en vez de asumir un default cualquiera toma
|
|
87
|
+
* la opción segura: nunca crea un `<script>` ejecutable. Si el SSR ya insertó
|
|
88
|
+
* uno legítimo, queda intacto porque `applyHeadToDocument` no lo toca; lo que
|
|
89
|
+
* se evita es que el propio cliente cree uno nuevo (o lo recree) al hidratar
|
|
90
|
+
* o re-renderizar.
|
|
91
|
+
*/
|
|
92
|
+
function clientSafeHeadTags(tags) {
|
|
93
|
+
return tags.filter((tag) => tag.type !== 'script' || isJsonLdScript(tag));
|
|
94
|
+
}
|
|
95
|
+
function isJsonLdScript(tag) {
|
|
96
|
+
return String(tag.attrs.type ?? '').toLowerCase().trim() === 'application/ld+json';
|
|
97
|
+
}
|
|
98
|
+
function applyHeadToDocument(tags) {
|
|
99
|
+
const inserted = [];
|
|
100
|
+
const displaced = [];
|
|
101
|
+
for (const tag of clientSafeHeadTags(tags)) {
|
|
102
|
+
const existing = findExistingHeadTag(tag);
|
|
103
|
+
if (existing) {
|
|
104
|
+
// Si ya está exactamente lo que se quiere insertar, no se toca: para un
|
|
105
|
+
// <script src> ya renderizado en el SSR, sacarlo y volver a ponerlo lo
|
|
106
|
+
// ejecuta una segunda vez.
|
|
107
|
+
if (elementMatchesTag(existing, tag))
|
|
108
|
+
continue;
|
|
109
|
+
displaced.push({ node: existing, next: existing.nextSibling });
|
|
110
|
+
existing.remove();
|
|
111
|
+
}
|
|
112
|
+
const element = document.createElement(tag.type);
|
|
113
|
+
for (const [name, value] of Object.entries(tag.attrs)) {
|
|
114
|
+
if (value == null || value === false)
|
|
115
|
+
continue;
|
|
116
|
+
if (value === true)
|
|
117
|
+
element.setAttribute(name, '');
|
|
118
|
+
else
|
|
119
|
+
element.setAttribute(name, String(value));
|
|
120
|
+
}
|
|
121
|
+
if (tag.content !== undefined)
|
|
122
|
+
element.textContent = tag.content;
|
|
123
|
+
document.head.appendChild(element);
|
|
124
|
+
inserted.push(element);
|
|
125
|
+
}
|
|
126
|
+
return () => {
|
|
127
|
+
for (const element of inserted)
|
|
128
|
+
element.remove();
|
|
129
|
+
for (const { node, next } of displaced.reverse())
|
|
130
|
+
document.head.insertBefore(node, next);
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** Compara un elemento del `<head>` con el tag que se quiere insertar. */
|
|
134
|
+
function elementMatchesTag(element, tag) {
|
|
135
|
+
if (element.localName !== tag.type)
|
|
136
|
+
return false;
|
|
137
|
+
if ((tag.content ?? '') !== (element.textContent ?? ''))
|
|
138
|
+
return false;
|
|
139
|
+
const expectedNames = Object.entries(tag.attrs)
|
|
140
|
+
.filter(([, value]) => value != null && value !== false)
|
|
141
|
+
.map(([name]) => name);
|
|
142
|
+
const actualNames = element.getAttributeNames?.() ?? [];
|
|
143
|
+
if (expectedNames.length !== actualNames.length)
|
|
144
|
+
return false;
|
|
145
|
+
return expectedNames.every((name) => {
|
|
146
|
+
const value = tag.attrs[name];
|
|
147
|
+
const expected = value === true ? '' : String(value);
|
|
148
|
+
return element.getAttribute(name) === expected;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function textContent(value) {
|
|
152
|
+
if (value == null)
|
|
153
|
+
return undefined;
|
|
154
|
+
if (typeof value === 'string' || typeof value === 'number')
|
|
155
|
+
return String(value);
|
|
156
|
+
if (Array.isArray(value)) {
|
|
157
|
+
const parts = value.map(textContent);
|
|
158
|
+
return parts.every((part) => part !== undefined) ? parts.join('') : undefined;
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
function innerHtml(value) {
|
|
163
|
+
if (!value || typeof value !== 'object')
|
|
164
|
+
return undefined;
|
|
165
|
+
const html = value.__html;
|
|
166
|
+
return html == null ? undefined : String(html);
|
|
167
|
+
}
|
|
168
|
+
function htmlAttributeName(name) {
|
|
169
|
+
const known = {
|
|
170
|
+
charSet: 'charset',
|
|
171
|
+
className: 'class',
|
|
172
|
+
crossOrigin: 'crossorigin',
|
|
173
|
+
httpEquiv: 'http-equiv',
|
|
174
|
+
noModule: 'nomodule',
|
|
175
|
+
};
|
|
176
|
+
return known[name] ?? name;
|
|
177
|
+
}
|
|
178
|
+
function findExistingHeadTag(tag) {
|
|
179
|
+
const candidates = Array.from(document.head.children);
|
|
180
|
+
if (tag.type === 'title' || tag.type === 'base') {
|
|
181
|
+
return candidates.find((element) => element.localName === tag.type) ?? null;
|
|
182
|
+
}
|
|
183
|
+
if (tag.type === 'meta') {
|
|
184
|
+
if (tag.attrs.name != null)
|
|
185
|
+
return findByAttribute(candidates, 'meta', 'name', tag.attrs.name);
|
|
186
|
+
if (tag.attrs.property != null)
|
|
187
|
+
return findByAttribute(candidates, 'meta', 'property', tag.attrs.property);
|
|
188
|
+
if (tag.attrs.charset != null) {
|
|
189
|
+
return candidates.find((element) => element.localName === 'meta' && element.hasAttribute('charset')) ?? null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (tag.type === 'script' && tag.attrs.src != null) {
|
|
193
|
+
return findByAttribute(candidates, 'script', 'src', tag.attrs.src);
|
|
194
|
+
}
|
|
195
|
+
if (tag.type === 'link' && String(tag.attrs.rel).toLowerCase() === 'canonical') {
|
|
196
|
+
return candidates.find((element) => element.localName === 'link' && element.getAttribute('rel')?.toLowerCase() === 'canonical') ?? null;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
function findByAttribute(elements, tag, name, value) {
|
|
201
|
+
return elements.find((element) => element.localName === tag && element.getAttribute(name) === String(value)) ?? null;
|
|
202
|
+
}
|
|
6
203
|
function Style({ children, media, nonce, title, id }) {
|
|
7
204
|
return (0, react_1.createElement)('style', {
|
|
8
205
|
...(media ? { media } : {}),
|
package/dist/head.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export type HeadAttributes = Record<string, string | number | boolean | null | undefined>;
|
|
3
|
+
export type HeadTagName = 'title' | 'meta' | 'link' | 'script' | 'style' | 'base' | 'noscript';
|
|
4
|
+
export interface CollectedHeadTag {
|
|
5
|
+
type: HeadTagName;
|
|
6
|
+
attrs: HeadAttributes;
|
|
7
|
+
content?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface HeadProps {
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
}
|
package/dist/head.js
ADDED
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { SUPPORTED_STYLE_EXTENSIONS, discoverStyleVariants } from './catalog';
|
|
2
2
|
export { resolveAssetVariant } from './resolve';
|
|
3
3
|
export { resolvePageAssets } from './page-assets';
|
|
4
|
-
export { Script, Style } from './components';
|
|
4
|
+
export { Head, HeadCollectorProvider, Script, Style, applyHeadToDocument, clientSafeHeadTags, collectHeadChildren, headTagsSignature, } from './components';
|
|
5
5
|
export type { AssetCatalog, AssetDefinition, AssetEntry, AssetIndex, AssetResolverOptions, AssetVariant, ResolvedAssetLinks } from './types';
|
|
6
6
|
export type { ScriptProps, StyleProps } from './components';
|
|
7
|
+
export type { CollectedHeadTag, HeadAttributes, HeadProps, HeadTagName } from './head';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Style = exports.Script = exports.resolvePageAssets = exports.resolveAssetVariant = exports.discoverStyleVariants = exports.SUPPORTED_STYLE_EXTENSIONS = void 0;
|
|
3
|
+
exports.headTagsSignature = exports.collectHeadChildren = exports.clientSafeHeadTags = exports.applyHeadToDocument = exports.Style = exports.Script = exports.HeadCollectorProvider = exports.Head = exports.resolvePageAssets = exports.resolveAssetVariant = exports.discoverStyleVariants = exports.SUPPORTED_STYLE_EXTENSIONS = void 0;
|
|
4
4
|
var catalog_1 = require("./catalog");
|
|
5
5
|
Object.defineProperty(exports, "SUPPORTED_STYLE_EXTENSIONS", { enumerable: true, get: function () { return catalog_1.SUPPORTED_STYLE_EXTENSIONS; } });
|
|
6
6
|
Object.defineProperty(exports, "discoverStyleVariants", { enumerable: true, get: function () { return catalog_1.discoverStyleVariants; } });
|
|
@@ -9,5 +9,11 @@ Object.defineProperty(exports, "resolveAssetVariant", { enumerable: true, get: f
|
|
|
9
9
|
var page_assets_1 = require("./page-assets");
|
|
10
10
|
Object.defineProperty(exports, "resolvePageAssets", { enumerable: true, get: function () { return page_assets_1.resolvePageAssets; } });
|
|
11
11
|
var components_1 = require("./components");
|
|
12
|
+
Object.defineProperty(exports, "Head", { enumerable: true, get: function () { return components_1.Head; } });
|
|
13
|
+
Object.defineProperty(exports, "HeadCollectorProvider", { enumerable: true, get: function () { return components_1.HeadCollectorProvider; } });
|
|
12
14
|
Object.defineProperty(exports, "Script", { enumerable: true, get: function () { return components_1.Script; } });
|
|
13
15
|
Object.defineProperty(exports, "Style", { enumerable: true, get: function () { return components_1.Style; } });
|
|
16
|
+
Object.defineProperty(exports, "applyHeadToDocument", { enumerable: true, get: function () { return components_1.applyHeadToDocument; } });
|
|
17
|
+
Object.defineProperty(exports, "clientSafeHeadTags", { enumerable: true, get: function () { return components_1.clientSafeHeadTags; } });
|
|
18
|
+
Object.defineProperty(exports, "collectHeadChildren", { enumerable: true, get: function () { return components_1.collectHeadChildren; } });
|
|
19
|
+
Object.defineProperty(exports, "headTagsSignature", { enumerable: true, get: function () { return components_1.headTagsSignature; } });
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ressjs/assets",
|
|
3
|
-
"version": "0.6.0-
|
|
3
|
+
"version": "0.6.0-rc.0",
|
|
4
4
|
"description": "Assets de ress.js: variantes de plataforma, Style y Script",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
5
6
|
"types": "dist/index.d.ts",
|
|
6
7
|
"main": "dist/index.js",
|
|
7
8
|
"exports": {
|
|
@@ -11,17 +12,22 @@
|
|
|
11
12
|
"import": "./dist/index.js"
|
|
12
13
|
},
|
|
13
14
|
"./client": {
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
"import": {
|
|
16
|
+
"types": "./dist/client/entry.d.mts",
|
|
17
|
+
"default": "./dist/client/entry.mjs"
|
|
18
|
+
},
|
|
19
|
+
"require": {
|
|
20
|
+
"types": "./dist/client/index.d.ts",
|
|
21
|
+
"default": "./dist/client/index.js"
|
|
22
|
+
}
|
|
17
23
|
}
|
|
18
24
|
},
|
|
19
25
|
"files": [
|
|
20
26
|
"dist"
|
|
21
27
|
],
|
|
22
28
|
"scripts": {
|
|
23
|
-
"build": "tsc",
|
|
24
|
-
"dev": "
|
|
29
|
+
"build": "tsc && esbuild src/components.ts --bundle --platform=browser --format=esm --external:react --outfile=dist/client/entry.mjs",
|
|
30
|
+
"dev": "node scripts/watch.mjs",
|
|
25
31
|
"clean": "rm -rf dist",
|
|
26
32
|
"test": "vitest run",
|
|
27
33
|
"prepublishOnly": "npm run build"
|
|
@@ -30,12 +36,15 @@
|
|
|
30
36
|
"access": "public"
|
|
31
37
|
},
|
|
32
38
|
"dependencies": {
|
|
33
|
-
"@ressjs/platform": "0.6.0-
|
|
39
|
+
"@ressjs/platform": "0.6.0-rc.0",
|
|
34
40
|
"fast-glob": "^3.3.3"
|
|
35
41
|
},
|
|
36
42
|
"peerDependencies": {
|
|
37
43
|
"react": "^19.0.0"
|
|
38
44
|
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"esbuild": "^0.28.2"
|
|
47
|
+
},
|
|
39
48
|
"engines": {
|
|
40
49
|
"node": ">=20.19.0"
|
|
41
50
|
}
|