Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,24 @@
"yarn": ">= 1.22.0"
},
"scripts": {
"init": "yarn install",
"prepare": "husky install",
"ng": "ng",
"build": "ng build --configuration production && yarn build:schematics && yarn copy:schematics && yarn copy:docs",
"build:schematics": "tsc -p projects/spectator/schematics/tsconfig.json",
"build": "ng build --configuration production && yarn build:schematics && yarn copy:docs",
"build:schematics": "node projects/spectator/build-schematics.js",
"pack": "cd dist/spectator && npm pack && bash -c 'mv openng-spectator-*.tgz ../../'",
"test": "ng test",
"test:jest": "ng run spectator:test-jest",
"test:vitest": "ng run spectator:test-vitest",
"test:types": "tsc -p type-tests/jasmine && tsc -p type-tests/jest && tsc -p type-tests/vitest",
"test:ci": "cross-env NODE_ENV=build yarn test && yarn test:jest --silent && yarn test:vitest",
"test:schematics": "jest --config projects/spectator/schematics/jest.config.js",
"test:ci": "cross-env NODE_ENV=build yarn test && yarn test:jest --silent && yarn test:vitest && yarn test:schematics",
"lint": "ng lint",
"format": "prettier --write \"{projects,src}/**/*.ts\"",
"commit": "git-cz",
"contributors:add": "all-contributors add",
"contributors:generate": "all-contributors generate",
"copy:docs": "cp *.md dist/spectator",
"copy:schematics": "cp -r projects/spectator/schematics/src/ dist/spectator/schematics",
"postbump": "yarn build",
"release": "cd projects/spectator && standard-version --infile ../../CHANGELOG.md",
"release:dry": "cd projects/spectator && standard-version --infile ../../CHANGELOG.md --dry-run"
Expand All @@ -52,10 +54,10 @@
"@angular/platform-browser-dynamic": "^22.0.5",
"@angular/router": "22.0.5",
"@commitlint/cli": "17.3.0",
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@commitlint/config-angular": "17.3.0",
"@commitlint/config-conventional": "17.3.0",
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@types/jasmine": "5.1.4",
"@types/jest": "30.0.0",
"@types/node": "^26.1.0",
Expand All @@ -66,6 +68,8 @@
"core-js": "^3.9.1",
"cross-env": "^5.1.4",
"cz-conventional-changelog": "^3.3.0",
"esbuild": "^0.28.1",
"esbuild-plugin-tsc": "^0.6.0",
"eslint": "^9.28.0",
"git-cz": "^4.7.6",
"helpful-decorators": "^2.1.0",
Expand Down
60 changes: 60 additions & 0 deletions projects/spectator/build-schematics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { build } from 'esbuild';
import esbuildPluginTsc from 'esbuild-plugin-tsc';
import { glob } from 'fs/promises';
import { rmSync } from 'fs';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const distPath = '../../dist/spectator/schematics';
Comment on lines +1 to +9

const schematics = build({
absWorkingDir: __dirname,
bundle: false,
entryPoints: ['schematics/src/**'],
format: 'esm',
loader: {
'.json': 'copy',
'.template': 'copy',
},
minify: false,
outdir: resolve(__dirname, distPath),
packages: 'external',
platform: 'node',
plugins: [
esbuildPluginTsc({
force: true,
tsconfigPath: resolve(__dirname, './schematics/tsconfig.json'),
}),
],
target: 'node22',
treeShaking: false,
});

/**
* Remove files/directories from dist/schematics using glob patterns.
*
* @param patterns - Glob pattern(s) relative to dist/schematics
*/
async function removeFromDistSchematics(patterns) {
const targetDir = resolve(__dirname, distPath);
const list = Array.isArray(patterns) ? patterns : [patterns];

for (const pattern of list) {
const globResults = await Array.fromAsync(glob(pattern, { cwd: targetDir }));
for (const match of globResults) {
const fullPath = resolve(targetDir, match);
rmSync(fullPath, { recursive: true, force: true });
}
}
}

schematics
.then(async () => {
// Remove unwanted artifact form the build
await removeFromDistSchematics(['*.config.js', '**/*.spec.js', '__mocks__']);
})
.catch((e) => {
console.error(e);
process.exit(1);
});
18 changes: 18 additions & 0 deletions projects/spectator/schematics/__mocks__/ora.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Mock for ora (ESM-only) package
// ora returns an object with start/stop/succeed/fail methods
const createMockSpinner = () => ({
start: (text) => createMockSpinner(),
stop: () => createMockSpinner(),
succeed: (text) => createMockSpinner(),
fail: (text) => createMockSpinner(),
warn: (text) => createMockSpinner(),
info: (text) => createMockSpinner(),
clear: () => createMockSpinner(),
render: () => createMockSpinner(),
color: 'cyan',
prefixText: '',
text: '',
});

module.exports = createMockSpinner;
module.exports.default = createMockSpinner;
19 changes: 19 additions & 0 deletions projects/spectator/schematics/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = {
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.spec.ts'],
testPathIgnorePatterns: ['/files/'],
transform: {
'^.+\\.ts$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
moduleNameMapper: {
'^ora$': '<rootDir>/__mocks__/ora.js',
},
moduleFileExtensions: ['ts', 'js'],
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.spec.ts', '!src/**/files/**/*'],
};
8 changes: 7 additions & 1 deletion projects/spectator/schematics/src/collection.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "./node_modules/@angular-devkit/schematics/collection-schema.json",
"$schema": "../../../../node_modules/@angular-devkit/schematics/collection-schema.json",
"extends": ["@schematics/angular"],
"schematics": {
"spectator-component": {
Expand All @@ -25,6 +25,12 @@
"factory": "./spectator/index#spectatorPipeSchematic",
"schema": "./spectator/pipe-schema.json",
"aliases": ["ps"]
},
"ngneat-to-openng": {
"description": "Convert ngneat/spectator to openng/spectator",
"factory": "./ngneatToOpenng/index#migrate",
"schema": "./ngneatToOpenng/schema.json",
"aliases": ["nto"]
}
}
}
24 changes: 24 additions & 0 deletions projects/spectator/schematics/src/ngneatToOpenng/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# ngneat to openng migration

Converts all imports of `@ngneat/spectator` to `@openng/spectator` across your project.

This migration scans every TypeScript file in your project (excluding `node_modules` and `dist` directories) and updates any import or export declarations that reference `@ngneat/spectator` to instead reference `@openng/spectator`. Named imports remain intact — only the module specifier path changes.

## What does this migration do?

- Finds all TypeScript files (`.ts`) in your project
- Skips files inside `node_modules` and `dist` directories
- Replaces `from '@ngneat/spectator'` with `from '@openng/spectator'` in import and export declarations
- Preserves all named imports (e.g., `createComponentFactory`, `Spectator`, etc.)

## How to run this migration?

```bash
ng generate @openng/spectator:ngneat-to-openng
```

Or using the alias:

```bash
ng g @openng/spectator:nto
```
151 changes: 151 additions & 0 deletions projects/spectator/schematics/src/ngneatToOpenng/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { Tree } from '@angular-devkit/schematics';
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
import path from 'path';

describe('NgNeat to Openng', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use namespace naming ngneatand openng for consistency.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

const runner = new SchematicTestRunner('schematics', path.join(__dirname, '../collection.json'));

let tree: Tree;

beforeEach(() => {
tree = Tree.empty();
});

describe('ngneat/directive', () => {
const path: string = '/src/ngneat.directive.ts';

beforeEach(() => {
tree.create(
path,
`import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator';
import { MyDirective } from './my.directive';

describe('MyDirective', () => {
let spectator: SpectatorDirective<MyDirective>;
const createDirective = createDirectiveFactory(MyDirective);

it('should change the background color', () => {
spectator = createDirective(\`<div highlight>Testing MyDirective</div>\`);

spectator.dispatchMouseEvent(spectator.element, 'mouseover');

expect(spectator.element).toHaveStyle({
backgroundColor: 'rgba(0,0,0, 0.1)'
});

spectator.dispatchMouseEvent(spectator.element, 'mouseout');
expect(spectator.element).toHaveStyle({
backgroundColor: '#fff'
});
});

});`,
);
});

it('should remane to @openng', async () => {
const result = await runner.runSchematic('ngneat-to-openng', {}, tree);
const content = result.readContent(path);

expect(content).toContain(`from '@openng/spectator'`);
expect(content).not.toContain(`from '@ngneat/spectator'`);
expect(content).toContain(`import { createDirectiveFactory, SpectatorDirective }`); // named imports intacts
});
});

describe('ngneat/pipe', () => {
const path: string = '/src/ngneat.pipe.ts';

beforeEach(() => {
tree.create(
path,
`import { createPipeFactory, SpectatorPipe } from '@ngneat/spectator';
import { MyPipe } from './my.pipe';

describe('MyPipe ', () => {
let spectator: SpectatorPipe<MyPipe>;
const createPipe = createPipeFactory(MyPipe);

it('should change the background color', () => {
spectator = createPipe(\`<div>{{ 'Testing' | my }}</div>\`);

expect(spectator.element).toHaveText('Testing');
});
});`,
);
});

it('should remane to @openng', async () => {
const result = await runner.runSchematic('nto', {}, tree);
const content = result.readContent(path);

expect(content).toContain(`from '@openng/spectator'`);
expect(content).not.toContain(`from '@ngneat/spectator'`);
expect(content).toContain(`import { createPipeFactory, SpectatorPipe }`); // named imports intacts
});
});

describe('ngneat/service', () => {
const path: string = '/src/ngneat.service.ts';

beforeEach(() => {
tree.create(
path,
`import { createServiceFactory, SpectatorService } from '@ngneat/spectator';
import { MyService } from './my.service';

describe('MyService', () => {
let spectator: SpectatorService<MyService>;
const createService = createServiceFactory(MyService);

beforeEach(() => spectator = createService());

it('should...', () => {
expect(spectator.service).toBeTruthy();
});
});`,
);
});

it('should remane to @openng', async () => {
const result = await runner.runSchematic('nto', {}, tree);
const content = result.readContent(path);

expect(content).toContain(`from '@openng/spectator'`);
expect(content).not.toContain(`from '@ngneat/spectator'`);
expect(content).toContain(`import { createServiceFactory, SpectatorService }`); // named imports intacts
});
});

describe('ngneat/component', () => {
const path: string = '/src/ngneat.component.ts';

beforeEach(() => {
tree.create(
path,
`import { Spectator, createComponentFactory } from '@ngneat/spectator';
import { MyComponent } from './my.component';

describe('MyComponent', () => {
let spectator: Spectator<MyComponent>;
const createComponent = createComponentFactory(MyComponent);

it('should create', () => {
spectator = createComponent();

expect(spectator.component).toBeTruthy();
});
});`,
);
});

it('should remane to @openng', async () => {
const result = await runner.runSchematic('nto', {}, tree);
const content = result.readContent(path);

expect(content).toContain(`from '@openng/spectator'`);
expect(content).not.toContain(`from '@ngneat/spectator'`);
expect(content).toContain(`import { Spectator, createComponentFactory }`); // named imports intacts
});
});
});
Loading