@gracile/engine 0.9.0-next.5 → 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.
@@ -1 +1 @@
1
- {"version":3,"file":"development.d.ts","sourceRoot":"","sources":["../../src/dev/development.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,MAAM,CAAC;AAG1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAEN,KAAK,cAAc,EACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAIvD,wBAAsB,wBAAwB,CAAC,EAC9C,MAAM,EACN,IAAI,EACJ,aAAa,GACb,EAAE;IACF,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,aAAa,CAAC;IACpB,aAAa,EAAE,aAAa,CAAC;CAC7B,GAAG,OAAO,CAAC;IACX,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,cAAc,CAAC;CACvB,CAAC,CAgDD"}
1
+ {"version":3,"file":"development.d.ts","sourceRoot":"","sources":["../../src/dev/development.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,MAAM,CAAC;AAG1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAEN,KAAK,cAAc,EACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAIvD,wBAAsB,wBAAwB,CAAC,EAC9C,MAAM,EACN,IAAI,EACJ,aAAa,GACb,EAAE;IACF,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,aAAa,CAAC;IACpB,aAAa,EAAE,aAAa,CAAC;CAC7B,GAAG,OAAO,CAAC;IACX,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,cAAc,CAAC;CACvB,CAAC,CAqDD"}
@@ -10,7 +10,7 @@ export async function createDevelopmentHandler({ routes, vite, gracileConfig, })
10
10
  logger.info('');
11
11
  logger.info(c.dim('Creating the request handler…'), { timestamp: true });
12
12
  const collectAndCodegen = async () => {
13
- await collectRoutes(routes, root, gracileConfig.routes?.exclude);
13
+ await collectRoutes(routes, root, gracileConfig.routes?.exclude, gracileConfig.trailingSlash);
14
14
  if (gracileConfig.experimental?.generateRoutesTypings)
15
15
  await generateRoutesTypings(root, routes).catch((error) => logger.error(String(error)));
16
16
  };
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"route-template.d.ts","sourceRoot":"","sources":["../../src/render/route-template.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAIvC,OAAO,EAA0B,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAExE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAO1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,KAAK,CAAC,MAAM,oBAAoB,CAAC;AAa7C,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEhF,wBAAsB,mBAAmB,CAAC,EACzC,GAAG,EACH,IAAI,EACJ,IAAI,EACJ,UAAU,EACV,WAAW,EACX,UAAU,EACV,OAAO,EACP,UAAU,GACV,EAAE;IACF,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACjC,IAAI,EAAE,KAAK,GAAG,OAAO,CAAC;IACtB,UAAU,EAAE,UAAU,CAAC;IACvB,WAAW,CAAC,EAAE,CAAC,CAAC,YAAY,GAAG,SAAS,CAAC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACjC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,UAAU,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,SAAS,CAAC;CAC7C,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,IAAI,GAAG,QAAQ,CAAC;IAAC,QAAQ,EAAE,IAAI,GAAG,MAAM,CAAA;CAAE,CAAC,CAwJhE"}
1
+ {"version":3,"file":"route-template.d.ts","sourceRoot":"","sources":["../../src/render/route-template.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAIvC,OAAO,EAA0B,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAOxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAO1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,KAAK,CAAC,MAAM,oBAAoB,CAAC;AAa7C,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEhF,wBAAsB,mBAAmB,CAAC,EACzC,GAAG,EACH,IAAI,EACJ,IAAI,EACJ,UAAU,EACV,WAAW,EACX,UAAU,EACV,OAAO,EACP,UAAU,GACV,EAAE;IACF,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACjC,IAAI,EAAE,KAAK,GAAG,OAAO,CAAC;IACtB,UAAU,EAAE,UAAU,CAAC;IACvB,WAAW,CAAC,EAAE,CAAC,CAAC,YAAY,GAAG,SAAS,CAAC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACjC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,UAAU,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,SAAS,CAAC;CAC7C,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,IAAI,GAAG,QAAQ,CAAC;IAAC,QAAQ,EAAE,IAAI,GAAG,MAAM,CAAA;CAAE,CAAC,CA4JhE"}
@@ -32,6 +32,9 @@ export async function renderRouteTemplate({ url, vite, mode, routeInfos, routeAs
32
32
  });
33
33
  const fragmentRender = renderLitSsr(fragmentOutput, mergedRenderInfo);
34
34
  const output = Readable.from(fragmentRender);
35
+ // TODO: Disabled for now. Causes issue in static renders.
36
+ // Needs investigations.
37
+ // const output = new RenderResultReadable(fragmentRender);
35
38
  return { output, document: null };
36
39
  }
37
40
  // MARK: Document
@@ -87,7 +90,8 @@ export async function renderRouteTemplate({ url, vite, mode, routeInfos, routeAs
87
90
  const routeOutput = await Promise.resolve(routeInfos.routeModule.template(context));
88
91
  if (assert.isLitTemplate(routeOutput) === false)
89
92
  throw new Error(`Wrong template result for page template ${routeInfos.foundRoute.filePath}.`);
90
- const renderStream = Readable.from(renderLitSsr(routeOutput, mergedRenderInfo));
93
+ const renderStream =
94
+ /* TODO: Use `new RenderResultReadable` */ Readable.from(renderLitSsr(routeOutput, mergedRenderInfo));
91
95
  const output = Readable.from(concatStreams(baseDocumentRenderStreamPre, renderStream, baseDocumentRenderStreamPost));
92
96
  return { output, document: baseDocumentHtml };
93
97
  }
@@ -1,8 +1,8 @@
1
1
  import type * as R from './route.js';
2
2
  /** @internal Exported for unit testing. */
3
- export declare function extractRoutePatterns(routeFilePath: string): Pick<R.Route, 'pattern' | 'hasParams'> & {
3
+ export declare function extractRoutePatterns(routeFilePath: string, trailingSlash?: 'always' | 'never' | 'ignore'): Pick<R.Route, 'pattern' | 'hasParams'> & {
4
4
  patternString: string;
5
5
  };
6
6
  export declare const WATCHED_FILES_REGEX: RegExp;
7
- export declare function collectRoutes(routes: R.RoutesManifest, root: string, excludePatterns?: string[]): Promise<void>;
7
+ export declare function collectRoutes(routes: R.RoutesManifest, root: string, excludePatterns?: string[], trailingSlash?: 'always' | 'never' | 'ignore'): Promise<void>;
8
8
  //# sourceMappingURL=collect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../../src/routes/collect.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,KAAK,CAAC,MAAM,YAAY,CAAC;AAIrC,2CAA2C;AAC3C,wBAAgB,oBAAoB,CACnC,aAAa,EAAE,MAAM,GACnB,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,SAAS,GAAG,WAAW,CAAC,GAAG;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE,CAkDpE;AAED,eAAO,MAAM,mBAAmB,QAC4C,CAAC;AAE7E,wBAAsB,aAAa,CAClC,MAAM,EAAE,CAAC,CAAC,cAAc,EACxB,IAAI,EAAE,MAAM,EACZ,eAAe,GAAE,MAAM,EAAO,GAC5B,OAAO,CAAC,IAAI,CAAC,CAiGf"}
1
+ {"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../../src/routes/collect.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,KAAK,CAAC,MAAM,YAAY,CAAC;AAIrC,2CAA2C;AAC3C,wBAAgB,oBAAoB,CACnC,aAAa,EAAE,MAAM,EACrB,aAAa,GAAE,QAAQ,GAAG,OAAO,GAAG,QAAmB,GACrD,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,SAAS,GAAG,WAAW,CAAC,GAAG;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE,CAmDpE;AAED,eAAO,MAAM,mBAAmB,QAC4C,CAAC;AAE7E,wBAAsB,aAAa,CAClC,MAAM,EAAE,CAAC,CAAC,cAAc,EACxB,IAAI,EAAE,MAAM,EACZ,eAAe,GAAE,MAAM,EAAO,EAC9B,aAAa,GAAE,QAAQ,GAAG,OAAO,GAAG,QAAmB,GACrD,OAAO,CAAC,IAAI,CAAC,CAiGf"}
@@ -5,7 +5,7 @@ import { fdir as Fdir } from 'fdir';
5
5
  import c from 'picocolors';
6
6
  // eslint-disable-next-line import-x/order
7
7
  import { URLPattern as URLPatternPolyfill } from 'urlpattern-polyfill/urlpattern';
8
- // NOTE: The polyfill type lacks `hasRegExpGroups` from the global URLPattern.
8
+ // HACK: The polyfill type lacks `hasRegExpGroups` from the global URLPattern.
9
9
  const URLPattern = URLPatternPolyfill;
10
10
  import { createFilter } from 'vite';
11
11
  import { emptyRoutes } from '../logging/messages.js';
@@ -13,7 +13,7 @@ import { prepareSortableRoutes, routeComparator } from './comparator.js';
13
13
  import { REGEXES } from './load-module.js';
14
14
  const logger = getLogger();
15
15
  /** @internal Exported for unit testing. */
16
- export function extractRoutePatterns(routeFilePath) {
16
+ export function extractRoutePatterns(routeFilePath, trailingSlash = 'ignore') {
17
17
  const routePathname = routeFilePath.replace(/\.(js|ts|jsx|tsx|html)$/, '');
18
18
  let pathParts = routePathname.split(paths.isWindows() ? paths.WINDOWS_PATH_SEPARATOR : '/');
19
19
  const last = pathParts.at(-1);
@@ -42,8 +42,9 @@ export function extractRoutePatterns(routeFilePath) {
42
42
  }
43
43
  return entry;
44
44
  });
45
- const trailingSlash = pathRelativeNormalized.length > 0 ? '/' : '';
46
- const normalizedUrlPattern = `/${pathRelativeNormalized.join('/')}${trailingSlash}`;
45
+ const isRoot = pathRelativeNormalized.length === 0;
46
+ const slash = isRoot || trailingSlash === 'never' ? '' : '/';
47
+ const normalizedUrlPattern = `/${pathRelativeNormalized.join('/')}${slash}`;
47
48
  return {
48
49
  patternString: normalizedUrlPattern,
49
50
  pattern: new URLPattern(normalizedUrlPattern, 'http://gracile/'),
@@ -51,7 +52,7 @@ export function extractRoutePatterns(routeFilePath) {
51
52
  };
52
53
  }
53
54
  export const WATCHED_FILES_REGEX = /\/src\/routes\/(.*)\.(js|ts|jsx|tsx|html|css|scss|sass|less|styl|stylus)$/;
54
- export async function collectRoutes(routes, root, excludePatterns = []) {
55
+ export async function collectRoutes(routes, root, excludePatterns = [], trailingSlash = 'ignore') {
55
56
  routes.clear();
56
57
  const routesFolder = 'src/routes';
57
58
  const routesFolderAbsolute = join(root, routesFolder);
@@ -105,7 +106,7 @@ export async function collectRoutes(routes, root, excludePatterns = []) {
105
106
  // MARK: Associate
106
107
  for (const routePath of serverEntrypointsSorted) {
107
108
  const filePath = join(routesFolder, routePath);
108
- const routeWithPatterns = extractRoutePatterns(routePath);
109
+ const routeWithPatterns = extractRoutePatterns(routePath, trailingSlash);
109
110
  routes.set(routeWithPatterns.patternString, {
110
111
  filePath,
111
112
  pattern: routeWithPatterns.pattern,
@@ -7,8 +7,11 @@ type MatchedRoute = {
7
7
  params: Parameters_;
8
8
  pathname: string;
9
9
  };
10
+ export type TrailingSlashRedirect = {
11
+ redirect: string;
12
+ };
10
13
  /** @internal Exported for unit testing. */
11
- export declare function matchRouteFromUrl(url: string, routes: R.RoutesManifest): MatchedRoute | null;
14
+ export declare function matchRouteFromUrl(url: string, routes: R.RoutesManifest, trailingSlash?: 'always' | 'never' | 'ignore'): MatchedRoute | TrailingSlashRedirect | null;
12
15
  type ExtractedStaticPaths = {
13
16
  staticPaths: R.StaticPathOptionsGeneric[];
14
17
  props: unknown;
@@ -39,6 +42,7 @@ export declare function getRoute(options: {
39
42
  vite?: ViteDevServer | undefined;
40
43
  routes: R.RoutesManifest;
41
44
  routeImports?: R.RoutesImports | undefined;
42
- }): Promise<RouteInfos | null>;
45
+ trailingSlash?: 'always' | 'never' | 'ignore';
46
+ }): Promise<RouteInfos | TrailingSlashRedirect | null>;
43
47
  export {};
44
48
  //# sourceMappingURL=match.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../../src/routes/match.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAG1C,OAAO,KAAK,KAAK,CAAC,MAAM,YAAY,CAAC;AAErC,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAEtD,KAAK,YAAY,GAAG;IACnB,KAAK,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACpC,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC;IACpB,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,2CAA2C;AAC3C,wBAAgB,iBAAiB,CAChC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,CAAC,CAAC,cAAc,GACtB,YAAY,GAAG,IAAI,CAuBrB;AAED,KAAK,oBAAoB,GAAG;IAC3B,WAAW,EAAE,CAAC,CAAC,wBAAwB,EAAE,CAAC;IAC1C,KAAK,EAAE,OAAO,CAAC;CACf,GAAG,IAAI,CAAC;AACT;;;;;;GAMG;AACH,2CAA2C;AAC3C,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IACjD,WAAW,EAAE,CAAC,CAAC,WAAW,CAAC;IAC3B,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC;IACpB,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA6BhC;AAED,MAAM,MAAM,UAAU,GAAG;IACxB,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IACrC,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,wBAAsB,QAAQ,CAAC,OAAO,EAAE;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACjC,MAAM,EAAE,CAAC,CAAC,cAAc,CAAC;IACzB,YAAY,CAAC,EAAE,CAAC,CAAC,aAAa,GAAG,SAAS,CAAC;CAC3C,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CA6B7B"}
1
+ {"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../../src/routes/match.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAG1C,OAAO,KAAK,KAAK,CAAC,MAAM,YAAY,CAAC;AAErC,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAEtD,KAAK,YAAY,GAAG;IACnB,KAAK,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACpC,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC;IACpB,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD,2CAA2C;AAC3C,wBAAgB,iBAAiB,CAChC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,CAAC,CAAC,cAAc,EACxB,aAAa,GAAE,QAAQ,GAAG,OAAO,GAAG,QAAmB,GACrD,YAAY,GAAG,qBAAqB,GAAG,IAAI,CAyC7C;AAED,KAAK,oBAAoB,GAAG;IAC3B,WAAW,EAAE,CAAC,CAAC,wBAAwB,EAAE,CAAC;IAC1C,KAAK,EAAE,OAAO,CAAC;CACf,GAAG,IAAI,CAAC;AACT;;;;;;GAMG;AACH,2CAA2C;AAC3C,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IACjD,WAAW,EAAE,CAAC,CAAC,WAAW,CAAC;IAC3B,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC;IACpB,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA6BhC;AAED,MAAM,MAAM,UAAU,GAAG;IACxB,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IACrC,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,wBAAsB,QAAQ,CAAC,OAAO,EAAE;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACjC,MAAM,EAAE,CAAC,CAAC,cAAc,CAAC;IACzB,YAAY,CAAC,EAAE,CAAC,CAAC,aAAa,GAAG,SAAS,CAAC;IAC3C,aAAa,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;CAC9C,GAAG,OAAO,CAAC,UAAU,GAAG,qBAAqB,GAAG,IAAI,CAAC,CAkCrD"}
@@ -1,9 +1,23 @@
1
1
  import { loadForeignRouteObject } from './load-module.js';
2
2
  /** @internal Exported for unit testing. */
3
- export function matchRouteFromUrl(url, routes) {
3
+ export function matchRouteFromUrl(url, routes, trailingSlash = 'ignore') {
4
4
  let match;
5
5
  let foundRoute;
6
- const pathname = new URL(url).pathname;
6
+ const rawPathname = new URL(url).pathname;
7
+ // Handle redirect cases for 'always' and 'never' before matching.
8
+ // Root '/' is exempt — it always keeps its slash.
9
+ if (rawPathname !== '/') {
10
+ if (trailingSlash === 'always' && !rawPathname.endsWith('/'))
11
+ return { redirect: rawPathname + '/' };
12
+ if (trailingSlash === 'never' && rawPathname.endsWith('/'))
13
+ return { redirect: rawPathname.slice(0, -1) };
14
+ }
15
+ // For 'ignore', normalize to trailing-slash so it matches stored patterns.
16
+ const pathname = trailingSlash === 'ignore' &&
17
+ rawPathname !== '/' &&
18
+ !rawPathname.endsWith('/')
19
+ ? rawPathname + '/'
20
+ : rawPathname;
7
21
  for (const [, route] of routes) {
8
22
  if (match)
9
23
  break;
@@ -49,8 +63,10 @@ export async function extractStaticPaths(options) {
49
63
  return { staticPaths, props: properties };
50
64
  }
51
65
  export async function getRoute(options) {
52
- const matchedRoute = matchRouteFromUrl(options.url, options.routes);
66
+ const matchedRoute = matchRouteFromUrl(options.url, options.routes, options.trailingSlash);
53
67
  if (!matchedRoute)
68
+ return null;
69
+ if ('redirect' in matchedRoute)
54
70
  return matchedRoute;
55
71
  const { foundRoute, pathname, params } = matchedRoute;
56
72
  const routeModule = await loadForeignRouteObject({
@@ -1 +1 @@
1
- {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/routes/render.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAG1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAIvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,WAAW,uBAAuB;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAEpB,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,MAAM,EAAE;QACP,KAAK,EAAE,OAAO,CAAC;QACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;KACxB,CAAC;IAEF,aAAa,EAAE,OAAO,GAAG,IAAI,CAAC;CAC9B;AAcD,wBAAsB,YAAY,CAAC,EAClC,MAAM,EACN,IAAI,EACJ,UAAU,EACV,IAAoB,EACpB,aAAa,GACb,EAAE;IACF,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,aAAa,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,aAAa,CAAC;CAC7B;;;GAmKA"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/routes/render.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAG1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAIvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,WAAW,uBAAuB;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAEpB,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,MAAM,EAAE;QACP,KAAK,EAAE,OAAO,CAAC;QACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;KACxB,CAAC;IAEF,aAAa,EAAE,OAAO,GAAG,IAAI,CAAC;CAC9B;AAwBD,wBAAsB,YAAY,CAAC,EAClC,MAAM,EACN,IAAI,EACJ,UAAU,EACV,IAAoB,EACpB,aAAa,GACb,EAAE;IACF,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,aAAa,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,aAAa,CAAC;CAC7B;;;GAwKA"}
@@ -8,11 +8,18 @@ import { loadForeignRouteObject } from './load-module.js';
8
8
  async function streamToString(stream) {
9
9
  const chunks = [];
10
10
  for await (const chunk of stream) {
11
+ // NOTE: Since using Lit's `RenderResultReadable` instead of pure
12
+ // Node Readable, the chunk can be a string or a Buffer.
13
+ // If it's a string, convert it to Buffer first.
11
14
  if (typeof chunk === 'string') {
12
15
  chunks.push(Buffer.from(chunk));
13
16
  }
14
17
  else
15
- throw new TypeError('Wrong buffer');
18
+ throw new TypeError('Wrong buffer type from stream. Should be a `string` only.');
19
+ // NOTE: Disabled for now. Causes issues with `RenderResultReadable`.
20
+ /* else {
21
+ chunks.push(chunk);
22
+ } */
16
23
  }
17
24
  return Buffer.concat(chunks).toString('utf8');
18
25
  }
@@ -20,7 +27,7 @@ export async function renderRoutes({ routes, vite, serverMode, root = process.cw
20
27
  const logger = getLogger();
21
28
  logger.info(c.green('Rendering routes…'), { timestamp: true });
22
29
  // MARK: Collect
23
- await collectRoutes(routes, root, gracileConfig.routes?.exclude);
30
+ await collectRoutes(routes, root, gracileConfig.routes?.exclude, gracileConfig.trailingSlash);
24
31
  const renderedRoutes = [];
25
32
  // MARK: Iterate modules
26
33
  await Promise.all([...routes].map(async ([patternString, route]) => {
@@ -1 +1 @@
1
- {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/server/request.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAOlD,OAAO,KAAK,KAAK,CAAC,MAAM,oBAAoB,CAAC;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,EACN,KAAK,aAAa,EAOlB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,cAAc;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,CAC5B,OAAO,EAAE,OAAO,EAChB,MAAM,CAAC,EAAE,OAAO,KACZ,OAAO,CAAC,aAAa,CAAC,CAAC;AAE5B,wBAAgB,oBAAoB,CAAC,EACpC,IAAI,EACJ,MAAM,EACN,YAAY,EACZ,WAAW,EACX,IAAI,EACJ,UAAU,EACV,aAAa,GACb,EAAE;IACF,IAAI,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACjC,MAAM,EAAE,CAAC,CAAC,cAAc,CAAC;IACzB,YAAY,CAAC,EAAE,CAAC,CAAC,aAAa,GAAG,SAAS,CAAC;IAC3C,WAAW,CAAC,EAAE,CAAC,CAAC,YAAY,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACjC,aAAa,EAAE,aAAa,CAAC;CAC7B,kBAoHA;AAyBD,OAAO,EACN,KAAK,gBAAgB,EACrB,KAAK,wBAAwB,EAC7B,UAAU,GACV,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/server/request.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAOlD,OAAO,KAAK,KAAK,CAAC,MAAM,oBAAoB,CAAC;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,EACN,KAAK,aAAa,EAOlB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,cAAc;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,CAC5B,OAAO,EAAE,OAAO,EAChB,MAAM,CAAC,EAAE,OAAO,KACZ,OAAO,CAAC,aAAa,CAAC,CAAC;AAE5B,wBAAgB,oBAAoB,CAAC,EACpC,IAAI,EACJ,MAAM,EACN,YAAY,EACZ,WAAW,EACX,IAAI,EACJ,UAAU,EACV,aAAa,GACb,EAAE;IACF,IAAI,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACjC,MAAM,EAAE,CAAC,CAAC,cAAc,CAAC;IACzB,YAAY,CAAC,EAAE,CAAC,CAAC,aAAa,GAAG,SAAS,CAAC;IAC3C,WAAW,CAAC,EAAE,CAAC,CAAC,YAAY,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACjC,aAAa,EAAE,aAAa,CAAC;CAC7B,kBAuIA;AAyBD,OAAO,EACN,KAAK,gBAAgB,EACrB,KAAK,wBAAwB,EAC7B,UAAU,GACV,MAAM,uBAAuB,CAAC"}
@@ -25,14 +25,31 @@ export function createGracileHandler({ vite, routes, routeImports, routeAssets,
25
25
  vite,
26
26
  routes,
27
27
  routeImports,
28
+ trailingSlash: gracileConfig.trailingSlash ?? 'ignore',
28
29
  };
29
30
  const responseInit = {};
30
- let routeInfos = await getRoute(routeOptions);
31
+ const routeResult = await getRoute(routeOptions);
32
+ // Trailing slash redirect (301 for GET, 308 for other methods)
33
+ if (routeResult && 'redirect' in routeResult) {
34
+ const redirectUrl = new URL(routeResult.redirect, fullUrl).href;
35
+ const status = method === 'GET' ? 301 : 308;
36
+ return { response: Response.redirect(redirectUrl, status) };
37
+ }
38
+ let routeInfos = routeResult;
31
39
  if (routeInfos === null) {
32
40
  responseInit.status = 404;
41
+ // Use 'ignore' for the internal 404 lookup to avoid redirect loops.
33
42
  const url = new URL('/404/', fullUrl).href;
34
- const options = { ...routeOptions, url };
35
- routeInfos = await getRoute(options);
43
+ const options = {
44
+ ...routeOptions,
45
+ url,
46
+ trailingSlash: 'ignore',
47
+ };
48
+ const notFoundResult = await getRoute(options);
49
+ routeInfos =
50
+ notFoundResult && !('redirect' in notFoundResult)
51
+ ? notFoundResult
52
+ : null;
36
53
  }
37
54
  if (routeInfos === null) {
38
55
  const page = builtIn404Page(new URL(fullUrl).pathname, Boolean(vite));
@@ -38,6 +38,19 @@ export interface GracileConfig {
38
38
  * @defaultValue 'static'
39
39
  */
40
40
  output?: 'static' | 'server';
41
+ /**
42
+ * Controls how trailing slashes are matched on incoming URLs.
43
+ *
44
+ * - `'ignore'` — Match regardless of whether a trailing `/` is present.
45
+ * `/about` and `/about/` both resolve to the same route. *(default)*
46
+ * - `'always'` — Only match URLs that include a trailing slash (e.g. `/about/`).
47
+ * Requests without one are redirected: `301` for GET, `308` for other methods.
48
+ * - `'never'` — Only match URLs that do not include a trailing slash (e.g. `/about`).
49
+ * Requests with one are redirected: `301` for GET, `308` for other methods.
50
+ *
51
+ * @defaultValue 'ignore'
52
+ */
53
+ trailingSlash?: 'always' | 'never' | 'ignore';
41
54
  /**
42
55
  * Settings for the development mode.
43
56
  */
@@ -1 +1 @@
1
- {"version":3,"file":"user-config.d.ts","sourceRoot":"","sources":["../src/user-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,WAAW,aAAa;IAC7B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAE7B;;OAEG;IACH,GAAG,CAAC,EAAE;QACL;;;;;WAKG;QACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE;YAAE,WAAW,EAAE,OAAO,CAAC,eAAe,CAAA;SAAE,KAAK,OAAO,CAAC;KACxE,CAAC;IAEF;;OAEG;IACH,MAAM,CAAC,EAAE;QACR;;WAEG;QACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC;IAEF;;OAEG;IACH,KAAK,CAAC,EAAE;QACP;;;;;;;;;;;;;WAaG;QACH,QAAQ,CAAC,EAAE;YACV;;eAEG;YACH,MAAM,CAAC,EAAE,OAAO,CAAC;YAGjB;;eAEG;YACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;YAEnB;;eAEG;YACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;SACnB,CAAC;KACF,CAAC;IACF,MAAM,CAAC,EAAE;QACR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WA8BG;QACH,UAAU,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;KACjC,CAAC;IAEF;;OAEG;IACH,YAAY,CAAC,EAAE;QACd;;;WAGG;QACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;KAOhC,CAAC;CACF"}
1
+ {"version":3,"file":"user-config.d.ts","sourceRoot":"","sources":["../src/user-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,WAAW,aAAa;IAC7B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAE7B;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;IAE9C;;OAEG;IACH,GAAG,CAAC,EAAE;QACL;;;;;WAKG;QACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE;YAAE,WAAW,EAAE,OAAO,CAAC,eAAe,CAAA;SAAE,KAAK,OAAO,CAAC;KACxE,CAAC;IAEF;;OAEG;IACH,MAAM,CAAC,EAAE;QACR;;WAEG;QACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC;IAEF;;OAEG;IACH,KAAK,CAAC,EAAE;QACP;;;;;;;;;;;;;WAaG;QACH,QAAQ,CAAC,EAAE;YACV;;eAEG;YACH,MAAM,CAAC,EAAE,OAAO,CAAC;YAGjB;;eAEG;YACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;YAEnB;;eAEG;YACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;SACnB,CAAC;KACF,CAAC;IACF,MAAM,CAAC,EAAE;QACR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WA8BG;QACH,UAAU,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;KACjC,CAAC;IAEF;;OAEG;IACH,YAAY,CAAC,EAAE;QACd;;;WAGG;QACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;KAOhC,CAAC;CACF"}
@@ -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.5",
3
+ "version": "0.9.0-next.7",
4
4
  "description": "A thin, full-stack, web framework",
5
5
  "keywords": [
6
6
  "custom-elements",
@@ -43,8 +43,8 @@
43
43
  "!/dist/typedoc-entrypoint.*"
44
44
  ],
45
45
  "dependencies": {
46
- "@gracile-labs/better-errors": "^0.2.0-next.4",
47
- "@gracile/internal-utils": "^0.6.0-next.3",
46
+ "@gracile-labs/better-errors": "^0.2.1-next.0",
47
+ "@gracile/internal-utils": "^0.6.1-next.0",
48
48
  "@whatwg-node/server": "^0.10.18",
49
49
  "fdir": "^6.5.0",
50
50
  "picocolors": "^1.1.1",
@@ -60,5 +60,5 @@
60
60
  "access": "public",
61
61
  "provenance": true
62
62
  },
63
- "gitHead": "534579e0c18fd2a9fbfaa7d9cb180187447fb193"
63
+ "gitHead": "9abb3f9de0217a6284a88b114194b4d254d0a104"
64
64
  }