Nacho Vazquez 🐛 💻 📖 💡 🤔 🧑🏫 🚧 📆 👀 ⚠️ 🔧 📓 |
+
= 16.0.0 <= 17.0.0",
"tslib": "^2.3.0",
"@svgr/webpack": "^8.0.1",
"copy-webpack-plugin": "^11.0.0",
@@ -52,7 +53,6 @@
"fs-extra": "^11.1.0",
"semver": "^7.5.4",
"url-loader": "^4.1.1",
- "@nx/devkit": ">= 16.0.0 <= 17.0.0",
"@nx/node": ">= 16.0.0 <= 17.0.0",
"wrangler": ">= 3.8.0 <= 4.0.0"
}
diff --git a/packages/plugins/nx-cloudflare/src/generators/application/application.ts b/packages/plugins/nx-cloudflare/src/generators/application/application.ts
index ab250091b..8b97ea22f 100644
--- a/packages/plugins/nx-cloudflare/src/generators/application/application.ts
+++ b/packages/plugins/nx-cloudflare/src/generators/application/application.ts
@@ -19,9 +19,10 @@ import initGenerator from '../init/init';
import { vitestImports } from './utils/vitest-imports';
import { getAccountId } from './utils/get-account-id';
import { vitestScript } from './utils/vitest-script';
+import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
export async function applicationGenerator(tree: Tree, schema: Schema) {
- const options = normalizeOptions(tree, schema);
+ const options = await normalizeOptions(tree, schema);
// Set up the needed packages.
const initTask = await initGenerator(tree, {
@@ -169,21 +170,24 @@ function removeTestFiles(tree: Tree, options: NormalizedSchema) {
}
// Transform the options to the normalized schema. Loads defaults options.
-function normalizeOptions(host: Tree, options: Schema): NormalizedSchema {
- const { layoutDirectory, projectDirectory } = extractLayoutDirectory(
- options.directory
- );
- const appsDir = layoutDirectory ?? getWorkspaceLayout(host).appsDir;
-
- const appDirectory = projectDirectory
- ? `${names(projectDirectory).fileName}/${names(options.name).fileName}`
- : names(options.name).fileName;
-
- const appProjectName = appDirectory.replace(new RegExp('/', 'g'), '-');
-
- const appProjectRoot = options.rootProject
- ? '.'
- : joinPathFragments(appsDir, appDirectory);
+async function normalizeOptions(
+ host: Tree,
+ options: Schema
+): Promise {
+ const {
+ projectName: appProjectName,
+ projectRoot: appProjectRoot,
+ projectNameAndRootFormat,
+ } = await determineProjectNameAndRootOptions(host, {
+ name: options.name,
+ projectType: 'application',
+ directory: options.directory,
+ projectNameAndRootFormat: options.projectNameAndRootFormat,
+ rootProject: options.rootProject,
+ callingGenerator: '@nx/node:application',
+ });
+ options.rootProject = appProjectRoot === '.';
+ options.projectNameAndRootFormat = projectNameAndRootFormat;
return {
...options,
diff --git a/packages/plugins/nx-cloudflare/src/generators/application/schema.d.ts b/packages/plugins/nx-cloudflare/src/generators/application/schema.d.ts
index c00930be2..44c8c7009 100644
--- a/packages/plugins/nx-cloudflare/src/generators/application/schema.d.ts
+++ b/packages/plugins/nx-cloudflare/src/generators/application/schema.d.ts
@@ -1,9 +1,12 @@
+import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
+
export interface Schema {
name: string;
template?: 'fetch-handler' | 'scheduled-handler' | 'hono' | 'none';
js?: boolean;
unitTestRunner?: 'vitest' | 'none';
directory?: string;
+ projectNameAndRootFormat?: ProjectNameAndRootFormat;
rootProject?: boolean;
tags?: string;
frontendProject?: string;
diff --git a/packages/plugins/nx-cloudflare/src/generators/application/schema.json b/packages/plugins/nx-cloudflare/src/generators/application/schema.json
index 24d8336fc..638428a5b 100644
--- a/packages/plugins/nx-cloudflare/src/generators/application/schema.json
+++ b/packages/plugins/nx-cloudflare/src/generators/application/schema.json
@@ -1,7 +1,8 @@
{
"$schema": "http://json-schema.org/schema",
- "$id": "Application",
- "title": "",
+ "$id": "NxCloudflareWorkerApplication",
+ "title": "Create a Cloudflare Worker Application",
+ "description": "Create a Cloudflare Worker Application",
"type": "object",
"properties": {
"name": {
@@ -17,6 +18,11 @@
"type": "boolean",
"description": "Use JavaScript instead of TypeScript"
},
+ "projectNameAndRootFormat": {
+ "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
+ "type": "string",
+ "enum": ["as-provided", "derived"]
+ },
"tags": {
"type": "string",
"description": "Add tags to the application (used for linting)."
diff --git a/packages/plugins/nx-cloudflare/src/generators/library/library.spec.ts b/packages/plugins/nx-cloudflare/src/generators/library/library.spec.ts
new file mode 100644
index 000000000..a8efded0c
--- /dev/null
+++ b/packages/plugins/nx-cloudflare/src/generators/library/library.spec.ts
@@ -0,0 +1,917 @@
+import {
+ getProjects,
+ readJson,
+ readProjectConfiguration,
+ Tree,
+ updateJson,
+} from '@nx/devkit';
+import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
+import { NxCloudflareLibraryGeneratorSchema } from './schema';
+import libraryGenerator from './library';
+
+describe('lib', () => {
+ let tree: Tree;
+ const defaultOptions: Omit = {
+ unitTestRunner: 'vitest',
+ skipFormat: false,
+ linter: 'eslint',
+ js: false,
+ strict: true,
+ };
+
+ beforeEach(() => {
+ tree = createTreeWithEmptyWorkspace();
+ tree.write('/.gitignore', '');
+ tree.write('/.gitignore', '');
+ });
+
+ describe('shared options', () => {
+ describe('not-nested', () => {
+ it('should update tags', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ tags: 'one,two',
+ projectNameAndRootFormat: 'as-provided',
+ });
+ const projects = Object.fromEntries(getProjects(tree));
+ expect(projects).toMatchObject({
+ 'my-lib': {
+ tags: ['one', 'two'],
+ },
+ });
+ });
+
+ it('should update root tsconfig.base.json', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+ const tsconfigJson = readJson(tree, '/tsconfig.base.json');
+ expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([
+ 'my-lib/src/index.ts',
+ ]);
+ });
+
+ it('should update tsconfig.lib.json with worker types', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+ const tsconfigJson = readJson(tree, 'my-lib/tsconfig.lib.json');
+ expect(tsconfigJson.compilerOptions.types).toEqual([
+ 'node',
+ '@cloudflare/workers-types',
+ ]);
+ });
+
+ it('should update root tsconfig.json when no tsconfig.base.json', async () => {
+ tree.rename('tsconfig.base.json', 'tsconfig.json');
+
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ const tsconfigJson = readJson(tree, 'tsconfig.json');
+ expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([
+ 'my-lib/src/index.ts',
+ ]);
+ });
+
+ it('should update root tsconfig.base.json (no existing path mappings)', async () => {
+ updateJson(tree, 'tsconfig.base.json', (json) => {
+ json.compilerOptions.paths = undefined;
+ return json;
+ });
+
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+ const tsconfigJson = readJson(tree, '/tsconfig.base.json');
+ expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([
+ 'my-lib/src/index.ts',
+ ]);
+ });
+
+ it('should create a local tsconfig.json', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+ const tsconfigJson = readJson(tree, 'my-lib/tsconfig.json');
+ expect(tsconfigJson).toMatchInlineSnapshot(`
+ {
+ "compilerOptions": {
+ "forceConsistentCasingInFileNames": true,
+ "module": "commonjs",
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitOverride": true,
+ "noImplicitReturns": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "strict": true,
+ "types": [
+ "vitest",
+ ],
+ },
+ "extends": "../tsconfig.base.json",
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json",
+ },
+ {
+ "path": "./tsconfig.spec.json",
+ },
+ ],
+ }
+ `);
+ });
+
+ it('should extend from root tsconfig.json when no tsconfig.base.json', async () => {
+ tree.rename('tsconfig.base.json', 'tsconfig.json');
+
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ const tsconfigJson = readJson(tree, 'my-lib/tsconfig.json');
+ expect(tsconfigJson.extends).toBe('../tsconfig.json');
+ });
+ });
+
+ describe('nested', () => {
+ it('should update tags', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ directory: 'my-dir/my-lib',
+ tags: 'one',
+ projectNameAndRootFormat: 'as-provided',
+ });
+ let projects = Object.fromEntries(getProjects(tree));
+ expect(projects).toMatchObject({
+ 'my-lib': {
+ tags: ['one'],
+ },
+ });
+
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib2',
+ directory: 'my-dir/my-lib-2',
+ tags: 'one,two',
+ projectNameAndRootFormat: 'as-provided',
+ });
+ projects = Object.fromEntries(getProjects(tree));
+ expect(projects).toMatchObject({
+ 'my-lib': {
+ tags: ['one'],
+ },
+ 'my-lib2': {
+ tags: ['one', 'two'],
+ },
+ });
+ });
+
+ it('should generate files', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ directory: 'my-dir/my-lib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+ expect(tree.exists('my-dir/my-lib/src/index.ts')).toBeTruthy();
+ expect(tree.exists('my-dir/my-lib/src/lib/my-lib.ts')).toBeTruthy();
+ expect(
+ tree.exists('my-dir/my-lib/src/lib/my-lib.spec.ts')
+ ).toBeTruthy();
+ expect(tree.exists('my-dir/my-lib/src/index.ts')).toBeTruthy();
+ expect(tree.exists(`my-dir/my-lib/.eslintrc.json`)).toBeTruthy();
+ expect(tree.exists(`my-dir/my-lib/package.json`)).toBeTruthy();
+ });
+
+ it('should update project configuration', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ directory: 'my-dir/my-lib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ expect(readProjectConfiguration(tree, 'my-lib').root).toEqual(
+ 'my-dir/my-lib'
+ );
+ });
+
+ it('should update root tsconfig.base.json', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ directory: 'my-dir/my-lib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+ const tsconfigJson = readJson(tree, '/tsconfig.base.json');
+ expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([
+ 'my-dir/my-lib/src/index.ts',
+ ]);
+ expect(tsconfigJson.compilerOptions.paths['my-lib/*']).toBeUndefined();
+ });
+
+ it('should update tsconfig.lib.json with worker types', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ directory: 'my-dir/my-lib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ const tsconfigJson = readJson(tree, 'my-dir/my-lib/tsconfig.lib.json');
+ expect(tsconfigJson.compilerOptions.types).toEqual([
+ 'node',
+ '@cloudflare/workers-types',
+ ]);
+ });
+
+ it('should update root tsconfig.json when no tsconfig.base.json', async () => {
+ tree.rename('tsconfig.base.json', 'tsconfig.json');
+
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ directory: 'my-dir/my-lib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ const tsconfigJson = readJson(tree, '/tsconfig.json');
+ expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([
+ 'my-dir/my-lib/src/index.ts',
+ ]);
+ expect(tsconfigJson.compilerOptions.paths['my-lib/*']).toBeUndefined();
+ });
+
+ it('should create a local tsconfig.json', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ directory: 'my-dir/my-lib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ const tsconfigJson = readJson(tree, 'my-dir/my-lib/tsconfig.json');
+ expect(tsconfigJson.references).toEqual([
+ {
+ path: './tsconfig.lib.json',
+ },
+ {
+ path: './tsconfig.spec.json',
+ },
+ ]);
+ });
+
+ it('should extend from root tsconfig.base.json', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ directory: 'my-dir/my-lib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ const tsconfigJson = readJson(tree, 'my-dir/my-lib/tsconfig.json');
+ expect(tsconfigJson.extends).toBe('../../tsconfig.base.json');
+ });
+
+ it('should extend from root tsconfig.json when no tsconfig.base.json', async () => {
+ tree.rename('tsconfig.base.json', 'tsconfig.json');
+
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ directory: 'my-dir/my-lib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ const tsconfigJson = readJson(tree, 'my-dir/my-lib/tsconfig.json');
+ expect(tsconfigJson.extends).toBe('../../tsconfig.json');
+ });
+ });
+
+ describe('--no-strict', () => {
+ it('should update the projects tsconfig with strict false', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ strict: false,
+ projectNameAndRootFormat: 'as-provided',
+ });
+ const tsconfigJson = readJson(tree, '/my-lib/tsconfig.json');
+
+ expect(
+ tsconfigJson.compilerOptions?.forceConsistentCasingInFileNames
+ ).not.toBeDefined();
+ expect(tsconfigJson.compilerOptions?.strict).not.toBeDefined();
+ expect(
+ tsconfigJson.compilerOptions?.noImplicitOverride
+ ).not.toBeDefined();
+ expect(
+ tsconfigJson.compilerOptions?.noPropertyAccessFromIndexSignature
+ ).not.toBeDefined();
+ expect(
+ tsconfigJson.compilerOptions?.noImplicitReturns
+ ).not.toBeDefined();
+ expect(
+ tsconfigJson.compilerOptions?.noFallthroughCasesInSwitch
+ ).not.toBeDefined();
+ });
+
+ it('should default to strict true', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+ const tsconfigJson = readJson(tree, '/my-lib/tsconfig.json');
+
+ expect(tsconfigJson.compilerOptions.strict).toBeTruthy();
+ expect(
+ tsconfigJson.compilerOptions.forceConsistentCasingInFileNames
+ ).toBeTruthy();
+ expect(tsconfigJson.compilerOptions.noImplicitReturns).toBeTruthy();
+ expect(
+ tsconfigJson.compilerOptions.noFallthroughCasesInSwitch
+ ).toBeTruthy();
+ });
+ });
+
+ describe('--importPath', () => {
+ it('should update the tsconfig with the given import path', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ directory: 'my-dir/my-lib',
+ importPath: '@myorg/lib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+ const tsconfigJson = readJson(tree, '/tsconfig.base.json');
+
+ expect(tsconfigJson.compilerOptions.paths['@myorg/lib']).toBeDefined();
+ });
+
+ it('should fail if the same importPath has already been used', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib1',
+ importPath: '@myorg/lib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ try {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib2',
+ importPath: '@myorg/lib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+ } catch (e) {
+ expect(e.message).toContain(
+ 'You already have a library using the import path'
+ );
+ }
+
+ expect.assertions(1);
+ });
+
+ it('should provide a default import path using npm scope', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ const tsconfigJson = readJson(tree, '/tsconfig.base.json');
+ expect(
+ tsconfigJson.compilerOptions.paths['@proj/my-lib']
+ ).toBeDefined();
+ });
+
+ it('should read import path from existing name in package.json', async () => {
+ updateJson(tree, 'package.json', (json) => {
+ json.name = '@acme/core';
+ return json;
+ });
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ rootProject: true,
+ name: 'myLib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ const tsconfigJson = readJson(tree, '/tsconfig.base.json');
+ expect(tsconfigJson.compilerOptions.paths['@acme/core']).toBeDefined();
+ });
+ });
+ });
+
+ describe('--linter', () => {
+ it('should add eslint dependencies', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ const packageJson = readJson(tree, 'package.json');
+ expect(packageJson.devDependencies['eslint']).toBeDefined();
+ expect(packageJson.devDependencies['@nx/linter']).toBeDefined();
+ expect(packageJson.devDependencies['@nx/eslint-plugin']).toBeDefined();
+ });
+
+ describe('not nested', () => {
+ it('should update configuration', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+ expect(readProjectConfiguration(tree, 'my-lib').targets.lint).toEqual({
+ executor: '@nx/linter:eslint',
+ outputs: ['{options.outputFile}'],
+ options: {
+ lintFilePatterns: ['my-lib/**/*.ts', 'my-lib/package.json'],
+ },
+ });
+ });
+
+ it('should create a local .eslintrc.json', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ const eslintJson = readJson(tree, 'my-lib/.eslintrc.json');
+ expect(eslintJson).toMatchInlineSnapshot(`
+ {
+ "extends": [
+ "../.eslintrc.json",
+ ],
+ "ignorePatterns": [
+ "!**/*",
+ ],
+ "overrides": [
+ {
+ "files": [
+ "*.ts",
+ "*.tsx",
+ "*.js",
+ "*.jsx",
+ ],
+ "rules": {},
+ },
+ {
+ "files": [
+ "*.ts",
+ "*.tsx",
+ ],
+ "rules": {},
+ },
+ {
+ "files": [
+ "*.js",
+ "*.jsx",
+ ],
+ "rules": {},
+ },
+ {
+ "files": [
+ "*.json",
+ ],
+ "parser": "jsonc-eslint-parser",
+ "rules": {
+ "@nx/dependency-checks": [
+ "error",
+ {
+ "ignoredFiles": [
+ "{projectRoot}/vite.config.{js,ts,mjs,mts}",
+ ],
+ },
+ ],
+ },
+ },
+ ],
+ }
+ `);
+ });
+ });
+
+ describe('nested', () => {
+ it('should update configuration', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ directory: 'my-dir/my-lib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ expect(readProjectConfiguration(tree, 'my-lib').targets.lint).toEqual({
+ executor: '@nx/linter:eslint',
+ outputs: ['{options.outputFile}'],
+ options: {
+ lintFilePatterns: [
+ 'my-dir/my-lib/**/*.ts',
+ 'my-dir/my-lib/package.json',
+ ],
+ },
+ });
+ });
+
+ it('should create a local .eslintrc.json', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ directory: 'my-dir/my-lib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ const eslintJson = readJson(tree, 'my-dir/my-lib/.eslintrc.json');
+ expect(eslintJson).toMatchInlineSnapshot(`
+ {
+ "extends": [
+ "../../.eslintrc.json",
+ ],
+ "ignorePatterns": [
+ "!**/*",
+ ],
+ "overrides": [
+ {
+ "files": [
+ "*.ts",
+ "*.tsx",
+ "*.js",
+ "*.jsx",
+ ],
+ "rules": {},
+ },
+ {
+ "files": [
+ "*.ts",
+ "*.tsx",
+ ],
+ "rules": {},
+ },
+ {
+ "files": [
+ "*.js",
+ "*.jsx",
+ ],
+ "rules": {},
+ },
+ {
+ "files": [
+ "*.json",
+ ],
+ "parser": "jsonc-eslint-parser",
+ "rules": {
+ "@nx/dependency-checks": [
+ "error",
+ {
+ "ignoredFiles": [
+ "{projectRoot}/vite.config.{js,ts,mjs,mts}",
+ ],
+ },
+ ],
+ },
+ },
+ ],
+ }
+ `);
+ });
+ });
+
+ describe('--js flag', () => {
+ it('should generate js files instead of ts files', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ js: true,
+ projectNameAndRootFormat: 'as-provided',
+ });
+ expect(tree.exists('my-lib/src/index.js')).toBeTruthy();
+ expect(tree.exists('my-lib/src/lib/my-lib.js')).toBeTruthy();
+ expect(tree.exists('my-lib/src/lib/my-lib.spec.js')).toBeTruthy();
+ });
+
+ it('should update tsconfig.json with compilerOptions.allowJs: true', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ js: true,
+ projectNameAndRootFormat: 'as-provided',
+ });
+ expect(
+ readJson(tree, 'my-lib/tsconfig.json').compilerOptions.allowJs
+ ).toBeTruthy();
+ });
+
+ it('should update tsconfig.lib.json include with src/**/*.js glob', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ js: true,
+ projectNameAndRootFormat: 'as-provided',
+ });
+ expect(readJson(tree, 'my-lib/tsconfig.lib.json').include).toEqual([
+ 'src/**/*.ts',
+ 'src/**/*.js',
+ ]);
+ });
+
+ it('should update root tsconfig.json with a js file path', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ js: true,
+ projectNameAndRootFormat: 'as-provided',
+ });
+ const tsconfigJson = readJson(tree, '/tsconfig.base.json');
+ expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([
+ 'my-lib/src/index.js',
+ ]);
+ });
+
+ it('should generate js files for nested libs as well', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ directory: 'my-dir/my-lib',
+ js: true,
+ projectNameAndRootFormat: 'as-provided',
+ });
+ expect(tree.exists('my-dir/my-lib/src/index.js')).toBeTruthy();
+ expect(tree.exists('my-dir/my-lib/src/lib/my-lib.js')).toBeTruthy();
+ expect(
+ tree.exists('my-dir/my-lib/src/lib/my-lib.spec.js')
+ ).toBeTruthy();
+ expect(tree.exists('my-dir/my-lib/src/index.js')).toBeTruthy();
+ });
+
+ it('should configure the project for linting js files', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ directory: 'my-dir/my-lib',
+ js: true,
+ projectNameAndRootFormat: 'as-provided',
+ });
+ expect(
+ readProjectConfiguration(tree, 'my-lib').targets.lint.options
+ .lintFilePatterns
+ ).toEqual(['my-dir/my-lib/**/*.js', 'my-dir/my-lib/package.json']);
+ expect(readJson(tree, 'my-dir/my-lib/.eslintrc.json'))
+ .toMatchInlineSnapshot(`
+ {
+ "extends": [
+ "../../.eslintrc.json",
+ ],
+ "ignorePatterns": [
+ "!**/*",
+ ],
+ "overrides": [
+ {
+ "files": [
+ "*.ts",
+ "*.tsx",
+ "*.js",
+ "*.jsx",
+ ],
+ "rules": {},
+ },
+ {
+ "files": [
+ "*.ts",
+ "*.tsx",
+ ],
+ "rules": {},
+ },
+ {
+ "files": [
+ "*.js",
+ "*.jsx",
+ ],
+ "rules": {},
+ },
+ {
+ "files": [
+ "*.json",
+ ],
+ "parser": "jsonc-eslint-parser",
+ "rules": {
+ "@nx/dependency-checks": [
+ "error",
+ {
+ "ignoredFiles": [
+ "{projectRoot}/vite.config.{js,ts,mjs,mts}",
+ ],
+ },
+ ],
+ },
+ },
+ ],
+ }
+ `);
+ });
+ });
+ });
+
+ describe('--bundler=vite', () => {
+ it('should add build and test targets with vite and vitest', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ bundler: 'vite',
+ unitTestRunner: undefined,
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ const project = readProjectConfiguration(tree, 'my-lib');
+ expect(project.targets.build).toMatchObject({
+ executor: '@nx/vite:build',
+ });
+ expect(project.targets.test).toMatchObject({
+ executor: '@nx/vite:test',
+ });
+ expect(tree.exists('my-lib/vite.config.ts')).toBeTruthy();
+ expect(readJson(tree, 'my-lib/.eslintrc.json').overrides).toContainEqual({
+ files: ['*.json'],
+ parser: 'jsonc-eslint-parser',
+ rules: {
+ '@nx/dependency-checks': [
+ 'error',
+ {
+ ignoredFiles: ['{projectRoot}/vite.config.{js,ts,mjs,mts}'],
+ },
+ ],
+ },
+ });
+ });
+
+ it.each`
+ unitTestRunner | executor
+ ${'none'} | ${undefined}
+ `(
+ 'should respect unitTestRunner if passed',
+ async ({ unitTestRunner, executor }) => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ bundler: 'vite',
+ unitTestRunner,
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ const project = readProjectConfiguration(tree, 'my-lib');
+ expect(project.targets.test?.executor).toEqual(executor);
+ }
+ );
+ });
+
+ describe('--bundler=esbuild', () => {
+ it('should add build with esbuild', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ bundler: 'esbuild',
+ unitTestRunner: 'none',
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ const project = readProjectConfiguration(tree, 'my-lib');
+ expect(project.targets.build).toMatchObject({
+ executor: '@nx/esbuild:esbuild',
+ });
+ expect(readJson(tree, 'my-lib/.eslintrc.json').overrides).toContainEqual({
+ files: ['*.json'],
+ parser: 'jsonc-eslint-parser',
+ rules: {
+ '@nx/dependency-checks': [
+ 'error',
+ {
+ ignoredFiles: ['{projectRoot}/esbuild.config.{js,ts,mjs,mts}'],
+ },
+ ],
+ },
+ });
+ });
+ });
+
+ describe('--bundler=rollup', () => {
+ it('should add build with rollup', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ bundler: 'rollup',
+ unitTestRunner: 'none',
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ const project = readProjectConfiguration(tree, 'my-lib');
+ expect(project.targets.build).toMatchObject({
+ executor: '@nx/rollup:rollup',
+ });
+ expect(readJson(tree, 'my-lib/.eslintrc.json').overrides).toContainEqual({
+ files: ['*.json'],
+ parser: 'jsonc-eslint-parser',
+ rules: {
+ '@nx/dependency-checks': [
+ 'error',
+ {
+ ignoredFiles: ['{projectRoot}/rollup.config.{js,ts,mjs,mts}'],
+ },
+ ],
+ },
+ });
+ });
+ });
+
+ describe('--minimal', () => {
+ it('should generate a README.md when minimal is set to false', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ minimal: false,
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ expect(tree.exists('my-lib/README.md')).toBeTruthy();
+ });
+
+ it('should not generate a README.md when minimal is set to true', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ minimal: true,
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ expect(tree.exists('my-lib/README.md')).toBeFalsy();
+ });
+
+ it('should generate a README.md and add it to the build assets when buildable is true and minimal is false', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ bundler: 'tsc',
+ minimal: false,
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ expect(tree.exists('my-lib/README.md')).toBeTruthy();
+
+ const project = readProjectConfiguration(tree, 'my-lib');
+ expect(project.targets.build.options.assets).toStrictEqual([
+ 'my-lib/*.md',
+ ]);
+ });
+
+ it('should not generate a README.md when both bundler and minimal are set', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ bundler: 'tsc',
+ minimal: true,
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ expect(tree.exists('my-lib/README.md')).toBeFalsy();
+
+ const project = readProjectConfiguration(tree, 'my-lib');
+ expect(project.targets.build.options.assets).toEqual([]);
+ });
+ });
+
+ describe('--simpleName', () => {
+ it('should generate a simple name', async () => {
+ await libraryGenerator(tree, {
+ ...defaultOptions,
+ name: 'myLib',
+ simpleName: true,
+ directory: 'web/my-lib',
+ projectNameAndRootFormat: 'as-provided',
+ });
+
+ expect(tree.read('web/my-lib/src/index.ts', 'utf-8')).toContain(
+ `export * from './lib/my-lib';`
+ );
+ expect(tree.exists('web/my-lib/src/lib/my-lib.ts')).toBeTruthy();
+ });
+ });
+});
diff --git a/packages/plugins/nx-cloudflare/src/generators/library/library.ts b/packages/plugins/nx-cloudflare/src/generators/library/library.ts
new file mode 100644
index 000000000..e03f0eb8f
--- /dev/null
+++ b/packages/plugins/nx-cloudflare/src/generators/library/library.ts
@@ -0,0 +1,106 @@
+import { Tree, formatFiles, updateJson } from '@nx/devkit';
+import type {
+ NormalizedSchema,
+ NxCloudflareLibraryGeneratorSchema,
+} from './schema';
+import { libraryGenerator } from '@nx/js';
+import { join } from 'path';
+import initGenerator from '../init/init';
+import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
+
+export async function nxCloudflareWorkerLibraryGenerator(
+ tree: Tree,
+ schema: NxCloudflareLibraryGeneratorSchema
+) {
+ const options = await normalizeOptions(tree, schema);
+
+ // Set up the needed packages.
+ const initTask = await initGenerator(tree, {
+ ...options,
+ skipFormat: true,
+ });
+
+ const libraryTask = await libraryGenerator(tree, options);
+ updateTsLibConfig(tree, options);
+
+ if (!options.skipFormat) {
+ await formatFiles(tree);
+ }
+
+ return async () => {
+ await initTask();
+ await libraryTask();
+ };
+}
+
+async function normalizeOptions(
+ tree: Tree,
+ options: NxCloudflareLibraryGeneratorSchema
+): Promise {
+ options.projectNameAndRootFormat =
+ options.projectNameAndRootFormat ?? 'as-provided';
+
+ // ensure programmatic runs have an expected default
+ if (!options.config) {
+ options.config = 'project';
+ }
+
+ if (options.publishable) {
+ if (!options.importPath) {
+ throw new Error(
+ `For publishable libs you have to provide a proper "--importPath" which needs to be a valid npm package name (e.g. my-awesome-lib or @myorg/my-lib)`
+ );
+ }
+
+ if (options.bundler === 'none') {
+ options.bundler = 'tsc';
+ }
+ }
+
+ if (options.config === 'npm-scripts') {
+ options.unitTestRunner = 'none';
+ options.linter = 'none';
+ options.bundler = 'none';
+ }
+
+ if (!options.linter && options.config !== 'npm-scripts') {
+ options.linter = 'none';
+ }
+
+ const { projectName, projectRoot, importPath } =
+ await determineProjectNameAndRootOptions(tree, {
+ name: options.name,
+ projectType: 'library',
+ directory: options.directory,
+ importPath: options.importPath,
+ projectNameAndRootFormat: options.projectNameAndRootFormat,
+ rootProject: options.rootProject,
+ callingGenerator: '@nx/js:library',
+ });
+ options.rootProject = projectRoot === '.';
+
+ options.minimal ??= false;
+
+ return {
+ ...options,
+ name: projectName,
+ libProjectRoot: projectRoot,
+ importPath,
+ };
+}
+
+function updateTsLibConfig(tree: Tree, options: NormalizedSchema) {
+ updateJson(
+ tree,
+ join(options.libProjectRoot, 'tsconfig.lib.json'),
+ (json) => {
+ json.compilerOptions.types = [
+ ...json.compilerOptions.types,
+ '@cloudflare/workers-types',
+ ];
+ return json;
+ }
+ );
+}
+
+export default nxCloudflareWorkerLibraryGenerator;
diff --git a/packages/plugins/nx-cloudflare/src/generators/library/schema.d.ts b/packages/plugins/nx-cloudflare/src/generators/library/schema.d.ts
new file mode 100644
index 000000000..060fb4536
--- /dev/null
+++ b/packages/plugins/nx-cloudflare/src/generators/library/schema.d.ts
@@ -0,0 +1,22 @@
+export interface NxCloudflareLibraryGeneratorSchema {
+ name: string;
+ directory?: string;
+ projectNameAndRootFormat?: ProjectNameAndRootFormat;
+ skipFormat?: boolean;
+ tags?: string;
+ unitTestRunner?: 'vitest' | 'none';
+ linter?: Linter;
+ importPath?: string;
+ js?: boolean;
+ strict?: boolean;
+ publishable?: boolean;
+ bundler?: Bundler;
+ minimal?: boolean;
+ config?: 'workspace' | 'project' | 'npm-scripts';
+ rootProject?: boolean;
+ simpleName?: boolean;
+}
+
+export interface NormalizedSchema extends NxCloudflareLibraryGeneratorSchema {
+ libProjectRoot: string;
+}
diff --git a/packages/plugins/nx-cloudflare/src/generators/library/schema.json b/packages/plugins/nx-cloudflare/src/generators/library/schema.json
new file mode 100644
index 000000000..444229adb
--- /dev/null
+++ b/packages/plugins/nx-cloudflare/src/generators/library/schema.json
@@ -0,0 +1,98 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "$id": "NxCloudflareWorkerLibrary",
+ "title": "Create a Cloudflare Worker Library",
+ "description": "Create a Cloudflare Worker Library",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Library name.",
+ "$default": {
+ "$source": "argv",
+ "index": 0
+ },
+ "x-prompt": "What name would you like to use for the library?",
+ "pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$"
+ },
+ "directory": {
+ "type": "string",
+ "description": "A directory where the lib is placed.",
+ "x-priority": "important"
+ },
+ "projectNameAndRootFormat": {
+ "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
+ "type": "string",
+ "enum": ["as-provided", "derived"]
+ },
+ "linter": {
+ "description": "The tool to use for running lint checks.",
+ "type": "string",
+ "enum": ["eslint", "none"],
+ "default": "eslint"
+ },
+ "unitTestRunner": {
+ "type": "string",
+ "enum": ["vitest", "none"],
+ "description": "Test runner to use for unit tests.",
+ "x-prompt": "Which unit test runner would you like to use?"
+ },
+ "tags": {
+ "type": "string",
+ "description": "Add tags to the library (used for linting)."
+ },
+ "skipFormat": {
+ "description": "Skip formatting files.",
+ "type": "boolean",
+ "default": false,
+ "x-priority": "internal"
+ },
+ "js": {
+ "type": "boolean",
+ "description": "Generate JavaScript files rather than TypeScript files.",
+ "default": false
+ },
+ "strict": {
+ "type": "boolean",
+ "description": "Whether to enable tsconfig strict mode or not.",
+ "default": true
+ },
+ "publishable": {
+ "type": "boolean",
+ "default": false,
+ "description": "Generate a publishable library.",
+ "x-priority": "important"
+ },
+ "importPath": {
+ "type": "string",
+ "description": "The library name used to import it, like @myorg/my-awesome-lib. Required for publishable library.",
+ "x-priority": "important"
+ },
+ "bundler": {
+ "description": "The bundler to use. Choosing 'none' means this library is not buildable.",
+ "type": "string",
+ "enum": ["swc", "tsc", "rollup", "vite", "esbuild", "none"],
+ "default": "tsc",
+ "x-prompt": "Which bundler would you like to use to build the library? Choose 'none' to skip build setup.",
+ "x-priority": "important"
+ },
+ "minimal": {
+ "type": "boolean",
+ "description": "Generate a library with a minimal setup. No README.md generated.",
+ "default": false
+ },
+ "simpleName": {
+ "description": "Don't include the directory in the generated file name.",
+ "type": "boolean",
+ "default": false
+ },
+ "config": {
+ "type": "string",
+ "enum": ["workspace", "project", "npm-scripts"],
+ "default": "project",
+ "description": "Determines whether the project's executors should be configured in `workspace.json`, `project.json` or as npm scripts.",
+ "x-priority": "internal"
+ }
+ },
+ "required": ["name"]
+}
|