native-sfc 0.0.1 → 0.0.3

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.js CHANGED
@@ -1,36 +1,66 @@
1
1
  // entrypoint for bundler
2
2
 
3
3
  // src/components.ts
4
- import * as stackTraceParser2 from "stacktrace-parser";
4
+ import * as stackTraceParser from "stacktrace-parser";
5
5
 
6
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);
7
+ import { parse as parseESM } from "es-module-lexer/js";
8
+ import { parse as parseStackTrace } from "stacktrace-parser";
9
+
10
+ // src/events.ts
11
+ var eventTarget = new EventTarget();
12
+ function emit(eventName, detail) {
13
+ const event = new CustomEvent(eventName, { detail });
14
+ eventTarget.dispatchEvent(event);
15
+ }
16
+ function on(eventName, listener) {
17
+ eventTarget.addEventListener(eventName, listener);
18
+ return () => {
19
+ eventTarget.removeEventListener(eventName, listener);
20
+ };
21
+ }
22
+
23
+ // src/config.ts
24
+ var config = {
25
+ fetch: globalThis.fetch,
26
+ rewriteModule: (code, sourceUrl) => `import.meta.url=${JSON.stringify(sourceUrl)};
27
+ ${code}`,
28
+ on
29
+ };
30
+ Object.preventExtensions(config);
31
+ var bind = (fn, thisArg) => fn.bind(thisArg);
32
+
33
+ // src/rewriter.ts
34
+ async function rewriteModule(code, sourceUrl) {
35
+ const [imports] = parseESM(code);
11
36
  const rewritableImports = imports.filter((i) => {
12
37
  const specifier = code.slice(i.s, i.e);
13
- return !isBrowserUrl(specifier) && !specifier.startsWith("data:");
38
+ return !isBrowserUrl(specifier);
14
39
  });
15
40
  for (const importEntry of rewritableImports.reverse()) {
16
41
  const specifier = code.slice(importEntry.s, importEntry.e);
17
42
  let rewritten = specifier;
18
43
  if (specifier.startsWith(".") || specifier.startsWith("/")) {
19
44
  rewritten = new URL(specifier, sourceUrl).href;
45
+ } else if (specifier.startsWith("node:")) {
46
+ const module = specifier.slice(5);
47
+ rewritten = `https://raw.esm.sh/@jspm/core/nodelibs/browser/${module}.js`;
48
+ } else if (specifier.startsWith("npm:")) {
49
+ rewritten = `https://esm.sh/${specifier.slice(4)}`;
20
50
  } else {
21
51
  rewritten = `https://esm.sh/${specifier}`;
22
52
  }
23
53
  code = code.slice(0, importEntry.s) + rewritten + code.slice(importEntry.e);
24
54
  }
25
- return `import.meta.url=${JSON.stringify(sourceUrl)};
26
- ${code}`;
55
+ const { rewriteModule: rewriteModule2 } = config;
56
+ return await rewriteModule2(code, sourceUrl);
27
57
  }
28
58
  function isBrowserUrl(url) {
29
- return url.startsWith("http://") || url.startsWith("https://") || url.startsWith("blob:http://") || url.startsWith("blob:https://") || url.startsWith("data:");
59
+ return url.startsWith("http://") || url.startsWith("https://") || url.startsWith("blob:http://") || url.startsWith("blob:https://");
30
60
  }
31
61
  var blobMap = /* @__PURE__ */ new Map();
32
62
  async function esm(code, sourceUrl) {
33
- code = rewriteModule(code, sourceUrl);
63
+ code = await rewriteModule(code, sourceUrl);
34
64
  const blob = new Blob([code], { type: "text/javascript" });
35
65
  const blobUrl = URL.createObjectURL(blob);
36
66
  blobMap.set(blobUrl, sourceUrl);
@@ -42,7 +72,7 @@ async function esm(code, sourceUrl) {
42
72
  }
43
73
  }
