@gracile/engine 0.9.0 → 0.9.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.
Files changed (62) hide show
  1. package/dist/dev/development.d.ts.map +1 -1
  2. package/dist/dev/development.js +1 -1
  3. package/dist/dev/ssr-ce-tracker.d.ts +27 -0
  4. package/dist/dev/ssr-ce-tracker.d.ts.map +1 -0
  5. package/dist/dev/ssr-ce-tracker.js +113 -0
  6. package/dist/plugin.d.ts.map +1 -1
  7. package/dist/plugin.js +51 -264
  8. package/dist/render/route-template-pipeline.d.ts +64 -0
  9. package/dist/render/route-template-pipeline.d.ts.map +1 -0
  10. package/dist/render/route-template-pipeline.js +144 -0
  11. package/dist/render/route-template.d.ts +1 -2
  12. package/dist/render/route-template.d.ts.map +1 -1
  13. package/dist/render/route-template.js +37 -92
  14. package/dist/routes/collect.d.ts +5 -1
  15. package/dist/routes/collect.d.ts.map +1 -1
  16. package/dist/routes/collect.js +8 -6
  17. package/dist/routes/load-module.d.ts.map +1 -1
  18. package/dist/routes/load-module.js +5 -2
  19. package/dist/routes/match.d.ts +31 -1
  20. package/dist/routes/match.d.ts.map +1 -1
  21. package/dist/routes/match.js +22 -4
  22. package/dist/routes/render.d.ts.map +1 -1
  23. package/dist/routes/render.js +11 -3
  24. package/dist/server/request-pipeline.d.ts +109 -0
  25. package/dist/server/request-pipeline.d.ts.map +1 -0
  26. package/dist/server/request-pipeline.js +198 -0
  27. package/dist/server/request.d.ts +3 -16
  28. package/dist/server/request.d.ts.map +1 -1
  29. package/dist/server/request.js +74 -171
  30. package/dist/test/init.d.ts +2 -0
  31. package/dist/test/init.d.ts.map +1 -0
  32. package/dist/test/init.js +7 -0
  33. package/dist/user-config.d.ts +13 -0
  34. package/dist/user-config.d.ts.map +1 -1
  35. package/dist/vite/build-routes.d.ts +1 -2
  36. package/dist/vite/build-routes.d.ts.map +1 -1
  37. package/dist/vite/build-routes.js +0 -98
  38. package/dist/vite/plugin-build-environment.d.ts +21 -0
  39. package/dist/vite/plugin-build-environment.d.ts.map +1 -0
  40. package/dist/vite/plugin-build-environment.js +83 -0
  41. package/dist/vite/plugin-ce-tracker.d.ts +19 -0
  42. package/dist/vite/plugin-ce-tracker.d.ts.map +1 -0
  43. package/dist/vite/plugin-ce-tracker.js +87 -0
  44. package/dist/vite/plugin-client-build.d.ts +20 -0
  45. package/dist/vite/plugin-client-build.d.ts.map +1 -0
  46. package/dist/vite/plugin-client-build.js +55 -0
  47. package/dist/vite/plugin-html-routes-build.d.ts +22 -0
  48. package/dist/vite/plugin-html-routes-build.d.ts.map +1 -0
  49. package/dist/vite/plugin-html-routes-build.js +140 -0
  50. package/dist/vite/plugin-serve.d.ts +18 -0
  51. package/dist/vite/plugin-serve.d.ts.map +1 -0
  52. package/dist/vite/plugin-serve.js +61 -0
  53. package/dist/vite/plugin-server-build.d.ts +43 -0
  54. package/dist/vite/plugin-server-build.d.ts.map +1 -0
  55. package/dist/vite/plugin-server-build.js +108 -0
  56. package/dist/vite/plugin-shared-state.d.ts +33 -0
  57. package/dist/vite/plugin-shared-state.d.ts.map +1 -0
  58. package/dist/vite/plugin-shared-state.js +23 -0
  59. package/dist/vite/virtual-routes.d.ts +8 -5
  60. package/dist/vite/virtual-routes.d.ts.map +1 -1
  61. package/dist/vite/virtual-routes.js +32 -31
  62. package/package.json +5 -5
