├── .gitignore ├── .npmignore ├── .prettierrc ├── CHANGELOG.md ├── README.md ├── package-lock.json ├── package.json ├── src ├── collection.json ├── common │ ├── models.ts │ └── utils.ts ├── ng-add-setup │ ├── files │ │ ├── tailwind │ │ │ └── tailwind.config.js │ │ └── webpack │ │ │ ├── webpack-prod.config.js │ │ │ └── webpack.config.js │ ├── index.ts │ ├── index_spec.ts │ ├── schema.json │ └── schema.ts └── ng-add │ ├── index.ts │ ├── index_spec.ts │ ├── schema.json │ └── schema.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Outputs 2 | src/**/*.js 3 | src/**/*.js.map 4 | src/**/*.d.ts 5 | !src/**/files/**/*.js 6 | 7 | # IDEs 8 | .idea/ 9 | jsconfig.json 10 | .vscode/ 11 | 12 | # Misc 13 | node_modules/ 14 | npm-debug.log* 15 | yarn-error.log* 16 | 17 | # Mac OSX Finder files. 18 | **/.DS_Store 19 | .DS_Store 20 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Ignores TypeScript files, but keeps definitions. 2 | *.ts 3 | !*.d.ts 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 0.0.5 4 | * added support for build-angular ~0.803.0 5 | 6 | ## 0.0.6 7 | * update tailwindcss and custom-webpack versions. 8 | * enable option to remove schematic from dependencies. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @flakolefluk/tailwind-schematics 2 | ## add TailwindCSS to your Angular project 3 | 4 | ### Requirements 5 | Currently Angular 8+ is supported 6 | 7 | ### Adding TailwindCSS to your project 8 | ``` 9 | ng add @flakolefluk/tailwind-schematics (will use default project) 10 | // or 11 | ng add @flakolefluk/tailwind-schematics --project= 12 | ``` 13 | 14 | ### Building for production 15 | When building your project for production, Prune CSS will be used to removed all the unused utility classes generated by TailwindCSS. 16 | 17 | ### Contributing 18 | This project is in an early stage. 19 | Please report any bugs and feel free to create PR's. 20 | Thank you! 21 | 22 | ### Unit Testing 23 | `npm run test` will run the unit tests, using Jasmine as a runner and test framework. -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flakolefluk/tailwind-schematics", 3 | "version": "0.0.5", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@angular-devkit/core": { 8 | "version": "8.3.20", 9 | "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-8.3.20.tgz", 10 | "integrity": "sha512-UCfW/BJBJnioJU34QennQhA4o+rLoCXWiSrI2LM7yw8/MEM9I8KbqRETP1My3HjHkQnvP+Qh3noedpcu3Nnt8A==", 11 | "requires": { 12 | "ajv": "6.10.2", 13 | "fast-json-stable-stringify": "2.0.0", 14 | "magic-string": "0.25.3", 15 | "rxjs": "6.4.0", 16 | "source-map": "0.7.3" 17 | } 18 | }, 19 | "@angular-devkit/schematics": { 20 | "version": "8.3.20", 21 | "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-8.3.20.tgz", 22 | "integrity": "sha512-sDHZakh4e3A5WenR9zr1x6Va9GNRqQlRhqT3xcbkG88v2M0YqEt7dHB7YwnOhm7zSxiWQM8PdWEQHiQ4iu9NyQ==", 23 | "requires": { 24 | "@angular-devkit/core": "8.3.20", 25 | "rxjs": "6.4.0" 26 | } 27 | }, 28 | "@schematics/angular": { 29 | "version": "8.3.20", 30 | "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-8.3.20.tgz", 31 | "integrity": "sha512-Y20pSJhQ0KQd8Tk2kPQlmpRDNDaoIKMeOOGLT2FgCFrumxZXuIbBgN9fGDgW40iI2sq80bccOeo24RKkn3QpcA==", 32 | "dev": true, 33 | "requires": { 34 | "@angular-devkit/core": "8.3.20", 35 | "@angular-devkit/schematics": "8.3.20" 36 | } 37 | }, 38 | "@types/jasmine": { 39 | "version": "3.5.0", 40 | "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.5.0.tgz", 41 | "integrity": "sha512-kGCRI9oiCxFS6soGKlyzhMzDydfcPix9PpTkr7h11huxOxhWwP37Tg7DYBaQ18eQTNreZEuLkhpbGSqVNZPnnw==" 42 | }, 43 | "@types/node": { 44 | "version": "12.12.17", 45 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.17.tgz", 46 | "integrity": "sha512-Is+l3mcHvs47sKy+afn2O1rV4ldZFU7W8101cNlOd+MRbjM4Onida8jSZnJdTe/0Pcf25g9BNIUsuugmE6puHA==" 47 | }, 48 | "ajv": { 49 | "version": "6.10.2", 50 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", 51 | "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", 52 | "requires": { 53 | "fast-deep-equal": "^2.0.1", 54 | "fast-json-stable-stringify": "^2.0.0", 55 | "json-schema-traverse": "^0.4.1", 56 | "uri-js": "^4.2.2" 57 | } 58 | }, 59 | "balanced-match": { 60 | "version": "1.0.0", 61 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 62 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 63 | }, 64 | "brace-expansion": { 65 | "version": "1.1.11", 66 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 67 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 68 | "requires": { 69 | "balanced-match": "^1.0.0", 70 | "concat-map": "0.0.1" 71 | } 72 | }, 73 | "concat-map": { 74 | "version": "0.0.1", 75 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 76 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 77 | }, 78 | "fast-deep-equal": { 79 | "version": "2.0.1", 80 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", 81 | "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" 82 | }, 83 | "fast-json-stable-stringify": { 84 | "version": "2.0.0", 85 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 86 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" 87 | }, 88 | "fs.realpath": { 89 | "version": "1.0.0", 90 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 91 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 92 | }, 93 | "glob": { 94 | "version": "7.1.6", 95 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 96 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 97 | "requires": { 98 | "fs.realpath": "^1.0.0", 99 | "inflight": "^1.0.4", 100 | "inherits": "2", 101 | "minimatch": "^3.0.4", 102 | "once": "^1.3.0", 103 | "path-is-absolute": "^1.0.0" 104 | } 105 | }, 106 | "inflight": { 107 | "version": "1.0.6", 108 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 109 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 110 | "requires": { 111 | "once": "^1.3.0", 112 | "wrappy": "1" 113 | } 114 | }, 115 | "inherits": { 116 | "version": "2.0.4", 117 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 118 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 119 | }, 120 | "jasmine": { 121 | "version": "3.5.0", 122 | "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.5.0.tgz", 123 | "integrity": "sha512-DYypSryORqzsGoMazemIHUfMkXM7I7easFaxAvNM3Mr6Xz3Fy36TupTrAOxZWN8MVKEU5xECv22J4tUQf3uBzQ==", 124 | "requires": { 125 | "glob": "^7.1.4", 126 | "jasmine-core": "~3.5.0" 127 | } 128 | }, 129 | "jasmine-core": { 130 | "version": "3.5.0", 131 | "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", 132 | "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==" 133 | }, 134 | "json-schema-traverse": { 135 | "version": "0.4.1", 136 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 137 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 138 | }, 139 | "magic-string": { 140 | "version": "0.25.3", 141 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.3.tgz", 142 | "integrity": "sha512-6QK0OpF/phMz0Q2AxILkX2mFhi7m+WMwTRg0LQKq/WBB0cDP4rYH3Wp4/d3OTXlrPLVJT/RFqj8tFeAR4nk8AA==", 143 | "requires": { 144 | "sourcemap-codec": "^1.4.4" 145 | } 146 | }, 147 | "minimatch": { 148 | "version": "3.0.4", 149 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 150 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 151 | "requires": { 152 | "brace-expansion": "^1.1.7" 153 | } 154 | }, 155 | "once": { 156 | "version": "1.4.0", 157 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 158 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 159 | "requires": { 160 | "wrappy": "1" 161 | } 162 | }, 163 | "path-is-absolute": { 164 | "version": "1.0.1", 165 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 166 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 167 | }, 168 | "punycode": { 169 | "version": "2.1.1", 170 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 171 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 172 | }, 173 | "rxjs": { 174 | "version": "6.4.0", 175 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", 176 | "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==", 177 | "requires": { 178 | "tslib": "^1.9.0" 179 | } 180 | }, 181 | "source-map": { 182 | "version": "0.7.3", 183 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", 184 | "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" 185 | }, 186 | "sourcemap-codec": { 187 | "version": "1.4.6", 188 | "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz", 189 | "integrity": "sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg==" 190 | }, 191 | "tslib": { 192 | "version": "1.10.0", 193 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", 194 | "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" 195 | }, 196 | "typescript": { 197 | "version": "3.7.3", 198 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.3.tgz", 199 | "integrity": "sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw==" 200 | }, 201 | "uri-js": { 202 | "version": "4.2.2", 203 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 204 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 205 | "requires": { 206 | "punycode": "^2.1.0" 207 | } 208 | }, 209 | "wrappy": { 210 | "version": "1.0.2", 211 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 212 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flakolefluk/tailwind-schematics", 3 | "version": "0.0.6", 4 | "description": "TailwindCSS schematics for angular", 5 | "scripts": { 6 | "build": "tsc -p tsconfig.json", 7 | "test": "npm run build && jasmine src/**/*_spec.js" 8 | }, 9 | "keywords": [ 10 | "schematics", 11 | "tailwindCSS", 12 | "tailwind", 13 | "angular" 14 | ], 15 | "ng-add": { 16 | "save": false 17 | }, 18 | "author": "Ignacio Falk", 19 | "license": "MIT", 20 | "schematics": "./src/collection.json", 21 | "dependencies": { 22 | "@angular-devkit/core": "^8.3.20", 23 | "@angular-devkit/schematics": "^8.3.20", 24 | "jasmine": "^3.5.0", 25 | "typescript": "~3.7.3" 26 | }, 27 | "devDependencies": { 28 | "@schematics/angular": "^8.3.20", 29 | "@types/jasmine": "^3.5.0", 30 | "@types/node": "^12.12.17" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "ssh://git@github.com:flakolefluk/tailwindcss-schematics.git" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/flakolefluk/tailwindcss-schematics/issues", 38 | "email": "flakolefluk@gmail.com" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json", 3 | "schematics": { 4 | "ng-add": { 5 | "description": "Adds tailwindCSS to an Angular project", 6 | "factory": "./ng-add/index#ngAdd", 7 | "schema": "./ng-add/schema.json" 8 | }, 9 | "ng-add-setup": { 10 | "description": "Setups project before installing dependencies", 11 | "factory": "./ng-add-setup/index#ngAddSetup", 12 | "private": true, 13 | "schema": "./ng-add-setup/schema.json" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/common/models.ts: -------------------------------------------------------------------------------- 1 | export interface Workspace { 2 | projects: { [name: string]: Project }; 3 | defaultProject: string; 4 | } 5 | 6 | export interface Project { 7 | architect: { 8 | build?: any; 9 | serve?: any; 10 | }; 11 | schematics?: any; 12 | sourceRoot?: string; 13 | root?: string; 14 | } 15 | 16 | export interface PackageJson { 17 | dependencies?: any; 18 | devDependencies?: any; 19 | } 20 | -------------------------------------------------------------------------------- /src/common/utils.ts: -------------------------------------------------------------------------------- 1 | import { Tree, SchematicsException } from '@angular-devkit/schematics'; 2 | import { Workspace, Project, PackageJson } from './models'; 3 | 4 | export function getWorkspace(tree: Tree): Workspace { 5 | if (!tree.exists('angular.json')) { 6 | throw new SchematicsException( 7 | 'Could not find Angular workspace configuration', 8 | ); 9 | } 10 | 11 | try { 12 | return JSON.parse(tree.read(`angular.json`)!.toString()) as Workspace; 13 | } catch (e) { 14 | throw new SchematicsException('Error parsing Workspace'); 15 | } 16 | } 17 | 18 | export function getProject(workspace: Workspace, name: string): Project { 19 | const projects = workspace.projects; 20 | 21 | if (!Object.keys(projects).includes(name)) { 22 | throw new SchematicsException('Project not found'); 23 | } 24 | 25 | return projects[name]; 26 | } 27 | 28 | export function getProjectSrcRoot(project: Project): string { 29 | return project.sourceRoot ? `/${project.sourceRoot}` : `/${project.root}/src`; 30 | } 31 | 32 | export function getProjectStylesExt(project: Project) { 33 | if ( 34 | project.schematics && 35 | project.schematics['@schematics/angular:component'] 36 | ) { 37 | return ( 38 | project.schematics['@schematics/angular:component'].style || 39 | project.schematics['@schematics/angular:component'].styleext || 40 | 'css' 41 | ); 42 | } 43 | return 'css'; 44 | } 45 | 46 | export function getPackageJson(tree: Tree) { 47 | if (!tree.exists('package.json')) { 48 | throw new SchematicsException('package.json not found.'); 49 | } 50 | 51 | try { 52 | return JSON.parse(tree.read('package.json')!.toString()) as PackageJson; 53 | } catch (e) { 54 | throw new SchematicsException('Error parsing packageJson'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/ng-add-setup/files/tailwind/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | extend: {} 4 | }, 5 | variants: {}, 6 | plugins: [] 7 | }; 8 | -------------------------------------------------------------------------------- /src/ng-add-setup/files/webpack/webpack-prod.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | rules: [ 4 | { 5 | test: /\.<%= stylesExt %>$/, 6 | use: [ 7 | { 8 | loader: "postcss-loader", 9 | options: { 10 | plugins: [ 11 | require("tailwindcss")("./tailwind.config.js"), 12 | require("@fullhuman/postcss-purgecss")({ 13 | content: [ 14 | "./src/index.html", 15 | "./src/**/*.component.html", 16 | "./src/**/*.component.ts" 17 | ], 18 | defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || [], 19 | whitelist: [ 20 | ':host', 21 | '::ng-deep', 22 | ':host-context' 23 | ] 24 | }), 25 | require("autoprefixer") 26 | ] 27 | } 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/ng-add-setup/files/webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | rules: [ 4 | { 5 | test: /\.<%= stylesExt %>$/, 6 | use: [ 7 | { 8 | loader: "postcss-loader", 9 | options: { 10 | plugins: [ 11 | require("tailwindcss")("./tailwind.config.js"), 12 | require("autoprefixer") 13 | ] 14 | } 15 | } 16 | ] 17 | } 18 | ] 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/ng-add-setup/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Rule, 3 | SchematicContext, 4 | Tree, 5 | chain, 6 | url, 7 | mergeWith, 8 | Source, 9 | template, 10 | apply, 11 | SchematicsException, 12 | move, 13 | } from '@angular-devkit/schematics'; 14 | 15 | import * as path from 'path'; 16 | 17 | import { NgAddSetupOptions } from './schema'; 18 | import { normalize } from '@angular-devkit/core'; 19 | import { PackageJson, Workspace } from '../common/models'; 20 | import { 21 | getWorkspace, 22 | getProject, 23 | getProjectSrcRoot, 24 | getProjectStylesExt, 25 | getPackageJson, 26 | } from '../common/utils'; 27 | 28 | export function ngAddSetup(options: NgAddSetupOptions): Rule { 29 | return (tree: Tree, _context: SchematicContext) => { 30 | const workspace = getWorkspace(tree); 31 | const project = getProject(workspace, options.project); 32 | const root = getProjectSrcRoot(project); 33 | const stylesExt = getProjectStylesExt(project); 34 | const packageJson = getPackageJson(tree); 35 | 36 | return chain([ 37 | addWebpackConfigFiles(stylesExt, root), 38 | addTailwindConfigFile(root), 39 | updateStylesFile(root, stylesExt), 40 | updateAngularConfig(workspace, options.project, root), 41 | updatePackageJson(packageJson), 42 | ])(tree, _context); 43 | }; 44 | } 45 | 46 | function addWebpackConfigFiles( 47 | stylesExt: string, 48 | projectSrcRoot: string, 49 | ): Rule { 50 | return (tree: Tree, _context: SchematicContext) => { 51 | const source: Source = url('files/webpack'); 52 | return mergeWith( 53 | apply(source, [ 54 | template({ stylesExt }), 55 | move(normalize(`${projectSrcRoot}/..`)), 56 | ]), 57 | )(tree, _context); 58 | }; 59 | } 60 | 61 | function addTailwindConfigFile(projectSrcRoot: string): Rule { 62 | return (tree: Tree, _context: SchematicContext) => { 63 | const source: Source = url('files/tailwind'); 64 | return mergeWith(apply(source, [move(normalize(`${projectSrcRoot}/..`))]))( 65 | tree, 66 | _context, 67 | ); 68 | }; 69 | } 70 | 71 | function updatePackageJson(pkgJson: PackageJson): Rule { 72 | return (tree: Tree, _context: SchematicContext): Tree => { 73 | let customBuilderVersion: string = ''; 74 | 75 | pkgJson.devDependencies = pkgJson.devDependencies || {}; 76 | 77 | const builderVersion = 78 | pkgJson.devDependencies['@angular-devkit/build-angular']; 79 | 80 | if (builderVersion) { 81 | const partialVersion = builderVersion.substring( 82 | builderVersion.indexOf('.') + 1, 83 | builderVersion.lastIndexOf('.'), 84 | ); 85 | 86 | if(parseInt(partialVersion, 10)>=801){ 87 | customBuilderVersion = '~8.4.1' 88 | } else 89 | customBuilderVersion = `~${partialVersion[0]}.${partialVersion[2]}.0`; 90 | } 91 | 92 | pkgJson.devDependencies['@angular-builders/custom-webpack'] = 93 | pkgJson.devDependencies['@angular-builders/custom-webpack'] || 94 | customBuilderVersion || 95 | '~8.0.0'; 96 | 97 | pkgJson.devDependencies['@angular-devkit/build-angular'] = 98 | builderVersion || '~0.800.0'; 99 | 100 | pkgJson.devDependencies['@fullhuman/postcss-purgecss'] = 101 | pkgJson.devDependencies['@fullhuman/postcss-purgecss'] || '~1.3.0'; 102 | 103 | pkgJson.devDependencies['tailwindcss'] = 104 | pkgJson.devDependencies['tailwindcss'] || '~1.1.4'; 105 | 106 | tree.overwrite('package.json', JSON.stringify(pkgJson, null, 2)); 107 | 108 | return tree; 109 | }; 110 | } 111 | 112 | function updateStylesFile(projectSrcRoot: string, stylesExt: string): Rule { 113 | return (tree: Tree, _context: SchematicContext) => { 114 | const file = tree.read(`${projectSrcRoot}/styles.${stylesExt}`); 115 | 116 | if (!file) { 117 | throw new SchematicsException('Style file not found.'); 118 | } 119 | 120 | const fileContent = file.toString(); 121 | 122 | const imports = [ 123 | '@tailwind base;', 124 | '@tailwind components;', 125 | '@tailwind utilities;', 126 | ]; 127 | 128 | const recorder = tree.beginUpdate(`${projectSrcRoot}/styles.${stylesExt}`); 129 | imports.forEach(imported => { 130 | if (!fileContent.includes(imported)) { 131 | recorder.insertLeft(0, `${imported}\n`); 132 | } 133 | }); 134 | tree.commitUpdate(recorder); 135 | return tree; 136 | }; 137 | } 138 | 139 | function updateAngularConfig( 140 | workspace: Workspace, 141 | projectName: string, 142 | projectSrcRoot: string, 143 | ): Rule { 144 | return (tree: Tree, _context: SchematicContext) => { 145 | workspace.projects[projectName].architect.build.builder = 146 | '@angular-builders/custom-webpack:browser'; 147 | 148 | workspace.projects[ 149 | projectName 150 | ].architect.build.options.customWebpackConfig = { 151 | path: path.join(projectSrcRoot, '..', 'webpack.config.js'), 152 | }; 153 | 154 | workspace.projects[ 155 | projectName 156 | ].architect.build.configurations.production.customWebpackConfig = { 157 | path: path.join(projectSrcRoot, '..', 'webpack-prod.config.js'), 158 | }; 159 | 160 | workspace.projects[projectName].architect.serve.builder = 161 | '@angular-builders/custom-webpack:dev-server'; 162 | tree.overwrite('angular.json', JSON.stringify(workspace, null, 2)); 163 | }; 164 | } 165 | -------------------------------------------------------------------------------- /src/ng-add-setup/index_spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SchematicTestRunner, 3 | UnitTestTree, 4 | } from '@angular-devkit/schematics/testing'; 5 | import * as path from 'path'; 6 | 7 | const collectionPath = path.join(__dirname, '../collection.json'); 8 | 9 | describe('ng-add-setup', () => { 10 | const workspaceOptions = { 11 | name: 'workspace', 12 | newProjectRoot: 'projects', 13 | version: '8.0.0', 14 | }; 15 | 16 | const appOptions = { 17 | name: 'testApp', 18 | inlineStyle: false, 19 | inlineTemplate: false, 20 | routing: false, 21 | style: 'css', 22 | skipTests: false, 23 | skipPackageJson: false, 24 | }; 25 | 26 | const runner = new SchematicTestRunner('schematics', collectionPath); 27 | 28 | let sourceTree: UnitTestTree; 29 | 30 | describe('CSS', () => { 31 | beforeEach(async () => { 32 | sourceTree = await runner 33 | .runExternalSchematicAsync( 34 | '@schematics/angular', 35 | 'workspace', 36 | workspaceOptions, 37 | ) 38 | .toPromise(); 39 | sourceTree = await runner 40 | .runExternalSchematicAsync( 41 | '@schematics/angular', 42 | 'application', 43 | appOptions, 44 | sourceTree, 45 | ) 46 | .toPromise(); 47 | }); 48 | 49 | it('adds and updates files', () => { 50 | const tree = runner.runSchematic( 51 | 'ng-add-setup', 52 | { project: 'testApp' }, 53 | sourceTree, 54 | ); 55 | 56 | expect(tree.files).toContain('/projects/testApp/tailwind.config.js'); 57 | expect(tree.files).toContain('/projects/testApp/webpack-prod.config.js'); 58 | expect(tree.files).toContain('/projects/testApp/webpack.config.js'); 59 | expect(tree.files).toContain('/projects/testApp/src/styles.css'); 60 | 61 | const stylesFile = tree.readContent('/projects/testApp/src/styles.css'); 62 | 63 | expect(stylesFile).toContain('@tailwind base;'); 64 | expect(stylesFile).toContain('@tailwind components;'); 65 | expect(stylesFile).toContain('@tailwind utilities;'); 66 | 67 | const workspace = JSON.parse(tree.readContent('angular.json')); 68 | const app = workspace.projects.testApp; 69 | 70 | expect(app.architect.build.builder).toBe( 71 | '@angular-builders/custom-webpack:browser', 72 | ); 73 | 74 | expect(app.architect.build.options.customWebpackConfig.path).toBe( 75 | '/projects/testApp/webpack.config.js', 76 | ); 77 | 78 | expect( 79 | app.architect.build.configurations.production.customWebpackConfig.path, 80 | ).toBe('/projects/testApp/webpack-prod.config.js'); 81 | 82 | expect(app.architect.serve.builder).toBe( 83 | '@angular-builders/custom-webpack:dev-server', 84 | ); 85 | }); 86 | }); 87 | 88 | describe('SCSS', () => { 89 | beforeEach(async () => { 90 | sourceTree = await runner 91 | .runExternalSchematicAsync( 92 | '@schematics/angular', 93 | 'workspace', 94 | workspaceOptions, 95 | ) 96 | .toPromise(); 97 | sourceTree = await runner 98 | .runExternalSchematicAsync( 99 | '@schematics/angular', 100 | 'application', 101 | { ...appOptions, style: 'scss' }, 102 | sourceTree, 103 | ) 104 | .toPromise(); 105 | }); 106 | 107 | it('adds and updates files', () => { 108 | const tree = runner.runSchematic( 109 | 'ng-add-setup', 110 | { project: 'testApp' }, 111 | sourceTree, 112 | ); 113 | 114 | expect(tree.files).toContain('/projects/testApp/tailwind.config.js'); 115 | expect(tree.files).toContain('/projects/testApp/webpack-prod.config.js'); 116 | expect(tree.files).toContain('/projects/testApp/webpack.config.js'); 117 | expect(tree.files).toContain('/projects/testApp/src/styles.scss'); 118 | 119 | const stylesFile = tree.readContent('/projects/testApp/src/styles.scss'); 120 | 121 | expect(stylesFile).toContain('@tailwind base;'); 122 | expect(stylesFile).toContain('@tailwind components;'); 123 | expect(stylesFile).toContain('@tailwind utilities;'); 124 | 125 | const workspace = JSON.parse(tree.readContent('angular.json')); 126 | const app = workspace.projects.testApp; 127 | 128 | expect(app.architect.build.builder).toBe( 129 | '@angular-builders/custom-webpack:browser', 130 | ); 131 | 132 | expect(app.architect.build.options.customWebpackConfig.path).toBe( 133 | '/projects/testApp/webpack.config.js', 134 | ); 135 | 136 | expect( 137 | app.architect.build.configurations.production.customWebpackConfig.path, 138 | ).toBe('/projects/testApp/webpack-prod.config.js'); 139 | 140 | expect(app.architect.serve.builder).toBe( 141 | '@angular-builders/custom-webpack:dev-server', 142 | ); 143 | }); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /src/ng-add-setup/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "id": "tailwind-ng-add-schematic", 4 | "title": "Add tailwind to angular project", 5 | "type": "object", 6 | "properties": { 7 | "project": { 8 | "type": "string", 9 | "description": "The name of the project." 10 | } 11 | }, 12 | "required": ["project"], 13 | "additionalProperties": true 14 | } 15 | -------------------------------------------------------------------------------- /src/ng-add-setup/schema.ts: -------------------------------------------------------------------------------- 1 | export interface NgAddSetupOptions { 2 | project: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/ng-add/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Rule, 3 | SchematicContext, 4 | Tree, 5 | SchematicsException, 6 | } from '@angular-devkit/schematics'; 7 | import { 8 | NodePackageInstallTask, 9 | RunSchematicTask, 10 | } from '@angular-devkit/schematics/tasks'; 11 | import { NgAddOptions } from './schema'; 12 | import { getPackageJson, getWorkspace } from '../common/utils'; 13 | 14 | export function ngAdd(options: NgAddOptions): Rule { 15 | return (tree: Tree, context: SchematicContext) => { 16 | const workspace = getWorkspace(tree); 17 | const packageJson = getPackageJson(tree); 18 | const projectName = options.project || workspace.defaultProject; 19 | 20 | const coreVersion: string = packageJson.dependencies['@angular/core']; 21 | 22 | if (!coreVersion) { 23 | throw new SchematicsException( 24 | 'Could not find @angular/core version in package.json.', 25 | ); 26 | } 27 | 28 | const majorVersion: number = parseInt( 29 | coreVersion.split('.')[0].replace(/\D/g, ''), 30 | 10, 31 | ); 32 | 33 | if (majorVersion < 8) { 34 | throw new SchematicsException('Minimum version requirement not met.'); 35 | } 36 | 37 | const setupId = context.addTask( 38 | new RunSchematicTask('ng-add-setup', {project: projectName }), 39 | ); 40 | 41 | if(!options.skipInstall){ 42 | context.addTask(new NodePackageInstallTask(), [setupId]); 43 | } 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/ng-add/index_spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SchematicTestRunner, 3 | UnitTestTree, 4 | } from '@angular-devkit/schematics/testing'; 5 | import * as path from 'path'; 6 | import { Tree } from '@angular-devkit/schematics'; 7 | 8 | const collectionPath = path.join(__dirname, '../collection.json'); 9 | const runner = new SchematicTestRunner('schematics', collectionPath); 10 | 11 | describe('ng-add', () => { 12 | const workspaceOptions = { 13 | name: 'workspace', 14 | newProjectRoot: 'projects', 15 | version: '8.0.0', 16 | }; 17 | 18 | const appOptions = { 19 | name: 'testApp', 20 | inlineStyle: false, 21 | inlineTemplate: false, 22 | routing: false, 23 | style: 'css', 24 | skipTests: false, 25 | skipPackageJson: false, 26 | }; 27 | 28 | let sourceTree: UnitTestTree; 29 | 30 | describe('when in an angular workspace', () => { 31 | beforeEach(async () => { 32 | sourceTree = await runner 33 | .runExternalSchematicAsync( 34 | '@schematics/angular', 35 | 'workspace', 36 | workspaceOptions, 37 | ) 38 | .toPromise(); 39 | sourceTree = await runner 40 | .runExternalSchematicAsync( 41 | '@schematics/angular', 42 | 'application', 43 | appOptions, 44 | sourceTree, 45 | ) 46 | .toPromise(); 47 | }); 48 | 49 | it('schedules two tasks by default', () => { 50 | runner.runSchematic('ng-add', { project: 'testApp' }, sourceTree); 51 | expect(runner.tasks.length).toBe(2); 52 | expect(runner.tasks.some(task => task.name === 'run-schematic')).toBe( 53 | true, 54 | ); 55 | expect(runner.tasks.some(task => task.name === 'node-package')).toBe( 56 | true, 57 | ); 58 | }); 59 | 60 | it('will not install dependencies if skipInstall is true', () => { 61 | runner.runSchematic( 62 | 'ng-add', 63 | { project: 'testApp', skipInstall: true }, 64 | sourceTree, 65 | ); 66 | expect(runner.tasks.length).toBe(1); 67 | expect(runner.tasks.some(task => task.name === 'run-schematic')).toBe( 68 | true, 69 | ); 70 | }); 71 | }); 72 | 73 | describe('when not in an angular workspace', () => { 74 | it('should throw', () => { 75 | let errorMessage; 76 | try { 77 | runner.runSchematic('ng-add', {}, Tree.empty()); 78 | } catch (e) { 79 | errorMessage = e.message; 80 | } 81 | expect(errorMessage).toMatch(/Could not find Angular workspace configuration/); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/ng-add/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "id": "tailwind-ng-add-schematic", 4 | "title": "Add tailwind to angular project", 5 | "type": "object", 6 | "properties": { 7 | "project": { 8 | "type": "string", 9 | "description": "The name of the project." 10 | }, 11 | "skipInstall": { 12 | "type": "boolean", 13 | "description": "Condition to run npm install" 14 | } 15 | }, 16 | "required": [], 17 | "additionalProperties": false 18 | } 19 | -------------------------------------------------------------------------------- /src/ng-add/schema.ts: -------------------------------------------------------------------------------- 1 | export interface NgAddOptions { 2 | project: string; 3 | skipInstall: boolean; 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "tsconfig", 4 | "lib": [ 5 | "es2018", 6 | "dom" 7 | ], 8 | "declaration": true, 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "noEmitOnError": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitAny": true, 14 | "noImplicitThis": true, 15 | "noUnusedParameters": true, 16 | "noUnusedLocals": true, 17 | "rootDir": "src/", 18 | "skipDefaultLibCheck": true, 19 | "skipLibCheck": true, 20 | "sourceMap": true, 21 | "strictNullChecks": true, 22 | "target": "es6", 23 | "types": [ 24 | "jasmine", 25 | "node" 26 | ] 27 | }, 28 | "include": [ 29 | "src/**/*" 30 | ], 31 | "exclude": [ 32 | "src/*/files/**/*" 33 | ] 34 | } 35 | --------------------------------------------------------------------------------