diff --git a/.changeset/spotty-bees-arrive.md b/.changeset/spotty-bees-arrive.md new file mode 100644 index 000000000000..67fbdf80067f --- /dev/null +++ b/.changeset/spotty-bees-arrive.md @@ -0,0 +1,5 @@ +--- +"create-cloudflare": minor +--- + +Update Angular template to use version 17 diff --git a/packages/create-cloudflare/src/frameworks/angular/index.ts b/packages/create-cloudflare/src/frameworks/angular/index.ts index 9163afa220f2..839e53b7f5b7 100644 --- a/packages/create-cloudflare/src/frameworks/angular/index.ts +++ b/packages/create-cloudflare/src/frameworks/angular/index.ts @@ -1,40 +1,29 @@ -import { cp, rm } from "node:fs/promises"; +import { cp } from "node:fs/promises"; import { resolve } from "node:path"; import { logRaw } from "@cloudflare/cli"; import { brandColor, dim } from "@cloudflare/cli/colors"; import { spinner } from "@cloudflare/cli/interactive"; -import { - installPackages, - runCommand, - runFrameworkGenerator, -} from "helpers/command"; +import { installPackages, runFrameworkGenerator } from "helpers/command"; import { compatDateFlag, readFile, readJSON, writeFile } from "helpers/files"; import { detectPackageManager } from "helpers/packages"; import { getFrameworkCli } from "../index"; import type { FrameworkConfig, PagesGeneratorContext } from "types"; -const { dlx, npx, npm } = detectPackageManager(); +const { dlx, npm } = detectPackageManager(); const generate = async (ctx: PagesGeneratorContext) => { const cli = getFrameworkCli(ctx); - await runFrameworkGenerator( - ctx, - `${dlx} ${cli} new ${ctx.project.name} --standalone` - ); + await runFrameworkGenerator(ctx, `${dlx} ${cli} ${ctx.project.name} --ssr`); logRaw(""); }; const configure = async (ctx: PagesGeneratorContext) => { process.chdir(ctx.project.path); - await runCommand(`${npx} ng analytics disable`, { - silent: true, - }); - await addSSRAdapter(); - await installCFWorker(ctx); - await updateAppCode(); updateAngularJson(ctx); + await updateAppCode(); + await installCFWorker(ctx); }; const config: FrameworkConfig = { @@ -42,15 +31,14 @@ const config: FrameworkConfig = { configure, displayName: "Angular", getPackageScripts: async () => ({ - process: - "node ./tools/copy-worker-files.mjs && node ./tools/copy-client-files.mjs && node ./tools/bundle.mjs", - "pages:build": `${npm} run build:ssr && ${npm} run process`, start: `${npm} run pages:build && wrangler pages dev dist/cloudflare ${await compatDateFlag()} --experimental-local`, + process: "node ./tools/copy-files.mjs && node ./tools/alter-polyfills.mjs", + "pages:build": `ng build && ${npm} run process`, deploy: `${npm} run pages:build && wrangler pages deploy dist/cloudflare`, }), deployCommand: "deploy", devCommand: "start", - testFlags: ["--routing", "--style", "sass"], + testFlags: ["--ssr", "--style", "sass"], }; export default config; @@ -66,15 +54,7 @@ async function installCFWorker(ctx: PagesGeneratorContext) { s.stop(`${brandColor("copied")} ${dim("adapter code")}`); await installPackages( - [ - "@cloudflare/workers-types", - "@esbuild-plugins/node-globals-polyfill", - "@esbuild-plugins/node-modules-polyfill", - "@miniflare/tre@next", - "esbuild", - "fast-glob", - "wrangler@beta", - ], + ["@cloudflare/workers-types", "@miniflare/tre@next", "wrangler@beta"], { dev: true, startText: "Installing adapter dependencies", @@ -82,51 +62,43 @@ async function installCFWorker(ctx: PagesGeneratorContext) { } ); } - -async function addSSRAdapter() { - const cmd = `${npx} ng add @nguniversal/express-engine`; - - await runCommand(`${cmd} --skip-confirmation`, { - silent: true, - startText: "Installing Angular SSR", - doneText: `${brandColor("installed")} ${dim(`via \`${cmd}\``)}`, - }); -} - async function updateAppCode() { const s = spinner(); s.start(`Updating application code`); // Update an app config file to: - // - add the `provideClientHydration()` provider to enable hydration // - add the `provideHttpClient(withFetch())` call to enable `fetch` usage in `HttpClient` const appConfigPath = "src/app/app.config.ts"; const appConfig = readFile(resolve(appConfigPath)); const newAppConfig = - "import { provideClientHydration } from '@angular/platform-browser';\n" + "import { provideHttpClient, withFetch } from '@angular/common/http';\n" + appConfig.replace( "providers: [", - "providers: [provideHttpClient(withFetch()), provideClientHydration(), " + "providers: [provideHttpClient(withFetch()), " ); writeFile(resolve(appConfigPath), newAppConfig); + s.stop(`${brandColor(`updated`)} ${dim(appConfigPath)}`); - // Remove the unwanted node.js server entry-point - await rm(resolve("server.ts")); + // Remove unwanted dependencies + s.start(`Updating package.json`); + const packageJsonPath = resolve("package.json"); + const packageManifest = readJSON(packageJsonPath); - s.stop(`${brandColor(`updated`)} ${dim(appConfigPath)}`); + delete packageManifest["dependencies"]["@angular/ssr"]; + delete packageManifest["dependencies"]["express"]; + + writeFile(packageJsonPath, JSON.stringify(packageManifest, null, 2)); + s.stop(`${brandColor(`updated`)} ${dim(`\`package.json\``)}`); } function updateAngularJson(ctx: PagesGeneratorContext) { const s = spinner(); s.start(`Updating angular.json config`); const angularJson = readJSON(resolve("angular.json")); + // Update builder const architectSection = angularJson.projects[ctx.project.name].architect; - architectSection.build.options.outputPath = "dist/browser"; + architectSection.build.options.outputPath = "dist"; architectSection.build.options.assets.push("src/_routes.json"); - architectSection.server.options.outputPath = "dist/server"; - architectSection.server.options.main = "src/main.server.ts"; - delete architectSection["serve-ssr"]; writeFile(resolve("angular.json"), JSON.stringify(angularJson, null, 2)); s.stop(`${brandColor(`updated`)} ${dim(`\`angular.json\``)}`); diff --git a/packages/create-cloudflare/src/frameworks/angular/templates/server.ts b/packages/create-cloudflare/src/frameworks/angular/templates/server.ts new file mode 100644 index 000000000000..a6da49ad852a --- /dev/null +++ b/packages/create-cloudflare/src/frameworks/angular/templates/server.ts @@ -0,0 +1,34 @@ +import { renderApplication } from '@angular/platform-server'; +import bootstrap from './src/main.server'; + +interface Env { + ASSETS: { fetch: typeof fetch }; +} + +// We attach the Cloudflare `fetch()` handler to the global scope +// so that we can export it when we process the Angular output. +// See tools/bundle.mjs +async function workerFetchHandler(request: Request, env: Env) { + const url = new URL(request.url); + console.log('render SSR', url.href); + + // Get the root `index.html` content. + const indexUrl = new URL('/', url); + const indexResponse = await env.ASSETS.fetch(new Request(indexUrl)); + const document = await indexResponse.text(); + + const content = await renderApplication(bootstrap, { + document, + url: url.pathname, + }); + + // console.log("rendered SSR", content); + return new Response(content, indexResponse); +} + +export default { + fetch: (request: Request, env: Env) => + (globalThis as any)['__zone_symbol__Promise'].resolve( + workerFetchHandler(request, env) + ), +}; diff --git a/packages/create-cloudflare/src/frameworks/angular/templates/src/main.server.ts b/packages/create-cloudflare/src/frameworks/angular/templates/src/main.server.ts deleted file mode 100644 index 9bb49e616b22..000000000000 --- a/packages/create-cloudflare/src/frameworks/angular/templates/src/main.server.ts +++ /dev/null @@ -1,35 +0,0 @@ -import "zone.js/dist/zone-node"; -import "@angular/platform-server/init"; - -import { bootstrapApplication } from "@angular/platform-browser"; -import { renderApplication } from "@angular/platform-server"; - -import { AppComponent } from "./app/app.component"; -import { config } from "./app/app.config.server"; - -interface Env { - ASSETS: { fetch: typeof fetch }; -} - -// We attach the Cloudflare `fetch()` handler to the global scope -// so that we can export it when we process the Angular output. -// See tools/bundle.mjs -(globalThis as any).__workerFetchHandler = async function fetch( - request: Request, - env: Env -) { - const url = new URL(request.url); - console.log("render SSR", url.href); - - // Get the root `index.html` content. - const indexUrl = new URL("/", url); - const indexResponse = await env.ASSETS.fetch(new Request(indexUrl)); - const document = await indexResponse.text(); - - const content = await renderApplication( - () => bootstrapApplication(AppComponent, config), - { document, url: url.pathname } - ); - // console.log("rendered SSR", content); - return new Response(content, indexResponse); -}; diff --git a/packages/create-cloudflare/src/frameworks/angular/templates/tools/alter-polyfills.mjs b/packages/create-cloudflare/src/frameworks/angular/templates/tools/alter-polyfills.mjs new file mode 100644 index 000000000000..c48247f34fc5 --- /dev/null +++ b/packages/create-cloudflare/src/frameworks/angular/templates/tools/alter-polyfills.mjs @@ -0,0 +1,32 @@ +import { EOL } from "node:os"; +import fs from "node:fs"; +import { join } from "node:path"; +import { worker } from "./paths.mjs"; + +/** + * Split by lines and comment the banner + * ``` + * import { createRequire } from 'node:module'; + * globalThis['require'] ??= createRequire(import.meta.url); + * ``` + */ +const serverPolyfillsFile = join(worker, "polyfills.server.mjs"); +const serverPolyfillsData = fs + .readFileSync(serverPolyfillsFile, "utf8") + .split(EOL); + +for (let index = 0; index < 2; index++) { + if (serverPolyfillsData[index].includes("createRequire")) { + serverPolyfillsData[index] = "// " + serverPolyfillsData[index]; + } +} + +// Add needed polyfills +serverPolyfillsData.unshift( + `globalThis['process'] = {};`, + `globalThis['global'] = globalThis;`, + // Needed as performance.mark is not a function in worker. + `performance.mark = () => {};`, +); + +fs.writeFileSync(serverPolyfillsFile, serverPolyfillsData.join(EOL)); diff --git a/packages/create-cloudflare/src/frameworks/angular/templates/tools/bundle.mjs b/packages/create-cloudflare/src/frameworks/angular/templates/tools/bundle.mjs deleted file mode 100644 index fcc2761acc42..000000000000 --- a/packages/create-cloudflare/src/frameworks/angular/templates/tools/bundle.mjs +++ /dev/null @@ -1,77 +0,0 @@ -import fs from "node:fs/promises"; -import path from "node:path"; -import { worker as workerPath } from "./paths.mjs"; -import * as esbuild from "esbuild"; -import { NodeGlobalsPolyfillPlugin } from "@esbuild-plugins/node-globals-polyfill"; -import { NodeModulesPolyfillPlugin } from "@esbuild-plugins/node-modules-polyfill"; -import fg from "fast-glob"; - -// Process each of the JS files in the `_worker.js` directory -for (const entry of await fg("**/*.js", { cwd: workerPath, onlyFiles: true })) { - if (entry === "index.js") { - // This is the main bundle and gets special treatment - await bundleMain(); - } else { - await bundleLazyModule(entry); - } -} - -// Use esbuild to process the main entry-point. -// - shim Node.js APIs -// - convert `global` to `globalThis` -// - convert dynamic `require()` calls to `await import()` calls -// - ensure that the Cloudflare `fetch()` handler is exported -async function bundleMain() { - const result = await esbuild.build({ - entryPoints: ["index.js"], - bundle: true, - format: "iife", - write: false, - absWorkingDir: workerPath, - define: { - global: "globalThis", - }, - plugins: [ - NodeGlobalsPolyfillPlugin({ buffer: true }), - NodeModulesPolyfillPlugin(), - ], - }); - - // Store the original promise (before Angular/Zone.js replaces it) on the global scope. - let main = "globalThis.OGPromise = Promise;\n" + result.outputFiles[0].text; - - // Patch any dynamic imports (converting `require()` calls to `import()` calls). - main = main.replace( - 'installChunk(__require("./" + __webpack_require__.u(chunkId))', - 'promises.push(import("./" + __webpack_require__.u(chunkId)).then((mod) => installChunk(mod.default))' - ); - // Export the fetch handler (grabbing it from the global). - // Also Cloudflare expects `fetch()` to return an original Promise (not a ZoneAwarePromise). - main += - "\nexport default { fetch: (request, env) => globalThis.OGPromise.resolve(globalThis.__workerFetchHandler(request, env)) };"; - - await fs.writeFile(path.resolve(workerPath, "index.js"), main); -} - -// Use esbuild to process the lazy load modules -// In particular we need to convert the CommonJS export syntax to ESM. -async function bundleLazyModule(filePath) { - const result = await esbuild.build({ - entryPoints: [filePath], - bundle: true, - format: "cjs", - write: false, - absWorkingDir: workerPath, - define: { - global: "globalThis", - }, - plugins: [NodeModulesPolyfillPlugin()], - }); - - let content = result.outputFiles[0].text; - - // Export the fetch handler (grabbing it from the global). - content = "const exports = {};\n" + content + "\nexport default exports"; - - await fs.writeFile(path.resolve(workerPath, filePath), content); -} diff --git a/packages/create-cloudflare/src/frameworks/angular/templates/tools/copy-client-files.mjs b/packages/create-cloudflare/src/frameworks/angular/templates/tools/copy-client-files.mjs deleted file mode 100644 index ff0f8822708a..000000000000 --- a/packages/create-cloudflare/src/frameworks/angular/templates/tools/copy-client-files.mjs +++ /dev/null @@ -1,4 +0,0 @@ -// Copy the client-side files over so that they can be uploaded by the pages publish command. -import fs from "node:fs"; -import { client, cloudflare } from "./paths.mjs"; -fs.cpSync(client, cloudflare, { recursive: true }); diff --git a/packages/create-cloudflare/src/frameworks/angular/templates/tools/copy-files.mjs b/packages/create-cloudflare/src/frameworks/angular/templates/tools/copy-files.mjs new file mode 100644 index 000000000000..d6b0301c5310 --- /dev/null +++ b/packages/create-cloudflare/src/frameworks/angular/templates/tools/copy-files.mjs @@ -0,0 +1,9 @@ +// Copy the files over so that they can be uploaded by the pages publish command. +import fs from "node:fs"; +import { join } from "node:path"; +import { client, cloudflare, worker, ssr } from "./paths.mjs"; + +fs.cpSync(client, cloudflare, { recursive: true }); +fs.cpSync(ssr, worker, { recursive: true }); + +fs.renameSync(join(worker, "server.mjs"), join(worker, "index.js")); diff --git a/packages/create-cloudflare/src/frameworks/angular/templates/tools/copy-worker-files.mjs b/packages/create-cloudflare/src/frameworks/angular/templates/tools/copy-worker-files.mjs deleted file mode 100644 index ee0319b9428a..000000000000 --- a/packages/create-cloudflare/src/frameworks/angular/templates/tools/copy-worker-files.mjs +++ /dev/null @@ -1,10 +0,0 @@ -// Copy the lazy loaded modules into the dist folder so that they can be -// uploaded along with the main Worker module. -import fs from "node:fs"; -import path from "node:path"; -import { ssr, worker } from "./paths.mjs"; -fs.cpSync(ssr, worker, { recursive: true }); -fs.renameSync( - path.resolve(worker, "main.js"), - path.resolve(worker, "index.js") -); diff --git a/packages/create-cloudflare/src/frameworks/angular/templates/tsconfig.server.json b/packages/create-cloudflare/src/frameworks/angular/templates/tsconfig.server.json deleted file mode 100644 index a5e2be34df6c..000000000000 --- a/packages/create-cloudflare/src/frameworks/angular/templates/tsconfig.server.json +++ /dev/null @@ -1,5 +0,0 @@ -/* To learn more about this file see: https://angular.io/config/tsconfig. */ -{ - "extends": "./tsconfig.app.json", - "files": ["./src/main.server.ts"] -} diff --git a/packages/create-cloudflare/src/frameworks/package.json b/packages/create-cloudflare/src/frameworks/package.json index 4168dfc2b574..6f5d0fc1bd79 100644 --- a/packages/create-cloudflare/src/frameworks/package.json +++ b/packages/create-cloudflare/src/frameworks/package.json @@ -6,8 +6,8 @@ "additionally it also contains a map that maps frameworks to their respective clis" ], "dependencies": { - "@angular/cli": "16.2.2", "create-astro": "4.4.1", + "@angular/create": "17.0.0-rc.1", "create-docusaurus": "2.4.3", "create-hono": "0.3.2", "create-next-app": "13.4.19", @@ -21,7 +21,7 @@ "nuxi": "3.9.0" }, "frameworkCliMap": { - "angular": "@angular/cli", + "angular": "@angular/create", "astro": "create-astro", "docusaurus": "create-docusaurus", "gatsby": "gatsby",