@gracile/engine 0.9.0-next.6 → 0.9.0-next.7

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.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Server-side Custom Elements registry tracker for dev HMR.
3
+ *
4
+ * Wraps the Lit SSR DOM shim's `customElements` so orphaned CEs
5
+ * can be blocked when their import is removed during development.
6
+ *
7
+ * @internal — dev-only, never runs in production builds.
8
+ */
9
+ export declare function installCeTracker(): void;
10
+ /**
11
+ * Block all CEs registered by a given module.
12
+ * They will be unblocked if the module is re-evaluated and calls define() again.
13
+ */
14
+ export declare function blockCesForModule(moduleId: string): void;
15
+ /** Check whether a module has registered any CEs. */
16
+ export declare function hasCeRegistrations(moduleId: string): boolean;
17
+ /**
18
+ * Reset tracking state.
19
+ * @param full - Also reset installation state (for test suite teardown
20
+ * so a subsequent `installCeTracker` call takes effect).
21
+ */
22
+ export declare function resetCeTracker(full?: boolean): void;
23
+ /** Read-only view of currently blocked tags. For test assertions. */
24
+ export declare function getBlockedTags(): ReadonlySet<string>;
25
+ /** Read-only view of module → tag-names mappings. For test assertions. */
26
+ export declare function getModuleToTags(): ReadonlyMap<string, ReadonlySet<string>>;
27
+ //# sourceMappingURL=ssr-ce-tracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssr-ce-tracker.d.ts","sourceRoot":"","sources":["../../src/dev/ssr-ce-tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAwDH,wBAAgB,gBAAgB,SA+B/B;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,QAMjD;AAED,qDAAqD;AACrD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,UAAQ,QAK1C;AAED,qEAAqE;AACrE,wBAAgB,cAAc,IAAI,WAAW,CAAC,MAAM,CAAC,CAEpD;AAED,0EAA0E;AAC1E,wBAAgB,eAAe,IAAI,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAE1E"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Server-side Custom Elements registry tracker for dev HMR.
3
+ *
4
+ * Wraps the Lit SSR DOM shim's `customElements` so orphaned CEs
5
+ * can be blocked when their import is removed during development.
6
+ *
7
+ * @internal — dev-only, never runs in production builds.
8
+ */
9
+ // ── Tracking state ──────────────────────────────────────────────────
10
+ /** module ID → tag names that module registered */
11
+ const moduleToTags = new Map();
12
+ /** Tags blocked from registry lookup (orphaned modules) */
13
+ const blocked = new Set();
14
+ /** Which module is currently being evaluated (set by transform-injected code) */
15
+ let currentModuleId = null;
16
+ let installed = false;
17
+ // ── Registry wrapper ────────────────────────────────────────────────
18
+ function wrapRegistry(registry) {
19
+ const origDefine = registry.define.bind(registry);
20
+ const origGet = registry.get.bind(registry);
21
+ registry.define = function (name, ctor, options) {
22
+ // If re-registered after being blocked, unblock.
23
+ blocked.delete(name);
24
+ if (currentModuleId) {
25
+ let tags = moduleToTags.get(currentModuleId);
26
+ if (!tags) {
27
+ tags = new Set();
28
+ moduleToTags.set(currentModuleId, tags);
29
+ }
30
+ tags.add(name);
31
+ }
32
+ try {
33
+ origDefine(name, ctor, options);
34
+ }
35
+ catch {
36
+ // Already defined — expected during HMR re-evaluation.
37
+ }
38
+ };
39
+ registry.get = function (name) {
40
+ if (blocked.has(name))
41
+ return;
42
+ return origGet(name);
43
+ };
44
+ }
45
+ // ── Public API ──────────────────────────────────────────────────────
46
+ export function installCeTracker() {
47
+ if (installed)
48
+ return;
49
+ installed = true;
50
+ if (globalThis.customElements) {
51
+ wrapRegistry(globalThis.customElements);
52
+ }
53
+ else {
54
+ // Shim not yet installed — intercept the assignment so we can
55
+ // wrap it the moment Lit SSR's DOM shim sets it up.
56
+ let _ce;
57
+ Object.defineProperty(globalThis, 'customElements', {
58
+ get: () => _ce,
59
+ set(value) {
60
+ _ce = value;
61
+ wrapRegistry(value);
62
+ },
63
+ configurable: true,
64
+ enumerable: true,
65
+ });
66
+ }
67
+ // Expose for transform-injected code.
68
+ // Runs in the same process via Vite's ssrLoadModule.
69
+ globalThis['__gracile_ce_tracker'] = {
70
+ setModule(id) {
71
+ currentModuleId = id;
72
+ },
73
+ clearModule() {
74
+ currentModuleId = null;
75
+ },
76
+ };
77
+ }
78
+ /**
79
+ * Block all CEs registered by a given module.
80
+ * They will be unblocked if the module is re-evaluated and calls define() again.
81
+ */
82
+ export function blockCesForModule(moduleId) {
83
+ const tags = moduleToTags.get(moduleId);
84
+ if (!tags)
85
+ return;
86
+ for (const tag of tags) {
87
+ blocked.add(tag);
88
+ }
89
+ }
90
+ /** Check whether a module has registered any CEs. */
91
+ export function hasCeRegistrations(moduleId) {
92
+ return moduleToTags.has(moduleId);
93
+ }
94
+ /**
95
+ * Reset tracking state.
96
+ * @param full - Also reset installation state (for test suite teardown
97
+ * so a subsequent `installCeTracker` call takes effect).
98
+ */
99
+ export function resetCeTracker(full = false) {
100
+ moduleToTags.clear();
101
+ blocked.clear();
102
+ currentModuleId = null;
103
+ if (full)
104
+ installed = false;
105
+ }
106
+ /** Read-only view of currently blocked tags. For test assertions. */
107
+ export function getBlockedTags() {
108
+ return blocked;
109
+ }
110
+ /** Read-only view of module → tag-names mappings. For test assertions. */
111
+ export function getModuleToTags() {
112
+ return moduleToTags;
113
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AActD;;;;;;;;;;;;;;;;GAgBG;AAIH,eAAO,MAAM,OAAO,GAAI,SAAS,aAAa,KAAG,GAAG,EAiEnD,CAAC;AAEF,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAetD;;;;;;;;;;;;;;;;GAgBG;AAIH,eAAO,MAAM,OAAO,GAAI,SAAS,aAAa,KAAG,GAAG,EAoEnD,CAAC;AAEF,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/plugin.js CHANGED
@@ -7,6 +7,7 @@ import { createPluginSharedState } from './vite/plugin-shared-state.js';
7
7
  import { gracileServePlugin } from './vite/plugin-serve.js';
8
8
  import { gracileClientBuildPlugin } from './vite/plugin-client-build.js';
9
9
  import { gracileCollectClientAssetsPlugin, gracileServerBuildPlugin, } from './vite/plugin-server-build.js';
10
+ import { gracileCETrackerPlugin } from './vite/plugin-ce-tracker.js';
10
11
  let isClientBuilt = false;
11
12
  /**
12
13
  * The main Vite plugin for loading the Gracile framework.
@@ -53,6 +54,8 @@ export const gracile = (config) => {
53
54
  sharedPluginContext.litSsrRenderInfo;
54
55
  },
55
56
  },
57
+ // MARK: 1.5. CE registry tracker (dev HMR cleanup)
58
+ gracileCETrackerPlugin(),
56
59
  // MARK: 2. HMR SSR reload
57
60
  hmrSsrReload(),
58
61
  // MARK: 3. Dev serve middleware
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Vite plugin that tracks Custom Elements registrations on the server
3
+ * and cleans up orphaned CEs when imports are removed during dev HMR.
4
+ *
5
+ * How it works:
6
+ * 1. `configureServer` — installs the registry wrapper (before any modules load).
7
+ * 2. `transform` (SSR only) — injects module-context markers around files that
8
+ * might call `customElements.define`, so the wrapper knows which module is
9
+ * responsible for each registration.
10
+ * 3. `hotUpdate` — when a file changes, walks the OLD import tree, finds CE
11
+ * modules in that tree, blocks their tags, and invalidates them. If they are
12
+ * still imported after re-evaluation, `define()` fires again and unblocks.
13
+ * If removed, they stay blocked.
14
+ *
15
+ * @internal
16
+ */
17
+ import type { Plugin } from 'vite';
18
+ export declare function gracileCETrackerPlugin(): Plugin;
19
+ //# sourceMappingURL=plugin-ce-tracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-ce-tracker.d.ts","sourceRoot":"","sources":["../../src/vite/plugin-ce-tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAyB,MAAM,MAAM,CAAC;AAmC1D,wBAAgB,sBAAsB,IAAI,MAAM,CA6D/C"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Vite plugin that tracks Custom Elements registrations on the server
3
+ * and cleans up orphaned CEs when imports are removed during dev HMR.
4
+ *
5
+ * How it works:
6
+ * 1. `configureServer` — installs the registry wrapper (before any modules load).
7
+ * 2. `transform` (SSR only) — injects module-context markers around files that
8
+ * might call `customElements.define`, so the wrapper knows which module is
9
+ * responsible for each registration.
10
+ * 3. `hotUpdate` — when a file changes, walks the OLD import tree, finds CE
11
+ * modules in that tree, blocks their tags, and invalidates them. If they are
12
+ * still imported after re-evaluation, `define()` fires again and unblocks.
13
+ * If removed, they stay blocked.
14
+ *
15
+ * @internal
16
+ */
17
+ import { installCeTracker, blockCesForModule, hasCeRegistrations, } from '../dev/ssr-ce-tracker.js';
18
+ // ── Helpers ─────────────────────────────────────────────────────────
19
+ /** Recursively collect all transitive imports of a module. */
20
+ function collectImportTree(module_, seen = new Set()) {
21
+ if (!module_.id || seen.has(module_.id))
22
+ return seen;
23
+ seen.add(module_.id);
24
+ for (const imported of module_.importedModules) {
25
+ collectImportTree(imported, seen);
26
+ }
27
+ return seen;
28
+ }
29
+ /** Heuristic: does this source likely define a Custom Element? */
30
+ function mightDefineCE(code) {
31
+ return (code.includes('customElements.define') ||
32
+ // Lit @customElement decorator — calls define() at eval time.
33
+ // Matches both TS source (`@customElement(`) and compiled output.
34
+ /\bcustomElement\s*\(/.test(code));
35
+ }
36
+ // ── Plugin ──────────────────────────────────────────────────────────
37
+ export function gracileCETrackerPlugin() {
38
+ return {
39
+ name: 'vite-plugin-gracile-ce-tracker',
40
+ configureServer() {
41
+ installCeTracker();
42
+ },
43
+ // Inject module-context markers so the registry wrapper knows
44
+ // which module is responsible for each define() call.
45
+ transform(code, id, options) {
46
+ if (!options?.ssr)
47
+ return;
48
+ if (!mightDefineCE(code))
49
+ return;
50
+ const escaped = JSON.stringify(id);
51
+ return {
52
+ code: [
53
+ `globalThis.__gracile_ce_tracker?.setModule(${escaped});`,
54
+ code,
55
+ `globalThis.__gracile_ce_tracker?.clearModule();`,
56
+ ].join('\n'),
57
+ map: null,
58
+ };
59
+ },
60
+ hotUpdate: {
61
+ order: 'pre',
62
+ handler({ modules, timestamp }) {
63
+ if (this.environment.name !== 'ssr')
64
+ return;
65
+ const invalidated = new Set();
66
+ for (const module_ of modules) {
67
+ if (!module_.id)
68
+ continue;
69
+ // Walk the OLD import tree (graph hasn't updated yet).
70
+ const tree = collectImportTree(module_);
71
+ for (const depId of tree) {
72
+ if (!hasCeRegistrations(depId))
73
+ continue;
74
+ // Block this module's CEs. If the module is still imported
75
+ // after re-evaluation, its define() will unblock them.
76
+ blockCesForModule(depId);
77
+ // Force re-evaluation so define() can re-fire.
78
+ const depModule = this.environment.moduleGraph.getModuleById(depId);
79
+ if (depModule) {
80
+ this.environment.moduleGraph.invalidateModule(depModule, invalidated, timestamp, true);
81
+ }
82
+ }
83
+ }
84
+ },
85
+ },
86
+ };
87
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"plugin-client-build.d.ts","sourceRoot":"","sources":["../../src/vite/plugin-client-build.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,MAAM,CAAC;AAGvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,wBAAgB,wBAAwB,CAAC,EACxC,KAAK,EACL,sBAAsB,GACtB,EAAE;IACF,KAAK,EAAE,iBAAiB,CAAC;IACzB,sBAAsB,EAAE,YAAY,CAAC;CACrC,GAAG,YAAY,CAkDf"}
1
+ {"version":3,"file":"plugin-client-build.d.ts","sourceRoot":"","sources":["../../src/vite/plugin-client-build.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,MAAM,CAAC;AAGvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,wBAAgB,wBAAwB,CAAC,EACxC,KAAK,EACL,sBAAsB,GACtB,EAAE;IACF,KAAK,EAAE,iBAAiB,CAAC;IACzB,sBAAsB,EAAE,YAAY,CAAC;CACrC,GAAG,YAAY,CA+Df"}
@@ -34,6 +34,16 @@ export function gracileClientBuildPlugin({ state, virtualRoutesForClient, }) {
34
34
  routes: state.routes,
35
35
  });
36
36
  state.renderedRoutes = htmlPages.renderedRoutes;
37
+ // NOTE: Vite's dev server does not invoke Rollup's `closeWatcher`
38
+ // hook when shutting down. Plugins like @rollup/plugin-typescript
39
+ // use `ts.createWatchProgram()` which sets up hundreds of FS
40
+ // watchers; without an explicit `closeWatcher` call they are
41
+ // leaked and the Node process hangs after build.
42
+ for (const plugin of viteServerForClientHtmlBuild.config.plugins) {
43
+ if (typeof plugin.closeWatcher === 'function') {
44
+ await plugin.closeWatcher();
45
+ }
46
+ }
37
47
  await viteServerForClientHtmlBuild.close();
38
48
  return {
39
49
  build: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gracile/engine",
3
- "version": "0.9.0-next.6",
3
+ "version": "0.9.0-next.7",
4
4
  "description": "A thin, full-stack, web framework",
5
5
  "keywords": [
6
6
  "custom-elements",
@@ -60,5 +60,5 @@
60
60
  "access": "public",
61
61
  "provenance": true
62
62
  },
63
- "gitHead": "a83e253566cdeb19aa75c7d0a686e3e52e921eed"
63
+ "gitHead": "9abb3f9de0217a6284a88b114194b4d254d0a104"
64
64
  }