ngx-cloudflare 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ngx-cloudflare.mjs","names":[],"sources":["../../../projects/ngx-cloudflare/src/lib/inject-cloudflare.ts","../../../projects/ngx-cloudflare/src/lib/cloudflare-fetch-backend.ts","../../../projects/ngx-cloudflare/src/lib/provide-cloudflare.ts"],"sourcesContent":["import { assertInInjectionContext, inject, REQUEST_CONTEXT } from '@angular/core';\nimport {\n isCloudflareContext,\n type CloudflareContext,\n type CloudflareExecutionContext,\n type CloudflareRequestCf,\n} from 'ngx-cloudflare/server';\n\nexport interface InjectCloudflareOptions {\n required?: boolean;\n}\n\nconst MISSING_CONTEXT =\n 'ngx-cloudflare: no Cloudflare request context. Bindings exist only while a request is rendered on the server ' +\n '(on Workers or in ng serve). They are null in the browser, during prerendering and at build time. ' +\n 'Check that src/server.ts uses createCloudflareRequestHandler(), or call this without { required: true } and handle null.';\n\nfunction readContext(fn: Function): CloudflareContext<any, any> | null {\n assertInInjectionContext(fn);\n const value = inject(REQUEST_CONTEXT, { optional: true });\n return isCloudflareContext(value) ? value : null;\n}\n\nfunction orThrow<T>(value: T | null, options: InjectCloudflareOptions | undefined): T | null {\n if (value === null && options?.required) {\n throw new Error(MISSING_CONTEXT);\n }\n return value;\n}\n\nexport function injectCloudflare<Env = Cloudflare.Env, Cf = CloudflareRequestCf>(options: {\n required: true;\n}): CloudflareContext<Env, Cf>;\nexport function injectCloudflare<Env = Cloudflare.Env, Cf = CloudflareRequestCf>(\n options?: InjectCloudflareOptions,\n): CloudflareContext<Env, Cf> | null;\nexport function injectCloudflare(\n options?: InjectCloudflareOptions,\n): CloudflareContext<any, any> | null {\n return orThrow(readContext(injectCloudflare), options);\n}\n\nexport function injectEnv<Env = Cloudflare.Env>(options: { required: true }): Env;\nexport function injectEnv<Env = Cloudflare.Env>(options?: InjectCloudflareOptions): Env | null;\nexport function injectEnv(options?: InjectCloudflareOptions): unknown {\n return orThrow(readContext(injectEnv)?.env ?? null, options);\n}\n\nexport function injectExecutionContext(options: { required: true }): CloudflareExecutionContext;\nexport function injectExecutionContext(\n options?: InjectCloudflareOptions,\n): CloudflareExecutionContext | null;\nexport function injectExecutionContext(\n options?: InjectCloudflareOptions,\n): CloudflareExecutionContext | null {\n return orThrow(readContext(injectExecutionContext)?.ctx ?? null, options);\n}\n\nexport function injectRequestCf<Cf = CloudflareRequestCf>(options: { required: true }): Cf;\nexport function injectRequestCf<Cf = CloudflareRequestCf>(\n options?: InjectCloudflareOptions,\n): Cf | null;\nexport function injectRequestCf(options?: InjectCloudflareOptions): unknown {\n return orThrow(readContext(injectRequestCf)?.cf ?? null, options);\n}\n","import {\n FetchBackend,\n HttpErrorResponse,\n HttpEvent,\n HttpEventType,\n HttpHeaders,\n HttpRequest,\n HttpResponse,\n} from '@angular/common/http';\nimport { inject, Injectable, REQUEST, REQUEST_CONTEXT } from '@angular/core';\nimport { Observable, type Subscription } from 'rxjs';\nimport { isCloudflareContext } from 'ngx-cloudflare/server';\n\nconst XSSI_PREFIX = /^\\)\\]\\}',?\\n/;\n\ntype DispatchPhase =\n | { kind: 'dispatching' }\n | { kind: 'fallback'; subscription: Subscription }\n | { kind: 'done' }\n | { kind: 'cancelled' };\n\nfunction toHeaders(headers: Headers): HttpHeaders {\n let result = new HttpHeaders();\n headers.forEach((value, name) => {\n result = result.append(name, value);\n });\n return result;\n}\n\nfunction toRequest(req: HttpRequest<unknown>, url: URL, signal: AbortSignal): Request {\n const headers = new Headers();\n for (const name of req.headers.keys()) {\n for (const value of req.headers.getAll(name) ?? []) {\n headers.append(name, value);\n }\n }\n if (!headers.has('accept')) {\n headers.set('accept', 'application/json, text/plain, */*');\n }\n const body = req.serializeBody();\n if (body !== null && !headers.has('content-type')) {\n const detected = req.detectContentTypeHeader();\n if (detected !== null) {\n headers.set('content-type', detected);\n }\n }\n const hasBody = body !== null && req.method !== 'GET' && req.method !== 'HEAD';\n return new Request(url, {\n method: req.method,\n headers,\n body: hasBody ? (body as BodyInit) : undefined,\n signal,\n });\n}\n\nasync function readBody(\n req: HttpRequest<unknown>,\n res: Response,\n): Promise<{ body: unknown; parseError?: unknown }> {\n switch (req.responseType) {\n case 'arraybuffer':\n return { body: await res.arrayBuffer() };\n case 'blob':\n return { body: await res.blob() };\n case 'text':\n return { body: await res.text() };\n default: {\n const text = await res.text();\n if (text === '') {\n return { body: null };\n }\n try {\n return { body: JSON.parse(text.replace(XSSI_PREFIX, '')) };\n } catch (parseError) {\n return { body: text, parseError };\n }\n }\n }\n}\n\n@Injectable()\nexport class CloudflareFetchBackend extends FetchBackend {\n private readonly cloudflare = inject(REQUEST_CONTEXT, { optional: true });\n private readonly incoming = inject(REQUEST, { optional: true });\n\n override handle(req: HttpRequest<unknown>): Observable<HttpEvent<unknown>> {\n const context = this.cloudflare;\n if (!this.incoming || !isCloudflareContext(context)) {\n return super.handle(req);\n }\n const base = new URL(this.incoming.url);\n const url = new URL(req.urlWithParams, base);\n if (url.origin !== base.origin) {\n return super.handle(req);\n }\n\n return new Observable<HttpEvent<unknown>>((observer) => {\n const aborter = new AbortController();\n let phase: DispatchPhase = { kind: 'dispatching' };\n\n const settle = (next: DispatchPhase): boolean => {\n if (phase.kind !== 'dispatching') {\n return false;\n }\n phase = next;\n return true;\n };\n\n (async () => {\n const response = await context.dispatch(toRequest(req, url, aborter.signal));\n if (phase.kind !== 'dispatching') {\n return;\n }\n if (!response) {\n const subscription = super.handle(req).subscribe(observer);\n if (!settle({ kind: 'fallback', subscription })) {\n subscription.unsubscribe();\n }\n return;\n }\n observer.next({ type: HttpEventType.Sent });\n const headers = toHeaders(response.headers);\n const { body, parseError } = await readBody(req, response);\n if (!settle({ kind: 'done' })) {\n return;\n }\n if (response.status >= 200 && response.status < 300 && !parseError) {\n observer.next(\n new HttpResponse({\n body,\n headers,\n status: response.status,\n statusText: response.statusText,\n url: url.href,\n }),\n );\n observer.complete();\n } else {\n observer.error(\n new HttpErrorResponse({\n error: parseError ? { error: parseError, text: body } : body,\n headers,\n status: response.status,\n statusText: response.statusText,\n url: url.href,\n }),\n );\n }\n })().catch((error: unknown) => {\n if (settle({ kind: 'done' })) {\n observer.error(\n new HttpErrorResponse({ error, status: 0, statusText: 'Unknown Error', url: url.href }),\n );\n }\n });\n\n return () => {\n if (phase.kind === 'fallback') {\n phase.subscription.unsubscribe();\n } else if (settle({ kind: 'cancelled' })) {\n aborter.abort();\n }\n };\n });\n }\n}\n","import { FetchBackend, HttpBackend } from '@angular/common/http';\nimport { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';\nimport { CloudflareFetchBackend } from './cloudflare-fetch-backend';\n\nexport interface ProvideCloudflareOptions {\n inProcessRequests?: boolean;\n}\n\nexport function provideCloudflare(options: ProvideCloudflareOptions = {}): EnvironmentProviders {\n if (options.inProcessRequests === false) {\n return makeEnvironmentProviders([]);\n }\n return makeEnvironmentProviders([\n CloudflareFetchBackend,\n { provide: FetchBackend, useExisting: CloudflareFetchBackend },\n { provide: HttpBackend, useExisting: CloudflareFetchBackend },\n ]);\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,68 @@
1
1
  {
2
2
  "name": "ngx-cloudflare",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "peerDependencies": {
5
+ "@angular/common": "^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
6
+ "@angular/core": "^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
7
+ "@angular/ssr": "^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
8
+ "rxjs": "^7.4.0",
9
+ "wrangler": "^4.0.0"
10
+ },
11
+ "dependencies": {
12
+ "tslib": "^2.3.0",
13
+ "jsonc-parser": "^3.3.1"
14
+ },
15
+ "sideEffects": false,
16
+ "description": "Workers bindings, a working hostname allow-list and in-process API calls for Angular SSR on Cloudflare Workers.",
17
+ "license": "MIT",
18
+ "author": "Erkam Yaman",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/erkamyaman/ngx-cloudflare.git",
22
+ "directory": "projects/ngx-cloudflare"
23
+ },
24
+ "homepage": "https://github.com/erkamyaman/ngx-cloudflare#readme",
25
+ "bugs": {
26
+ "url": "https://github.com/erkamyaman/ngx-cloudflare/issues"
27
+ },
28
+ "keywords": [
29
+ "angular",
30
+ "ssr",
31
+ "cloudflare",
32
+ "workers",
33
+ "wrangler",
34
+ "bindings",
35
+ "d1",
36
+ "kv",
37
+ "schematics"
38
+ ],
39
+ "peerDependenciesMeta": {
40
+ "wrangler": {
41
+ "optional": true
42
+ }
43
+ },
44
+ "schematics": "./schematics/collection.json",
45
+ "ng-add": {
46
+ "save": "dependencies"
47
+ },
48
+ "module": "fesm2022/ngx-cloudflare.mjs",
49
+ "typings": "types/ngx-cloudflare.d.ts",
50
+ "exports": {
51
+ "./package.json": {
52
+ "default": "./package.json"
53
+ },
54
+ ".": {
55
+ "types": "./types/ngx-cloudflare.d.ts",
56
+ "default": "./fesm2022/ngx-cloudflare.mjs"
57
+ },
58
+ "./server": {
59
+ "types": "./types/ngx-cloudflare-server.d.ts",
60
+ "default": "./fesm2022/ngx-cloudflare-server.mjs"
61
+ },
62
+ "./testing": {
63
+ "types": "./types/ngx-cloudflare-testing.d.ts",
64
+ "default": "./fesm2022/ngx-cloudflare-testing.mjs"
65
+ }
66
+ },
67
+ "type": "module"
6
68
  }
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json",
3
+ "schematics": {
4
+ "ng-add": {
5
+ "description": "Set up an Angular SSR app for Cloudflare Workers with ngx-cloudflare.",
6
+ "factory": "./ng-add/index#ngAdd",
7
+ "schema": "./ng-add/schema.json"
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.addProvider = addProvider;
4
+ function closingBracket(text, open) {
5
+ let depth = 0;
6
+ let quote = null;
7
+ for (let i = open; i < text.length; i++) {
8
+ const char = text[i];
9
+ if (quote) {
10
+ if (char === '\\')
11
+ i++;
12
+ else if (char === quote)
13
+ quote = null;
14
+ continue;
15
+ }
16
+ if (char === "'" || char === '"' || char === '`')
17
+ quote = char;
18
+ else if (char === '[' || char === '(' || char === '{')
19
+ depth++;
20
+ else if (char === ']' || char === ')' || char === '}') {
21
+ depth--;
22
+ if (depth === 0)
23
+ return i;
24
+ }
25
+ }
26
+ return -1;
27
+ }
28
+ function addProvider(text, expression, importLines) {
29
+ const call = text.indexOf('provideServerRendering(');
30
+ const providers = text.lastIndexOf('providers', call);
31
+ const open = providers === -1 ? -1 : text.indexOf('[', providers);
32
+ const close = open === -1 ? -1 : closingBracket(text, open);
33
+ if (call === -1 || close === -1 || close < call) {
34
+ return null;
35
+ }
36
+ const inner = text.slice(open + 1, close);
37
+ const before = text.slice(0, close).replace(/[\s,]*$/, '');
38
+ const tail = text.slice(before.length, close);
39
+ let insertion = `, ${expression}`;
40
+ if (inner.includes('\n')) {
41
+ const indent = /\n([ \t]*)\S/.exec(inner)?.[1] ?? ' ';
42
+ const closingIndent = /\n([ \t]*)$/.exec(tail)?.[1] ?? '';
43
+ insertion = `,\n${indent}${expression},\n${closingIndent}`;
44
+ }
45
+ let result = before + insertion + text.slice(close);
46
+ const imports = [...result.matchAll(/^import[\s\S]*?from\s+['"][^'"]+['"];?[ \t]*$/gm)];
47
+ const last = imports.at(-1);
48
+ const insertAt = last ? last.index + last[0].length : 0;
49
+ const missing = importLines.filter((line) => !result.includes(line));
50
+ if (missing.length) {
51
+ result = `${result.slice(0, insertAt)}\n${missing.join('\n')}${result.slice(insertAt)}`;
52
+ }
53
+ return result;
54
+ }
@@ -0,0 +1,376 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ngAdd = ngAdd;
4
+ const schematics_1 = require("@angular-devkit/schematics");
5
+ const utility_1 = require("@schematics/angular/utility");
6
+ const jsonc_parser_1 = require("jsonc-parser");
7
+ const node_path_1 = require("node:path");
8
+ const add_provider_1 = require("./add-provider");
9
+ const server_entry_1 = require("./server-entry");
10
+ const wrangler_config_1 = require("./wrangler-config");
11
+ const FORMAT = { formattingOptions: { insertSpaces: true, tabSize: 2 } };
12
+ function readText(tree, path) {
13
+ return tree.exists(path) ? tree.readText(path) : null;
14
+ }
15
+ function editJson(tree, path, edit) {
16
+ const original = tree.readText(path);
17
+ let text = original;
18
+ edit(original, (jsonPath, value) => {
19
+ text = (0, jsonc_parser_1.applyEdits)(text, (0, jsonc_parser_1.modify)(text, jsonPath, value, FORMAT));
20
+ });
21
+ if (text !== original) {
22
+ tree.overwrite(path, text);
23
+ }
24
+ }
25
+ function packageVersion(tree, name) {
26
+ const installed = readText(tree, `node_modules/${name}/package.json`);
27
+ const declared = (0, jsonc_parser_1.parse)(readText(tree, 'package.json') ?? '{}')
28
+ .dependencies?.[name];
29
+ const version = installed
30
+ ? JSON.parse(installed).version
31
+ : declared;
32
+ const match = /(\d+)(?:\.(\d+))?/.exec(version ?? '');
33
+ return { major: Number(match?.[1] ?? 0), minor: Number(match?.[2] ?? 0) };
34
+ }
35
+ function angularMajor(tree) {
36
+ const { major } = packageVersion(tree, '@angular/core');
37
+ if (!major) {
38
+ throw new schematics_1.SchematicsException('ngx-cloudflare: could not find @angular/core in package.json.');
39
+ }
40
+ if (major < 19) {
41
+ throw new schematics_1.SchematicsException(`ngx-cloudflare supports Angular 19 and newer. Angular ${major} has no Workers-compatible SSR engine (AngularAppEngine and REQUEST_CONTEXT arrived in 19). Update Angular first: https://angular.dev/update-guide`);
42
+ }
43
+ return major;
44
+ }
45
+ function resolveOutput(projectName, outputPath) {
46
+ let base = `dist/${projectName}`;
47
+ let browser = 'browser';
48
+ let server = 'server';
49
+ if (typeof outputPath === 'string') {
50
+ base = outputPath;
51
+ }
52
+ else if (outputPath && typeof outputPath === 'object') {
53
+ const value = outputPath;
54
+ base = value.base ?? base;
55
+ browser = value.browser ?? browser;
56
+ server = value.server ?? server;
57
+ }
58
+ base = node_path_1.posix.normalize(base.replace(/\\/g, '/')).replace(/^\.\//, '').replace(/\/$/, '');
59
+ const join = (...parts) => './' + node_path_1.posix.join(...parts.filter(Boolean));
60
+ return { base, output: { main: join(base, server, 'server.mjs'), assets: join(base, browser) } };
61
+ }
62
+ async function createSetupPlan(tree, options) {
63
+ const workspace = await (0, utility_1.readWorkspace)(tree);
64
+ const applications = [...workspace.projects].filter(([, project]) => project.extensions['projectType'] === 'application');
65
+ const name = options.project ??
66
+ applications.find(([, project]) => project.targets.get('build')?.options?.['ssr'])?.[0] ??
67
+ applications[0]?.[0];
68
+ const project = name ? workspace.projects.get(name) : undefined;
69
+ if (!name || !project) {
70
+ throw new schematics_1.SchematicsException(`ngx-cloudflare: project "${options.project ?? ''}" was not found in angular.json.`);
71
+ }
72
+ const build = project.targets.get('build');
73
+ const buildOptions = (build?.options ?? {});
74
+ if (!buildOptions['ssr'] || !buildOptions['server']) {
75
+ throw new schematics_1.SchematicsException(`ngx-cloudflare needs server-side rendering. Run "ng add @angular/ssr --project ${name}" first, then run "ng add ngx-cloudflare" again.`);
76
+ }
77
+ const root = project.root;
78
+ const sourceRoot = project.sourceRoot ?? node_path_1.posix.join(root, 'src');
79
+ const ssr = buildOptions['ssr'];
80
+ const serverEntry = typeof ssr === 'object' && ssr !== null && typeof ssr['entry'] === 'string'
81
+ ? ssr['entry']
82
+ : node_path_1.posix.join(sourceRoot, 'server.ts');
83
+ const { base, output } = resolveOutput(name, buildOptions['outputPath']);
84
+ return {
85
+ projectName: name,
86
+ root,
87
+ sourceRoot,
88
+ angularMajor: angularMajor(tree),
89
+ serverEntry: serverEntry.replace(/^\.\//, ''),
90
+ tsConfigs: [
91
+ buildOptions['tsConfig'],
92
+ project.targets.get('test')?.options?.['tsConfig'] ?? node_path_1.posix.join(root, 'tsconfig.spec.json'),
93
+ ].filter((path, index, all) => typeof path === 'string' && all.indexOf(path) === index),
94
+ output,
95
+ outputBase: base,
96
+ };
97
+ }
98
+ function updateAngularJson(setup) {
99
+ return (0, utility_1.updateWorkspace)((workspace) => {
100
+ const build = workspace.projects.get(setup.projectName).targets.get('build');
101
+ const options = (build.options ??= {});
102
+ const ssr = typeof options['ssr'] === 'object' && options['ssr'] !== null
103
+ ? { ...options['ssr'] }
104
+ : {};
105
+ ssr['entry'] ??= setup.serverEntry;
106
+ const platformKey = setup.angularMajor >= 22 ? 'platform' : 'experimentalPlatform';
107
+ delete ssr[setup.angularMajor >= 22 ? 'experimentalPlatform' : 'platform'];
108
+ ssr[platformKey] = 'neutral';
109
+ options['ssr'] = ssr;
110
+ options['outputMode'] = 'server';
111
+ delete options['prerender'];
112
+ const external = Array.isArray(options['externalDependencies'])
113
+ ? options['externalDependencies']
114
+ : [];
115
+ if (!external.includes('wrangler')) {
116
+ options['externalDependencies'] = [...external, 'wrangler'];
117
+ }
118
+ });
119
+ }
120
+ function wranglerConfigPath(tree) {
121
+ return (['wrangler.jsonc', 'wrangler.json', 'wrangler.toml'].find((file) => tree.exists(file)) ?? null);
122
+ }
123
+ function updateWranglerConfig(setup, options) {
124
+ return (tree, context) => {
125
+ const path = wranglerConfigPath(tree);
126
+ if (!path) {
127
+ const date = options.compatibilityDate ?? new Date().toISOString().slice(0, 10);
128
+ tree.create('wrangler.jsonc', (0, wrangler_config_1.createWranglerConfig)((0, wrangler_config_1.workerName)(setup.projectName), setup.output, date));
129
+ context.logger.info(`Created wrangler.jsonc for Worker "${(0, wrangler_config_1.workerName)(setup.projectName)}".`);
130
+ return;
131
+ }
132
+ if (path.endsWith('.toml')) {
133
+ context.logger.warn(`wrangler.toml is not edited automatically. Set main = "${setup.output.main}" and [assets] directory = "${setup.output.assets}", binding = "ASSETS".`);
134
+ return;
135
+ }
136
+ const { text, changes } = (0, wrangler_config_1.repairWranglerConfig)(tree.readText(path), setup.output);
137
+ if (changes.length) {
138
+ tree.overwrite(path, text);
139
+ context.logger.info(`Updated ${path}:\n - ${changes.join('\n - ')}`);
140
+ }
141
+ };
142
+ }
143
+ function routeHosts(tree) {
144
+ const path = wranglerConfigPath(tree);
145
+ if (!path || path.endsWith('.toml')) {
146
+ return [];
147
+ }
148
+ return (0, wrangler_config_1.hostsFromConfig)((0, wrangler_config_1.parseWranglerConfig)(tree.readText(path)));
149
+ }
150
+ function updateServerEntry(setup) {
151
+ return (tree, context) => {
152
+ const current = readText(tree, setup.serverEntry);
153
+ const info = (0, server_entry_1.classifyServerEntry)(current);
154
+ const customHosts = [...new Set([...info.keptHosts, ...routeHosts(tree)])].filter((host) => !['localhost', '127.0.0.1', '*.workers.dev'].includes(host));
155
+ const configPath = wranglerConfigPath(tree);
156
+ const hostsInConfig = configPath !== null && !configPath.endsWith('.toml');
157
+ if (hostsInConfig && customHosts.length) {
158
+ const { text, added } = (0, wrangler_config_1.addAllowedHostsVar)(tree.readText(configPath), customHosts);
159
+ if (added.length) {
160
+ tree.overwrite(configPath, text);
161
+ context.logger.info(`Added ${added.join(', ')} to ALLOWED_HOSTS in ${configPath}.`);
162
+ }
163
+ }
164
+ const hosts = hostsInConfig ? [] : customHosts;
165
+ switch (info.kind) {
166
+ case 'ours':
167
+ context.logger.info(`${setup.serverEntry} already uses ngx-cloudflare. Left unchanged.`);
168
+ return;
169
+ case 'custom':
170
+ context.logger.warn(`${setup.serverEntry} has custom code, so ngx-cloudflare did not change it. Move your routes into the api option and use:\n\n${server_entry_1.SERVER_ENTRY_INSTRUCTIONS}\n`);
171
+ return;
172
+ default:
173
+ if (current === null) {
174
+ tree.create(setup.serverEntry, (0, server_entry_1.renderServerEntry)(hosts));
175
+ }
176
+ else {
177
+ tree.overwrite(setup.serverEntry, (0, server_entry_1.renderServerEntry)(hosts));
178
+ }
179
+ context.logger.info(`${current === null ? 'Created' : 'Replaced'} ${setup.serverEntry} (${info.kind === 'angular-node' ? 'stock Node server' : info.kind === 'cloudflare-template' ? "Cloudflare's template" : 'new'}).`);
180
+ if (info.kind === 'angular-node') {
181
+ editJson(tree, 'package.json', (text, set) => {
182
+ const pkg = (0, jsonc_parser_1.parse)(text);
183
+ if (pkg.dependencies?.['express'])
184
+ set(['dependencies', 'express'], undefined);
185
+ if (pkg.devDependencies?.['@types/express'])
186
+ set(['devDependencies', '@types/express'], undefined);
187
+ });
188
+ }
189
+ }
190
+ };
191
+ }
192
+ function findServerConfig(tree, setup) {
193
+ const conventional = node_path_1.posix.join(setup.sourceRoot, 'app/app.config.server.ts');
194
+ if (tree.exists(conventional)) {
195
+ return conventional;
196
+ }
197
+ let found = null;
198
+ tree.getDir(setup.sourceRoot).visit((path) => {
199
+ if (!found &&
200
+ path.endsWith('.ts') &&
201
+ !path.endsWith('.spec.ts') &&
202
+ tree.readText(path).includes('provideServerRendering(')) {
203
+ found = path.replace(/^\//, '');
204
+ }
205
+ });
206
+ return found;
207
+ }
208
+ const MANUAL_PROVIDER = 'Add provideCloudflare() from ngx-cloudflare to the providers of your server application config.';
209
+ function updateServerConfig(setup) {
210
+ return (tree, context) => {
211
+ const path = findServerConfig(tree, setup);
212
+ if (!path) {
213
+ context.logger.warn(`Could not find the server application config. ${MANUAL_PROVIDER}`);
214
+ return;
215
+ }
216
+ const text = tree.readText(path);
217
+ if (text.includes('provideCloudflare(')) {
218
+ return;
219
+ }
220
+ const updated = (0, add_provider_1.addProvider)(text, 'provideCloudflare()', [
221
+ "import { provideCloudflare } from 'ngx-cloudflare';",
222
+ ]);
223
+ if (updated === null) {
224
+ context.logger.warn(`Could not edit ${path}. ${MANUAL_PROVIDER}`);
225
+ return;
226
+ }
227
+ tree.overwrite(path, updated);
228
+ context.logger.info(`Added provideCloudflare() to ${path}.`);
229
+ };
230
+ }
231
+ const SERVER_ROUTING_CALLS = ['withRoutes(', 'provideServerRouting(', 'provideServerRoutesConfig('];
232
+ function addServerRouting(setup) {
233
+ return (tree, context) => {
234
+ const path = findServerConfig(tree, setup);
235
+ if (!path) {
236
+ return;
237
+ }
238
+ const text = tree.readText(path);
239
+ if (SERVER_ROUTING_CALLS.some((call) => text.includes(call))) {
240
+ return;
241
+ }
242
+ const routesPath = node_path_1.posix.join(node_path_1.posix.dirname(path), 'app.routes.server.ts');
243
+ const routesImport = `./${node_path_1.posix.basename(routesPath, '.ts')}`;
244
+ const manual = `${path} has no server routes, which AngularAppEngine needs. Add a ${routesPath} with RenderMode.Server routes and register them, see https://angular.dev/guide/ssr#configure-server-side-rendering`;
245
+ if (setup.angularMajor !== 19) {
246
+ context.logger.warn(manual);
247
+ return;
248
+ }
249
+ const ssr = packageVersion(tree, '@angular/ssr');
250
+ const provider = ssr.major > 19 || ssr.minor >= 2 ? 'provideServerRouting' : 'provideServerRoutesConfig';
251
+ const updated = (0, add_provider_1.addProvider)(text, `${provider}(serverRoutes)`, [
252
+ `import { ${provider} } from '@angular/ssr';`,
253
+ `import { serverRoutes } from '${routesImport}';`,
254
+ ]);
255
+ if (updated === null) {
256
+ context.logger.warn(manual);
257
+ return;
258
+ }
259
+ if (!tree.exists(routesPath)) {
260
+ tree.create(routesPath, `import { RenderMode, ServerRoute } from '@angular/ssr';\n\nexport const serverRoutes: ServerRoute[] = [\n {\n path: '**',\n renderMode: RenderMode.Server,\n },\n];\n`);
261
+ }
262
+ tree.overwrite(path, updated);
263
+ context.logger.info(`Added server routes (${provider}) to ${path}; every route is rendered per request.`);
264
+ };
265
+ }
266
+ const STOCK_SERVER_ROUTES = /^import\{RenderMode,ServerRoute\}from'@angular\/ssr';exportconstserverRoutes:ServerRoute\[\]=\[\{path:'\*\*',renderMode:RenderMode\.Prerender,?\},?\];?$/;
267
+ function updateServerRoutes(setup) {
268
+ return (tree, context) => {
269
+ const path = node_path_1.posix.join(setup.sourceRoot, 'app/app.routes.server.ts');
270
+ const text = readText(tree, path);
271
+ if (text === null || !STOCK_SERVER_ROUTES.test(text.replace(/\s+/g, '').replace(/"/g, "'"))) {
272
+ return;
273
+ }
274
+ tree.overwrite(path, text.replace('RenderMode.Prerender', 'RenderMode.Server'));
275
+ context.logger.info(`Switched ${path} from RenderMode.Prerender to RenderMode.Server, so pages are rendered per request with bindings.`);
276
+ };
277
+ }
278
+ function updatePackageScripts(setup) {
279
+ return (tree) => {
280
+ editJson(tree, 'package.json', (text, set) => {
281
+ const pkg = (0, jsonc_parser_1.parse)(text);
282
+ const scripts = pkg.scripts ?? {};
283
+ for (const [name, command] of Object.entries(scripts)) {
284
+ if (name.startsWith('serve:ssr') && /server\.mjs/.test(command)) {
285
+ set(['scripts', name], undefined);
286
+ }
287
+ }
288
+ const wanted = {
289
+ preview: 'ng build && wrangler dev',
290
+ deploy: 'ng build && wrangler deploy',
291
+ 'cf-typegen': 'wrangler types',
292
+ };
293
+ for (const [name, command] of Object.entries(wanted)) {
294
+ if (!scripts[name]) {
295
+ set(['scripts', name], command);
296
+ }
297
+ }
298
+ });
299
+ };
300
+ }
301
+ function updateTsConfigs(setup) {
302
+ return (tree) => {
303
+ for (const tsConfig of setup.tsConfigs) {
304
+ if (tree.exists(tsConfig)) {
305
+ addTypesFile(tree, tsConfig);
306
+ }
307
+ }
308
+ };
309
+ }
310
+ function addTypesFile(tree, tsConfig) {
311
+ {
312
+ const typesFile = node_path_1.posix.relative(node_path_1.posix.dirname(tsConfig), 'worker-configuration.d.ts') ||
313
+ 'worker-configuration.d.ts';
314
+ editJson(tree, tsConfig, (text, set) => {
315
+ const config = (0, jsonc_parser_1.parse)(text);
316
+ if (config.include?.includes(typesFile) || config.files?.includes(typesFile)) {
317
+ return;
318
+ }
319
+ if (config.include) {
320
+ set(['include', config.include.length], typesFile);
321
+ }
322
+ else {
323
+ set(['files', config.files?.length ?? 0], typesFile);
324
+ }
325
+ });
326
+ }
327
+ }
328
+ function updateGitignore() {
329
+ return (tree) => {
330
+ const current = readText(tree, '.gitignore');
331
+ if (current === null) {
332
+ return;
333
+ }
334
+ const lines = current.split(/\r?\n/);
335
+ const missing = ['.wrangler', '.dev.vars*'].filter((entry) => !lines.some((line) => line.trim() === entry || line.trim() === `/${entry}`));
336
+ if (missing.length) {
337
+ tree.overwrite('.gitignore', `${current.replace(/\s*$/, '')}\n\n# Cloudflare Workers\n${missing.join('\n')}\n`);
338
+ }
339
+ };
340
+ }
341
+ function nextSteps(setup) {
342
+ return (_tree, context) => {
343
+ context.logger.info([
344
+ '',
345
+ 'ngx-cloudflare is set up. Next:',
346
+ ' npm run cf-typegen generate the Env type from wrangler.jsonc',
347
+ " ng serve bindings come from wrangler's local platform proxy",
348
+ ' npm run preview build, then run it in the local Workers runtime',
349
+ ' npm run deploy build, then wrangler deploy',
350
+ '',
351
+ `Custom domains go in ALLOWED_HOSTS in the vars of wrangler.jsonc. Routes rendered at build time (RenderMode.Prerender) have no bindings.`,
352
+ ].join('\n'));
353
+ };
354
+ }
355
+ function ngAdd(options = {}) {
356
+ return async (tree, _context) => {
357
+ const setup = await createSetupPlan(tree, options);
358
+ return (0, schematics_1.chain)([
359
+ updateAngularJson(setup),
360
+ updateWranglerConfig(setup, options),
361
+ updateServerEntry(setup),
362
+ addServerRouting(setup),
363
+ updateServerConfig(setup),
364
+ updateServerRoutes(setup),
365
+ updatePackageScripts(setup),
366
+ updateTsConfigs(setup),
367
+ updateGitignore(),
368
+ (0, utility_1.addDependency)('wrangler', '^4.0.0', {
369
+ type: utility_1.DependencyType.Dev,
370
+ existing: utility_1.ExistingBehavior.Skip,
371
+ install: options.skipInstall ? utility_1.InstallBehavior.None : utility_1.InstallBehavior.Auto,
372
+ }),
373
+ nextSteps(setup),
374
+ ]);
375
+ };
376
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema",
3
+ "$id": "NgxCloudflareNgAdd",
4
+ "title": "ngx-cloudflare ng-add",
5
+ "type": "object",
6
+ "properties": {
7
+ "project": {
8
+ "type": "string",
9
+ "description": "The application to set up. Defaults to the first application with server-side rendering.",
10
+ "$default": { "$source": "projectName" }
11
+ },
12
+ "skipInstall": {
13
+ "type": "boolean",
14
+ "description": "Do not run the package manager after adding wrangler.",
15
+ "default": false
16
+ },
17
+ "compatibilityDate": {
18
+ "type": "string",
19
+ "description": "compatibility_date for a newly created wrangler.jsonc. Defaults to today."
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SERVER_ENTRY_INSTRUCTIONS = void 0;
4
+ exports.classifyServerEntry = classifyServerEntry;
5
+ exports.renderServerEntry = renderServerEntry;
6
+ function stripComments(source) {
7
+ return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:'"`])\/\/.*$/gm, '$1');
8
+ }
9
+ function compact(source) {
10
+ return stripComments(source).replace(/\s+/g, '').replace(/"/g, "'");
11
+ }
12
+ const CLOUDFLARE_TEMPLATE = /^import\{AngularAppEngine,createRequestHandler\}from'@angular\/ssr';constangularApp=newAngularAppEngine\(\{allowedHosts:\[([^\]]*)\],?\}\);exportconstreqHandler=createRequestHandler\(async\(req\)=>\{constres=awaitangularApp\.handle\(req\);returnres\?\?newResponse\('Pagenotfound\.',\{status:404\}\);?\}\);exportdefault\{fetch:reqHandler,?\};?$/;
13
+ const NODE_IMPORTS = new Set([
14
+ '@angular/common',
15
+ './main.server',
16
+ '@angular/ssr/node',
17
+ 'express',
18
+ 'node:path',
19
+ 'path',
20
+ 'node:url',
21
+ 'url',
22
+ ]);
23
+ function classifyServerEntry(source) {
24
+ if (source === null) {
25
+ return { kind: 'missing', keptHosts: [] };
26
+ }
27
+ if (source.includes('ngx-cloudflare/server')) {
28
+ return { kind: 'ours', keptHosts: [] };
29
+ }
30
+ const code = compact(source);
31
+ const template = CLOUDFLARE_TEMPLATE.exec(code);
32
+ if (template) {
33
+ const keptHosts = template[1]
34
+ .split(',')
35
+ .map((host) => host.replace(/['`]/g, ''))
36
+ .filter(Boolean);
37
+ return { kind: 'cloudflare-template', keptHosts };
38
+ }
39
+ const imports = [...stripComments(source).matchAll(/from\s+['"]([^'"]+)['"]/g)].map((m) => m[1]);
40
+ const routes = code.match(/\bapp\.(get|post|put|delete|patch|all|use|route|options|head)\(/g) ?? [];
41
+ if (code.includes('newAngularNodeAppEngine()') &&
42
+ code.includes('exportconstreqHandler=createNodeRequestHandler(app)') &&
43
+ imports.every((name) => NODE_IMPORTS.has(name)) &&
44
+ routes.length === 2) {
45
+ return { kind: 'angular-node', keptHosts: [] };
46
+ }
47
+ if (code.includes('newCommonEngine()') &&
48
+ code.includes('exportdefaultapp') &&
49
+ imports.every((name) => NODE_IMPORTS.has(name)) &&
50
+ routes.length === 2) {
51
+ return { kind: 'angular-node', keptHosts: [] };
52
+ }
53
+ return { kind: 'custom', keptHosts: [] };
54
+ }
55
+ function renderServerEntry(hosts) {
56
+ const hostOption = hosts.length
57
+ ? `\n allowedHosts: [${hosts.map((host) => `'${host}'`).join(', ')}],\n`
58
+ : '';
59
+ const options = hostOption ? `{${hostOption}}` : '';
60
+ return `import { createCloudflareRequestHandler } from 'ngx-cloudflare/server';
61
+
62
+ export const reqHandler = createCloudflareRequestHandler(${options});
63
+
64
+ export default { fetch: reqHandler };
65
+ `;
66
+ }
67
+ exports.SERVER_ENTRY_INSTRUCTIONS = `import { createCloudflareRequestHandler } from 'ngx-cloudflare/server';
68
+
69
+ export const reqHandler = createCloudflareRequestHandler({
70
+ allowedHosts: [/* your custom domains */],
71
+ api: (request, env, ctx) => /* your API handler, or a Hono app */ null,
72
+ });
73
+
74
+ export default { fetch: reqHandler };`;