diff --git a/tools/generators/migrate-scripts-to-pkg/README.md b/tools/generators/migrate-scripts-to-pkg/README.md new file mode 100644 index 00000000000000..e7c04368860dde --- /dev/null +++ b/tools/generators/migrate-scripts-to-pkg/README.md @@ -0,0 +1,38 @@ +# migrate-scripts-to-pkg + +Workspace Generator ...TODO... + + + +- [Usage](#usage) + - [Examples](#examples) +- [Options](#options) + - [`name`](#name) + + + +## Usage + +```sh +yarn nx workspace-generator migrate-scripts-to-pkg ... +``` + +Show what will be generated without writing to disk: + +```sh +yarn nx workspace-generator migrate-scripts-to-pkg --dry-run +``` + +### Examples + +```sh +yarn nx workspace-generator migrate-scripts-to-pkg +``` + +## Options + +#### `name` + +Type: `string` + +TODO... diff --git a/tools/generators/migrate-scripts-to-pkg/files/constants.ts__tmpl__ b/tools/generators/migrate-scripts-to-pkg/files/constants.ts__tmpl__ new file mode 100644 index 00000000000000..d9913111a9dc76 --- /dev/null +++ b/tools/generators/migrate-scripts-to-pkg/files/constants.ts__tmpl__ @@ -0,0 +1 @@ +export const variable = "<%= name %>"; \ No newline at end of file diff --git a/tools/generators/migrate-scripts-to-pkg/index.spec.ts b/tools/generators/migrate-scripts-to-pkg/index.spec.ts new file mode 100644 index 00000000000000..18c927e139093b --- /dev/null +++ b/tools/generators/migrate-scripts-to-pkg/index.spec.ts @@ -0,0 +1,731 @@ +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { + Tree, + stripIndents, + writeJson, + updateJson, + visitNotIgnoredFiles, + readJson, + addProjectConfiguration, + getProjects, +} from '@nrwl/devkit'; + +import generator from './index'; +import { MigrateScriptsToPkgGeneratorSchema } from './schema'; + +describe('migrate-scripts-to-pkg generator', () => { + let tree: Tree; + const options: MigrateScriptsToPkgGeneratorSchema = {}; + + function setup() { + addProjectConfiguration(tree, '@fluentui/scripts', { root: 'scripts', projectType: 'library' }); + // setup globals + writeJson(tree, '/lerna.json', { + packages: [ + 'apps/*', + 'packages/*', + 'packages/react-components/*', + 'scripts', + 'packages/fluentui/*', + 'tools', + 'typings', + ], + npmClient: 'yarn', + useWorkspaces: true, + version: '0.0.0', + }); + updateJson(tree, '/package.json', json => { + json.workspaces = { + packages: [ + 'apps/*', + 'packages/*', + 'packages/fluentui/*', + 'packages/react-components/*', + 'scripts', + 'tools', + 'typings', + ], + nohoist: ['@fluentui/web-components/@storybook/html'], + }; + return json; + }); + + tree.write( + '/dangerfile.ts', + stripIndents` + import {foo} from '@fluentui/scripts/dangerjs' + + foo(); + `, + ); + + tree.write( + '/.storybook/main.js', + stripIndents` + const { hello } = require('@fluentui/scripts/lib-one'); + + module.exports = { + stories: [], + }; + `, + ); + + // setup scripts/ root + writeJson(tree, '/scripts/tsconfig.json', { + include: [], + files: [], + references: [], + }); + writeJson(tree, '/scripts/package.json', {}); + tree.write( + '/scripts/jest.config.js', + stripIndents` + module.exports = { + displayName: 'scripts', + } + `, + ); + + // setup scripts sub-folders + tree.write( + 'scripts/lib-one/index.js', + stripIndents` + module.exports = { + ...require('./hello') + } + `, + ); + tree.write( + 'scripts/lib-one/hello.js', + stripIndents` + const { hello: hello3 } = require('../lib-three'); + const path = require('path'); + exports.hello = function hello(){} + `, + ); + writeJson(tree, 'scripts/lib-one/tsconfig.json', { + extends: '../tsconfig.json', + compilerOptions: { + types: ['node', 'jest'], + }, + include: ['*'], + }); + + tree.write( + 'scripts/lib-two/index.js', + stripIndents` + module.exports = { + ...require('./hello') + } + `, + ); + tree.write( + 'scripts/lib-two/hello.js', + stripIndents` + const { hello: hello1 } = require('../lib-one'); + const path = require('path'); + const devkit = require('@scope/devkit'); + exports.hello = function hello(){} + `, + ); + writeJson(tree, 'scripts/lib-two/tsconfig.json', { + extends: '../tsconfig.json', + compilerOptions: { + types: ['node', 'jest'], + }, + include: ['*'], + }); + + tree.write( + 'scripts/lib-three/index.ts', + stripIndents` + export { hello } from './hello'; + `, + ); + tree.write( + 'scripts/lib-three/hello.ts', + stripIndents` + import {hello as hello2} from '../lib-two'; + import * as path from 'path'; + import * as fs from 'fs'; + export function hello(){} + `, + ); + writeJson(tree, 'scripts/lib-three/tsconfig.json', { + extends: '../tsconfig.json', + compilerOptions: { + types: ['node', 'jest'], + }, + include: ['*'], + }); + + tree.write( + 'scripts/lib-four-no-barrel/index.js', + stripIndents` + const { hello } = require('./hello'); + function main(){ + hello(); + } + + main(); + `, + ); + tree.write( + 'scripts/lib-four-no-barrel/hello.js', + stripIndents` + const { hello: hello2 } = require('../lib-two'); + const path = require('path'); + const fs = require('fs'); + exports.hello = function hello(){} + `, + ); + writeJson(tree, 'scripts/lib-four-no-barrel/tsconfig.json', { + extends: '../tsconfig.json', + compilerOptions: { + types: ['node', 'jest'], + }, + include: ['*'], + }); + } + function setupPackages() { + tree.write('packages/react-one/src/index.ts', stripIndents`export {Button} from './button'`); + tree.write( + 'packages/react-one/tool.config.js', + stripIndents` + const {hello} = require('@fluentui/scripts/lib-one'); + const {hello: hello2} = require('@fluentui/scripts/lib-two'); + `, + ); + writeJson(tree, 'packages/react-one/config/api-extractor.json', { + extends: '@fluentui/scripts/api-extractor/api-extractor.common.json', + }); + writeJson(tree, 'packages/react-one/package.json', { + name: `@proj/react-one`, + devDependencies: { '@fluentui/scripts': '*' }, + }); + + tree.write( + 'packages/react-v0/tool.config.ts', + stripIndents` + import { hello } from '@fluentui/scripts/lib-three'; + import {hello as hello2} from '@fluentui/scripts/lib-two'; + `, + ); + tree.write( + 'packages/react-v0/gulpfile.ts', + stripIndents` + import '../../gulpfile'; + `, + ); + writeJson(tree, 'packages/react-v0/package.json', { + name: `@proj/react-v0`, + devDependencies: { '@fluentui/scripts': '*' }, + }); + + tree.write( + 'packages/react-no-direct-import/src/index.ts', + stripIndents` + import * as path from 'path'; + import * fs path from 'fs'; + export function hello(){} + `, + ); + writeJson(tree, 'packages/react-no-direct-import/package.json', { + name: `@proj/react-v0`, + devDependencies: { '@fluentui/scripts': '*' }, + }); + + tree.write( + 'apps/todo-app/config.ts', + stripIndents` + import { hello } from '@fluentui/scripts/lib-three'; + `, + ); + writeJson(tree, 'apps/todo-app/package.json', { + name: `@proj/todo-app`, + devDependencies: { '@fluentui/scripts': '*' }, + }); + } + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + setup(); + setupPackages(); + }); + + it(`should update lerna and package.json`, async () => { + await generator(tree, options); + + expect(readJson(tree, '/package.json').workspaces).toMatchInlineSnapshot(` + Object { + "nohoist": Array [ + "@fluentui/web-components/@storybook/html", + ], + "packages": Array [ + "apps/*", + "packages/*", + "packages/fluentui/*", + "packages/react-components/*", + "scripts/*", + "tools", + "typings", + ], + } + `); + expect(readJson(tree, '/package.json').dependencies).toMatchInlineSnapshot(`Object {}`); + expect(readJson(tree, '/package.json').devDependencies).toMatchInlineSnapshot(`Object {}`); + expect(readJson(tree, '/lerna.json').packages).toMatchInlineSnapshot(` + Array [ + "apps/*", + "packages/*", + "packages/react-components/*", + "scripts/*", + "packages/fluentui/*", + "tools", + "typings", + ] + `); + }); + + it(`should remove all config files in existing /scripts`, async () => { + const childrenPaths = tree.children('/scripts').map(p => `/scripts/${p}`); + + let foldersOnly = childrenPaths.every(p => tree.isFile(p) === false); + expect(foldersOnly).toBe(false); + + await generator(tree, options); + + foldersOnly = childrenPaths.every(p => tree.isFile(p) === false); + + expect(foldersOnly).toBe(true); + }); + + it(`should update workspace.json`, async () => { + expect(getProjects(tree).get('@fluentui/scripts')).toBeDefined(); + + await generator(tree, options); + + expect(getProjects(tree).get('@fluentui/scripts')).not.toBeDefined(); + expect(getProjects(tree)).toMatchInlineSnapshot(` + Map { + "@fluentui/scripts-lib-four-no-barrel" => Object { + "projectType": "library", + "root": "scripts/lib-four-no-barrel", + "sourceRoot": "scripts/lib-four-no-barrel", + "tags": Array [ + "tools", + ], + }, + "@fluentui/scripts-lib-one" => Object { + "projectType": "library", + "root": "scripts/lib-one", + "sourceRoot": "scripts/lib-one/src", + "tags": Array [ + "tools", + ], + }, + "@fluentui/scripts-lib-three" => Object { + "projectType": "library", + "root": "scripts/lib-three", + "sourceRoot": "scripts/lib-three/src", + "tags": Array [ + "tools", + ], + }, + "@fluentui/scripts-lib-two" => Object { + "projectType": "library", + "root": "scripts/lib-two", + "sourceRoot": "scripts/lib-two/src", + "tags": Array [ + "tools", + ], + }, + } + `); + }); + + describe(`file generation`, () => { + it(`should generate new configs`, async () => { + await generator(tree, options); + + expect(readDirectory(tree, 'scripts/lib-one')).toMatchInlineSnapshot(` + Array [ + "scripts/lib-one/tsconfig.json", + "scripts/lib-one/src/index.js", + "scripts/lib-one/src/hello.js", + "scripts/lib-one/package.json", + "scripts/lib-one/tsconfig.lib.json", + "scripts/lib-one/tsconfig.spec.json", + "scripts/lib-one/jest.config.js", + "scripts/lib-one/.eslintrc.json", + ] + `); + expect(readJson(tree, 'scripts/lib-one/.eslintrc.json')).toMatchInlineSnapshot(` + Object { + "extends": Array [ + "plugin:@fluentui/eslint-plugin/node", + "plugin:@fluentui/eslint-plugin/imports", + ], + "overrides": Array [ + Object { + "files": "src/index.d.ts", + "rules": Object { + "import/no-self-import": "off", + }, + }, + ], + "root": true, + "rules": Object { + "@fluentui/max-len": "off", + "import/no-extraneous-dependencies": Array [ + "error", + Object { + "packageDir": Array [ + ".", + "../../", + ], + }, + ], + }, + } + `); + expect(tree.read('scripts/lib-one/jest.config.js', 'utf-8')).toMatchInlineSnapshot(` + "// @ts-check + + /** + * @type {import('@jest/types').Config.InitialOptions} + */ + module.exports = { + displayName: 'scripts-lib-one', + preset: '../../jest.preset.js', + transform: { + '^.+\\\\\\\\.tsx?$': 'babel-jest', + }, + coverageDirectory: './coverage', + testEnvironment: 'node', + };" + `); + expect(readJson(tree, 'scripts/lib-one/tsconfig.json')).toMatchInlineSnapshot(` + Object { + "compilerOptions": Object { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "noUnusedLocals": true, + "pretty": true, + "sourceMap": true, + "target": "ES2019", + }, + "extends": "@tsconfig/node14/tsconfig.json", + "files": Array [], + "include": Array [], + "references": Array [ + Object { + "path": "./tsconfig.lib.json", + }, + Object { + "path": "./tsconfig.spec.json", + }, + ], + } + `); + expect(readJson(tree, 'scripts/lib-one/tsconfig.lib.json')).toMatchInlineSnapshot(` + Object { + "compilerOptions": Object { + "lib": Array [ + "ES2019", + ], + "noEmit": false, + "outDir": "../../dist/out-tsc", + "types": Array [ + "node", + "jest", + ], + }, + "exclude": Array [ + "**/*.spec.ts", + "**/*.test.ts", + ], + "extends": "./tsconfig.json", + "include": Array [ + "./src/**/*.ts", + "./src/**/*.js", + ], + } + `); + expect(readJson(tree, 'scripts/lib-one/tsconfig.spec.json')).toMatchInlineSnapshot(` + Object { + "compilerOptions": Object { + "module": "CommonJS", + "outDir": "dist", + "types": Array [ + "jest", + "node", + ], + }, + "extends": "./tsconfig.json", + "include": Array [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.d.ts", + ], + } + `); + + expect(readDirectory(tree, 'scripts/lib-two')).toMatchInlineSnapshot(` + Array [ + "scripts/lib-two/tsconfig.json", + "scripts/lib-two/src/index.js", + "scripts/lib-two/src/hello.js", + "scripts/lib-two/package.json", + "scripts/lib-two/tsconfig.lib.json", + "scripts/lib-two/tsconfig.spec.json", + "scripts/lib-two/jest.config.js", + "scripts/lib-two/.eslintrc.json", + ] + `); + expect(readDirectory(tree, 'scripts/lib-three')).toMatchInlineSnapshot(` + Array [ + "scripts/lib-three/tsconfig.json", + "scripts/lib-three/src/index.ts", + "scripts/lib-three/src/hello.ts", + "scripts/lib-three/package.json", + "scripts/lib-three/tsconfig.lib.json", + "scripts/lib-three/tsconfig.spec.json", + "scripts/lib-three/jest.config.js", + "scripts/lib-three/.eslintrc.json", + ] + `); + + expect(readDirectory(tree, 'scripts/lib-four-no-barrel')).toMatchInlineSnapshot(` + Array [ + "scripts/lib-four-no-barrel/index.js", + "scripts/lib-four-no-barrel/hello.js", + "scripts/lib-four-no-barrel/tsconfig.json", + "scripts/lib-four-no-barrel/package.json", + "scripts/lib-four-no-barrel/tsconfig.lib.json", + "scripts/lib-four-no-barrel/tsconfig.spec.json", + "scripts/lib-four-no-barrel/jest.config.js", + "scripts/lib-four-no-barrel/.eslintrc.json", + ] + `); + expect(tree.read('scripts/lib-four-no-barrel/jest.config.js', 'utf-8')).toMatchInlineSnapshot(` + "// @ts-check + + /** + * @type {import('@jest/types').Config.InitialOptions} + */ + module.exports = { + displayName: 'scripts-lib-four-no-barrel', + preset: '../../jest.preset.js', + transform: { + '^.+\\\\\\\\.tsx?$': 'babel-jest', + }, + coverageDirectory: './coverage', + testEnvironment: 'node', + };" + `); + expect(readJson(tree, 'scripts/lib-four-no-barrel/.eslintrc.json')).toMatchInlineSnapshot(` + Object { + "extends": Array [ + "plugin:@fluentui/eslint-plugin/node", + "plugin:@fluentui/eslint-plugin/imports", + ], + "overrides": Array [ + Object { + "files": "index.d.ts", + "rules": Object { + "import/no-self-import": "off", + }, + }, + ], + "root": true, + "rules": Object { + "@fluentui/max-len": "off", + "import/no-extraneous-dependencies": Array [ + "error", + Object { + "packageDir": Array [ + ".", + "../../", + ], + }, + ], + }, + } + `); + expect(readJson(tree, 'scripts/lib-four-no-barrel/tsconfig.json')).toMatchInlineSnapshot(` + Object { + "compilerOptions": Object { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "noUnusedLocals": true, + "pretty": true, + "sourceMap": true, + "target": "ES2019", + }, + "extends": "@tsconfig/node14/tsconfig.json", + "files": Array [], + "include": Array [], + "references": Array [ + Object { + "path": "./tsconfig.lib.json", + }, + Object { + "path": "./tsconfig.spec.json", + }, + ], + } + `); + expect(readJson(tree, 'scripts/lib-four-no-barrel/tsconfig.lib.json')).toMatchInlineSnapshot(` + Object { + "compilerOptions": Object { + "lib": Array [ + "ES2019", + ], + "noEmit": false, + "outDir": "../../dist/out-tsc", + "types": Array [ + "node", + "jest", + ], + }, + "exclude": Array [ + "**/*.spec.ts", + "**/*.test.ts", + ], + "extends": "./tsconfig.json", + "include": Array [ + "./**/*.ts", + "./**/*.js", + ], + } + `); + expect(readJson(tree, 'scripts/lib-four-no-barrel/tsconfig.spec.json')).toMatchInlineSnapshot(` + Object { + "compilerOptions": Object { + "module": "CommonJS", + "outDir": "dist", + "types": Array [ + "jest", + "node", + ], + }, + "extends": "./tsconfig.json", + "include": Array [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.d.ts", + ], + } + `); + }); + }); + + describe(`AST`, () => { + it(`should replace relative imports with new package imports within /scripts`, async () => { + await generator(tree, options); + expect(tree.read('scripts/lib-one/src/hello.js', 'utf-8')).toMatchInlineSnapshot(` + "const { hello: hello3 } = require('@fluentui/scripts-lib-three'); + const path = require('path'); + exports.hello = function hello(){}" + `); + expect(readJson(tree, 'scripts/lib-one/package.json').dependencies).toEqual({ + '@fluentui/scripts-lib-three': '*', + }); + + expect(tree.read('scripts/lib-two/src/hello.js', 'utf-8')).toMatchInlineSnapshot(` + "const { hello: hello1 } = require('@fluentui/scripts-lib-one'); + const path = require('path'); + const devkit = require('@scope/devkit'); + exports.hello = function hello(){}" + `); + expect(readJson(tree, 'scripts/lib-two/package.json').dependencies).toEqual({ + '@fluentui/scripts-lib-one': '*', + }); + + expect(tree.read('scripts/lib-three/src/hello.ts', 'utf-8')).toMatchInlineSnapshot(` + "import {hello as hello2} from '@fluentui/scripts-lib-two'; + import * as path from 'path'; + import * as fs from 'fs'; + export function hello(){}" + `); + expect(readJson(tree, 'scripts/lib-three/package.json').dependencies).toEqual({ + '@fluentui/scripts-lib-two': '*', + }); + + expect(tree.read('scripts/lib-four-no-barrel/hello.js', 'utf-8')).toMatchInlineSnapshot(` + "const { hello: hello2 } = require('@fluentui/scripts-lib-two'); + const path = require('path'); + const fs = require('fs'); + exports.hello = function hello(){}" + `); + expect(readJson(tree, 'scripts/lib-four-no-barrel/package.json').dependencies).toEqual({ + '@fluentui/scripts-lib-two': '*', + }); + }); + + it(`should replace absolute imports outside /scripts and update devDependencies`, async () => { + await generator(tree, options); + + expect(tree.read('/dangerfile.ts', 'utf-8')).toMatchInlineSnapshot(` + "import {foo} from '@fluentui/scripts-dangerjs' + + foo();" + `); + + expect(tree.read('/.storybook/main.js', 'utf-8')).toMatchInlineSnapshot(` + "const { hello } = require('@fluentui/scripts-lib-one'); + + module.exports = { + stories: [], + };" + `); + + expect(tree.read('packages/react-one/tool.config.js', 'utf-8')).toMatchInlineSnapshot(` + "const {hello} = require('@fluentui/scripts-lib-one'); + const {hello: hello2} = require('@fluentui/scripts-lib-two');" + `); + expect(readJson(tree, 'packages/react-one/config/api-extractor.json')).toMatchInlineSnapshot(` + Object { + "extends": "@fluentui/scripts-api-extractor/api-extractor.common.json", + } + `); + expect(readJson(tree, 'packages/react-one/package.json').devDependencies).toEqual({ + '@fluentui/scripts-lib-one': '*', + '@fluentui/scripts-lib-two': '*', + '@fluentui/scripts-api-extractor': '*', + }); + + expect(tree.read('packages/react-v0/tool.config.ts', 'utf-8')).toMatchInlineSnapshot(` + "import { hello } from '@fluentui/scripts-lib-three'; + import {hello as hello2} from '@fluentui/scripts-lib-two';" + `); + expect(readJson(tree, 'packages/react-v0/package.json').devDependencies).toEqual({ + '@fluentui/scripts-lib-two': '*', + '@fluentui/scripts-lib-three': '*', + '@fluentui/scripts-gulp': '*', + }); + + expect(readJson(tree, 'packages/react-no-direct-import/package.json').devDependencies).toEqual({}); + + expect(tree.read('apps/todo-app/config.ts', 'utf-8')).toMatchInlineSnapshot( + `"import { hello } from '@fluentui/scripts-lib-three';"`, + ); + expect(readJson(tree, 'apps/todo-app/package.json').devDependencies).toEqual({ + '@fluentui/scripts-lib-three': '*', + }); + }); + }); +}); + +function readDirectory(tree: Tree, dirPath: string) { + const paths: string[] = []; + + visitNotIgnoredFiles(tree, dirPath, path => { + paths.push(path); + }); + + return paths; +} diff --git a/tools/generators/migrate-scripts-to-pkg/index.ts b/tools/generators/migrate-scripts-to-pkg/index.ts new file mode 100644 index 00000000000000..a379bf2b532aa8 --- /dev/null +++ b/tools/generators/migrate-scripts-to-pkg/index.ts @@ -0,0 +1,457 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { + Tree, + formatFiles, + addProjectConfiguration, + writeJson, + joinPathFragments, + offsetFromRoot, + readJson, + stripIndents, + updateJson, + visitNotIgnoredFiles, + applyChangesToString, + ChangeType, + StringChange, + removeProjectConfiguration, +} from '@nrwl/devkit'; +import * as path from 'path'; + +import { MigrateScriptsToPkgGeneratorSchema } from './schema'; +import { TsConfig } from '../../types'; +import { findUpTree } from './lib/utils'; + +interface PackageOptions { + root: string; + sourceRoot?: string; + sourceRootFile?: string; + name: string; + dependencies?: Record; +} + +export default async function (tree: Tree, _schema: MigrateScriptsToPkgGeneratorSchema) { + updateJson(tree, `/package.json`, json => { + (json.workspaces.packages as string[])[4] = 'scripts/*'; + return json; + }); + updateJson(tree, `/lerna.json`, json => { + (json.packages as string[])[3] = 'scripts/*'; + return json; + }); + + tree.delete('/scripts/node_modules'); + tree + .children('/scripts') + .map(childName => joinPathFragments('scripts', childName)) + .filter(childPath => tree.isFile(childPath)) + .forEach(filePath => { + tree.delete(filePath); + }); + + const folders = tree.children('/scripts'); + + // migrate /scripts + folders.forEach(folderName => { + const folderPath = `scripts/${folderName}`; + const projectName = `@fluentui/scripts-${folderName}`; + const options = { root: folderPath, name: projectName }; + + const { dependencies } = createBetweenScriptsDependencies(tree, { ...options, folders }); + + const metadata = createSourceRoot(tree, options); + const extendedOptions = { ...options, dependencies, ...(metadata ? { ...metadata } : null) }; + + createPackageJson(tree, extendedOptions); + createTsConfig(tree, extendedOptions); + createJestConfig(tree, extendedOptions); + createLintConfig(tree, extendedOptions); + addProjectConfiguration(tree, projectName, { + root: options.root, + sourceRoot: extendedOptions.sourceRoot ?? options.root, + projectType: 'library', + tags: ['tools'], + }); + }); + + migrateWholeMonorepo(tree); + removeProjectConfiguration(tree, '@fluentui/scripts'); + + // console.log({ packagesMap: JSON.stringify(packagesMap, null, 2) }); + + // await libraryGenerator(tree, { name: 'foo-bar' }); + + // const normalizedOptions = normalizeOptions(tree, { ...schema, name: 'foo-bar' }); + + // addFiles(tree, normalizedOptions); + + await formatFiles(tree); + + return () => { + // installPackagesTask(tree); + }; +} + +function migrateWholeMonorepo(tree: Tree) { + const ignoredFiles = /.+\.(yml|md)$/; + const ignoredFolders = /^(scripts|tools|typings|.git).+/; + const filesWithoutExtension = /.+\.[a-z]+$/; + const usesScripts = /['"]@fluentui\/scripts\/[a-z-/.]+['"]/; + const scriptsImportRegexToReplace = /@fluentui\/scripts\/[a-z-]+/g; + const packagesMap: { [packageJsonPath: string]: string[] } = {}; + + visitNotIgnoredFiles(tree, '/', filePath => { + if (!filesWithoutExtension.test(filePath)) { + return; + } + if (ignoredFolders.test(filePath)) { + return; + } + if (ignoredFiles.test(filePath)) { + return; + } + // special json files that we need to process + if (filePath.endsWith('.json')) { + if (filePath.endsWith('/package.json')) { + updateJson(tree, filePath, json => { + if (json.devDependencies) { + delete json.devDependencies['@fluentui/scripts']; + + if (tree.exists(joinPathFragments(path.dirname(filePath), 'gulpfile.ts'))) { + json.devDependencies['@fluentui/scripts-gulp'] = '*'; + } + } + return json; + }); + + return; + } + if (!filePath.includes('api-extractor.json')) { + return; + } + } + + const content = tree.read(filePath, 'utf-8') as string; + + if (usesScripts.test(content)) { + const packageJsonPath = findUpTree(tree, 'package.json', filePath); + + // we don't wanna add local devDeps to monorepo package.json root + if (packageJsonPath) { + packagesMap[packageJsonPath] = packagesMap[packageJsonPath] ?? []; + } + + const newContent = content.replace(scriptsImportRegexToReplace, match => { + const newPackagePath = match.replace('scripts/', 'scripts-'); + if (packageJsonPath) { + packagesMap[packageJsonPath].push(newPackagePath); + } + return newPackagePath; + }); + + tree.write(filePath, newContent); + } + }); + + // Update package.json devDeps + Object.entries(packagesMap).forEach(([packageJsonPath, scriptDependencies]) => { + const devDepsObj = scriptDependencies.reduce((acc, curr) => { + acc[curr] = '*'; + return acc; + }, {} as Record); + + updateJson(tree, packageJsonPath, json => { + json.devDependencies = json.devDependencies ?? {}; + delete json.devDependencies['@fluentui/scripts']; + Object.assign(json.devDependencies, devDepsObj); + return json; + }); + }); +} + +function createBetweenScriptsDependencies(tree: Tree, options: PackageOptions & { folders: string[] }) { + const tsImportRegex = /import.+from\s+'([@./a-z-_]+)';/g; + const jsImportRegex = /require\('([@./a-z-_]+)'\);/g; + + const importMaps: { + [filePath: string]: { + content: string; + changes: Array<{ + currentImportPath: string; + newImportPath: string; + startIndex: number; + endIndex: number; + }>; + }; + } = {}; + const dependencies: Record = {}; + + visitNotIgnoredFiles(tree, options.root, filePath => { + if (!(filePath.endsWith('.js') || filePath.endsWith('.ts'))) { + return; + } + if (filePath.includes('.spec.')) { + return; + } + + const content = tree.read(filePath, 'utf-8') as string; + + const tsImports: Array<{ path: string; start: number; end: number }> = []; + const jsImports: Array<{ path: string; start: number; end: number }> = []; + let tsMatchArr: RegExpExecArray | null; + let jsMatchArr: RegExpExecArray | null; + + while ((tsMatchArr = tsImportRegex.exec(content)) !== null) { + const lastIndex = tsImportRegex.lastIndex; + + tsImports.push({ + path: tsMatchArr[1], + start: lastIndex - 2 - tsMatchArr[1].length, + end: lastIndex, + }); + } + + while ((jsMatchArr = jsImportRegex.exec(content)) !== null) { + const lastIndex = jsImportRegex.lastIndex; + jsImports.push({ + path: jsMatchArr[1], + start: lastIndex - 3 - jsMatchArr[1].length, + end: lastIndex, + }); + } + + const imports = [...tsImports, ...jsImports].filter(Boolean); + + imports.forEach(importPath => { + if (!importPath.path.startsWith('../')) { + return; + } + + const scriptsPotentialPackageFolder = path.basename(path.resolve(filePath, importPath.path)); + const scriptsPackageFolder = options.folders.find(folder => folder === scriptsPotentialPackageFolder); + + if (!scriptsPackageFolder) { + return; + } + + importMaps[filePath] = importMaps[filePath] ? importMaps[filePath] : { content, changes: [] }; + importMaps[filePath].changes.push({ + startIndex: importPath.start, + endIndex: importPath.end, + currentImportPath: importPath.path, + newImportPath: `@fluentui/scripts-${scriptsPackageFolder}`, + }); + }); + }); + + Object.entries(importMaps).forEach(([filePath, data]) => { + const applyChanges = data.changes + .map(change => { + dependencies[change.newImportPath] = '*'; + + return [ + { + type: ChangeType.Insert, + index: change.startIndex, + text: change.newImportPath, + }, + { + type: ChangeType.Delete, + start: change.startIndex, + length: change.currentImportPath.length, + }, + ] as StringChange[]; + }) + .flat(); + + const updatedCode = applyChangesToString(data.content, applyChanges); + + // logger.info(`Updating: ${filePath} ===>`); + + tree.write(filePath, updatedCode); + }); + + return { dependencies }; +} + +function createSourceRoot(tree: Tree, options: PackageOptions) { + const indexFile = getIndexFile(); + + if (!indexFile) { + return null; + } + + if (!isBarrelApi(indexFile.content)) { + return null; + } + + // move implementation files to /src folder + visitNotIgnoredFiles(tree, options.root, filePath => { + if (['tsconfig.json', 'README.md'].includes(path.basename(filePath))) { + return; + } + + tree.rename(filePath, `${options.root}/src/${path.relative(options.root, filePath)}`); + }); + + return { + sourceRoot: `${options.root}/src`, + sourceRootFile: `src/${indexFile.indexFile}`, + }; + + function isBarrelApi(content: string) { + if (tree.exists(joinPathFragments(options.root, 'register.js'))) { + return false; + } + if (content.includes(`export {}`)) { + return false; + } + if (!(content.includes(`export`) || content.includes(`module.exports`))) { + return false; + } + return true; + } + + function getIndexFile() { + const paths = { js: joinPathFragments(options.root, 'index.js'), ts: joinPathFragments(options.root, 'index.ts') }; + if (tree.exists(paths.js)) { + return { + content: tree.read(paths.js, 'utf-8') as string, + indexFile: 'index.js', + }; + } + if (tree.exists(paths.ts)) { + return { + content: tree.read(paths.ts, 'utf-8') as string, + indexFile: 'index.ts', + }; + } + return null; + } +} + +function createTsConfig(tree: Tree, options: PackageOptions) { + const sourceRootRelativePath = options.sourceRootFile ? `./src` : '.'; + const tsconfigPaths = { + main: `${options.root}/tsconfig.json`, + spec: `${options.root}/tsconfig.spec.json`, + lib: `${options.root}/tsconfig.lib.json`, + }; + + const currentTsConfig = readJson(tree, tsconfigPaths.main); + + const template = { + main: { + extends: '@tsconfig/node14/tsconfig.json', + compilerOptions: { + target: 'ES2019', + pretty: true, + noEmit: true, + allowJs: true, + checkJs: true, + sourceMap: true, + noUnusedLocals: true, + } as TsConfig['compilerOptions'], + include: [], + files: [], + references: [ + { + path: './tsconfig.lib.json', + }, + { + path: './tsconfig.spec.json', + }, + ], + }, + spec: { + extends: './tsconfig.json', + compilerOptions: { + module: 'CommonJS', + outDir: 'dist', + types: ['jest', 'node'], + } as TsConfig['compilerOptions'], + include: ['**/*.spec.ts', '**/*.test.ts', '**/*.d.ts'], + }, + lib: { + extends: './tsconfig.json', + compilerOptions: { + noEmit: false, + lib: ['ES2019'], + outDir: offsetFromRoot(options.root) + 'dist/out-tsc', + types: currentTsConfig.compilerOptions.types, + } as TsConfig['compilerOptions'], + exclude: ['**/*.spec.ts', '**/*.test.ts'], + include: [`${sourceRootRelativePath}/**/*.ts`, `${sourceRootRelativePath}/**/*.js`], + ...(currentTsConfig.files ? { files: currentTsConfig.files } : null), + }, + }; + + writeJson(tree, tsconfigPaths.main, template.main); + writeJson(tree, tsconfigPaths.lib, template.lib); + writeJson(tree, tsconfigPaths.spec, template.spec); + + return tree; +} +function createJestConfig(tree: Tree, options: PackageOptions) { + const jestConfigPath = `${options.root}/jest.config.js`; + const template = stripIndents` +// @ts-check + +/** + * @type {import('@jest/types').Config.InitialOptions} + */ +module.exports = { + displayName: '${options.name.replace('@fluentui/', '')}', + preset: '${offsetFromRoot(options.root)}jest.preset.js', + transform: { + '^.+\\.tsx?$': 'babel-jest', + }, + coverageDirectory: './coverage', + testEnvironment: 'node', +}; + `; + + tree.write(jestConfigPath, template); + return tree; +} +function createLintConfig(tree: Tree, options: PackageOptions) { + const eslintJsonPath = `${options.root}/.eslintrc.json`; + const template = { + extends: ['plugin:@fluentui/eslint-plugin/node', 'plugin:@fluentui/eslint-plugin/imports'], + rules: { + '@fluentui/max-len': 'off', + 'import/no-extraneous-dependencies': [ + 'error', + { + packageDir: ['.', offsetFromRoot(options.root)], + }, + ], + }, + overrides: [ + { files: `${options.sourceRootFile ? 'src/' : ''}index.d.ts`, rules: { 'import/no-self-import': 'off' } }, + ], + root: true, + }; + + writeJson(tree, eslintJsonPath, template); + return tree; +} +function createPackageJson(tree: Tree, options: PackageOptions) { + const pkgJsonPath = `${options.root}/package.json`; + const template = { + name: options.name, + version: '0.0.1', + private: true, + main: options.sourceRootFile ?? 'index.js', + scripts: { + format: `prettier -w --ignore-path ${offsetFromRoot(options.root)}.prettierignore .`, + 'format:check': `yarn format -c`, + lint: 'eslint --ext .ts,.js .', + test: 'jest --passWithNoTests', + 'type-check': 'tsc -b tsconfig.json', + }, + dependencies: { ...options.dependencies }, + }; + + writeJson(tree, pkgJsonPath, template); + return tree; +} diff --git a/tools/generators/migrate-scripts-to-pkg/lib/utils.spec.ts b/tools/generators/migrate-scripts-to-pkg/lib/utils.spec.ts new file mode 100644 index 00000000000000..fc9d88bcee2d85 --- /dev/null +++ b/tools/generators/migrate-scripts-to-pkg/lib/utils.spec.ts @@ -0,0 +1,43 @@ +import { Tree, writeJson } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { dummyHelper, findUpTree } from './utils'; + +describe(`utils`, () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + }); + it(`should behave...`, () => { + expect(dummyHelper()).toBe(undefined); + }); + + it(`should findUp tree`, () => { + function setup() { + tree.write('packages/one/config/hello.txt', ''); + tree.write('packages/one/src/index.txt', ''); + tree.write('packages/one/config.txt', ''); + writeJson(tree, 'packages/one/package.json', {}); + + tree.write('packages/no-pkg/hello.txt', ''); + tree.write('packages/no-pkg/src/index.txt', ''); + } + + setup(); + + let found = findUpTree(tree, 'package.json', 'packages/one/config/hello.txt'); + expect(found).toBe('packages/one/package.json'); + + found = findUpTree(tree, 'package.json', 'packages/one/src/index.txt'); + expect(found).toBe('packages/one/package.json'); + + found = findUpTree(tree, 'package.json', 'packages/one/config.txt'); + expect(found).toBe('packages/one/package.json'); + + let notFound = findUpTree(tree, 'package.json', 'packages/no-pkg/hello.txt'); + expect(notFound).toBe(undefined); + + notFound = findUpTree(tree, 'package.json', 'packages/no-pkg/src/index.txt'); + expect(notFound).toBe(undefined); + }); +}); diff --git a/tools/generators/migrate-scripts-to-pkg/lib/utils.ts b/tools/generators/migrate-scripts-to-pkg/lib/utils.ts new file mode 100644 index 00000000000000..4ef07c9cc136c8 --- /dev/null +++ b/tools/generators/migrate-scripts-to-pkg/lib/utils.ts @@ -0,0 +1,27 @@ +// use this module to define any kind of generic utilities that are used in more than 1 place within the generator implementation + +import { joinPathFragments, Tree } from '@nrwl/devkit'; +import * as path from 'path'; + +export function dummyHelper() { + return; +} + +export function findUpTree(tree: Tree, fileName: string, start: string, stop = '.') { + let currentDirname = path.dirname(start); + + let foundPath: string | undefined; + + while (currentDirname !== stop) { + const pathToLookFor = joinPathFragments(currentDirname, fileName); + + if (tree.exists(pathToLookFor)) { + foundPath = pathToLookFor; + break; + } + + currentDirname = path.dirname(currentDirname); + } + + return foundPath; +} diff --git a/tools/generators/migrate-scripts-to-pkg/schema.json b/tools/generators/migrate-scripts-to-pkg/schema.json new file mode 100644 index 00000000000000..d9da9036f97eb8 --- /dev/null +++ b/tools/generators/migrate-scripts-to-pkg/schema.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "id": "migrate-scripts-to-pkg", + "type": "object", + "properties": {}, + "required": [] +} diff --git a/tools/generators/migrate-scripts-to-pkg/schema.ts b/tools/generators/migrate-scripts-to-pkg/schema.ts new file mode 100644 index 00000000000000..55b9aca2f7c070 --- /dev/null +++ b/tools/generators/migrate-scripts-to-pkg/schema.ts @@ -0,0 +1 @@ +export interface MigrateScriptsToPkgGeneratorSchema {}