44
74
  function getImporterUrl() {
45
- const stack = stackTraceParser.parse(new Error().stack);
75
+ const stack = parseStackTrace(new Error().stack);
46
76
  for (const { file } of stack) {
47
77
  if (file && file !== import.meta.url) {
48
78
  if (file.startsWith("blob:")) {
@@ -69,14 +99,11 @@ function warn(...args) {
69
99
  }
70
100
 
71
101
  // src/network.ts
72
- var fetch = globalThis.fetch;
73
- function defineFetch(customFetch) {
74
- fetch = customFetch;
75
- }
76
102
  async function requestText(url, userFriendlySource) {
77
103
  return request(url, userFriendlySource).then((res) => res.text());
78
104
  }
79
105
  async function request(url, userFriendlySource) {
106
+ const { fetch } = config;
80
107
  let response;
81
108
  try {
82
109
  response = await fetch(url);
@@ -93,22 +120,364 @@ async function request(url, userFriendlySource) {
93
120
  return response;
94
121
  }
95
122
 
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);
123
+ // src/signals.ts
124
+ var activeEffect = null;
125
+ var jobQueue = [];
126
+ var isFlushPending = false;
127
+ function queueJob(job) {
128
+ if (!jobQueue.includes(job)) {
129
+ jobQueue.push(job);
130
+ }
131
+ if (!isFlushPending) {
132
+ isFlushPending = true;
133
+ Promise.resolve().then(flushJobs);
134
+ }
101
135
  }
102
- function on(eventName, listener) {
103
- eventTarget.addEventListener(eventName, listener);
104
- return () => {
105
- eventTarget.removeEventListener(eventName, listener);
136
+ function flushJobs() {
137
+ isFlushPending = false;
138
+ const jobs = [...jobQueue];
139
+ jobQueue.length = 0;
140
+ jobs.forEach((job) => job());
141
+ }
142
+ function cleanup(effect2) {
143
+ effect2.deps.forEach((dep) => {
144
+ dep.delete(effect2);
145
+ });
146
+ effect2.deps.clear();
147
+ }
148
+ function createReactiveEffect(fn, deps = /* @__PURE__ */ new Set(), options) {
149
+ const effect2 = fn;
150
+ effect2.deps = deps;
151
+ effect2.options = options;
152
+ return effect2;
153
+ }
154
+ var EffectScope = class {
155
+ effects = [];
156
+ active = true;
157
+ run(fn) {
158
+ if (!this.active) return;
159
+ const prevScope = activeScope;
160
+ activeScope = this;
161
+ try {
162
+ return fn();
163
+ } finally {
164
+ activeScope = prevScope;
165
+ }
166
+ }
167
+ add(stopFn) {
168
+ if (this.active) {
169
+ this.effects.push(stopFn);
170
+ } else {
171
+ stopFn();
172
+ }
173
+ }
174
+ stop() {
175
+ if (this.active) {
176
+ this.effects.forEach((stop) => stop());
177
+ this.effects = [];
178
+ this.active = false;
179
+ }
180
+ }
181
+ };
182
+ var activeScope = null;
183
+ function untrack(fn) {
184
+ const prevEffect = activeEffect;
185
+ activeEffect = null;
186
+ try {
187
+ return fn();
188
+ } finally {
189
+ activeEffect = prevEffect;
190
+ }
191
+ }
192
+ function effectScope(fn) {
193
+ const scope = new EffectScope();
194
+ if (fn) scope.run(fn);
195
+ return () => scope.stop();
196
+ }
197
+ function effect(fn) {
198
+ const effect2 = createReactiveEffect(
199
+ () => {
200
+ cleanup(effect2);
201
+ const prevEffect = activeEffect;
202
+ activeEffect = effect2;
203
+ try {
204
+ fn();
205
+ } finally {
206
+ activeEffect = prevEffect;
207
+ }
208
+ },
209
+ /* @__PURE__ */ new Set(),
210
+ { scheduler: queueJob }
211
+ );
212
+ effect2();
213
+ const stop = () => {
214
+ cleanup(effect2);
215
+ };
216
+ if (activeScope) {
217
+ activeScope.add(stop);
218
+ }
219
+ return stop;
220
+ }
221
+ function signal(initialValue) {
222
+ let value = initialValue;
223
+ const subscribers = /* @__PURE__ */ new Set();
224
+ const read = () => {
225
+ if (activeEffect) {
226
+ subscribers.add(activeEffect);
227
+ activeEffect.deps.add(subscribers);
228
+ }
229
+ return value;
230
+ };
231
+ read.toString = () => {
232
+ throw new NativeSFCError(
233
+ `signal<<${value}>>: This is a signal reader, you MUST call it to get the value.`
234
+ );
235
+ };
236
+ const write = (newValue) => {
237
+ if (value !== newValue) {
238
+ value = newValue;
239
+ const effectsToRun = new Set(subscribers);
240
+ effectsToRun.forEach((effect2) => {
241
+ if (effect2.options?.scheduler) {
242
+ effect2.options.scheduler(effect2);
243
+ } else {
244
+ effect2();
245
+ }
246
+ });
247
+ }
248
+ };
249
+ write.toString = () => {
250
+ throw new NativeSFCError(
251
+ `signal<<${value}>>: This is a signal writer, you MUST call it to set the value.`
252
+ );
253
+ };
254
+ return [read, write];
255
+ }
256
+ function computed(fn) {
257
+ let value;
258
+ let dirty = true;
259
+ let isComputing = false;
260
+ const runner = () => {
261
+ if (!dirty) {
262
+ dirty = true;
263
+ trigger(subscribers);
264
+ }
265
+ };
266
+ const internalEffect = createReactiveEffect(runner);
267
+ const subscribers = /* @__PURE__ */ new Set();
268
+ const trigger = (subs) => {
269
+ const effectsToRun = new Set(subs);
270
+ effectsToRun.forEach((effect2) => {
271
+ if (effect2.options?.scheduler) {
272
+ effect2.options.scheduler(effect2);
273
+ } else {
274
+ effect2();
275
+ }
276
+ });
277
+ };
278
+ const read = () => {
279
+ if (isComputing) {
280
+ throw new NativeSFCError(`Circular dependency detected in computed<<${value}>>`);
281
+ }
282
+ if (activeEffect) {
283
+ subscribers.add(activeEffect);
284
+ activeEffect.deps.add(subscribers);
285
+ }
286
+ if (dirty) {
287
+ isComputing = true;
288
+ const prevEffect = activeEffect;
289
+ activeEffect = internalEffect;
290
+ cleanup(internalEffect);
291
+ try {
292
+ value = fn();
293
+ dirty = false;
294
+ } finally {
295
+ activeEffect = prevEffect;
296
+ isComputing = false;
297
+ }
298
+ }
299
+ return value;
300
+ };
301
+ read.toString = () => {
302
+ throw new NativeSFCError(
303
+ `computed<<${value}>>: This is a computed reader, you MUST call it to get the value.`
304
+ );
106
305
  };
306
+ return read;
307
+ }
308
+
309
+ // src/template.ts
310
+ function toCamelCase(str) {
311
+ return str.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
312
+ }
313
+ function parseTextContent(text) {
314
+ const regex = /\{\{(.+?)\}\}/g;
315
+ const parts = [];
316
+ let lastIndex = 0;
317
+ let match;
318
+ while ((match = regex.exec(text)) !== null) {
319
+ if (match.index > lastIndex) {
320
+ parts.push({
321
+ type: "static",
322
+ content: text.slice(lastIndex, match.index)
323
+ });
324
+ }
325
+ parts.push({
326
+ type: "dynamic",
327
+ content: match[1].trim()
328
+ });
329
+ lastIndex = regex.lastIndex;
330
+ }
331
+ if (lastIndex < text.length) {
332
+ parts.push({
333
+ type: "static",
334
+ content: text.slice(lastIndex)
335
+ });
336
+ }
337
+ return parts;
338
+ }
339
+ function reactiveNodes(nodes, context) {
340
+ const evalExpr = (expr, additionalContext = {}) => {
341
+ const ctx = typeof context === "object" ? Object.assign({}, context, additionalContext) : additionalContext;
342
+ const keys = Object.keys(ctx);
343
+ const values = Object.values(ctx);
344
+ try {
345
+ const func = new Function(...keys, `return ${expr.trimStart()}`);
346
+ return func(...values);
347
+ } catch (error) {
348
+ warn(`Failed to evaluate expression: "${expr}"`, error);
349
+ }
350
+ };
351
+ const recursive = (nodes2) => {
352
+ for (const node of Array.from(nodes2)) {
353
+ if (node.nodeType === Node.ELEMENT_NODE) {
354
+ const element = node;
355
+ const ifAttr = element.getAttribute("#if");
356
+ if (ifAttr) {
357
+ if (element.hasAttribute("#for")) {
358
+ console.warn("Cannot use #if and #for on the same element");
359
+ }
360
+ const template = element.cloneNode(true);
361
+ const parent = element.parentNode;
362
+ const placeholder = document.createComment("if");
363
+ parent?.replaceChild(placeholder, element);
364
+ template.removeAttribute("#if");
365
+ let renderedNode = null;
366
+ let cleanup2 = null;
367
+ effect(() => {
368
+ const condition = evalExpr(ifAttr);
369
+ if (condition) {
370
+ if (!renderedNode) {
371
+ const clone = template.cloneNode(true);
372
+ cleanup2 = reactiveNodes([clone], context);
373
+ placeholder.parentNode?.insertBefore(clone, placeholder.nextSibling);
374
+ renderedNode = clone;
375
+ }
376
+ } else {
377
+ if (renderedNode) {
378
+ cleanup2?.();
379
+ renderedNode.remove();
380
+ renderedNode = null;
381
+ cleanup2 = null;
382
+ }
383
+ }
384
+ });
385
+ continue;
386
+ }
387
+ const forAttr = element.getAttribute("#for");
388
+ if (forAttr) {
389
+ const template = element.cloneNode(true);
390
+ const parent = element.parentNode;
391
+ const placeholder = document.createComment("for");
392
+ parent?.replaceChild(placeholder, element);
393
+ template.removeAttribute("#for");
394
+ let renderedItems = [];
395
+ effect(() => {
396
+ const contexts = evalExpr(forAttr);
397
+ if (!Array.isArray(contexts)) {
398
+ console.warn("#for expression must return an array");
399
+ return;
400
+ }
401
+ renderedItems.forEach(({ node: node2, cleanup: cleanup2 }) => {
402
+ cleanup2();
403
+ node2.remove();
404
+ });
405
+ renderedItems = [];
406
+ contexts.forEach((itemContext) => {
407
+ const clone = template.cloneNode(true);
408
+ const cleanup2 = reactiveNodes([clone], { ...context, ...itemContext });
409
+ placeholder.parentNode?.insertBefore(clone, placeholder.nextSibling);
410
+ renderedItems.push({ node: clone, cleanup: cleanup2 });
411
+ });
412
+ });
413
+ continue;
414
+ }
415
+ for (const attr of Array.from(element.attributes)) {
416
+ if (attr.name.startsWith(".")) {
417
+ const propName = toCamelCase(attr.name.slice(1));
418
+ const expr = attr.value;
419
+ effect(() => {
420
+ const value = evalExpr(expr);
421
+ untrack(() => Reflect.set(element, propName, value));
422
+ });
423
+ element.removeAttribute(attr.name);
424
+ } else if (attr.name.startsWith(":")) {
425
+ const attrName = attr.name.slice(1);
426
+ const expr = attr.value;
427
+ effect(() => {
428
+ const value = evalExpr(expr);
429
+ untrack(() => element.setAttribute(attrName, value));
430
+ });
431
+ element.removeAttribute(attr.name);
432
+ } else if (attr.name.startsWith("@")) {
433
+ const eventName = attr.name.slice(1);
434
+ const expr = attr.value;
435
+ const listener = computed(() => (event) => {
436
+ evalExpr(expr, { event });
437
+ })();
438
+ element.addEventListener(eventName, listener);
439
+ element.removeAttribute(attr.name);
440
+ }
441
+ }
442
+ } else if (node.nodeType === Node.TEXT_NODE) {
443
+ const textNode = node;
444
+ const text = textNode.textContent || "";
445
+ const parts = parseTextContent(text);
446
+ if (parts.some((part) => part.type === "dynamic")) {
447
+ const parentNode = textNode.parentNode;
448
+ if (parentNode) {
449
+ const fragment = document.createDocumentFragment();
450
+ const textNodes = [];
451
+ for (const part of parts) {
452
+ const newTextNode = document.createTextNode(
453
+ part.type === "static" ? part.content : ""
454
+ );
455
+ fragment.appendChild(newTextNode);
456
+ textNodes.push(newTextNode);
457
+ if (part.type === "dynamic") {
458
+ effect(() => {
459
+ const value = evalExpr(part.content);
460
+ untrack(() => newTextNode.textContent = String(value));
461
+ });
462
+ }
463
+ }
464
+ parentNode.replaceChild(fragment, textNode);
465
+ }
466
+ }
467
+ }
468
+ if (node.childNodes.length > 0) {
469
+ recursive(node.childNodes);
470
+ }
471
+ }
472
+ };
473
+ return effectScope(() => {
474
+ recursive(nodes);
475
+ });
107
476
  }
108
477
 
109
478
  // src/components.ts
110
479
  var loadedComponentsRecord = /* @__PURE__ */ new Map();
111
- async function loadComponent(name, url, afterConstructor) {
480
+ async function loadComponent(name, url) {
112
481
  const importerUrl = getImporterUrl() || location.href;
113
482
  url = new URL(url, importerUrl).href;
114
483
  emit("component-loading", { name, url });
@@ -142,11 +511,11 @@ async function loadComponent(name, url, afterConstructor) {
142
511
  );
143
512
  }
144
513
  const define = (component2) => {
145
- const cec = extendsElement(component2, doc.body.innerHTML, adoptedStyleSheets, afterConstructor);
146
- customElements.define(name, cec);
514
+ const CEC = extendsElement(component2, doc.body.innerHTML, adoptedStyleSheets);
515
+ customElements.define(name, CEC);
147
516
  emit("component-defined", { name, url });
148
- loadedComponentsRecord.set(name, { cec, url });
149
- return cec;
517
+ loadedComponentsRecord.set(name, { cec: CEC, url });
518
+ return CEC;
150
519
  };
151
520
  if (!component || !defaultExportIsComponent) {
152
521
  return define(HTMLElement);
@@ -154,7 +523,7 @@ async function loadComponent(name, url, afterConstructor) {
154
523
  return define(component);
155
524
  }
156
525
  }
157
- function extendsElement(BaseClass = HTMLElement, innerHTML, adoptedStyleSheets, afterConstructor) {
526
+ function extendsElement(BaseClass = HTMLElement, innerHTML, adoptedStyleSheets) {
158
527
  return class extends BaseClass {
159
528
  constructor(...args) {
160
529
  super(innerHTML, adoptedStyleSheets);
@@ -165,22 +534,46 @@ function extendsElement(BaseClass = HTMLElement, innerHTML, adoptedStyleSheets,
165
534
  shadowRoot.adoptedStyleSheets = adoptedStyleSheets;
166
535
  }
167
536
  }
168
- if (afterConstructor) {
169
- afterConstructor.call(this);
537
+ if ("setup" in this && typeof this.setup === "function") {
538
+ const context = this.setup();
539
+ reactiveNodes(this.shadowRoot.childNodes, context);
540
+ } else {
541
+ reactiveNodes(this.shadowRoot.childNodes, {});
170
542
  }
171
543
  }
172
544
  };
173
545
  }
174
- function defineComponent(fc) {
175
- const whoDefineMe = stackTraceParser2.parse(new Error().stack).at(-1).file;
546
+ function defineComponent(setup) {
547
+ const whoDefineMe = stackTraceParser.parse(new Error().stack).at(-1).file;
176
548
  if (blobMap.has(whoDefineMe)) {
177
549
  return class extends HTMLElement {
550
+ _onConnectedEvents = [];
551
+ _onDisconnectedEvents = [];
552
+ setup() {
553
+ const setup22 = bind(setup, this);
554
+ return setup22({
555
+ onConnected: (event) => this._onConnectedEvents.push(bind(event, this)),
556
+ onDisconnected: (event) => this._onDisconnectedEvents.push(bind(event, this))
557
+ });
558
+ }
178
559
  connectedCallback() {
179
- fc.call(this, this.shadowRoot || this.attachShadow({ mode: "open" }));
560
+ const root = this.shadowRoot || this.attachShadow({ mode: "open" });
561
+ untrack(() => this._onConnectedEvents.forEach((cb) => cb(root)));
562
+ }
563
+ disconnectedCallback() {
564
+ const root = this.shadowRoot;
565
+ untrack(() => this._onDisconnectedEvents.forEach((cb) => cb(root)));
180
566
  }
181
567
  };
182
568
  }
183
- return fc.call(globalThis, document);
569
+ const setup2 = bind(setup, void 0);
570
+ const context = setup2({
571
+ onConnected: (cb) => queueMicrotask(() => cb(document)),
572
+ onDisconnected: () => {
573
+ }
574
+ });
575
+ reactiveNodes(document.body.childNodes, context);
576
+ return;
184
577
  }
185
578
  function filterGlobalStyle(doc) {
186
579
  for (const styleElement of doc.querySelectorAll("style")) {
@@ -263,10 +656,17 @@ async function evaluateModules(doc, url) {
263
656
  }
264
657
  export {
265
658
  NativeSFCError,
659
+ computed,
660
+ config,
266
661
  defineComponent,
267
- defineFetch,
662
+ effect,
663
+ effectScope,
268
664
  loadComponent,
269
- on
665
+ signal,
666
+ untrack
270
667
  };
271
668
  //! we provide an extra argument to user's component constructor
272
669
  //! if the user's constructor does not create a shadow root, we will create one here
670
+ //! we bind `this` to the setup function, so that users can access `this` in setup, and `this` is the component instance
671
+ //! `this` is undefined in normal document context's setup function
672
+ //! make the document.body reactive, just like the web component (defined at `extendsElement`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "native-sfc",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"
@@ -16,9 +16,9 @@
16
16
  "stacktrace-parser": "^0.1.11"
17
17
  },
18
18
  "devDependencies": {
19
- "@types/node": "^25.0.6",
19
+ "@types/node": "^25.2.3",
20
20
  "dts-bundle-generator": "^9.5.1",
21
- "esbuild": "^0.27.2"
21
+ "esbuild": "^0.27.3"
22
22
  },
23
23
  "author": "YieldRay",
24
24
  "license": "MIT",