@@ -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"}
@@ -1,4 +1,4 @@
1
- import { type Plugin, type ViteDevServer } from 'vite';
1
+ import { type ViteDevServer } from 'vite';
2
2
  import { type RenderedRouteDefinition } from '../routes/render.js';
3
3
  import type { RoutesManifest } from '../routes/route.js';
4
4
  import type { GracileConfig } from '../user-config.js';
@@ -12,6 +12,5 @@ export declare const buildRoutes: ({ routes, viteServerForBuild, root, gracileCo
12
12
  routes: RoutesManifest;
13
13
  renderedRoutes: RenderedRouteDefinition[];
14
14
  inputList: string[];
15
- plugin: Plugin[];
16
15
  }>;
17
16
  //# sourceMappingURL=build-routes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"build-routes.d.ts","sourceRoot":"","sources":["../../src/vite/build-routes.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,MAAM,EAAE,KAAK,aAAa,EAAE,MAAM,MAAM,CAAC;AAErE,OAAO,EAEN,KAAK,uBAAuB,EAC5B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AASvD,eAAO,MAAM,WAAW,GAAU,kEAM/B;IACF,MAAM,EAAE,cAAc,CAAC;IACvB,kBAAkB,EAAE,aAAa,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,aAAa,CAAC;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB,KAAG,OAAO,CAAC;IACX,MAAM,EAAE,cAAc,CAAC;IACvB,cAAc,EAAE,uBAAuB,EAAE,CAAC;IAC1C,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CACjB,CA6JA,CAAC"}
1
+ {"version":3,"file":"build-routes.d.ts","sourceRoot":"","sources":["../../src/vite/build-routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,aAAa,EAAE,MAAM,MAAM,CAAC;AAExD,OAAO,EAEN,KAAK,uBAAuB,EAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AASvD,eAAO,MAAM,WAAW,GAAU,kEAM/B;IACF,MAAM,EAAE,cAAc,CAAC;IACvB,kBAAkB,EAAE,aAAa,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,aAAa,CAAC;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB,KAAG,OAAO,CAAC;IACX,MAAM,EAAE,cAAc,CAAC;IACvB,cAAc,EAAE,uBAAuB,EAAE,CAAC;IAC1C,SAAS,EAAE,MAAM,EAAE,CAAC;CACpB,CAkCA,CAAC"}
@@ -1,7 +1,5 @@
1
- import { basename, extname, join } from 'node:path';
2
1
  import { createFilter } from 'vite';
3
2
  import { renderRoutes, } from '../routes/render.js';
4
- import { REGEX_TAG_LINK, REGEX_TAG_SCRIPT } from '../render/route-template.js';
5
3
  function stripPremises(input) {
6
4
  return input
7
5
  .replace(/index\.html$/, '__index.doc.html')
@@ -9,7 +7,6 @@ function stripPremises(input) {
9
7
  .replace(/500\.html$/, '__500.doc.html');
10
8
  }
11
9
  export const buildRoutes = async ({ routes, viteServerForBuild, root, gracileConfig, serverMode = false, }) => {
12
- // TODO: extract upstream, return just the plugins
13
10
  const { renderedRoutes } = await renderRoutes({
14
11
  vite: viteServerForBuild,
15
12
  serverMode,
@@ -35,100 +32,5 @@ export const buildRoutes = async ({ routes, viteServerForBuild, root, gracileCon
35
32
  routes,
36
33
  renderedRoutes,
37
34
  inputList,
38
- plugin: [
39
- {
40
- name: 'gracile-html-routes',
41
- apply: 'build',
42
- enforce: 'pre',
43
- resolveId(id) {
44
- if (extname(id) === '.html' && inputList.includes(id))
45
- return join(root, id);
46
- return null;
47
- },
48
- load(id) {
49
- if (extname(id) === '.html') {
50
- if (['index.html', '404.html', '500.html'].includes(basename(id))) {
51
- const content = renderedRoutes.find((index) => index.absoluteId === id)?.html;
52
- if (content)
53
- return content;
54
- }
55
- if (gracileConfig.pages?.premises?.expose) {
56
- if (basename(id).endsWith('doc.html')) {
57
- const content = renderedRoutes.find((index) => stripPremises(index.absoluteId) === id)?.static.document;
58
- if (content)
59
- return content;
60
- }
61
- const content = renderedRoutes.find((index) => index.name === basename(id))?.html;
62
- if (content)
63
- return content;
64
- // return '';}
65
- }
66
- }
67
- return null;
68
- },
69
- },
70
- {
71
- name: 'gracile-collect-handler-assets',
72
- enforce: 'post',
73
- buildStart() {
74
- if (!gracileConfig.pages?.premises?.expose || !premisesFilter)
75
- return;
76
- for (const route of renderedRoutes) {
77
- if (premisesFilter(route.name) === false)
78
- continue;
79
- if (serverMode && route.savePrerender !== true)
80
- continue;
81
- const fileNameParts = route.name.split('/');
82
- const last = fileNameParts.pop();
83
- const properties = last?.replace(/(.*)\.html$/, (_, r) => `__${r}.props.json`);
84
- if (!properties)
85
- throw new Error('No props.');
86
- fileNameParts.push(properties);
87
- const fileName = fileNameParts.join('/');
88
- this.emitFile({
89
- fileName,
90
- type: 'asset',
91
- source: JSON.stringify(route.static.props ?? {}),
92
- });
93
- }
94
- },
95
- generateBundle(_, bundle) {
96
- if (serverMode === false)
97
- return;
98
- for (const fileKey in bundle) {
99
- const file = bundle[fileKey];
100
- if (fileKey.endsWith('.html') && file && 'source' in file) {
101
- const source = file.source.toString();
102
- const collectedAssets = [
103
- ...[...source.matchAll(REGEX_TAG_SCRIPT)]
104
- .map((v) => v[0])
105
- // NOTE: Too brittle
106
- .filter((v) => v.includes(`type="module"`)),
107
- ...[...source.matchAll(REGEX_TAG_LINK)]
108
- .map((v) => v[0])
109
- // NOTE: Too brittle
110
- .filter((v) => /rel="(stylesheet|modulepreload)"/.test(v)),
111
- ].join('\n');
112
- // NOTE: Not used (for now?)
113
- // file.source = alteredContent;
114
- const route = renderedRoutes.find((r) => {
115
- if (gracileConfig.pages?.premises?.expose)
116
- return (`/${r.name}` ===
117
- `/${fileKey}`.replace(/(.*?)\/__(.*?)\.doc\.html$/, (a, b, c) => `${b}/${c}.html`));
118
- return r.name === fileKey;
119
- });
120
- if (route)
121
- route.handlerAssets = collectedAssets;
122
- if (route?.savePrerender !== true) {
123
- delete bundle[fileKey];
124
- // NOTE: Not sure if it's useful
125
- if (route?.html)
126
- route.html = null;
127
- }
128
- }
129
- }
130
- },
131
- },
132
- ],
133
35
  };
134
36
  };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Vite plugin: build environment configuration.
3
+ *
4
+ * Owns all Environment API concerns for builds:
5
+ * - Top-level config: `builder.sharedConfigBuild`, `ssr.external` (server mode)
6
+ * - `configEnvironment('client')`: rollupOptions.input, outDir
7
+ * - `configEnvironment('ssr')`: outDir, SSR-specific build options, rollupOptions
8
+ * - `buildApp()`: orchestrates build order (client-only or client → SSR)
9
+ *
10
+ * `rollupOptions.input` must be set via `configEnvironment` (not the
11
+ * top-level `config` hook) because `builder.build(env)` uses
12
+ * environment-resolved config, not the top-level config.
13
+ *
14
+ * @internal
15
+ */
16
+ import type { PluginOption } from 'vite';
17
+ import type { PluginSharedState } from './plugin-shared-state.js';
18
+ export declare function gracileBuildEnvironmentPlugin({ state, }: {
19
+ state: PluginSharedState;
20
+ }): PluginOption;
21
+ //# sourceMappingURL=plugin-build-environment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-build-environment.d.ts","sourceRoot":"","sources":["../../src/vite/plugin-build-environment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,MAAM,CAAC;AAEtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,wBAAgB,6BAA6B,CAAC,EAC7C,KAAK,GACL,EAAE;IACF,KAAK,EAAE,iBAAiB,CAAC;CACzB,GAAG,YAAY,CA4Ef"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Vite plugin: build environment configuration.
3
+ *
4
+ * Owns all Environment API concerns for builds:
5
+ * - Top-level config: `builder.sharedConfigBuild`, `ssr.external` (server mode)
6
+ * - `configEnvironment('client')`: rollupOptions.input, outDir
7
+ * - `configEnvironment('ssr')`: outDir, SSR-specific build options, rollupOptions
8
+ * - `buildApp()`: orchestrates build order (client-only or client → SSR)
9
+ *
10
+ * `rollupOptions.input` must be set via `configEnvironment` (not the
11
+ * top-level `config` hook) because `builder.build(env)` uses
12
+ * environment-resolved config, not the top-level config.
13
+ *
14
+ * @internal
15
+ */
16
+ import { join } from 'node:path';
17
+ export function gracileBuildEnvironmentPlugin({ state, }) {
18
+ const isServerMode = state.outputMode === 'server';
19
+ return {
20
+ name: 'vite-plugin-gracile-build-env',
21
+ apply: 'build',
22
+ config() {
23
+ return {
24
+ ...(isServerMode ? { ssr: { external: ['@gracile/gracile'] } } : {}),
25
+ builder: { sharedConfigBuild: true },
26
+ };
27
+ },
28
+ configEnvironment(name, config) {
29
+ if (name === 'client') {
30
+ if (!state.clientBuildInputList)
31
+ return null;
32
+ return {
33
+ build: {
34
+ rollupOptions: {
35
+ input: state.clientBuildInputList,
36
+ },
37
+ outDir: join(config.build?.outDir || 'dist', isServerMode ? 'client' : ''),
38
+ },
39
+ };
40
+ }
41
+ if (name === 'ssr' && isServerMode) {
42
+ return {
43
+ build: {
44
+ outDir: 'dist/server',
45
+ copyPublicDir: false,
46
+ ssrEmitAssets: true,
47
+ cssMinify: true,
48
+ cssCodeSplit: true,
49
+ rollupOptions: {
50
+ input: 'entrypoint.js',
51
+ output: {
52
+ entryFileNames: '[name].js',
53
+ assetFileNames: (chunkInfo) => {
54
+ if (chunkInfo.name) {
55
+ const fileName = state.clientAssets[chunkInfo.name];
56
+ if (fileName)
57
+ return fileName;
58
+ return `assets/${chunkInfo.name.replace(/\.(.*)$/, '')}-[hash].[ext]`;
59
+ }
60
+ return 'assets/[name]-[hash].[ext]';
61
+ },
62
+ chunkFileNames: 'chunk/[name].js',
63
+ },
64
+ },
65
+ },
66
+ };
67
+ }
68
+ return null;
69
+ },
70
+ async buildApp(builder) {
71
+ const client = builder.environments['client'];
72
+ if (!client)
73
+ throw new Error('Missing client build environment.');
74
+ await builder.build(client);
75
+ if (isServerMode) {
76
+ const ssr = builder.environments['ssr'];
77
+ if (!ssr)
78
+ throw new Error('Missing ssr build environment.');
79
+ await builder.build(ssr);
80
+ }
81
+ },
82
+ };
83
+ }
@@ -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
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Vite plugin: client-side build route rendering.
3
+ *
4
+ * During `vite build`, this spins up a temporary dev server to render
5
+ * all routes into static HTML, then populates shared state with the
6
+ * rendered routes and input list.
7
+ *
8
+ * The actual HTML route resolution and asset collection are handled by
9
+ * separate Vite plugins (see `plugin-html-routes-build.ts`) that read
10
+ * from shared state and use `applyToEnvironment` to scope to the client.
11
+ *
12
+ * @internal
13
+ */
14
+ import { type PluginOption } from 'vite';
15
+ import type { PluginSharedState } from './plugin-shared-state.js';
16
+ export declare function gracileClientBuildPlugin({ state, virtualRoutesForClient, }: {
17
+ state: PluginSharedState;
18
+ virtualRoutesForClient: PluginOption;
19
+ }): PluginOption;
20
+ //# sourceMappingURL=plugin-client-build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-client-build.d.ts","sourceRoot":"","sources":["../../src/vite/plugin-client-build.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,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,CAoDf"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Vite plugin: client-side build route rendering.
3
+ *
4
+ * During `vite build`, this spins up a temporary dev server to render
5
+ * all routes into static HTML, then populates shared state with the
6
+ * rendered routes and input list.
7
+ *
8
+ * The actual HTML route resolution and asset collection are handled by
9
+ * separate Vite plugins (see `plugin-html-routes-build.ts`) that read
10
+ * from shared state and use `applyToEnvironment` to scope to the client.
11
+ *
12
+ * @internal
13
+ */
14
+ import { getPluginContext } from '@gracile/internal-utils/plugin-context';
15
+ import { createServer } from 'vite';
16
+ import { buildRoutes } from './build-routes.js';
17
+ export function gracileClientBuildPlugin({ state, virtualRoutesForClient, }) {
18
+ return {
19
+ name: 'vite-plugin-gracile-build',
20
+ apply: 'build',
21
+ async config(viteConfig) {
22
+ state.root = viteConfig.root || process.cwd();
23
+ const viteServerForClientHtmlBuild = await createServer({
24
+ root: state.root,
25
+ server: { middlewareMode: true },
26
+ // NOTE: Stub. KEEP IT!
27
+ optimizeDeps: { include: [] },
28
+ plugins: [virtualRoutesForClient],
29
+ });
30
+ // NOTE: Important. Get the dev. server elements renderers.
31
+ state.gracileConfig.litSsr ??= {};
32
+ state.gracileConfig.litSsr.renderInfo = getPluginContext(viteServerForClientHtmlBuild.config)?.litSsrRenderInfo;
33
+ const htmlPages = await buildRoutes({
34
+ viteServerForBuild: viteServerForClientHtmlBuild,
35
+ root: viteConfig.root || process.cwd(),
36
+ gracileConfig: state.gracileConfig,
37
+ serverMode: state.outputMode === 'server',
38
+ routes: state.routes,
39
+ });
40
+ state.renderedRoutes = htmlPages.renderedRoutes;
41
+ state.clientBuildInputList = htmlPages.inputList;
42
+ // NOTE: Vite's dev server does not invoke Rollup's `closeWatcher`
43
+ // hook when shutting down. Plugins like @rollup/plugin-typescript
44
+ // use `ts.createWatchProgram()` which sets up hundreds of FS
45
+ // watchers; without an explicit `closeWatcher` call they are
46
+ // leaked and the Node process hangs after build.
47
+ for (const plugin of viteServerForClientHtmlBuild.config.plugins) {
48
+ if (typeof plugin.closeWatcher === 'function') {
49
+ await plugin.closeWatcher();
50
+ }
51
+ }
52
+ await viteServerForClientHtmlBuild.close();
53
+ },
54
+ };
55
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Vite plugins: HTML route resolution and asset collection for builds.
3
+ *
4
+ * These are registered as Vite plugins (not Rollup plugins via
5
+ * `rollupOptions.plugins`) so they work correctly with the Environment
6
+ * API's `builder.build(env)`, which does not inherit top-level
7
+ * `rollupOptions` into environment builds.
8
+ *
9
+ * They use `applyToEnvironment` to scope to the client build only,
10
+ * preventing the SSR build from trying to resolve HTML entries.
11
+ *
12
+ * Data is read lazily from shared state, which is populated by the
13
+ * client build plugin's `config` hook before these hooks run.
14
+ *
15
+ * @internal
16
+ */
17
+ import { type PluginOption } from 'vite';
18
+ import type { PluginSharedState } from './plugin-shared-state.js';
19
+ export declare function gracileHtmlRoutesBuildPlugins({ state, }: {
20
+ state: PluginSharedState;
21
+ }): PluginOption[];
22
+ //# sourceMappingURL=plugin-html-routes-build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-html-routes-build.d.ts","sourceRoot":"","sources":["../../src/vite/plugin-html-routes-build.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,MAAM,CAAC;AAIvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AASlE,wBAAgB,6BAA6B,CAAC,EAC7C,KAAK,GACL,EAAE;IACF,KAAK,EAAE,iBAAiB,CAAC;CACzB,GAAG,YAAY,EAAE,CAiJjB"}
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Vite plugins: HTML route resolution and asset collection for builds.
3
+ *
4
+ * These are registered as Vite plugins (not Rollup plugins via
5
+ * `rollupOptions.plugins`) so they work correctly with the Environment
6
+ * API's `builder.build(env)`, which does not inherit top-level
7
+ * `rollupOptions` into environment builds.
8
+ *
9
+ * They use `applyToEnvironment` to scope to the client build only,
10
+ * preventing the SSR build from trying to resolve HTML entries.
11
+ *
12
+ * Data is read lazily from shared state, which is populated by the
13
+ * client build plugin's `config` hook before these hooks run.
14
+ *
15
+ * @internal
16
+ */
17
+ import { basename, extname, join } from 'node:path';
18
+ import { createFilter } from 'vite';
19
+ import { REGEX_TAG_LINK, REGEX_TAG_SCRIPT } from '../render/route-template.js';
20
+ function stripPremises(input) {
21
+ return input
22
+ .replace(/index\.html$/, '__index.doc.html')
23
+ .replace(/404\.html$/, '__404.doc.html')
24
+ .replace(/500\.html$/, '__500.doc.html');
25
+ }
26
+ export function gracileHtmlRoutesBuildPlugins({ state, }) {
27
+ return [
28
+ {
29
+ name: 'gracile-html-routes',
30
+ apply: 'build',
31
+ enforce: 'pre',
32
+ applyToEnvironment(environment) {
33
+ return environment.name !== 'ssr';
34
+ },
35
+ resolveId(id) {
36
+ const inputList = state.clientBuildInputList;
37
+ const root = state.root;
38
+ if (!inputList || !root)
39
+ return null;
40
+ if (extname(id) === '.html' && inputList.includes(id))
41
+ return join(root, id);
42
+ return null;
43
+ },
44
+ load(id) {
45
+ const renderedRoutes = state.renderedRoutes;
46
+ if (!renderedRoutes)
47
+ return null;
48
+ if (extname(id) === '.html') {
49
+ if (['index.html', '404.html', '500.html'].includes(basename(id))) {
50
+ const content = renderedRoutes.find((index) => index.absoluteId === id)?.html;
51
+ if (content)
52
+ return content;
53
+ }
54
+ if (state.gracileConfig.pages?.premises?.expose) {
55
+ if (basename(id).endsWith('doc.html')) {
56
+ const content = renderedRoutes.find((index) => stripPremises(index.absoluteId) === id)?.static.document;
57
+ if (content)
58
+ return content;
59
+ }
60
+ const content = renderedRoutes.find((index) => index.name === basename(id))?.html;
61
+ if (content)
62
+ return content;
63
+ }
64
+ }
65
+ return null;
66
+ },
67
+ },
68
+ {
69
+ name: 'gracile-collect-handler-assets',
70
+ apply: 'build',
71
+ enforce: 'post',
72
+ applyToEnvironment(environment) {
73
+ return environment.name !== 'ssr';
74
+ },
75
+ buildStart() {
76
+ const renderedRoutes = state.renderedRoutes;
77
+ if (!renderedRoutes)
78
+ return;
79
+ if (!state.gracileConfig.pages?.premises?.expose)
80
+ return;
81
+ const premisesFilter = createFilter(state.gracileConfig.pages.premises.include, state.gracileConfig.pages.premises.exclude);
82
+ for (const route of renderedRoutes) {
83
+ if (premisesFilter(route.name) === false)
84
+ continue;
85
+ if (state.outputMode === 'server' && route.savePrerender !== true)
86
+ continue;
87
+ const fileNameParts = route.name.split('/');
88
+ const last = fileNameParts.pop();
89
+ const properties = last?.replace(/(.*)\.html$/, (_, r) => `__${r}.props.json`);
90
+ if (!properties)
91
+ throw new Error('No props.');
92
+ fileNameParts.push(properties);
93
+ const fileName = fileNameParts.join('/');
94
+ this.emitFile({
95
+ fileName,
96
+ type: 'asset',
97
+ source: JSON.stringify(route.static.props ?? {}),
98
+ });
99
+ }
100
+ },
101
+ generateBundle(_, bundle) {
102
+ const renderedRoutes = state.renderedRoutes;
103
+ if (!renderedRoutes)
104
+ return;
105
+ if (state.outputMode !== 'server')
106
+ return;
107
+ for (const fileKey in bundle) {
108
+ const file = bundle[fileKey];
109
+ if (fileKey.endsWith('.html') && file && 'source' in file) {
110
+ const source = file.source.toString();
111
+ const collectedAssets = [
112
+ ...[...source.matchAll(REGEX_TAG_SCRIPT)]
113
+ .map((v) => v[0])
114
+ // NOTE: Too brittle
115
+ .filter((v) => v.includes(`type="module"`)),
116
+ ...[...source.matchAll(REGEX_TAG_LINK)]
117
+ .map((v) => v[0])
118
+ // NOTE: Too brittle
119
+ .filter((v) => /rel="(stylesheet|modulepreload)"/.test(v)),
120
+ ].join('\n');
121
+ const route = renderedRoutes.find((r) => {
122
+ if (state.gracileConfig.pages?.premises?.expose)
123
+ return (`/${r.name}` ===
124
+ `/${fileKey}`.replace(/(.*?)\/__(.*?)\.doc\.html$/, (a, b, c) => `${b}/${c}.html`));
125
+ return r.name === fileKey;
126
+ });
127
+ if (route)
128
+ route.handlerAssets = collectedAssets;
129
+ if (route?.savePrerender !== true) {
130
+ delete bundle[fileKey];
131
+ // NOTE: Not sure if it's useful
132
+ if (route?.html)
133
+ route.html = null;
134
+ }
135
+ }
136
+ }
137
+ },
138
+ },
139
+ ];
140
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Vite plugin: development server middleware.
3
+ *
4
+ * Sets up the Gracile request handler, route watcher, and dev-time
5
+ * logging for `vite dev`.
6
+ *
7
+ * @internal
8
+ */
9
+ import type { Logger, PluginOption } from 'vite';
10
+ import type { GracileConfig } from '../user-config.js';
11
+ import type { PluginSharedState } from './plugin-shared-state.js';
12
+ export declare function gracileServePlugin({ state, config, logger, resetClientBuiltFlag, }: {
13
+ state: PluginSharedState;
14
+ config: GracileConfig | undefined;
15
+ logger: Logger;
16
+ resetClientBuiltFlag: () => void;
17
+ }): PluginOption;
18
+ //# sourceMappingURL=plugin-serve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-serve.d.ts","sourceRoot":"","sources":["../../src/vite/plugin-serve.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAIjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,wBAAgB,kBAAkB,CAAC,EAClC,KAAK,EACL,MAAM,EACN,MAAM,EACN,oBAAoB,GACpB,EAAE;IACF,KAAK,EAAE,iBAAiB,CAAC;IACzB,MAAM,EAAE,aAAa,GAAG,SAAS,CAAC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,oBAAoB,EAAE,MAAM,IAAI,CAAC;CACjC,GAAG,YAAY,CA4Df"}