native-sfc 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/index.bundled.js +2317 -0
- package/dist/index.cdn.js +272 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +272 -0
- package/package.json +36 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// dependencies loaded from esm.sh
|
|
2
|
+
|
|
3
|
+
// src/components.ts
|
|
4
|
+
import * as stackTraceParser2 from "https://esm.sh/stacktrace-parser";
|
|
5
|
+
|
|
6
|
+
// src/rewriter.ts
|
|
7
|
+
import { parse } from "https://esm.sh/es-module-lexer/js";
|
|
8
|
+
import * as stackTraceParser from "https://esm.sh/stacktrace-parser";
|
|
9
|
+
function rewriteModule(code, sourceUrl) {
|
|
10
|
+
const [imports] = parse(code);
|
|
11
|
+
const rewritableImports = imports.filter((i) => {
|
|
12
|
+
const specifier = code.slice(i.s, i.e);
|
|
13
|
+
return !isBrowserUrl(specifier) && !specifier.startsWith("data:");
|
|
14
|
+
});
|
|
15
|
+
for (const importEntry of rewritableImports.reverse()) {
|
|
16
|
+
const specifier = code.slice(importEntry.s, importEntry.e);
|
|
17
|
+
let rewritten = specifier;
|
|
18
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
19
|
+
rewritten = new URL(specifier, sourceUrl).href;
|
|
20
|
+
} else {
|
|
21
|
+
rewritten = `https://esm.sh/${specifier}`;
|
|
22
|
+
}
|
|
23
|
+
code = code.slice(0, importEntry.s) + rewritten + code.slice(importEntry.e);
|
|
24
|
+
}
|
|
25
|
+
return `import.meta.url=${JSON.stringify(sourceUrl)};
|
|
26
|
+
${code}`;
|
|
27
|
+
}
|
|
28
|
+
function isBrowserUrl(url) {
|
|
29
|
+
return url.startsWith("http://") || url.startsWith("https://") || url.startsWith("blob:http://") || url.startsWith("blob:https://") || url.startsWith("data:");
|
|
30
|
+
}
|
|
31
|
+
var blobMap = /* @__PURE__ */ new Map();
|
|
32
|
+
async function esm(code, sourceUrl) {
|
|
33
|
+
code = rewriteModule(code, sourceUrl);
|
|
34
|
+
const blob = new Blob([code], { type: "text/javascript" });
|
|
35
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
36
|
+
blobMap.set(blobUrl, sourceUrl);
|
|
37
|
+
try {
|
|
38
|
+
const module = await import(blobUrl);
|
|
39
|
+
return module;
|
|
40
|
+
} finally {
|
|
41
|
+
URL.revokeObjectURL(blobUrl);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function getImporterUrl() {
|
|
45
|
+
const stack = stackTraceParser.parse(new Error().stack);
|
|
46
|
+
for (const { file } of stack) {
|
|
47
|
+
if (file && file !== import.meta.url) {
|
|
48
|
+
if (file.startsWith("blob:")) {
|
|
49
|
+
if (blobMap.has(file)) {
|
|
50
|
+
return blobMap.get(file);
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
return file;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/error.ts
|
|
61
|
+
var NativeSFCError = class extends Error {
|
|
62
|
+
constructor(message, options) {
|
|
63
|
+
super(message, options);
|
|
64
|
+
this.name = "NativeSFCError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
function warn(...args) {
|
|
68
|
+
console.warn("NativeSFC Warning:", ...args);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/network.ts
|
|
72
|
+
var fetch = globalThis.fetch;
|
|
73
|
+
function defineFetch(customFetch) {
|
|
74
|
+
fetch = customFetch;
|
|
75
|
+
}
|
|
76
|
+
async function requestText(url, userFriendlySource) {
|
|
77
|
+
return request(url, userFriendlySource).then((res) => res.text());
|
|
78
|
+
}
|
|
79
|
+
async function request(url, userFriendlySource) {
|
|
80
|
+
let response;
|
|
81
|
+
try {
|
|
82
|
+
response = await fetch(url);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
throw new NativeSFCError(`Failed to fetch ${url} at ${userFriendlySource}`, {
|
|
85
|
+
cause: error
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
throw new NativeSFCError(`Failed to fetch ${url} at ${userFriendlySource}`, {
|
|
90
|
+
cause: new Error(`HTTP status ${response.status}`)
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return response;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/events.ts
|
|
97
|
+
var eventTarget = new EventTarget();
|
|
98
|
+
function emit(eventName, detail) {
|
|
99
|
+
const event = new CustomEvent(eventName, { detail });
|
|
100
|
+
eventTarget.dispatchEvent(event);
|
|
101
|
+
}
|
|
102
|
+
function on(eventName, listener) {
|
|
103
|
+
eventTarget.addEventListener(eventName, listener);
|
|
104
|
+
return () => {
|
|
105
|
+
eventTarget.removeEventListener(eventName, listener);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/components.ts
|
|
110
|
+
var loadedComponentsRecord = /* @__PURE__ */ new Map();
|
|
111
|
+
async function loadComponent(name, url, afterConstructor) {
|
|
112
|
+
const importerUrl = getImporterUrl() || location.href;
|
|
113
|
+
url = new URL(url, importerUrl).href;
|
|
114
|
+
emit("component-loading", { name, url });
|
|
115
|
+
if (customElements.get(name)) {
|
|
116
|
+
if (!loadedComponentsRecord.has(name)) {
|
|
117
|
+
throw new NativeSFCError(`Component name ${JSON.stringify(name)} is already being used`);
|
|
118
|
+
}
|
|
119
|
+
const loadedComponentRecord = loadedComponentsRecord.get(name);
|
|
120
|
+
if (loadedComponentRecord.url === url) {
|
|
121
|
+
return loadedComponentRecord.cec;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const html = await requestText(
|
|
125
|
+
url,
|
|
126
|
+
`loadComponent(${JSON.stringify(name)}, ${JSON.stringify(url)})`
|
|
127
|
+
);
|
|
128
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
129
|
+
filterGlobalStyle(doc);
|
|
130
|
+
rewriteStyleAndScript(doc, url);
|
|
131
|
+
const adoptedStyleSheets = await collectAdoptedStyleSheets(doc);
|
|
132
|
+
const result = await evaluateModules(doc, url);
|
|
133
|
+
for (const el of doc.querySelectorAll("script")) {
|
|
134
|
+
doc.body.prepend(el);
|
|
135
|
+
}
|
|
136
|
+
const component = result.default;
|
|
137
|
+
const defaultExportIsComponent = component?.prototype instanceof HTMLElement;
|
|
138
|
+
if (component && !defaultExportIsComponent) {
|
|
139
|
+
warn(
|
|
140
|
+
`The default export of component ${JSON.stringify(name)} loaded from ${url} is not a web component class`,
|
|
141
|
+
component
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const define = (component2) => {
|
|
145
|
+
const cec = extendsElement(component2, doc.body.innerHTML, adoptedStyleSheets, afterConstructor);
|
|
146
|
+
customElements.define(name, cec);
|
|
147
|
+
emit("component-defined", { name, url });
|
|
148
|
+
loadedComponentsRecord.set(name, { cec, url });
|
|
149
|
+
return cec;
|
|
150
|
+
};
|
|
151
|
+
if (!component || !defaultExportIsComponent) {
|
|
152
|
+
return define(HTMLElement);
|
|
153
|
+
} else {
|
|
154
|
+
return define(component);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function extendsElement(BaseClass = HTMLElement, innerHTML, adoptedStyleSheets, afterConstructor) {
|
|
158
|
+
return class extends BaseClass {
|
|
159
|
+
constructor(...args) {
|
|
160
|
+
super(innerHTML, adoptedStyleSheets);
|
|
161
|
+
if (!this.shadowRoot) {
|
|
162
|
+
const shadowRoot = this.attachShadow({ mode: "open" });
|
|
163
|
+
shadowRoot.innerHTML = innerHTML;
|
|
164
|
+
if (adoptedStyleSheets && adoptedStyleSheets.length > 0) {
|
|
165
|
+
shadowRoot.adoptedStyleSheets = adoptedStyleSheets;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (afterConstructor) {
|
|
169
|
+
afterConstructor.call(this);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function defineComponent(fc) {
|
|
175
|
+
const whoDefineMe = stackTraceParser2.parse(new Error().stack).at(-1).file;
|
|
176
|
+
if (blobMap.has(whoDefineMe)) {
|
|
177
|
+
return class extends HTMLElement {
|
|
178
|
+
connectedCallback() {
|
|
179
|
+
fc.call(this, this.shadowRoot || this.attachShadow({ mode: "open" }));
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return fc.call(globalThis, document);
|
|
184
|
+
}
|
|
185
|
+
function filterGlobalStyle(doc) {
|
|
186
|
+
for (const styleElement of doc.querySelectorAll("style")) {
|
|
187
|
+
if (styleElement.hasAttribute("global")) {
|
|
188
|
+
document.head.append(styleElement);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const linkElement of doc.querySelectorAll('link[rel="stylesheet"]')) {
|
|
192
|
+
if (linkElement.hasAttribute("global")) {
|
|
193
|
+
document.head.append(linkElement);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function cssStyleSheetFromText(styleText, userFriendlySource) {
|
|
198
|
+
const sheet = new CSSStyleSheet();
|
|
199
|
+
try {
|
|
200
|
+
sheet.replaceSync(styleText);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
warn(`Failed to create CSSStyleSheet at ${userFriendlySource}`, error);
|
|
203
|
+
}
|
|
204
|
+
return sheet;
|
|
205
|
+
}
|
|
206
|
+
async function collectAdoptedStyleSheets(doc) {
|
|
207
|
+
const adoptedStyleSheets = [];
|
|
208
|
+
for (const link of doc.querySelectorAll(`link[rel="stylesheet"]`)) {
|
|
209
|
+
const styleText = await requestText(link.href, link.outerHTML).catch(
|
|
210
|
+
(error) => {
|
|
211
|
+
warn(`Failed to load ${link.outerHTML}`, error);
|
|
212
|
+
return "";
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
if (!styleText) continue;
|
|
216
|
+
const sheet = cssStyleSheetFromText(styleText, link.outerHTML);
|
|
217
|
+
adoptedStyleSheets.push(sheet);
|
|
218
|
+
link.remove();
|
|
219
|
+
}
|
|
220
|
+
for (const style of doc.querySelectorAll("style")) {
|
|
221
|
+
const styleText = style.innerHTML;
|
|
222
|
+
if (!styleText) continue;
|
|
223
|
+
const sheet = cssStyleSheetFromText(styleText, style.outerHTML);
|
|
224
|
+
adoptedStyleSheets.push(sheet);
|
|
225
|
+
style.remove();
|
|
226
|
+
}
|
|
227
|
+
return adoptedStyleSheets;
|
|
228
|
+
}
|
|
229
|
+
function rewriteStyleAndScript(doc, url) {
|
|
230
|
+
for (const script of doc.querySelectorAll("script[src]")) {
|
|
231
|
+
const src = new URL(
|
|
232
|
+
script.getAttribute("src") || "",
|
|
233
|
+
// getAttribute to avoid getting absolute URL directly, do not use script.src
|
|
234
|
+
url
|
|
235
|
+
).href;
|
|
236
|
+
script.src = src;
|
|
237
|
+
}
|
|
238
|
+
for (const link of doc.querySelectorAll("link[href]")) {
|
|
239
|
+
const href = new URL(
|
|
240
|
+
link.getAttribute("href") || "",
|
|
241
|
+
// getAttribute to avoid getting absolute URL directly, do not use link.href
|
|
242
|
+
url
|
|
243
|
+
).href;
|
|
244
|
+
link.href = href;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function evaluateModules(doc, url) {
|
|
248
|
+
const result = {};
|
|
249
|
+
for (const script of doc.querySelectorAll('script[type="module"]')) {
|
|
250
|
+
const src = script.src;
|
|
251
|
+
if (src) {
|
|
252
|
+
const res = await request(src, script.outerHTML);
|
|
253
|
+
const code = await res.text();
|
|
254
|
+
const module = await esm(code, res.url);
|
|
255
|
+
Object.assign(result, module);
|
|
256
|
+
} else {
|
|
257
|
+
const module = await esm(script.textContent, url);
|
|
258
|
+
Object.assign(result, module);
|
|
259
|
+
}
|
|
260
|
+
script.remove();
|
|
261
|
+
}
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
export {
|
|
265
|
+
NativeSFCError,
|
|
266
|
+
defineComponent,
|
|
267
|
+
defineFetch,
|
|
268
|
+
loadComponent,
|
|
269
|
+
on
|
|
270
|
+
};
|
|
271
|
+
//! we provide an extra argument to user's component constructor
|
|
272
|
+
//! if the user's constructor does not create a shadow root, we will create one here
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Generated by dts-bundle-generator v9.5.1
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Component loading and registration system for Native SFC (Single File Components).
|
|
5
|
+
*
|
|
6
|
+
* This module provides the core functionality to:
|
|
7
|
+
* - Load HTML-based web components from external files
|
|
8
|
+
* - Parse and process component templates, styles, and scripts
|
|
9
|
+
* - Register custom elements with the browser's CustomElementRegistry
|
|
10
|
+
* - Handle style encapsulation via Shadow DOM and adoptedStyleSheets
|
|
11
|
+
*
|
|
12
|
+
* The component loading process:
|
|
13
|
+
* 1. Fetch the component HTML file
|
|
14
|
+
* 2. Extract and process global styles (move to document head)
|
|
15
|
+
* 3. Rewrite relative URLs in scripts and stylesheets to absolute URLs
|
|
16
|
+
* 4. Convert styles to CSSStyleSheet objects for adoptedStyleSheets (prevents FOUC)
|
|
17
|
+
* 5. Evaluate ES modules and extract the default export (component class)
|
|
18
|
+
* 6. Create an extended component class that injects shadow root with template
|
|
19
|
+
* 7. Register the component with customElements.define()
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Load and register a web component from an HTML file.
|
|
23
|
+
*
|
|
24
|
+
* This is the primary entry point for loading Native SFC components.
|
|
25
|
+
* The HTML file can contain:
|
|
26
|
+
* - Template markup (the component's shadow DOM content)
|
|
27
|
+
* - `<style>` tags (scoped to the component's shadow DOM)
|
|
28
|
+
* - `<style global>` tags (injected into the main document)
|
|
29
|
+
* - `<script type="module">` (ES modules, default export should be the component class)
|
|
30
|
+
* - `<script>` (classic scripts, executed when component is instantiated)
|
|
31
|
+
* - `<link rel="stylesheet">` (external stylesheets, also supports `global` attribute)
|
|
32
|
+
*
|
|
33
|
+
* @param name - The custom element tag name (must contain a hyphen, e.g., "my-component")
|
|
34
|
+
* @param url - URL to the component HTML file (relative to the importer or absolute)
|
|
35
|
+
* @param afterConstructor - Optional callback invoked after component constructor completes
|
|
36
|
+
* @returns The CustomElementConstructor for the registered component
|
|
37
|
+
* @throws {NativeSFCError} If the component name is already registered by external code
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // Load and use a component
|
|
41
|
+
* await loadComponent('my-button', './components/my-button.html');
|
|
42
|
+
* document.body.innerHTML = '<my-button>Click me</my-button>';
|
|
43
|
+
*/
|
|
44
|
+
export declare function loadComponent(name: string, url: string, afterConstructor?: VoidFunction): Promise<CustomElementConstructor>;
|
|
45
|
+
/**
|
|
46
|
+
* a dual component definition helper function
|
|
47
|
+
* - when used inside loadComponent-imported module, it defines a web component class
|
|
48
|
+
* - when used in normal document context, it just runs the function with document as root
|
|
49
|
+
*/
|
|
50
|
+
export declare function defineComponent(fc: (root: Document | ShadowRoot) => void): any;
|
|
51
|
+
export declare class NativeSFCError extends Error {
|
|
52
|
+
constructor(message: string, options?: ErrorOptions);
|
|
53
|
+
}
|
|
54
|
+
declare let fetch$1: typeof globalThis.fetch;
|
|
55
|
+
export declare function defineFetch(customFetch: typeof fetch$1): void;
|
|
56
|
+
export interface NativeSFCEvents {
|
|
57
|
+
"component-loading": {
|
|
58
|
+
name: string;
|
|
59
|
+
url: string;
|
|
60
|
+
};
|
|
61
|
+
"component-defined": {
|
|
62
|
+
name: string;
|
|
63
|
+
url: string;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export type NativeSFCEventsMap = {
|
|
67
|
+
[K in keyof NativeSFCEvents]: CustomEvent<NativeSFCEvents[K]>;
|
|
68
|
+
};
|
|
69
|
+
export declare function on<K extends keyof NativeSFCEvents>(eventName: K, listener: (ev: NativeSFCEventsMap[K]) => any): () => void;
|
|
70
|
+
|
|
71
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// entrypoint for bundler
|
|
2
|
+
|
|
3
|
+
// src/components.ts
|
|
4
|
+
import * as stackTraceParser2 from "stacktrace-parser";
|
|
5
|
+
|
|
6
|
+
// src/rewriter.ts
|
|
7
|
+
import { parse } from "es-module-lexer/js";
|
|
8
|
+
import * as stackTraceParser from "stacktrace-parser";
|
|
9
|
+
function rewriteModule(code, sourceUrl) {
|
|
10
|
+
const [imports] = parse(code);
|
|
11
|
+
const rewritableImports = imports.filter((i) => {
|
|
12
|
+
const specifier = code.slice(i.s, i.e);
|
|
13
|
+
return !isBrowserUrl(specifier) && !specifier.startsWith("data:");
|
|
14
|
+
});
|
|
15
|
+
for (const importEntry of rewritableImports.reverse()) {
|
|
16
|
+
const specifier = code.slice(importEntry.s, importEntry.e);
|
|
17
|
+
let rewritten = specifier;
|
|
18
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
19
|
+
rewritten = new URL(specifier, sourceUrl).href;
|
|
20
|
+
} else {
|
|
21
|
+
rewritten = `https://esm.sh/${specifier}`;
|
|
22
|
+
}
|
|
23
|
+
code = code.slice(0, importEntry.s) + rewritten + code.slice(importEntry.e);
|
|
24
|
+
}
|
|
25
|
+
return `import.meta.url=${JSON.stringify(sourceUrl)};
|
|
26
|
+
${code}`;
|
|
27
|
+
}
|
|
28
|
+
function isBrowserUrl(url) {
|
|
29
|
+
return url.startsWith("http://") || url.startsWith("https://") || url.startsWith("blob:http://") || url.startsWith("blob:https://") || url.startsWith("data:");
|
|
30
|
+
}
|
|
31
|
+
var blobMap = /* @__PURE__ */ new Map();
|
|
32
|
+
async function esm(code, sourceUrl) {
|
|
33
|
+
code = rewriteModule(code, sourceUrl);
|
|
34
|
+
const blob = new Blob([code], { type: "text/javascript" });
|
|
35
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
36
|
+
blobMap.set(blobUrl, sourceUrl);
|
|
37
|
+
try {
|
|
38
|
+
const module = await import(blobUrl);
|
|
39
|
+
return module;
|
|
40
|
+
} finally {
|
|
41
|
+
URL.revokeObjectURL(blobUrl);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function getImporterUrl() {
|
|
45
|
+
const stack = stackTraceParser.parse(new Error().stack);
|
|
46
|
+
for (const { file } of stack) {
|
|
47
|
+
if (file && file !== import.meta.url) {
|
|
48
|
+
if (file.startsWith("blob:")) {
|
|
49
|
+
if (blobMap.has(file)) {
|
|
50
|
+
return blobMap.get(file);
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
return file;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/error.ts
|
|
61
|
+
var NativeSFCError = class extends Error {
|
|
62
|
+
constructor(message, options) {
|
|
63
|
+
super(message, options);
|
|
64
|
+
this.name = "NativeSFCError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
function warn(...args) {
|
|
68
|
+
console.warn("NativeSFC Warning:", ...args);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/network.ts
|
|
72
|
+
var fetch = globalThis.fetch;
|
|
73
|
+
function defineFetch(customFetch) {
|
|
74
|
+
fetch = customFetch;
|
|
75
|
+
}
|
|
76
|
+
async function requestText(url, userFriendlySource) {
|
|
77
|
+
return request(url, userFriendlySource).then((res) => res.text());
|
|
78
|
+
}
|
|
79
|
+
async function request(url, userFriendlySource) {
|
|
80
|
+
let response;
|
|
81
|
+
try {
|
|
82
|
+
response = await fetch(url);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
throw new NativeSFCError(`Failed to fetch ${url} at ${userFriendlySource}`, {
|
|
85
|
+
cause: error
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
throw new NativeSFCError(`Failed to fetch ${url} at ${userFriendlySource}`, {
|
|
90
|
+
cause: new Error(`HTTP status ${response.status}`)
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return response;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/events.ts
|
|
97
|
+
var eventTarget = new EventTarget();
|
|
98
|
+
function emit(eventName, detail) {
|
|
99
|
+
const event = new CustomEvent(eventName, { detail });
|
|
100
|
+
eventTarget.dispatchEvent(event);
|
|
101
|
+
}
|
|
102
|
+
function on(eventName, listener) {
|
|
103
|
+
eventTarget.addEventListener(eventName, listener);
|
|
104
|
+
return () => {
|
|
105
|
+
eventTarget.removeEventListener(eventName, listener);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/components.ts
|
|
110
|
+
var loadedComponentsRecord = /* @__PURE__ */ new Map();
|
|
111
|
+
async function loadComponent(name, url, afterConstructor) {
|
|
112
|
+
const importerUrl = getImporterUrl() || location.href;
|
|
113
|
+
url = new URL(url, importerUrl).href;
|
|
114
|
+
emit("component-loading", { name, url });
|
|
115
|
+
if (customElements.get(name)) {
|
|
116
|
+
if (!loadedComponentsRecord.has(name)) {
|
|
117
|
+
throw new NativeSFCError(`Component name ${JSON.stringify(name)} is already being used`);
|
|
118
|
+
}
|
|
119
|
+
const loadedComponentRecord = loadedComponentsRecord.get(name);
|
|
120
|
+
if (loadedComponentRecord.url === url) {
|
|
121
|
+
return loadedComponentRecord.cec;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const html = await requestText(
|
|
125
|
+
url,
|
|
126
|
+
`loadComponent(${JSON.stringify(name)}, ${JSON.stringify(url)})`
|
|
127
|
+
);
|
|
128
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
129
|
+
filterGlobalStyle(doc);
|
|
130
|
+
rewriteStyleAndScript(doc, url);
|
|
131
|
+
const adoptedStyleSheets = await collectAdoptedStyleSheets(doc);
|
|
132
|
+
const result = await evaluateModules(doc, url);
|
|
133
|
+
for (const el of doc.querySelectorAll("script")) {
|
|
134
|
+
doc.body.prepend(el);
|
|
135
|
+
}
|
|
136
|
+
const component = result.default;
|
|
137
|
+
const defaultExportIsComponent = component?.prototype instanceof HTMLElement;
|
|
138
|
+
if (component && !defaultExportIsComponent) {
|
|
139
|
+
warn(
|
|
140
|
+
`The default export of component ${JSON.stringify(name)} loaded from ${url} is not a web component class`,
|
|
141
|
+
component
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const define = (component2) => {
|
|
145
|
+
const cec = extendsElement(component2, doc.body.innerHTML, adoptedStyleSheets, afterConstructor);
|
|
146
|
+
customElements.define(name, cec);
|
|
147
|
+
emit("component-defined", { name, url });
|
|
148
|
+
loadedComponentsRecord.set(name, { cec, url });
|
|
149
|
+
return cec;
|
|
150
|
+
};
|
|
151
|
+
if (!component || !defaultExportIsComponent) {
|
|
152
|
+
return define(HTMLElement);
|
|
153
|
+
} else {
|
|
154
|
+
return define(component);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function extendsElement(BaseClass = HTMLElement, innerHTML, adoptedStyleSheets, afterConstructor) {
|
|
158
|
+
return class extends BaseClass {
|
|
159
|
+
constructor(...args) {
|
|
160
|
+
super(innerHTML, adoptedStyleSheets);
|
|
161
|
+
if (!this.shadowRoot) {
|
|
162
|
+
const shadowRoot = this.attachShadow({ mode: "open" });
|
|
163
|
+
shadowRoot.innerHTML = innerHTML;
|
|
164
|
+
if (adoptedStyleSheets && adoptedStyleSheets.length > 0) {
|
|
165
|
+
shadowRoot.adoptedStyleSheets = adoptedStyleSheets;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (afterConstructor) {
|
|
169
|
+
afterConstructor.call(this);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function defineComponent(fc) {
|
|
175
|
+
const whoDefineMe = stackTraceParser2.parse(new Error().stack).at(-1).file;
|
|
176
|
+
if (blobMap.has(whoDefineMe)) {
|
|
177
|
+
return class extends HTMLElement {
|
|
178
|
+
connectedCallback() {
|
|
179
|
+
fc.call(this, this.shadowRoot || this.attachShadow({ mode: "open" }));
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return fc.call(globalThis, document);
|
|
184
|
+
}
|
|
185
|
+
function filterGlobalStyle(doc) {
|
|
186
|
+
for (const styleElement of doc.querySelectorAll("style")) {
|
|
187
|
+
if (styleElement.hasAttribute("global")) {
|
|
188
|
+
document.head.append(styleElement);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const linkElement of doc.querySelectorAll('link[rel="stylesheet"]')) {
|
|
192
|
+
if (linkElement.hasAttribute("global")) {
|
|
193
|
+
document.head.append(linkElement);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function cssStyleSheetFromText(styleText, userFriendlySource) {
|
|
198
|
+
const sheet = new CSSStyleSheet();
|
|
199
|
+
try {
|
|
200
|
+
sheet.replaceSync(styleText);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
warn(`Failed to create CSSStyleSheet at ${userFriendlySource}`, error);
|
|
203
|
+
}
|
|
204
|
+
return sheet;
|
|
205
|
+
}
|
|
206
|
+
async function collectAdoptedStyleSheets(doc) {
|
|
207
|
+
const adoptedStyleSheets = [];
|
|
208
|
+
for (const link of doc.querySelectorAll(`link[rel="stylesheet"]`)) {
|
|
209
|
+
const styleText = await requestText(link.href, link.outerHTML).catch(
|
|
210
|
+
(error) => {
|
|
211
|
+
warn(`Failed to load ${link.outerHTML}`, error);
|
|
212
|
+
return "";
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
if (!styleText) continue;
|
|
216
|
+
const sheet = cssStyleSheetFromText(styleText, link.outerHTML);
|
|
217
|
+
adoptedStyleSheets.push(sheet);
|
|
218
|
+
link.remove();
|
|
219
|
+
}
|
|
220
|
+
for (const style of doc.querySelectorAll("style")) {
|
|
221
|
+
const styleText = style.innerHTML;
|
|
222
|
+
if (!styleText) continue;
|
|
223
|
+
const sheet = cssStyleSheetFromText(styleText, style.outerHTML);
|
|
224
|
+
adoptedStyleSheets.push(sheet);
|
|
225
|
+
style.remove();
|
|
226
|
+
}
|
|
227
|
+
return adoptedStyleSheets;
|
|
228
|
+
}
|
|
229
|
+
function rewriteStyleAndScript(doc, url) {
|
|
230
|
+
for (const script of doc.querySelectorAll("script[src]")) {
|
|
231
|
+
const src = new URL(
|
|
232
|
+
script.getAttribute("src") || "",
|
|
233
|
+
// getAttribute to avoid getting absolute URL directly, do not use script.src
|
|
234
|
+
url
|
|
235
|
+
).href;
|
|
236
|
+
script.src = src;
|
|
237
|
+
}
|
|
238
|
+
for (const link of doc.querySelectorAll("link[href]")) {
|
|
239
|
+
const href = new URL(
|
|
240
|
+
link.getAttribute("href") || "",
|
|
241
|
+
// getAttribute to avoid getting absolute URL directly, do not use link.href
|
|
242
|
+
url
|
|
243
|
+
).href;
|
|
244
|
+
link.href = href;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function evaluateModules(doc, url) {
|
|
248
|
+
const result = {};
|
|
249
|
+
for (const script of doc.querySelectorAll('script[type="module"]')) {
|
|
250
|
+
const src = script.src;
|
|
251
|
+
if (src) {
|
|
252
|
+
const res = await request(src, script.outerHTML);
|
|
253
|
+
const code = await res.text();
|
|
254
|
+
const module = await esm(code, res.url);
|
|
255
|
+
Object.assign(result, module);
|
|
256
|
+
} else {
|
|
257
|
+
const module = await esm(script.textContent, url);
|
|
258
|
+
Object.assign(result, module);
|
|
259
|
+
}
|
|
260
|
+
script.remove();
|
|
261
|
+
}
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
export {
|
|
265
|
+
NativeSFCError,
|
|
266
|
+
defineComponent,
|
|
267
|
+
defineFetch,
|
|
268
|
+
loadComponent,
|
|
269
|
+
on
|
|
270
|
+
};
|
|
271
|
+
//! we provide an extra argument to user's component constructor
|
|
272
|
+
//! if the user's constructor does not create a shadow root, we will create one here
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "native-sfc",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist"
|
|
7
|
+
],
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "node esbuild.ts && dts-bundle-generator -o dist/index.d.ts src/index.ts",
|
|
12
|
+
"dev": "node esbuild.ts --dev"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"es-module-lexer": "^2.0.0",
|
|
16
|
+
"stacktrace-parser": "^0.1.11"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^25.0.6",
|
|
20
|
+
"dts-bundle-generator": "^9.5.1",
|
|
21
|
+
"esbuild": "^0.27.2"
|
|
22
|
+
},
|
|
23
|
+
"author": "YieldRay",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/YieldRay/native-sfc.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/YieldRay/native-sfc/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/YieldRay/native-sfc#readme",
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"registry": "https://registry.npmjs.org"
|
|
35
|
+
}
|
|
36
|
+
}
|