@analogjs/router 3.0.0-alpha.6 → 3.0.0-alpha.60
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/content/package.json +4 -0
- package/fesm2022/analogjs-router-content.mjs +63 -0
- package/fesm2022/analogjs-router-content.mjs.map +1 -0
- package/fesm2022/analogjs-router-i18n.mjs +156 -0
- package/fesm2022/analogjs-router-i18n.mjs.map +1 -0
- package/fesm2022/analogjs-router-server-actions.mjs +2 -27
- package/fesm2022/analogjs-router-server.mjs +60 -3
- package/fesm2022/analogjs-router-server.mjs.map +1 -0
- package/fesm2022/analogjs-router-tanstack-query-server.mjs +71 -0
- package/fesm2022/analogjs-router-tanstack-query-server.mjs.map +1 -0
- package/fesm2022/analogjs-router-tanstack-query.mjs +39 -0
- package/fesm2022/analogjs-router-tanstack-query.mjs.map +1 -0
- package/fesm2022/analogjs-router-tokens.mjs +7 -2
- package/fesm2022/analogjs-router-tokens.mjs.map +1 -0
- package/fesm2022/analogjs-router.mjs +559 -61
- package/fesm2022/analogjs-router.mjs.map +1 -0
- package/fesm2022/debug.page.mjs +53 -31
- package/fesm2022/debug.page.mjs.map +1 -0
- package/fesm2022/provide-analog-query.mjs +72 -0
- package/fesm2022/provide-analog-query.mjs.map +1 -0
- package/fesm2022/route-files.mjs +361 -0
- package/fesm2022/route-files.mjs.map +1 -0
- package/fesm2022/routes.mjs +5 -278
- package/fesm2022/routes.mjs.map +1 -0
- package/fesm2022/src.mjs +335 -0
- package/fesm2022/src.mjs.map +1 -0
- package/i18n/package.json +4 -0
- package/package.json +77 -26
- package/tanstack-query/package.json +4 -0
- package/tanstack-query/server/package.json +4 -0
- package/types/content/src/index.d.ts +4 -0
- package/types/content/src/lib/debug/routes.d.ts +10 -0
- package/types/{src → content/src}/lib/markdown-helpers.d.ts +1 -1
- package/types/content/src/lib/routes.d.ts +8 -0
- package/types/content/src/lib/with-content-routes.d.ts +2 -0
- package/types/i18n/src/index.d.ts +1 -0
- package/types/i18n/src/provide-i18n.d.ts +92 -0
- package/types/server/actions/src/define-action.d.ts +54 -0
- package/types/server/actions/src/define-api-route.d.ts +57 -0
- package/types/server/actions/src/define-page-load.d.ts +55 -0
- package/types/server/actions/src/define-server-route.d.ts +68 -0
- package/types/server/actions/src/index.d.ts +9 -1
- package/types/server/actions/src/parse-request-data.d.ts +9 -0
- package/types/server/actions/src/validate.d.ts +8 -0
- package/types/server/src/provide-server-context.d.ts +15 -1
- package/types/server/src/render.d.ts +1 -1
- package/types/server/src/server-component-render.d.ts +1 -1
- package/types/src/index.d.ts +16 -5
- package/types/src/lib/cache-key.d.ts +1 -1
- package/types/src/lib/cookie-interceptor.d.ts +1 -1
- package/types/src/lib/debug/debug.page.d.ts +4 -2
- package/types/src/lib/define-route.d.ts +6 -1
- package/types/src/lib/endpoints.d.ts +1 -1
- package/types/src/lib/experimental.d.ts +140 -0
- package/types/src/lib/form-action.directive.d.ts +12 -5
- package/types/src/lib/inject-load.d.ts +5 -2
- package/types/src/lib/inject-navigate.d.ts +23 -0
- package/types/src/lib/inject-route-context.d.ts +32 -0
- package/types/src/lib/inject-typed-params.d.ts +63 -0
- package/types/src/lib/json-ld.d.ts +32 -0
- package/types/src/lib/meta-tags.d.ts +3 -1
- package/types/src/lib/models.d.ts +3 -0
- package/types/src/lib/provide-file-router-base.d.ts +4 -0
- package/types/src/lib/provide-file-router.d.ts +2 -8
- package/types/src/lib/route-builder.d.ts +5 -0
- package/types/src/lib/route-files.d.ts +18 -0
- package/types/src/lib/route-path.d.ts +124 -0
- package/types/src/lib/route-types.d.ts +2 -1
- package/types/src/lib/routes.d.ts +2 -10
- package/types/src/lib/validation-errors.d.ts +7 -0
- package/types/tanstack-query/server/src/define-page-load-queries.d.ts +66 -0
- package/types/tanstack-query/server/src/index.d.ts +3 -0
- package/types/tanstack-query/src/constants.d.ts +9 -0
- package/types/tanstack-query/src/index.d.ts +2 -0
- package/types/tanstack-query/src/provide-analog-query.d.ts +4 -0
- package/types/tanstack-query/src/provide-server-analog-query.d.ts +2 -0
- package/types/tanstack-query/src/server-query.d.ts +16 -0
- package/types/tokens/src/index.d.ts +2 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { r as createRoutes, t as ANALOG_EXTRA_ROUTE_FILE_SOURCES } from "./route-files.mjs";
|
|
2
|
+
import { InjectionToken, inject } from "@angular/core";
|
|
3
|
+
import { ContentRenderer, MarkdownRouteComponent, parseRawContentFile } from "@analogjs/content";
|
|
4
|
+
//#region packages/router/content/src/lib/markdown-helpers.ts
|
|
5
|
+
var isNgZoneEnabled = typeof Zone !== "undefined" && !!Zone.root;
|
|
6
|
+
function toMarkdownModule(markdownFileFactory) {
|
|
7
|
+
return async () => {
|
|
8
|
+
const { content, attributes } = parseRawContentFile(await (isNgZoneEnabled ? Zone.root.run(markdownFileFactory) : markdownFileFactory()));
|
|
9
|
+
const { title, meta, jsonLd } = attributes;
|
|
10
|
+
return {
|
|
11
|
+
default: MarkdownRouteComponent,
|
|
12
|
+
routeMeta: {
|
|
13
|
+
data: { _analogContent: content },
|
|
14
|
+
title,
|
|
15
|
+
meta,
|
|
16
|
+
jsonLd,
|
|
17
|
+
resolve: { renderedAnalogContent: async () => {
|
|
18
|
+
const rendered = await inject(ContentRenderer).render(content);
|
|
19
|
+
return typeof rendered === "string" ? rendered : rendered.content;
|
|
20
|
+
} }
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region packages/router/content/src/lib/routes.ts
|
|
27
|
+
/**
|
|
28
|
+
* This variable reference is replaced with a glob of all content routes.
|
|
29
|
+
*/
|
|
30
|
+
var ANALOG_CONTENT_ROUTE_FILES = {};
|
|
31
|
+
function createContentRoutes(files, debug = false) {
|
|
32
|
+
return createRoutes(files, (filename, fileLoader) => filename.endsWith(".md") ? toMarkdownModule(fileLoader) : fileLoader, debug);
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region packages/router/content/src/lib/with-content-routes.ts
|
|
36
|
+
function withContentRoutes() {
|
|
37
|
+
return {
|
|
38
|
+
ɵkind: 101,
|
|
39
|
+
ɵproviders: [{
|
|
40
|
+
provide: ANALOG_EXTRA_ROUTE_FILE_SOURCES,
|
|
41
|
+
multi: true,
|
|
42
|
+
useValue: {
|
|
43
|
+
files: ANALOG_CONTENT_ROUTE_FILES,
|
|
44
|
+
resolveModule: (filename, fileLoader) => filename.endsWith(".md") ? toMarkdownModule(fileLoader) : fileLoader
|
|
45
|
+
}
|
|
46
|
+
}]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region packages/router/content/src/lib/debug/routes.ts
|
|
51
|
+
var DEBUG_CONTENT_ROUTES = new InjectionToken("@analogjs/router/content debug routes", {
|
|
52
|
+
providedIn: "root",
|
|
53
|
+
factory() {
|
|
54
|
+
return createContentRoutes(ANALOG_CONTENT_ROUTE_FILES, true);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
function injectDebugContentRoutes() {
|
|
58
|
+
return inject(DEBUG_CONTENT_ROUTES);
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { ANALOG_CONTENT_ROUTE_FILES, createContentRoutes, injectDebugContentRoutes, withContentRoutes };
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=analogjs-router-content.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analogjs-router-content.mjs","names":[],"sources":["../../content/src/lib/markdown-helpers.ts","../../content/src/lib/routes.ts","../../content/src/lib/with-content-routes.ts","../../content/src/lib/debug/routes.ts"],"sourcesContent":["import { inject } from '@angular/core';\nimport {\n ContentRenderer,\n MarkdownRouteComponent,\n parseRawContentFile,\n} from '@analogjs/content';\n\nimport type { RouteExport } from '../../../src/lib/models';\n\ndeclare const Zone: any;\n\ntype RenderResult = string | { content: string };\ntype ContentRendererLike = {\n render: (content: string) => Promise<RenderResult>;\n};\n\nconst isNgZoneEnabled = typeof Zone !== 'undefined' && !!Zone.root;\n\nexport function toMarkdownModule(\n markdownFileFactory: () => Promise<string>,\n): () => Promise<RouteExport> {\n return async () => {\n const markdownFile = await (isNgZoneEnabled\n ? Zone.root.run(markdownFileFactory)\n : markdownFileFactory());\n\n const { content, attributes } = parseRawContentFile(markdownFile);\n const { title, meta, jsonLd } = attributes;\n\n return {\n default: MarkdownRouteComponent,\n routeMeta: {\n data: { _analogContent: content },\n title,\n meta,\n jsonLd,\n resolve: {\n renderedAnalogContent: async () => {\n const contentRenderer = inject<any>(\n ContentRenderer as any,\n ) as ContentRendererLike;\n const rendered = await contentRenderer.render(content);\n return typeof rendered === 'string' ? rendered : rendered.content;\n },\n },\n },\n };\n };\n}\n","import type { Route } from '@angular/router';\n\nimport type { RouteExport } from '../../../src/lib/models';\nimport { createRoutes as createBaseRoutes } from '../../../src/lib/route-builder';\nimport { toMarkdownModule } from './markdown-helpers';\n\n/**\n * This variable reference is replaced with a glob of all content routes.\n */\nexport const ANALOG_CONTENT_ROUTE_FILES = {};\n\nexport type Files = Record<string, () => Promise<RouteExport | string>>;\n\nexport function createContentRoutes(files: Files, debug = false): Route[] {\n return createBaseRoutes(\n files,\n (filename, fileLoader) =>\n filename.endsWith('.md')\n ? toMarkdownModule(fileLoader as () => Promise<string>)\n : (fileLoader as () => Promise<RouteExport>),\n debug,\n );\n}\n","import { type RouterFeatures } from '@angular/router';\n\nimport { ANALOG_CONTENT_ROUTE_FILES } from './routes';\nimport { toMarkdownModule } from './markdown-helpers';\nimport type { RouteExport } from '../../../src/lib/models';\nimport {\n ANALOG_EXTRA_ROUTE_FILE_SOURCES,\n type ExtraRouteFileSource,\n} from '../../../src/lib/route-files';\n\nexport function withContentRoutes(): RouterFeatures {\n return {\n ɵkind: 101 as number,\n ɵproviders: [\n {\n provide: ANALOG_EXTRA_ROUTE_FILE_SOURCES,\n multi: true,\n useValue: {\n files: ANALOG_CONTENT_ROUTE_FILES,\n resolveModule: (\n filename: string,\n fileLoader: () => Promise<unknown>,\n ) =>\n filename.endsWith('.md')\n ? toMarkdownModule(fileLoader as () => Promise<string>)\n : (fileLoader as () => Promise<RouteExport>),\n } satisfies ExtraRouteFileSource,\n },\n ],\n };\n}\n","import { inject, InjectionToken } from '@angular/core';\nimport { Route } from '@angular/router';\n\nimport { ANALOG_CONTENT_ROUTE_FILES, createContentRoutes } from '../routes';\n\nexport const DEBUG_CONTENT_ROUTES: InjectionToken<(Route & DebugRoute)[]> =\n new InjectionToken<(Route & DebugRoute)[]>(\n '@analogjs/router/content debug routes',\n {\n providedIn: 'root',\n factory() {\n const debugRoutes = createContentRoutes(\n ANALOG_CONTENT_ROUTE_FILES as Record<string, () => Promise<string>>,\n true,\n );\n\n return debugRoutes as (Route & DebugRoute)[];\n },\n },\n );\n\nexport type DebugRoute = {\n path: string;\n filename: string;\n isLayout: boolean;\n children?: DebugRoute[];\n};\n\nexport function injectDebugContentRoutes(): (Route & DebugRoute)[] {\n return inject(DEBUG_CONTENT_ROUTES);\n}\n"],"mappings":";;;;AAgBA,IAAM,kBAAkB,OAAO,SAAS,eAAe,CAAC,CAAC,KAAK;AAE9D,SAAgB,iBACd,qBAC4B;AAC5B,QAAO,YAAY;EAQjB,MAAO,EAAA,SAAA,eAAA,oBAPc,OAAO,kBAIpB,KAAS,KAAA,IAAA,oBAAe,GACxB,qBAAwB,EAEzB;EACL,MAAS,EAAA,OAAA,MAAA,WAAA;AACT,SAAW;GACD,SAAA;GACR,WAAA;IACA,MAAA,EAAA,gBAAA,SAAA;IACA;IAEE;IACQ;IAGA,SAAW,EACH,uBAAwB,YAAW;;AAIxD,YAAA,OAAA,aAAA,WAAA,WAAA,SAAA;;;;;;;;;;;ACrCL,IAAa,6BAA6B,EAAE;AAI5C,SAAgB,oBAAoB,OAAc,QAAQ,OAAgB;AACxE,QAAO,aACL,QACC,UAAU,eACT,SAAS,SAAS,MACd,GAAA,iBAAA,WAAA,GAAA,YAAA,MAAA;;;;ACRV,SAAgB,oBAAoC;AAClD,QAAO;EACL,OAAO;EACP,YACE,CACW;GACF,SAAA;GACG,OAAA;GACD,UAAA;IAEL,OAAA;wEAOL,iBAAA,WAAA,GAEJ;;;;;;;ACxBH,IAAa,uBACX,IAAI,eACF,yCACA;CACE,YAAY;CACZ,UAAU;AAMR,SALoB,oBAClB,4BAED,KAAA;;CAIJ,CACF;AASH,SAAgB,2BAAmD;AACjE,QAAO,OAAO,qBAAqB"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { InjectionToken, assertInInjectionContext, inject, makeEnvironmentProviders, provideAppInitializer } from "@angular/core";
|
|
2
|
+
import { LOCALE, REQUEST } from "@analogjs/router/tokens";
|
|
3
|
+
//#region packages/router/i18n/src/provide-i18n.ts
|
|
4
|
+
/**
|
|
5
|
+
* Injection token for the resolved i18n configuration.
|
|
6
|
+
* Provided by `provideI18n()` and consumed by `injectSwitchLocale()`.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
var I18N_CONFIG = new InjectionToken("@analogjs/router I18n Config");
|
|
10
|
+
/**
|
|
11
|
+
* Resolves the full i18n config by merging explicit values with
|
|
12
|
+
* build-time globals injected by the platform plugin.
|
|
13
|
+
*/
|
|
14
|
+
function resolveI18nConfig(config) {
|
|
15
|
+
const defaultLocale = config.defaultLocale ?? (typeof ANALOG_I18N_DEFAULT_LOCALE !== "undefined" ? ANALOG_I18N_DEFAULT_LOCALE : void 0);
|
|
16
|
+
const locales = config.locales ?? (typeof ANALOG_I18N_LOCALES !== "undefined" ? ANALOG_I18N_LOCALES : void 0);
|
|
17
|
+
if (!defaultLocale || !locales) throw new Error("[@analogjs/router] provideI18n() requires defaultLocale and locales. Either pass them explicitly or configure i18n in the analog() plugin in vite.config.ts.");
|
|
18
|
+
return {
|
|
19
|
+
defaultLocale,
|
|
20
|
+
locales,
|
|
21
|
+
loader: config.loader
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Provides runtime i18n support using Angular's $localize.
|
|
26
|
+
*
|
|
27
|
+
* This provider:
|
|
28
|
+
* 1. Detects the active locale from the URL or falls back to the default.
|
|
29
|
+
* 2. Makes the current locale available via the LOCALE injection token.
|
|
30
|
+
* 3. Loads translations for the active locale at startup using $localize.
|
|
31
|
+
*
|
|
32
|
+
* Works in both SSR and client-only modes. On the client, locale is detected
|
|
33
|
+
* from `window.location.pathname`. On the server, locale is detected from
|
|
34
|
+
* the request in `provideServerContext()` and provided at the platform level;
|
|
35
|
+
* this function does not shadow it.
|
|
36
|
+
*
|
|
37
|
+
* When the platform plugin is configured with `i18n` in `vite.config.ts`,
|
|
38
|
+
* `defaultLocale` and `locales` are injected automatically — only
|
|
39
|
+
* `loader` is required:
|
|
40
|
+
*
|
|
41
|
+
* ```typescript
|
|
42
|
+
* provideI18n({
|
|
43
|
+
* loader: (locale) => import(`./i18n/${locale}.json`),
|
|
44
|
+
* })
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
function provideI18n(config) {
|
|
48
|
+
const resolved = resolveI18nConfig(config);
|
|
49
|
+
const localeProviders = typeof window !== "undefined" ? [{
|
|
50
|
+
provide: LOCALE,
|
|
51
|
+
useValue: detectClientLocale(resolved)
|
|
52
|
+
}] : [];
|
|
53
|
+
return makeEnvironmentProviders([
|
|
54
|
+
{
|
|
55
|
+
provide: I18N_CONFIG,
|
|
56
|
+
useValue: resolved
|
|
57
|
+
},
|
|
58
|
+
...localeProviders,
|
|
59
|
+
provideAppInitializer(async () => {
|
|
60
|
+
await initI18n(resolved, resolveActiveLocale(resolved));
|
|
61
|
+
})
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolves the active locale, preferring the injected `LOCALE` token
|
|
66
|
+
* (which on the server reads from the platform-level provider set by
|
|
67
|
+
* `provideServerContext()`) and falling back to the request URL,
|
|
68
|
+
* `window.location.pathname`, or `defaultLocale`.
|
|
69
|
+
*/
|
|
70
|
+
function resolveActiveLocale(config) {
|
|
71
|
+
const injected = inject(LOCALE, { optional: true });
|
|
72
|
+
if (injected && config.locales.includes(injected)) return injected;
|
|
73
|
+
const req = inject(REQUEST, { optional: true });
|
|
74
|
+
const first = (req?.originalUrl ?? req?.url ?? (typeof window !== "undefined" ? window.location.pathname : "/")).split("?")[0].split("/").filter(Boolean)[0];
|
|
75
|
+
if (first && config.locales.includes(first)) return first;
|
|
76
|
+
return config.defaultLocale;
|
|
77
|
+
}
|
|
78
|
+
function detectClientLocale(config) {
|
|
79
|
+
if (typeof window === "undefined") return config.defaultLocale;
|
|
80
|
+
const firstSegment = window.location.pathname.split("/").filter(Boolean)[0];
|
|
81
|
+
if (firstSegment && config.locales.includes(firstSegment)) return firstSegment;
|
|
82
|
+
return config.defaultLocale;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Loads translations for the given locale and registers them with $localize.
|
|
86
|
+
*
|
|
87
|
+
* Always clears any previously loaded translations first so that switching
|
|
88
|
+
* between locales in a single SSR process does not mix translation maps.
|
|
89
|
+
*/
|
|
90
|
+
async function initI18n(config, locale) {
|
|
91
|
+
const activeLocale = locale ?? config.defaultLocale;
|
|
92
|
+
await clearTranslationsRuntime();
|
|
93
|
+
if (activeLocale === config.locales[0]) return;
|
|
94
|
+
const translations = await config.loader(activeLocale);
|
|
95
|
+
if (translations && Object.keys(translations).length > 0) await loadTranslationsRuntime(translations);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Loads translations into the global $localize translation map.
|
|
99
|
+
*
|
|
100
|
+
* Uses `@angular/localize`'s `loadTranslations` when available so that
|
|
101
|
+
* each translation string is parsed into the `{text, messageParts,
|
|
102
|
+
* placeholderNames}` shape that `$localize.translate` expects. Falls back
|
|
103
|
+
* to writing raw strings only as a last resort (in which case lookups
|
|
104
|
+
* will not succeed — the fallback exists to keep error messages useful
|
|
105
|
+
* for users who have not installed `@angular/localize`).
|
|
106
|
+
*
|
|
107
|
+
* Requires `@angular/localize/init` to be imported in the application
|
|
108
|
+
* entry point so that `globalThis.$localize` is defined.
|
|
109
|
+
*/
|
|
110
|
+
async function loadTranslationsRuntime(translations) {
|
|
111
|
+
const $localize = globalThis.$localize;
|
|
112
|
+
if (!$localize) {
|
|
113
|
+
console.warn("[@analogjs/router] $localize is not available. Make sure to import @angular/localize/init in your application entry point.");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const { loadTranslations } = await import("@angular/localize");
|
|
118
|
+
loadTranslations(translations);
|
|
119
|
+
} catch {
|
|
120
|
+
console.warn("[@analogjs/router] Unable to import @angular/localize. Install it as a dependency to enable runtime translation loading.");
|
|
121
|
+
$localize.TRANSLATIONS ??= {};
|
|
122
|
+
for (const [id, message] of Object.entries(translations)) $localize.TRANSLATIONS[id] = message;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** @internal — exported for tests; not re-exported from the package entry. */
|
|
126
|
+
async function clearTranslationsRuntime() {
|
|
127
|
+
const $localize = globalThis.$localize;
|
|
128
|
+
if (!$localize) return;
|
|
129
|
+
try {
|
|
130
|
+
const { clearTranslations } = await import("@angular/localize");
|
|
131
|
+
clearTranslations();
|
|
132
|
+
} catch {
|
|
133
|
+
$localize.translate = void 0;
|
|
134
|
+
$localize.TRANSLATIONS = {};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function injectSwitchLocale() {
|
|
138
|
+
assertInInjectionContext(injectSwitchLocale);
|
|
139
|
+
const config = inject(I18N_CONFIG);
|
|
140
|
+
return (targetLocale) => {
|
|
141
|
+
if (typeof window === "undefined") return;
|
|
142
|
+
const { pathname, search, hash } = window.location;
|
|
143
|
+
const newPath = replaceLocaleInPath(pathname, targetLocale, config.locales);
|
|
144
|
+
window.location.href = `${newPath}${search}${hash}`;
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function replaceLocaleInPath(pathname, targetLocale, locales) {
|
|
148
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
149
|
+
if (segments.length > 0 && locales.includes(segments[0])) segments[0] = targetLocale;
|
|
150
|
+
else segments.unshift(targetLocale);
|
|
151
|
+
return "/" + segments.join("/");
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
export { injectSwitchLocale, loadTranslationsRuntime, provideI18n };
|
|
155
|
+
|
|
156
|
+
//# sourceMappingURL=analogjs-router-i18n.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analogjs-router-i18n.mjs","names":[],"sources":["../../i18n/src/provide-i18n.ts"],"sourcesContent":["import {\n EnvironmentProviders,\n InjectionToken,\n Type,\n assertInInjectionContext,\n inject,\n makeEnvironmentProviders,\n provideAppInitializer,\n} from '@angular/core';\nimport { LOCALE, REQUEST, ServerRequest } from '@analogjs/router/tokens';\n\ndeclare const ANALOG_I18N_DEFAULT_LOCALE: string;\ndeclare const ANALOG_I18N_LOCALES: string[];\n\n/**\n * Configuration for runtime i18n support.\n *\n * `defaultLocale` and `locales` are optional when the platform plugin\n * is configured with `i18n` in `vite.config.ts` — the values are\n * injected as build-time globals automatically.\n */\nexport interface I18nConfig {\n /**\n * The default locale to use when no locale is detected.\n * If omitted, reads from the platform plugin's `i18n.defaultLocale`.\n */\n defaultLocale?: string;\n\n /**\n * List of supported locale identifiers.\n * If omitted, reads from the platform plugin's `i18n.locales`.\n */\n locales?: string[];\n\n /**\n * A function that returns translations for a given locale.\n * The returned record maps message IDs to translated strings.\n */\n loader: (\n locale: string,\n ) => Promise<Record<string, string>> | Record<string, string>;\n}\n\n/**\n * Fully resolved i18n config with all required fields.\n */\nexport type ResolvedI18nConfig = Required<I18nConfig>;\n\n/**\n * Injection token for the resolved i18n configuration.\n * Provided by `provideI18n()` and consumed by `injectSwitchLocale()`.\n * @internal\n */\nconst I18N_CONFIG = new InjectionToken<ResolvedI18nConfig>(\n '@analogjs/router I18n Config',\n);\n\n/**\n * Resolves the full i18n config by merging explicit values with\n * build-time globals injected by the platform plugin.\n */\nexport function resolveI18nConfig(config: I18nConfig): Required<I18nConfig> {\n const defaultLocale =\n config.defaultLocale ??\n (typeof ANALOG_I18N_DEFAULT_LOCALE !== 'undefined'\n ? ANALOG_I18N_DEFAULT_LOCALE\n : undefined);\n\n const locales =\n config.locales ??\n (typeof ANALOG_I18N_LOCALES !== 'undefined'\n ? ANALOG_I18N_LOCALES\n : undefined);\n\n if (!defaultLocale || !locales) {\n throw new Error(\n '[@analogjs/router] provideI18n() requires defaultLocale and locales. ' +\n 'Either pass them explicitly or configure i18n in the analog() plugin in vite.config.ts.',\n );\n }\n\n return { defaultLocale, locales, loader: config.loader };\n}\n\n/**\n * Provides runtime i18n support using Angular's $localize.\n *\n * This provider:\n * 1. Detects the active locale from the URL or falls back to the default.\n * 2. Makes the current locale available via the LOCALE injection token.\n * 3. Loads translations for the active locale at startup using $localize.\n *\n * Works in both SSR and client-only modes. On the client, locale is detected\n * from `window.location.pathname`. On the server, locale is detected from\n * the request in `provideServerContext()` and provided at the platform level;\n * this function does not shadow it.\n *\n * When the platform plugin is configured with `i18n` in `vite.config.ts`,\n * `defaultLocale` and `locales` are injected automatically — only\n * `loader` is required:\n *\n * ```typescript\n * provideI18n({\n * loader: (locale) => import(`./i18n/${locale}.json`),\n * })\n * ```\n */\nexport function provideI18n(config: I18nConfig): EnvironmentProviders {\n const resolved = resolveI18nConfig(config);\n\n // Only provide LOCALE at the environment level on the client. On the\n // server, the platform-level LOCALE set by `provideServerContext()` is\n // authoritative and must not be shadowed by an environment-level provider.\n const localeProviders =\n typeof window !== 'undefined'\n ? [{ provide: LOCALE, useValue: detectClientLocale(resolved) }]\n : [];\n\n return makeEnvironmentProviders([\n { provide: I18N_CONFIG, useValue: resolved },\n ...localeProviders,\n provideAppInitializer(async () => {\n const locale = resolveActiveLocale(resolved);\n await initI18n(resolved, locale);\n }),\n ]);\n}\n\n/**\n * Resolves the active locale, preferring the injected `LOCALE` token\n * (which on the server reads from the platform-level provider set by\n * `provideServerContext()`) and falling back to the request URL,\n * `window.location.pathname`, or `defaultLocale`.\n */\nfunction resolveActiveLocale(config: ResolvedI18nConfig): string {\n const injected = inject(LOCALE, { optional: true });\n if (injected && config.locales.includes(injected)) {\n return injected;\n }\n\n // Fallback: read the path directly from the request on the server or\n // from the browser URL on the client. This covers cases where a locale\n // prefix is present in the URL but no token provider set it explicitly.\n const req = inject(REQUEST, { optional: true }) as ServerRequest | null;\n const pathname =\n req?.originalUrl ??\n req?.url ??\n (typeof window !== 'undefined' ? window.location.pathname : '/');\n const first = pathname.split('?')[0].split('/').filter(Boolean)[0];\n if (first && config.locales.includes(first)) {\n return first;\n }\n\n return config.defaultLocale;\n}\n\nexport function detectClientLocale(config: ResolvedI18nConfig): string {\n if (typeof window === 'undefined') {\n return config.defaultLocale;\n }\n\n const pathname = window.location.pathname;\n const segments = pathname.split('/').filter(Boolean);\n const firstSegment = segments[0];\n\n if (firstSegment && config.locales.includes(firstSegment)) {\n return firstSegment;\n }\n\n return config.defaultLocale;\n}\n\n/**\n * Loads translations for the given locale and registers them with $localize.\n *\n * Always clears any previously loaded translations first so that switching\n * between locales in a single SSR process does not mix translation maps.\n */\nexport async function initI18n(\n config: ResolvedI18nConfig,\n locale?: string,\n): Promise<void> {\n const activeLocale = locale ?? config.defaultLocale;\n await clearTranslationsRuntime();\n\n // The source locale (first entry in `locales`) has its messages baked\n // directly into the template source, so there is nothing to load.\n if (activeLocale === config.locales[0]) {\n return;\n }\n\n const translations = await config.loader(activeLocale);\n if (translations && Object.keys(translations).length > 0) {\n await loadTranslationsRuntime(translations);\n }\n}\n\n/**\n * Loads translations into the global $localize translation map.\n *\n * Uses `@angular/localize`'s `loadTranslations` when available so that\n * each translation string is parsed into the `{text, messageParts,\n * placeholderNames}` shape that `$localize.translate` expects. Falls back\n * to writing raw strings only as a last resort (in which case lookups\n * will not succeed — the fallback exists to keep error messages useful\n * for users who have not installed `@angular/localize`).\n *\n * Requires `@angular/localize/init` to be imported in the application\n * entry point so that `globalThis.$localize` is defined.\n */\nexport async function loadTranslationsRuntime(\n translations: Record<string, string>,\n): Promise<void> {\n const $localize = (globalThis as any).$localize;\n if (!$localize) {\n console.warn(\n '[@analogjs/router] $localize is not available. ' +\n 'Make sure to import @angular/localize/init in your application entry point.',\n );\n return;\n }\n\n try {\n const localizePkg = '@angular/localize';\n const { loadTranslations } = (await import(\n /* @vite-ignore */ localizePkg\n )) as {\n loadTranslations: (t: Record<string, string>) => void;\n };\n loadTranslations(translations);\n } catch {\n console.warn(\n '[@analogjs/router] Unable to import @angular/localize. ' +\n 'Install it as a dependency to enable runtime translation loading.',\n );\n $localize.TRANSLATIONS ??= {};\n for (const [id, message] of Object.entries(translations)) {\n $localize.TRANSLATIONS[id] = message;\n }\n }\n}\n\n/** @internal — exported for tests; not re-exported from the package entry. */\nexport async function clearTranslationsRuntime(): Promise<void> {\n const $localize = (globalThis as any).$localize;\n if (!$localize) {\n return;\n }\n try {\n const localizePkg = '@angular/localize';\n const { clearTranslations } = (await import(\n /* @vite-ignore */ localizePkg\n )) as {\n clearTranslations: () => void;\n };\n clearTranslations();\n } catch {\n $localize.translate = undefined;\n $localize.TRANSLATIONS = {};\n }\n}\n\n// ---------------------------------------------------------------------------\n// Component definition registry\n// ---------------------------------------------------------------------------\n\nconst componentDefRegistry = new Set<any>();\n\n/** @internal */\nexport function ɵregisterI18nComponentDef(typeOrDef: Type<any> | any): void {\n if (!typeOrDef) return;\n const def = (typeOrDef as any).ɵcmp ?? typeOrDef;\n if (def && typeof def === 'object' && 'template' in def) {\n componentDefRegistry.add(def);\n }\n}\n\n/** @internal */\nexport function ɵresetI18nComponentDefCache(): void {\n for (const def of componentDefRegistry) {\n def.tView = null;\n }\n}\n\n/** @internal */\nexport function getI18nComponentDefRegistrySize(): number {\n return componentDefRegistry.size;\n}\n\n/** @internal */\nexport function clearI18nComponentDefRegistry(): void {\n componentDefRegistry.clear();\n}\n\nexport function injectSwitchLocale(): (targetLocale: string) => void {\n assertInInjectionContext(injectSwitchLocale);\n const config = inject(I18N_CONFIG);\n\n return (targetLocale: string) => {\n if (typeof window === 'undefined') {\n return;\n }\n\n const { pathname, search, hash } = window.location;\n const newPath = replaceLocaleInPath(pathname, targetLocale, config.locales);\n window.location.href = `${newPath}${search}${hash}`;\n };\n}\n\nexport function replaceLocaleInPath(\n pathname: string,\n targetLocale: string,\n locales: string[],\n): string {\n const segments = pathname.split('/').filter(Boolean);\n\n if (segments.length > 0 && locales.includes(segments[0])) {\n segments[0] = targetLocale;\n } else {\n segments.unshift(targetLocale);\n }\n\n return '/' + segments.join('/');\n}\n"],"mappings":";;;;;;;;AAqDA,IAAM,cAAc,IAAI,eACtB,+BACD;;;;;AAMD,SAAgB,kBAAkB,QAA0C;CAC1E,MAAM,gBACJ,OAAO,kBAKH,OAAA,+BAEI,cAIL,6BACO,KAAA;oCAML,OAAA,wBAAA,cAAE,sBAAe,KAAA;AAAS,KAAA,CAAA,iBAAe,CAAA,QAAQ,OAAA,IAAA,MAAA,+JAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;SAkCtC,YAAA,QAAA;CAAQ,MAAA,WAAU,kBAAmB,OAAA;CAInD,MAAA,kBAAS,OAAA,WAAA,cAAa,CAAA;EAAU,SAAA;EAAA,UAAA,mBAAA,SAAA;EAAA,CAAA,GAAU,EAAA;AAC5C,QAAG,yBAAA;EACH;GAAA,SAAA;GAAsB,UAAY;GAAA;EAChC,GAAM;EACN,sBAAyB,YAAO;AAElC,SAAA,SAAA,UAAA,oBAAA,SAAA,CAAA;;;;;;;;;;AAWF,SAAI,oBAAmB,QAAQ;CAC7B,MAAO,WAAA,OAAA,QAAA,EAAA,UAAA,MAAA,CAAA;mDAMH,QAAM;;CAcZ,MAAI,SAJU,KAAA,eAAA,KAAA,QAGT,OAAS,WAAA,cAAmB,OAAoC,SAAA,WAAA,MAC/C,MAAA,IAAa,CAAA,GAAA,MAAA,IAAA,CAAA,OAAA,QAAA,CAAA;AACjC,KAAA,SAAc,OAAA,QAAA,SAAA,MAAA,CAAA,QAAA;AAIhB,QAAM,OAAW;;AAGjB,SAAI,mBAAuB,QAAQ;AACjC,KAAA,OAAO,WAAA,YAAA,QAAA,OAAA;;;;;;;;;;;AA0BT,eAAoB,SAAO,QAAK,QAAc;CAC5C,MAAM,eAAA,UAAwB,OAAA;;;;;;;;;;;;;;;;;;;;CAgCT,MAAA,YAAA,WAAA;;AAIrB,UAAA,KAAiB,6HACX;AACN;;AAKA,KAAK;4CACoB;;SAMtB;AACC,UAAA,KAAa,2HACH;AACd,YAAA,iBAAA,EAAA;2DAEE,WAAA,aAAA,MAAA;;;;;CAOF,MAAA,YAAmB,WAAA;MACb,UACN;;EASE,MAAA,EAAA,sBAAqC,MAAA,OAAA;AAGpC,qBAAS;SAER;AACF,YAAO,YAAe,KAAA;AACxB,YAAA,eAA6B,EAAA;;;SAyCzB,qBAA8B;AAEpC,0BAA2B,mBAAiB;CAC1C,MAAA,SAAc,OAAA,YAAA;SACT,iBAAA;AACL,MAAS,OAAQ,WAAA,YAAA"}
|
|
@@ -1,27 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
return new Response(JSON.stringify(errors), {
|
|
4
|
-
status,
|
|
5
|
-
headers: { "X-Analog-Errors": "true" }
|
|
6
|
-
});
|
|
7
|
-
}
|
|
8
|
-
function json(data, config) {
|
|
9
|
-
return new Response(JSON.stringify(data), {
|
|
10
|
-
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
11
|
-
...config
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
function redirect(url, config = 302) {
|
|
15
|
-
if (typeof config === "number") return new Response(null, {
|
|
16
|
-
status: config,
|
|
17
|
-
headers: { Location: `${url}` }
|
|
18
|
-
});
|
|
19
|
-
return new Response(null, {
|
|
20
|
-
headers: { Location: `${url}` },
|
|
21
|
-
...config
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
//#endregion
|
|
25
|
-
export { fail, json, redirect };
|
|
26
|
-
|
|
27
|
-
//# sourceMappingURL=analogjs-router-server-actions.mjs.map
|
|
1
|
+
import { a as fail, i as validateWithSchema, n as defineServerRoute, o as json, r as defineAction, s as redirect, t as definePageLoad } from "./src.mjs";
|
|
2
|
+
export { defineAction, definePageLoad, defineServerRoute, fail, json, redirect, validateWithSchema };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { BASE_URL, INTERNAL_FETCH, LOCALE, REQUEST, RESPONSE } from "./analogjs-router-tokens.mjs";
|
|
1
2
|
import { APP_ID, InjectionToken, TransferState, assertInInjectionContext, enableProdMode, inject, makeStateKey, reflectComponentType, ɵConsole } from "@angular/core";
|
|
2
|
-
import { BASE_URL, INTERNAL_FETCH, REQUEST, RESPONSE } from "@analogjs/router/tokens";
|
|
3
3
|
import { bootstrapApplication } from "@angular/platform-browser";
|
|
4
4
|
import { provideServerRendering, renderApplication, ɵSERVER_CONTEXT } from "@angular/platform-server";
|
|
5
5
|
import { json } from "node:stream/consumers";
|
|
@@ -15,6 +15,7 @@ function getRequestHeader(req, name) {
|
|
|
15
15
|
}
|
|
16
16
|
function provideServerContext({ req, res, fetch }) {
|
|
17
17
|
const baseUrl = getBaseUrl(req);
|
|
18
|
+
const locale = detectLocale(req);
|
|
18
19
|
return [
|
|
19
20
|
{
|
|
20
21
|
provide: ɵSERVER_CONTEXT,
|
|
@@ -35,7 +36,11 @@ function provideServerContext({ req, res, fetch }) {
|
|
|
35
36
|
{
|
|
36
37
|
provide: INTERNAL_FETCH,
|
|
37
38
|
useValue: fetch
|
|
38
|
-
}
|
|
39
|
+
},
|
|
40
|
+
...locale ? [{
|
|
41
|
+
provide: LOCALE,
|
|
42
|
+
useValue: locale
|
|
43
|
+
}] : []
|
|
39
44
|
];
|
|
40
45
|
}
|
|
41
46
|
function getBaseUrl(req) {
|
|
@@ -49,6 +54,39 @@ function getRequestProtocol(req, opts = {}) {
|
|
|
49
54
|
if (opts.xForwardedProto !== false && forwardedProto === "https") return "https";
|
|
50
55
|
return req.connection?.encrypted ? "https" : "http";
|
|
51
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Detects the locale from the request URL path prefix or Accept-Language header.
|
|
59
|
+
* URL prefix takes priority (e.g. /fr/about -> 'fr').
|
|
60
|
+
*/
|
|
61
|
+
function detectLocale(req) {
|
|
62
|
+
const localeFromUrl = extractLocaleFromUrl(req.originalUrl || req.url || "");
|
|
63
|
+
if (localeFromUrl) return localeFromUrl;
|
|
64
|
+
return parseAcceptLanguage(getRequestHeader(req, "accept-language"));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Extracts a locale from the first URL path segment if it matches
|
|
68
|
+
* a BCP 47-like pattern (e.g. 'en', 'en-US', 'zh-Hans-CN').
|
|
69
|
+
*/
|
|
70
|
+
function extractLocaleFromUrl(url) {
|
|
71
|
+
const segments = url.split("?")[0].split("/").filter(Boolean);
|
|
72
|
+
if (segments.length === 0) return;
|
|
73
|
+
const firstSegment = segments[0];
|
|
74
|
+
if (/^[a-z]{2}(-[a-zA-Z]{2,4})?(-[a-zA-Z]{2}|\d{3})?$/.test(firstSegment)) return firstSegment;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Parses the Accept-Language header and returns the most preferred language.
|
|
78
|
+
*/
|
|
79
|
+
function parseAcceptLanguage(header) {
|
|
80
|
+
if (!header) return;
|
|
81
|
+
return header.split(",").map((part) => {
|
|
82
|
+
const [locale, qPart] = part.trim().split(";");
|
|
83
|
+
const q = qPart ? parseFloat(qPart.replace("q=", "")) : 1;
|
|
84
|
+
return {
|
|
85
|
+
locale: locale.trim(),
|
|
86
|
+
q
|
|
87
|
+
};
|
|
88
|
+
}).sort((a, b) => b.q - a.q)[0]?.locale || void 0;
|
|
89
|
+
}
|
|
52
90
|
//#endregion
|
|
53
91
|
//#region packages/router/server/src/tokens.ts
|
|
54
92
|
var STATIC_PROPS = new InjectionToken("Static Props");
|
|
@@ -82,7 +120,7 @@ var components = /* @__PURE__ */ Object.assign({});
|
|
|
82
120
|
async function renderServerComponent(url, serverContext, config) {
|
|
83
121
|
const { componentLoader, componentId } = getComponentLoader(serverComponentRequest(serverContext));
|
|
84
122
|
if (!componentLoader) return new Response(`Server Component Not Found ${componentId}`, { status: 404 });
|
|
85
|
-
const component = (await componentLoader())
|
|
123
|
+
const component = (await componentLoader()).default;
|
|
86
124
|
if (!component) return new Response(`No default export for ${componentId}`, { status: 422 });
|
|
87
125
|
const selector = reflectComponentType(component)?.selector.split(",")?.[0] || "server-component";
|
|
88
126
|
const body = await json(serverContext.req).catch(() => ({})) || {};
|
|
@@ -151,6 +189,24 @@ function retrieveTransferredState(html, appId) {
|
|
|
151
189
|
//#region packages/router/server/src/render.ts
|
|
152
190
|
enableProdMode();
|
|
153
191
|
/**
|
|
192
|
+
* Nulls `def.tView` on every component definition that Angular has
|
|
193
|
+
* compiled in this process. Angular caches the result of `consts()` on
|
|
194
|
+
* `def.tView` — that factory is where `$localize` tagged templates are
|
|
195
|
+
* evaluated — so without this reset the first rendered locale would be
|
|
196
|
+
* frozen into the cache for the process lifetime.
|
|
197
|
+
*
|
|
198
|
+
* The set on `globalThis.__ngComponentDefs` is populated by a Vite
|
|
199
|
+
* transform in `@analogjs/platform` that patches `@angular/core`'s
|
|
200
|
+
* `getComponentId()` to mirror every compiled component definition to
|
|
201
|
+
* a global Set, bypassing the `ngServerMode` guard that normally
|
|
202
|
+
* prevents registration on the server.
|
|
203
|
+
*/
|
|
204
|
+
function resetComponentDefTViews() {
|
|
205
|
+
const defs = globalThis.__ngComponentDefs;
|
|
206
|
+
if (!defs) return;
|
|
207
|
+
for (const def of defs) def.tView = null;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
154
210
|
* Returns a function that accepts the navigation URL,
|
|
155
211
|
* the root HTML, and server context.
|
|
156
212
|
*
|
|
@@ -165,6 +221,7 @@ function render(rootComponent, config, platformProviders = []) {
|
|
|
165
221
|
}
|
|
166
222
|
return async function render(url, document, serverContext) {
|
|
167
223
|
if (serverComponentRequest(serverContext)) return await renderServerComponent(url, serverContext);
|
|
224
|
+
resetComponentDefTViews();
|
|
168
225
|
return await renderApplication(bootstrap, {
|
|
169
226
|
document,
|
|
170
227
|
url,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analogjs-router-server.mjs","names":[],"sources":["../../server/src/provide-server-context.ts","../../server/src/tokens.ts","../../server/src/server-component-render.ts","../../server/src/render.ts"],"sourcesContent":["import { StaticProvider, ɵresetCompiledComponents } from '@angular/core';\nimport { ɵSERVER_CONTEXT as SERVER_CONTEXT } from '@angular/platform-server';\n\nimport {\n BASE_URL,\n INTERNAL_FETCH,\n LOCALE,\n REQUEST,\n RESPONSE,\n ServerInternalFetch,\n ServerRequest,\n ServerResponse,\n} from '../../tokens/src/index.js';\n\nfunction getHeaderValue(\n value: string | string[] | undefined,\n): string | undefined {\n return Array.isArray(value) ? value[0] : value;\n}\n\nfunction getRequestHeader(\n req: ServerRequest,\n name: string,\n): string | undefined {\n const headers = (req as { headers?: unknown }).headers;\n\n if (!headers) {\n return undefined;\n }\n\n if (\n typeof headers === 'object' &&\n headers !== null &&\n 'get' in headers &&\n typeof headers.get === 'function'\n ) {\n return headers.get(name) ?? undefined;\n }\n\n return getHeaderValue(\n (headers as Record<string, string | string[] | undefined>)[name],\n );\n}\n\nexport function provideServerContext({\n req,\n res,\n fetch,\n}: {\n req: ServerRequest;\n res: ServerResponse;\n fetch?: ServerInternalFetch;\n}): StaticProvider[] {\n const baseUrl = getBaseUrl(req);\n const locale = detectLocale(req);\n\n if (import.meta.env.DEV) {\n ɵresetCompiledComponents();\n }\n\n return [\n { provide: SERVER_CONTEXT, useValue: 'ssr-analog' },\n { provide: REQUEST, useValue: req },\n { provide: RESPONSE, useValue: res },\n { provide: BASE_URL, useValue: baseUrl },\n { provide: INTERNAL_FETCH, useValue: fetch },\n ...(locale ? [{ provide: LOCALE, useValue: locale }] : []),\n ];\n}\n\nexport function getBaseUrl(req: ServerRequest): string {\n const protocol = getRequestProtocol(req);\n const host =\n getRequestHeader(req, 'x-forwarded-host') ??\n getRequestHeader(req, 'host') ??\n 'localhost';\n const originalUrl = req.originalUrl || req.url || '/';\n const parsedUrl = new URL(\n '',\n `${protocol}://${host}${\n originalUrl.endsWith('/')\n ? originalUrl.substring(0, originalUrl.length - 1)\n : originalUrl\n }`,\n );\n const baseUrl = parsedUrl.origin;\n\n return baseUrl;\n}\n\nexport function getRequestProtocol(\n req: ServerRequest,\n opts: { xForwardedProto?: boolean } = {},\n): string {\n const forwardedProto = getRequestHeader(req, 'x-forwarded-proto')\n ?.split(',')[0]\n ?.trim();\n\n if (opts.xForwardedProto !== false && forwardedProto === 'https') {\n return 'https';\n }\n\n return (req.connection as { encrypted?: boolean })?.encrypted\n ? 'https'\n : 'http';\n}\n\n/**\n * Detects the locale from the request URL path prefix or Accept-Language header.\n * URL prefix takes priority (e.g. /fr/about -> 'fr').\n */\nexport function detectLocale(req: ServerRequest): string | undefined {\n const url = req.originalUrl || req.url || '';\n const localeFromUrl = extractLocaleFromUrl(url);\n if (localeFromUrl) {\n return localeFromUrl;\n }\n\n return parseAcceptLanguage(getRequestHeader(req, 'accept-language'));\n}\n\n/**\n * Extracts a locale from the first URL path segment if it matches\n * a BCP 47-like pattern (e.g. 'en', 'en-US', 'zh-Hans-CN').\n */\nexport function extractLocaleFromUrl(url: string): string | undefined {\n const pathname = url.split('?')[0];\n const segments = pathname.split('/').filter(Boolean);\n if (segments.length === 0) {\n return undefined;\n }\n\n const firstSegment = segments[0];\n // Match BCP 47 language tags: 2-letter language code with optional region/script\n if (/^[a-z]{2}(-[a-zA-Z]{2,4})?(-[a-zA-Z]{2}|\\d{3})?$/.test(firstSegment)) {\n return firstSegment;\n }\n\n return undefined;\n}\n\n/**\n * Parses the Accept-Language header and returns the most preferred language.\n */\nexport function parseAcceptLanguage(\n header: string | undefined,\n): string | undefined {\n if (!header) {\n return undefined;\n }\n\n const locales = header\n .split(',')\n .map((part) => {\n const [locale, qPart] = part.trim().split(';');\n const q = qPart ? parseFloat(qPart.replace('q=', '')) : 1;\n return { locale: locale.trim(), q };\n })\n .sort((a, b) => b.q - a.q);\n\n return locales[0]?.locale || undefined;\n}\n","import {\n assertInInjectionContext,\n inject,\n InjectionToken,\n makeStateKey,\n Provider,\n TransferState,\n} from '@angular/core';\n\nexport const STATIC_PROPS: InjectionToken<Record<string, any>> =\n new InjectionToken<Record<string, any>>('Static Props');\n\nexport function provideStaticProps<T = Record<string, any>>(\n props: T,\n): Provider {\n return {\n provide: STATIC_PROPS,\n useFactory() {\n return props;\n },\n };\n}\n\nexport function injectStaticProps(): Record<string, any> {\n assertInInjectionContext(injectStaticProps);\n\n return inject(STATIC_PROPS);\n}\n\nexport function injectStaticOutputs<T>(): { set(data: T): void } {\n const transferState = inject(TransferState);\n const outputsKey = makeStateKey<T>('_analog_output');\n\n return {\n set(data: T): void {\n transferState.set(outputsKey, data);\n },\n };\n}\n","import { ApplicationConfig, Type } from '@angular/core';\nimport {\n bootstrapApplication,\n BootstrapContext,\n} from '@angular/platform-browser';\nimport {\n reflectComponentType,\n ɵConsole as Console,\n APP_ID,\n} from '@angular/core';\nimport {\n provideServerRendering,\n renderApplication,\n ɵSERVER_CONTEXT as SERVER_CONTEXT,\n} from '@angular/platform-server';\nimport type { ServerContext } from '../../tokens/src/index.js';\nimport { json as readJsonStream } from 'node:stream/consumers';\n\nimport { provideStaticProps } from './tokens';\n\ntype ComponentLoader = () => Promise<Type<unknown>>;\n\nexport function serverComponentRequest(\n serverContext: ServerContext,\n): string | undefined {\n // `ServerContext` is still backed by raw Node req/res, so read the header\n // directly instead of reconstructing an H3Event just for lookup.\n // In h3 v2 / Nitro v3, req may be undefined during fetch-based prerendering\n // (where event.node is not populated), so guard with optional chaining.\n const serverComponentId = serverContext.req?.headers?.[\n 'x-analog-component'\n ] as string | undefined;\n\n if (\n !serverComponentId &&\n serverContext.req?.url &&\n serverContext.req.url.startsWith('/_analog/components')\n ) {\n const componentId = serverContext.req.url.split('/')?.[3];\n\n return componentId;\n }\n\n return serverComponentId;\n}\n\nconst components = import.meta.glob([\n '/src/server/components/**/*.{ts,analog,ag}',\n]);\n\nexport async function renderServerComponent(\n url: string,\n serverContext: ServerContext,\n config?: ApplicationConfig,\n): Promise<Response> {\n const componentReqId = serverComponentRequest(serverContext) as string;\n const { componentLoader, componentId } = getComponentLoader(componentReqId);\n\n if (!componentLoader) {\n return new Response(`Server Component Not Found ${componentId}`, {\n status: 404,\n });\n }\n\n const component = ((await componentLoader()) as { default?: Type<unknown> })\n .default;\n\n if (!component) {\n return new Response(`No default export for ${componentId}`, {\n status: 422,\n });\n }\n\n const mirror = reflectComponentType(component);\n const selector = mirror?.selector.split(',')?.[0] || 'server-component';\n // Server component requests POST JSON props from the client bridge, so parse\n // the Node request body directly instead of rebuilding an H3Event wrapper.\n const body =\n (await readJsonStream(serverContext.req).catch(() => ({}))) || {};\n const appId = `analog-server-${selector.toLowerCase()}-${new Date().getTime()}`;\n\n const bootstrap = (context: BootstrapContext) =>\n bootstrapApplication(\n component,\n {\n providers: [\n provideServerRendering(),\n provideStaticProps(body),\n { provide: SERVER_CONTEXT, useValue: 'analog-server-component' },\n {\n provide: APP_ID,\n useFactory() {\n return appId;\n },\n },\n ...(config?.providers || []),\n ],\n },\n context,\n );\n\n const html = await renderApplication(bootstrap as any, {\n url,\n document: `<${selector}></${selector}>`,\n platformProviders: [\n {\n provide: Console,\n useFactory() {\n return {\n warn: () => {\n /* noop */\n },\n log: () => {\n /* noop */\n },\n };\n },\n },\n ],\n });\n\n const outputs = retrieveTransferredState(html, appId);\n const responseData: { html: string; outputs: Record<string, unknown> } = {\n html,\n outputs,\n };\n\n return new Response(JSON.stringify(responseData), {\n headers: {\n 'X-Analog-Component': 'true',\n },\n });\n}\n\nfunction getComponentLoader(componentReqId: string): {\n componentLoader: ComponentLoader | undefined;\n componentId: string;\n} {\n const _componentId = `/src/server/components/${componentReqId.toLowerCase()}`;\n let componentLoader: ComponentLoader | undefined = undefined;\n let componentId = _componentId;\n\n if (components[`${_componentId}.ts`]) {\n componentId = `${_componentId}.ts`;\n componentLoader = components[componentId] as ComponentLoader;\n }\n\n return { componentLoader, componentId };\n}\n\nfunction retrieveTransferredState(\n html: string,\n appId: string,\n): Record<string, unknown | undefined> {\n const regex = new RegExp(\n `<script id=\"${appId}-state\" type=\"application/json\">(.*?)</script>`,\n );\n const match = html.match(regex);\n\n if (match) {\n const scriptContent = match[1];\n\n if (scriptContent) {\n try {\n const parsedContent: {\n _analog_output: Record<string, unknown | undefined>;\n } = JSON.parse(scriptContent);\n return parsedContent._analog_output || {};\n } catch (e) {\n console.warn('Exception while parsing static outputs for ' + appId, e);\n }\n }\n\n return {};\n } else {\n return {};\n }\n}\n","import {\n ApplicationConfig,\n Provider,\n Type,\n enableProdMode,\n} from '@angular/core';\nimport {\n bootstrapApplication,\n type BootstrapContext,\n} from '@angular/platform-browser';\nimport { renderApplication } from '@angular/platform-server';\nimport type { ServerContext } from '../../tokens/src/index.js';\n\nimport { provideServerContext } from './provide-server-context';\nimport {\n serverComponentRequest,\n renderServerComponent,\n} from './server-component-render';\n\nif (import.meta.env.PROD) {\n enableProdMode();\n}\n\n/**\n * Nulls `def.tView` on every component definition that Angular has\n * compiled in this process. Angular caches the result of `consts()` on\n * `def.tView` — that factory is where `$localize` tagged templates are\n * evaluated — so without this reset the first rendered locale would be\n * frozen into the cache for the process lifetime.\n *\n * The set on `globalThis.__ngComponentDefs` is populated by a Vite\n * transform in `@analogjs/platform` that patches `@angular/core`'s\n * `getComponentId()` to mirror every compiled component definition to\n * a global Set, bypassing the `ngServerMode` guard that normally\n * prevents registration on the server.\n */\nfunction resetComponentDefTViews(): void {\n const defs = (globalThis as any).__ngComponentDefs as Set<any> | undefined;\n if (!defs) return;\n for (const def of defs) {\n def.tView = null;\n }\n}\n\n/**\n * Returns a function that accepts the navigation URL,\n * the root HTML, and server context.\n *\n * @param rootComponent\n * @param config\n * @param platformProviders\n * @returns Promise<string | Reponse>\n */\nexport function render(\n rootComponent: Type<unknown>,\n config: ApplicationConfig,\n platformProviders: Provider[] = [],\n) {\n function bootstrap(context?: BootstrapContext) {\n return bootstrapApplication(rootComponent, config, context);\n }\n\n return async function render(\n url: string,\n document: string,\n serverContext: ServerContext,\n ): Promise<string | Response> {\n if (serverComponentRequest(serverContext)) {\n return await renderServerComponent(url, serverContext);\n }\n\n resetComponentDefTViews();\n\n const html = await renderApplication(bootstrap, {\n document,\n url,\n platformProviders: [\n provideServerContext(serverContext),\n platformProviders,\n ],\n });\n\n return html;\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { r as ANALOG_QUERIES_KEY, t as ANALOG_QUERY_STATE_KEY } from "./provide-analog-query.mjs";
|
|
2
|
+
import { t as definePageLoad } from "./src.mjs";
|
|
3
|
+
import { ApplicationRef, ENVIRONMENT_INITIALIZER, TransferState, inject, makeEnvironmentProviders } from "@angular/core";
|
|
4
|
+
import { filter, skipWhile, take } from "rxjs/operators";
|
|
5
|
+
import { QueryClient, dehydrate } from "@tanstack/angular-query-experimental";
|
|
6
|
+
//#region packages/router/tanstack-query/src/provide-server-analog-query.ts
|
|
7
|
+
function provideServerAnalogQuery() {
|
|
8
|
+
return makeEnvironmentProviders([{
|
|
9
|
+
provide: ENVIRONMENT_INITIALIZER,
|
|
10
|
+
multi: true,
|
|
11
|
+
useValue() {
|
|
12
|
+
const appRef = inject(ApplicationRef);
|
|
13
|
+
const queryClient = inject(QueryClient);
|
|
14
|
+
const transferState = inject(TransferState);
|
|
15
|
+
const subscription = appRef.isStable.pipe(skipWhile((stable) => stable), filter((stable) => stable), take(1)).subscribe(() => {
|
|
16
|
+
transferState.set(ANALOG_QUERY_STATE_KEY, dehydrate(queryClient));
|
|
17
|
+
});
|
|
18
|
+
appRef.onDestroy(() => subscription.unsubscribe());
|
|
19
|
+
}
|
|
20
|
+
}]);
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region packages/router/tanstack-query/server/src/define-page-load-queries.ts
|
|
24
|
+
/**
|
|
25
|
+
* Page load helper that prefetches TanStack Query queries inside the
|
|
26
|
+
* `.server.ts` handler and ships the dehydrated cache alongside any
|
|
27
|
+
* additional data. The router-side hydrator in `provideAnalogQuery()`
|
|
28
|
+
* merges the dehydrated payload into the active `QueryClient` on
|
|
29
|
+
* `ResolveEnd`, so components reading the same query options see a
|
|
30
|
+
* warm cache on first render.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* // src/app/pages/posts.server.ts
|
|
35
|
+
* import { definePageLoadQueries } from '@analogjs/router/tanstack-query/server';
|
|
36
|
+
* import { queryOptions } from '@tanstack/angular-query-experimental';
|
|
37
|
+
*
|
|
38
|
+
* export const postsQuery = queryOptions({
|
|
39
|
+
* queryKey: ['posts'],
|
|
40
|
+
* queryFn: async ({ signal }) =>
|
|
41
|
+
* fetch('https://api.example.com/posts', { signal }).then((r) => r.json()),
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* export const load = definePageLoadQueries({
|
|
45
|
+
* handler: async ({ client }) => {
|
|
46
|
+
* await client.prefetchQuery(postsQuery);
|
|
47
|
+
* },
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
function definePageLoadQueries(options) {
|
|
52
|
+
return definePageLoad({
|
|
53
|
+
params: options.params,
|
|
54
|
+
query: options.query,
|
|
55
|
+
handler: async (ctx) => {
|
|
56
|
+
const client = options.client?.() ?? new QueryClient();
|
|
57
|
+
const data = await options.handler({
|
|
58
|
+
...ctx,
|
|
59
|
+
client
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
[ANALOG_QUERIES_KEY]: dehydrate(client),
|
|
63
|
+
data
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { ANALOG_QUERIES_KEY, definePageLoadQueries, provideServerAnalogQuery };
|
|
70
|
+
|
|
71
|
+
//# sourceMappingURL=analogjs-router-tanstack-query-server.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analogjs-router-tanstack-query-server.mjs","names":[],"sources":["../../tanstack-query/src/provide-server-analog-query.ts","../../tanstack-query/server/src/define-page-load-queries.ts"],"sourcesContent":["import {\n ApplicationRef,\n ENVIRONMENT_INITIALIZER,\n TransferState,\n inject,\n makeEnvironmentProviders,\n} from '@angular/core';\nimport type { EnvironmentProviders } from '@angular/core';\nimport { filter, skipWhile, take } from 'rxjs/operators';\nimport { QueryClient, dehydrate } from '@tanstack/angular-query-experimental';\n\nimport { ANALOG_QUERY_STATE_KEY } from './provide-analog-query';\n\nexport function provideServerAnalogQuery(): EnvironmentProviders {\n return makeEnvironmentProviders([\n {\n provide: ENVIRONMENT_INITIALIZER,\n multi: true,\n useValue() {\n // Dehydrate the QueryClient into `TransferState` once the app\n // becomes stable POST-RENDER on the server. `renderApplication`\n // awaits `whenStable()` itself before invoking\n // `BEFORE_APP_SERIALIZED` (which is where Angular's\n // `TRANSFER_STATE_SERIALIZATION_PROVIDERS` reads the store and\n // writes the `ng-state` script). Settling our write at the same\n // point — but ahead of Angular's own subscriber — lets the\n // dehydrated cache land in `TransferState` before the serializer\n // snapshots it, so component-issued queries make it to the client\n // without depending on provider declaration order.\n //\n // `skipWhile(stable => stable)` waits past the initial \"no\n // pending tasks yet\" emission `BehaviorSubject` semantics give\n // us at subscribe time; the next stable transition is the\n // post-render one with all queries settled.\n const appRef = inject(ApplicationRef);\n const queryClient = inject(QueryClient);\n const transferState = inject(TransferState);\n\n const subscription = appRef.isStable\n .pipe(\n skipWhile((stable) => stable),\n filter((stable) => stable),\n take(1),\n )\n .subscribe(() => {\n transferState.set(ANALOG_QUERY_STATE_KEY, dehydrate(queryClient));\n });\n\n appRef.onDestroy(() => subscription.unsubscribe());\n },\n },\n ]);\n}\n","import type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { QueryClient, dehydrate } from '@tanstack/angular-query-experimental';\nimport type { DehydratedState } from '@tanstack/angular-query-experimental';\nimport type { H3Event, H3EventContext } from 'nitro/h3';\nimport type { $Fetch } from 'nitro/types';\n\nimport {\n definePageLoad,\n type PageLoadContext,\n} from '../../../server/actions/src/index.js';\nimport { ANALOG_QUERIES_KEY } from '../../src/constants.js';\n\nexport { ANALOG_QUERIES_KEY } from '../../src/constants.js';\n\ntype NodeContext = NonNullable<H3Event['node']>;\ntype OptionalSchema = StandardSchemaV1 | undefined;\n\nexport interface PageLoadQueriesResult<TData> {\n __analogQueries: DehydratedState;\n data: TData;\n}\n\nexport interface DefinePageLoadQueriesOptions<\n TParamsSchema extends OptionalSchema,\n TQuerySchema extends OptionalSchema,\n TData,\n> {\n params?: TParamsSchema;\n query?: TQuerySchema;\n /**\n * Optional QueryClient factory. Defaults to `new QueryClient()`.\n * Override to set `defaultOptions` (e.g. `queries: { staleTime: Infinity }`).\n */\n client?: () => QueryClient;\n /**\n * Handler receives the standard PageLoadContext plus a per-request\n * QueryClient. Use `client.prefetchQuery` / `ensureQueryData` /\n * `prefetchInfiniteQuery` to warm the cache; the dehydrated client\n * is returned as `__analogQueries` on the load result. Anything you\n * return from the handler becomes `data` on the same result.\n */\n handler: (\n ctx: PageLoadContext<TParamsSchema, TQuerySchema> & { client: QueryClient },\n ) => Promise<TData> | TData;\n}\n\n/**\n * Page load helper that prefetches TanStack Query queries inside the\n * `.server.ts` handler and ships the dehydrated cache alongside any\n * additional data. The router-side hydrator in `provideAnalogQuery()`\n * merges the dehydrated payload into the active `QueryClient` on\n * `ResolveEnd`, so components reading the same query options see a\n * warm cache on first render.\n *\n * @example\n * ```ts\n * // src/app/pages/posts.server.ts\n * import { definePageLoadQueries } from '@analogjs/router/tanstack-query/server';\n * import { queryOptions } from '@tanstack/angular-query-experimental';\n *\n * export const postsQuery = queryOptions({\n * queryKey: ['posts'],\n * queryFn: async ({ signal }) =>\n * fetch('https://api.example.com/posts', { signal }).then((r) => r.json()),\n * });\n *\n * export const load = definePageLoadQueries({\n * handler: async ({ client }) => {\n * await client.prefetchQuery(postsQuery);\n * },\n * });\n * ```\n */\nexport function definePageLoadQueries<\n TParamsSchema extends OptionalSchema = undefined,\n TQuerySchema extends OptionalSchema = undefined,\n TData = void,\n>(\n options: DefinePageLoadQueriesOptions<TParamsSchema, TQuerySchema, TData>,\n): (ctx: {\n params: H3EventContext['params'];\n req: NodeContext['req'];\n res: NonNullable<NodeContext['res']>;\n fetch: $Fetch;\n event: H3Event;\n}) => Promise<PageLoadQueriesResult<TData> | Response> {\n return definePageLoad({\n params: options.params,\n query: options.query,\n handler: async (ctx): Promise<PageLoadQueriesResult<TData>> => {\n const client = options.client?.() ?? new QueryClient();\n const data = await options.handler({ ...ctx, client });\n return {\n [ANALOG_QUERIES_KEY]: dehydrate(client),\n data,\n };\n },\n });\n}\n"],"mappings":";;;;;;AAaA,SAAgB,2BAAiD;AAC/D,QAAO,yBACL,CACE;EACO,SAAA;EACP,OAAW;EAgBH,WAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACuCvB,SAAgB,sBAKd,SAOqD;AACrD,QAAO,eAAe;EACpB,QAAQ,QAAQ;EAChB,OAAO,QAAQ;EACf,SAAS,OAAO,QAA+C;GACvD,MAAS,SAAQ,QAAA,UAAkB,IAAA,IAAA,aAAa;GAChD,MAAO,OAAM,MAAQ,QAAQ,QAAA;IAAA,GAAA;IAAA;IAAA,CAAA;AAAK,UAAA;KAAK,qBAAA,UAAA,OAAA;IAAS;IAC/C;;EAEL,CAAA"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { n as provideAnalogQuery, t as ANALOG_QUERY_STATE_KEY } from "./provide-analog-query.mjs";
|
|
2
|
+
import { lastValueFrom } from "rxjs";
|
|
3
|
+
//#region packages/router/tanstack-query/src/server-query.ts
|
|
4
|
+
function buildUrl(base, params) {
|
|
5
|
+
if (!params) return base;
|
|
6
|
+
const parts = [];
|
|
7
|
+
for (const [key, value] of Object.entries(params)) {
|
|
8
|
+
if (value === void 0 || value === null) continue;
|
|
9
|
+
const k = encodeURIComponent(key);
|
|
10
|
+
if (Array.isArray(value)) for (const item of value) parts.push(`${k}=${encodeURIComponent(String(item))}`);
|
|
11
|
+
else parts.push(`${k}=${encodeURIComponent(String(value))}`);
|
|
12
|
+
}
|
|
13
|
+
if (parts.length === 0) return base;
|
|
14
|
+
return `${base}${base.includes("?") ? "&" : "?"}${parts.join("&")}`;
|
|
15
|
+
}
|
|
16
|
+
function serverQueryOptions(http, url, options) {
|
|
17
|
+
const { query, ...rest } = options;
|
|
18
|
+
return {
|
|
19
|
+
...rest,
|
|
20
|
+
queryFn: () => lastValueFrom(http.get(buildUrl(url, query)))
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function serverMutationOptions(http, url, options) {
|
|
24
|
+
return {
|
|
25
|
+
mutationFn: (body) => lastValueFrom(http.post(url, body)),
|
|
26
|
+
...options
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function serverInfiniteQueryOptions(http, url, options) {
|
|
30
|
+
const { query: buildQuery, ...rest } = options;
|
|
31
|
+
return {
|
|
32
|
+
...rest,
|
|
33
|
+
queryFn: (context) => lastValueFrom(http.get(buildUrl(url, buildQuery(context))))
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { ANALOG_QUERY_STATE_KEY, provideAnalogQuery, serverInfiniteQueryOptions, serverMutationOptions, serverQueryOptions };
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=analogjs-router-tanstack-query.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analogjs-router-tanstack-query.mjs","names":[],"sources":["../../tanstack-query/src/server-query.ts"],"sourcesContent":["import type { HttpClient } from '@angular/common/http';\nimport { lastValueFrom } from 'rxjs';\nimport type {\n CreateQueryOptions,\n CreateMutationOptions,\n CreateInfiniteQueryOptions,\n DefaultError,\n InfiniteData,\n QueryKey,\n} from '@tanstack/angular-query-experimental';\nimport type {\n ServerRouteHandler,\n InferRouteQuery,\n InferRouteBody,\n InferRouteResult,\n} from '../../server/actions/src/index.js';\n\nfunction buildUrl(base: string, params?: Record<string, unknown>): string {\n if (!params) return base;\n const parts: string[] = [];\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined || value === null) continue;\n const k = encodeURIComponent(key);\n if (Array.isArray(value)) {\n for (const item of value) {\n parts.push(`${k}=${encodeURIComponent(String(item))}`);\n }\n } else {\n parts.push(`${k}=${encodeURIComponent(String(value))}`);\n }\n }\n if (parts.length === 0) return base;\n const separator = base.includes('?') ? '&' : '?';\n return `${base}${separator}${parts.join('&')}`;\n}\n\nexport function serverQueryOptions<\n TRoute extends ServerRouteHandler<any, any, any>,\n TError = DefaultError,\n TData = InferRouteResult<TRoute>,\n TQueryKey extends QueryKey = QueryKey,\n>(\n http: HttpClient,\n url: string,\n options: { queryKey: TQueryKey; query?: InferRouteQuery<TRoute> } & Omit<\n CreateQueryOptions<InferRouteResult<TRoute>, TError, TData, TQueryKey>,\n 'queryKey' | 'queryFn'\n >,\n): CreateQueryOptions<InferRouteResult<TRoute>, TError, TData, TQueryKey> {\n const { query, ...rest } = options;\n return {\n ...rest,\n queryFn: () =>\n lastValueFrom(\n http.get<InferRouteResult<TRoute>>(\n buildUrl(url, query as Record<string, any>),\n ),\n ),\n } as CreateQueryOptions<InferRouteResult<TRoute>, TError, TData, TQueryKey>;\n}\n\nexport function serverMutationOptions<\n TRoute extends ServerRouteHandler<any, any, any>,\n TError = DefaultError,\n TOnMutateResult = unknown,\n>(\n http: HttpClient,\n url: string,\n options?: Omit<\n CreateMutationOptions<\n InferRouteResult<TRoute>,\n TError,\n InferRouteBody<TRoute>,\n TOnMutateResult\n >,\n 'mutationFn'\n >,\n): CreateMutationOptions<\n InferRouteResult<TRoute>,\n TError,\n InferRouteBody<TRoute>,\n TOnMutateResult\n> {\n return {\n mutationFn: (body: InferRouteBody<TRoute>) =>\n lastValueFrom(http.post<InferRouteResult<TRoute>>(url, body)),\n ...options,\n } as CreateMutationOptions<\n InferRouteResult<TRoute>,\n TError,\n InferRouteBody<TRoute>,\n TOnMutateResult\n >;\n}\n\nexport function serverInfiniteQueryOptions<\n TRoute extends ServerRouteHandler<any, any, any>,\n TError = DefaultError,\n TData = InfiniteData<InferRouteResult<TRoute>>,\n TQueryKey extends QueryKey = QueryKey,\n TPageParam = unknown,\n>(\n http: HttpClient,\n url: string,\n options: {\n queryKey: TQueryKey;\n query: (context: { pageParam: TPageParam }) => InferRouteQuery<TRoute>;\n initialPageParam: TPageParam;\n getNextPageParam: (\n lastPage: InferRouteResult<TRoute>,\n allPages: InferRouteResult<TRoute>[],\n ) => TPageParam | undefined | null;\n } & Omit<\n CreateInfiniteQueryOptions<\n InferRouteResult<TRoute>,\n TError,\n TData,\n TQueryKey,\n TPageParam\n >,\n 'queryKey' | 'queryFn' | 'initialPageParam' | 'getNextPageParam'\n >,\n): CreateInfiniteQueryOptions<\n InferRouteResult<TRoute>,\n TError,\n TData,\n TQueryKey,\n TPageParam\n> {\n const { query: buildQuery, ...rest } = options;\n return {\n ...rest,\n queryFn: (context: { pageParam: TPageParam }) =>\n lastValueFrom(\n http.get<InferRouteResult<TRoute>>(\n buildUrl(url, buildQuery(context) as Record<string, any>),\n ),\n ),\n } as CreateInfiniteQueryOptions<\n InferRouteResult<TRoute>,\n TError,\n TData,\n TQueryKey,\n TPageParam\n >;\n}\n"],"mappings":";;;AAiBA,SAAS,SAAS,MAAc,QAA0C;AACxE,KAAK,CAAA,OACC,QAAkB;CACxB,MAAK,QAAY,EAAA;AACf,MAAI,MAAA,CAAU,KAAA,UAAa,OAAA,QAAgB,OAAA,EAAA;AACrC,MAAI,UAAA,KAAA,KAAuB,UAAA,KACvB;EACH,MAAM,IAAA,mBAAe,IAAA;AACxB,MAAM,MAAQ,QAAK,MAAA,CAAA,MAAA,MAAA,QAAA,MAEhB,OAAA,KAAA,GAAA,EAAA,GAAA,mBAAA,OAAA,KAAA,CAAA,GAAA;MAIC,OAAA,KAAW,GAAG,EAAA,GAAA,mBAAO,OAAA,MAAA,CAAA,GAAA;;wBAK1B,QAAA;AAcL,QAAO,GAAA,OADW,KAAA,SAAS,IAAA,GAAA,MAAA,MACpB,MAAA,KAAA,IAAA;;SAEL,mBAEI,MACE,KAAA,SAAc;CAGrB,MAAA,EAAA,OAAA,GAAA,SAAA;;EAGI,GAAA;EAsBE,eAAA,cAAA,KAAA,IAAA,SAAA,KAAA,MAAA,CAAA,CAAA;EACL;;SAGD,sBAAA,MAAA,KAAA,SAAA;;EAQI,aAAS,SAAA,cAOd,KACA,KACA,KAAA,KAwBA,CAAA;EACM,GAAE;EACR;;SAEY,2BAED,MACH,KAAA,SAAc;CAGrB,MAAA,EAAA,OAAA,YAAA,GAAA,SAAA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InjectionToken, inject } from "@angular/core";
|
|
1
|
+
import { InjectionToken, assertInInjectionContext, inject } from "@angular/core";
|
|
2
2
|
//#region packages/router/tokens/src/index.ts
|
|
3
3
|
var REQUEST = new InjectionToken("@analogjs/router Server Request");
|
|
4
4
|
var RESPONSE = new InjectionToken("@analogjs/router Server Response");
|
|
@@ -20,7 +20,12 @@ function injectInternalServerFetch() {
|
|
|
20
20
|
function injectAPIPrefix() {
|
|
21
21
|
return inject(API_PREFIX);
|
|
22
22
|
}
|
|
23
|
+
var LOCALE = new InjectionToken("@analogjs/router Locale");
|
|
24
|
+
function injectLocale() {
|
|
25
|
+
assertInInjectionContext(injectLocale);
|
|
26
|
+
return inject(LOCALE, { optional: true });
|
|
27
|
+
}
|
|
23
28
|
//#endregion
|
|
24
|
-
export { API_PREFIX, BASE_URL, INTERNAL_FETCH, REQUEST, RESPONSE, injectAPIPrefix, injectBaseURL, injectInternalServerFetch, injectRequest, injectResponse };
|
|
29
|
+
export { API_PREFIX, BASE_URL, INTERNAL_FETCH, LOCALE, REQUEST, RESPONSE, injectAPIPrefix, injectBaseURL, injectInternalServerFetch, injectLocale, injectRequest, injectResponse };
|
|
25
30
|
|
|
26
31
|
//# sourceMappingURL=analogjs-router-tokens.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analogjs-router-tokens.mjs","names":[],"sources":["../../tokens/src/index.ts"],"sourcesContent":["import {\n InjectionToken,\n assertInInjectionContext,\n inject,\n} from '@angular/core';\nimport type { $Fetch } from 'nitro/types';\nimport type {\n IncomingMessage,\n ServerResponse as NodeServerResponse,\n} from 'node:http';\n\nexport type ServerRequest = IncomingMessage & { originalUrl: string };\nexport type ServerResponse = NodeServerResponse;\nexport type ServerInternalFetch = $Fetch;\nexport type ServerContext = {\n req: ServerRequest;\n res: ServerResponse;\n fetch?: ServerInternalFetch;\n};\n\nexport const REQUEST: InjectionToken<ServerRequest> =\n new InjectionToken<ServerRequest>('@analogjs/router Server Request');\nexport const RESPONSE: InjectionToken<ServerResponse> =\n new InjectionToken<ServerResponse>('@analogjs/router Server Response');\nexport const BASE_URL: InjectionToken<string> = new InjectionToken<string>(\n '@analogjs/router Base URL',\n);\nexport const INTERNAL_FETCH: InjectionToken<ServerInternalFetch> =\n new InjectionToken<ServerInternalFetch>(\n '@analogjs/router Internal Server Fetch',\n );\n\nexport const API_PREFIX: InjectionToken<string> = new InjectionToken<string>(\n '@analogjs/router API Prefix',\n);\n\nexport function injectRequest(): ServerRequest | null {\n return inject(REQUEST, { optional: true });\n}\n\nexport function injectResponse(): ServerResponse | null {\n return inject(RESPONSE, { optional: true });\n}\n\nexport function injectBaseURL(): string | null {\n return inject(BASE_URL, { optional: true });\n}\n\nexport function injectInternalServerFetch(): ServerInternalFetch | null {\n return inject(INTERNAL_FETCH, { optional: true });\n}\n\nexport function injectAPIPrefix(): string {\n return inject(API_PREFIX);\n}\n\nexport const LOCALE: InjectionToken<string> = new InjectionToken<string>(\n '@analogjs/router Locale',\n);\n\nexport function injectLocale(): string | null {\n assertInInjectionContext(injectLocale);\n return inject(LOCALE, { optional: true });\n}\n"],"mappings":";;AAoBA,IAAa,UACX,IAAI,eAA8B,kCAAkC;AACtE,IAAa,WACX,IAAI,eAA+B,mCAAmC;AACxE,IAAa,WAAmC,IAAI,eAClD,4BACD;AACD,IAAa,iBACX,IAAI,eACF,yCACD;AAEH,IAAa,aAAqC,IAAI,eACpD,8BACD;AAED,SAAgB,gBAAsC;AACpD,QAAO,OAAO,SAAW,EAAA,UAAU,MAAO,CAAA;;AAG5C,SAAgB,iBAAwC;AACtD,QAAO,OAAO,UAAY,EAAA,UAAU,MAAO,CAAA;;AAG7C,SAAgB,gBAA+B;AAC7C,QAAO,OAAO,UAAY,EAAA,UAAU,MAAO,CAAA;;AAG7C,SAAgB,4BAAwD;AACtE,QAAO,OAAO,gBAAkB,EAAA,UAAU,MAAO,CAAA;;AAGnD,SAAgB,kBAA0B;AACxC,QAAO,OAAO,WAAW;;AAG3B,IAAa,SAAiC,IAAI,eAChD,0BACD;AAED,SAAgB,eAA8B;AAC5C,0BAAyB,aAAa;AACtC,QAAO,OAAO,QAAU,EAAA,UAAU,MAAO,CAAA"}
|