diff --git a/README.md b/README.md index b4f5ef2..c067a14 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ This is a highly experimental snowpack plugin. Has only been tested to work on brand new Angular 11 project generated through `ng new `. Working setup could be seen [here](https://github.com/phantasmalmira/AngularSnowpackDemo). +## Style Preprocessors + +This plugin does not support style preprocessing yet, implementing a working style preprocessing plugin is simple enough, but it would mean that the plugin has to preprocess the styles as well, an ideal solution is to use other snowpack plugins to feed their output into this plugin, which for the time being I haven't found the solution for yet. Please do contribute by opening a pull request if you have an idea. + ## Usage ```bash @@ -12,7 +16,6 @@ npm i --save-dev angular-snowpack-plugin // snowpack.config.js { - "installs": ["@angular/common"], "plugins": [ [ "angular-snowpack-plugin", @@ -26,12 +29,13 @@ npm i --save-dev angular-snowpack-plugin ## Plugin Options -| Name | Type | Description | Default | -| ------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | -| `src` | `string` | Relative path to the source directory of your angular project. | `src` | -| `logLevel` | `'normal' \| 'debug'` | Logging verbosity of the plugin. | `normal` | -| `tsConfig` | `string` | Relative path to the build options tsconfig of your Angular project, check in `angular.json`. | `tsconfig.app.json` | -| `ngccTargets` | `string[]` | `ngcc` targets that the plugin will attempt to run `ngcc` with on each startup, values here will be extending the default value. | `['@angular/platform-browser']` | +| Name | Type | Description | Default | +| ---------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| `src` | `string` | Relative path to the source directory of your angular project. | `src` | +| `angularJson` | `string` | Relative path to `angular.json` of your Angular project. | `angular.json` | +| `angularProject` | `string` | Target project of the build as according to `angular.json` | default project defined in `angular.json` | +| `ngccTargets` | `string[]` | `ngcc` targets that the plugin will attempt to run `ngcc` with on each startup, values here will be extending the default value. | `['@angular/core', '@angular/common', '@angular/platform-browser-dynamic']` | +| `errorToBrowser` | `boolean` | Determines whether a type-check error will be pushed to the browser as a build error, note that this only applies to dev mode, build and first compilation will push error to browser regardless, `false` will mimic the behavior of `ng serve` | `true` | ## Important Notes diff --git a/package.json b/package.json index 8c98036..e6b7908 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-snowpack-plugin", - "version": "1.0.1", + "version": "2.0.0", "description": "Snowpack Plugin for angular projects", "main": "dist/index.js", "repository": { @@ -15,21 +15,16 @@ "author": "PhantasmalMira", "license": "ISC", "dependencies": { - "execa": "^5.0.0", - "less": "^4.0.0", - "sass": "^1.32.0", - "stylus": "^0.54.8" + "execa": "^5.0.0" }, "devDependencies": { "@angular/compiler": "^11.0.5", "@angular/compiler-cli": "^11.0.5", - "@types/less": "^3.0.2", - "@types/sass": "^1.16.0", - "@types/stylus": "^0.48.33", "snowpack": "^2.18.5", "typescript": "4.0" }, "peerDependencies": { + "@angular/compiler": "*", "@angular/compiler-cli": "*", "snowpack": "^2.18.5", "typescript": "4.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 621dd83..3a9a05a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,14 +1,8 @@ dependencies: execa: 5.0.0 - less: 4.0.0 - sass: 1.32.0 - stylus: 0.54.8 devDependencies: '@angular/compiler': 11.0.5 '@angular/compiler-cli': 11.0.5_ae04eea250591f41b901d33a8893b81f - '@types/less': 3.0.2 - '@types/sass': 1.16.0 - '@types/stylus': 0.48.33 snowpack: 2.18.5 typescript: 4.0.5 lockfileVersion: 5.2 @@ -363,10 +357,6 @@ packages: dev: true resolution: integrity: sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw== - /@types/less/3.0.2: - dev: true - resolution: - integrity: sha512-62vfe65cMSzYaWmpmhqCMMNl0khen89w57mByPi1OseGfcV/LV03fO8YVrNj7rFQsRWNJo650WWyh6m7p8vZmA== /@types/node/14.14.16: dev: true resolution: @@ -387,18 +377,6 @@ packages: dev: true resolution: integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== - /@types/sass/1.16.0: - dependencies: - '@types/node': 14.14.16 - dev: true - resolution: - integrity: sha512-2XZovu4NwcqmtZtsBR5XYLw18T8cBCnU2USFHTnYLLHz9fkhnoEMoDsqShJIOFsFhn5aJHjweiUUdTrDGujegA== - /@types/stylus/0.48.33: - dependencies: - '@types/node': 14.14.16 - dev: true - resolution: - integrity: sha512-2uxz/OykfCkFOewBMw55GYVW9MGGgvmOvMR0bnLKD6HybK1QFspJlEwrHG9L5NDImoGRCbCfGFqlcoczfcf+RA== /address/1.1.2: dev: true engines: @@ -440,6 +418,7 @@ packages: dependencies: normalize-path: 3.0.0 picomatch: 2.2.2 + dev: true engines: node: '>= 8' resolution: @@ -451,14 +430,8 @@ packages: dev: true resolution: integrity: sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== - /atob/2.1.2: - dev: false - engines: - node: '>= 4.5.0' - hasBin: true - resolution: - integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== /balanced-match/1.0.0: + dev: true resolution: integrity: sha1-ibTRmasr7kneFk6gK4nORi1xt2c= /big.js/5.2.2: @@ -466,6 +439,7 @@ packages: resolution: integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== /binary-extensions/2.1.0: + dev: true engines: node: '>=8' resolution: @@ -478,11 +452,13 @@ packages: dependencies: balanced-match: 1.0.0 concat-map: 0.0.1 + dev: true resolution: integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== /braces/3.0.2: dependencies: fill-range: 7.0.1 + dev: true engines: node: '>=8' resolution: @@ -602,6 +578,7 @@ packages: is-glob: 4.0.1 normalize-path: 3.0.0 readdirp: 3.5.0 + dev: true engines: node: '>= 8.10.0' optionalDependencies: @@ -677,6 +654,7 @@ packages: resolution: integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== /concat-map/0.0.1: + dev: true resolution: integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= /convert-source-map/1.7.0: @@ -685,12 +663,6 @@ packages: dev: true resolution: integrity: sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== - /copy-anything/2.0.1: - dependencies: - is-what: 3.12.0 - dev: false - resolution: - integrity: sha512-lA57e7viQHOdPQcrytv5jFeudZZOXuyk47lZym279FiDQ8jeZomXiGuVf6ffMKkJ+3TIai3J1J3yi6M+/4U35g== /cosmiconfig/7.0.0: dependencies: '@types/parse-json': 4.0.0 @@ -712,12 +684,6 @@ packages: node: '>= 8' resolution: integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - /css-parse/2.0.0: - dependencies: - css: 2.2.4 - dev: false - resolution: - integrity: sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q= /css-select/3.1.2: dependencies: boolbase: 1.0.0 @@ -734,15 +700,6 @@ packages: node: '>= 6' resolution: integrity: sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A== - /css/2.2.4: - dependencies: - inherits: 2.0.4 - source-map: 0.6.1 - source-map-resolve: 0.5.3 - urix: 0.1.0 - dev: false - resolution: - integrity: sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw== /cssesc/3.0.0: dev: true engines: @@ -756,12 +713,6 @@ packages: dev: true resolution: integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - /debug/3.1.0: - dependencies: - ms: 2.0.0 - dev: false - resolution: - integrity: sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== /debug/4.3.1: dependencies: ms: 2.1.2 @@ -775,12 +726,6 @@ packages: optional: true resolution: integrity: sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== - /decode-uri-component/0.2.0: - dev: false - engines: - node: '>=0.10' - resolution: - integrity: sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= /decompress-response/6.0.0: dependencies: mimic-response: 3.1.0 @@ -865,14 +810,6 @@ packages: dev: true resolution: integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== - /errno/0.1.8: - dependencies: - prr: 1.0.1 - dev: false - hasBin: true - optional: true - resolution: - integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== /error-ex/1.3.2: dependencies: is-arrayish: 0.2.1 @@ -980,6 +917,7 @@ packages: /fill-range/7.0.1: dependencies: to-regex-range: 5.0.1 + dev: true engines: node: '>=8' resolution: @@ -1040,10 +978,12 @@ packages: resolution: integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== /fs.realpath/1.0.0: + dev: true resolution: integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8= /fsevents/2.1.3: deprecated: Please update to v 2.2.x + dev: true engines: node: ^8.16.0 || ^10.6.0 || >=11.0.0 optional: true @@ -1090,6 +1030,7 @@ packages: /glob-parent/5.1.1: dependencies: is-glob: 4.0.1 + dev: true engines: node: '>= 6' resolution: @@ -1102,6 +1043,7 @@ packages: minimatch: 3.0.4 once: 1.4.0 path-is-absolute: 1.0.1 + dev: true resolution: integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== /globals/11.12.0: @@ -1129,6 +1071,7 @@ packages: resolution: integrity: sha512-9aYdZL+6nHmvJwHALLwKSUZ0hMwGaJGYv3hoPLPgnT8BoBXm1SjnZeky+91tfwJaDzun2s4RsBRy48IEYv2q2Q== /graceful-fs/4.2.4: + dev: true resolution: integrity: sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== /has-flag/3.0.0: @@ -1207,14 +1150,6 @@ packages: node: '>= 6' resolution: integrity: sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== - /image-size/0.5.5: - dev: false - engines: - node: '>=0.10.0' - hasBin: true - optional: true - resolution: - integrity: sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= /import-fresh/3.3.0: dependencies: parent-module: 1.0.1 @@ -1248,6 +1183,7 @@ packages: dependencies: once: 1.4.0 wrappy: 1.0.2 + dev: true resolution: integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= /inherits/2.0.1: @@ -1255,6 +1191,7 @@ packages: resolution: integrity: sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= /inherits/2.0.4: + dev: true resolution: integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== /is-arrayish/0.2.1: @@ -1264,6 +1201,7 @@ packages: /is-binary-path/2.1.0: dependencies: binary-extensions: 2.1.0 + dev: true engines: node: '>=8' resolution: @@ -1290,6 +1228,7 @@ packages: resolution: integrity: sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw== /is-extglob/2.1.1: + dev: true engines: node: '>=0.10.0' resolution: @@ -1303,6 +1242,7 @@ packages: /is-glob/4.0.1: dependencies: is-extglob: 2.1.1 + dev: true engines: node: '>=0.10.0' resolution: @@ -1312,6 +1252,7 @@ packages: resolution: integrity: sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= /is-number/7.0.0: + dev: true engines: node: '>=0.12.0' resolution: @@ -1339,10 +1280,6 @@ packages: dev: true resolution: integrity: sha512-mpS5EGqXOwzXtKAg6I44jIAqeBfntFLxpAth1rrKbxtKyI6LPktyDYpHBI+tHlduhhX/SF26mFXmxQu995QVqg== - /is-what/3.12.0: - dev: false - resolution: - integrity: sha512-2ilQz5/f/o9V7WRWJQmpFYNmQFZ9iM+OXRonZKcYgTkCzjb949Vi4h282PD1UfmgHk666rcWonbRJ++KI41VGw== /is-wsl/2.2.0: dependencies: is-docker: 2.1.1 @@ -1417,25 +1354,6 @@ packages: node: '>=6' resolution: integrity: sha512-H1tr8QP2PxFTNwAFM74Mui2b6ovcY9FoxJefgrwxY+OCJcq01k5nvhf4M/KnizzrJvLRap5STUy7dgDV35iUBw== - /less/4.0.0: - dependencies: - copy-anything: 2.0.1 - parse-node-version: 1.0.1 - tslib: 1.14.1 - dev: false - engines: - node: '>=6' - hasBin: true - optionalDependencies: - errno: 0.1.8 - graceful-fs: 4.2.4 - image-size: 0.5.5 - make-dir: 2.1.0 - mime: 1.6.0 - native-request: 1.0.8 - source-map: 0.6.1 - resolution: - integrity: sha512-av1eEa2D0xZfF7fjLJS/Dld7zAYSLU7EOEJvuOELeaNI3i6L/81AdjbK5/pytaRkBwi7ZEa0433IDvMLskKCOw== /lines-and-columns/1.1.6: dev: true resolution: @@ -1494,16 +1412,6 @@ packages: dev: true resolution: integrity: sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== - /make-dir/2.1.0: - dependencies: - pify: 4.0.1 - semver: 5.7.1 - dev: false - engines: - node: '>=6' - optional: true - resolution: - integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== /make-dir/3.1.0: dependencies: semver: 6.3.0 @@ -1535,14 +1443,6 @@ packages: node: '>= 0.6' resolution: integrity: sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== - /mime/1.6.0: - dev: false - engines: - node: '>=4' - hasBin: true - optional: true - resolution: - integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== /mimic-fn/2.1.0: engines: node: '>=6' @@ -1563,6 +1463,7 @@ packages: /minimatch/3.0.4: dependencies: brace-expansion: 1.1.11 + dev: true resolution: integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== /minimist/1.2.5: @@ -1611,12 +1512,14 @@ packages: resolution: integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== /mkdirp/1.0.4: + dev: true engines: node: '>=10' hasBin: true resolution: integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== /ms/2.0.0: + dev: true resolution: integrity: sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= /ms/2.1.2: @@ -1630,12 +1533,8 @@ packages: hasBin: true resolution: integrity: sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw== - /native-request/1.0.8: - dev: false - optional: true - resolution: - integrity: sha512-vU2JojJVelUGp6jRcLwToPoWGxSx23z/0iX+I77J3Ht17rf2INGjrhOoQnjVo60nQd8wVsgzKkPfRXBiVdD2ag== /normalize-path/3.0.0: + dev: true engines: node: '>=0.10.0' resolution: @@ -1668,6 +1567,7 @@ packages: /once/1.4.0: dependencies: wrappy: 1.0.2 + dev: true resolution: integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E= /onetime/5.1.2: @@ -1780,12 +1680,6 @@ packages: node: '>=8' resolution: integrity: sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ== - /parse-node-version/1.0.1: - dev: false - engines: - node: '>= 0.10' - resolution: - integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== /parse5-htmlparser2-tree-adapter/6.0.1: dependencies: parse5: 6.0.1 @@ -1803,6 +1697,7 @@ packages: resolution: integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== /path-is-absolute/1.0.1: + dev: true engines: node: '>=0.10.0' resolution: @@ -1823,17 +1718,11 @@ packages: resolution: integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== /picomatch/2.2.2: + dev: true engines: node: '>=8.6' resolution: integrity: sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== - /pify/4.0.1: - dev: false - engines: - node: '>=6' - optional: true - resolution: - integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== /pkg-dir/4.2.0: dependencies: find-up: 4.1.0 @@ -1930,11 +1819,6 @@ packages: dev: true resolution: integrity: sha1-mEcocL8igTL8vdhoEputEsPAKeM= - /prr/1.0.1: - dev: false - optional: true - resolution: - integrity: sha1-0/wRS6BplaRexok/SEzrHXj19HY= /pump/3.0.0: dependencies: end-of-stream: 1.4.4 @@ -1951,6 +1835,7 @@ packages: /readdirp/3.5.0: dependencies: picomatch: 2.2.2 + dev: true engines: node: '>=8.10.0' resolution: @@ -1985,11 +1870,6 @@ packages: node: '>=8' resolution: integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - /resolve-url/0.2.1: - deprecated: 'https://github.com/lydell/resolve-url#deprecated' - dev: false - resolution: - integrity: sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= /resolve/1.19.0: dependencies: is-core-module: 2.2.0 @@ -2044,28 +1924,13 @@ packages: dev: true resolution: integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - /safer-buffer/2.1.2: - dev: false - resolution: - integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - /sass/1.32.0: - dependencies: - chokidar: 3.4.3 - dev: false - engines: - node: '>=8.9.0' - hasBin: true - resolution: - integrity: sha512-fhyqEbMIycQA4blrz/C0pYhv2o4x2y6FYYAH0CshBw3DXh5D5wyERgxw0ptdau1orc/GhNrhF7DFN2etyOCEng== - /sax/1.2.4: - dev: false - resolution: - integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== /semver/5.7.1: + dev: true hasBin: true resolution: integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== /semver/6.3.0: + dev: true hasBin: true resolution: integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -2159,20 +2024,6 @@ packages: hasBin: true resolution: integrity: sha512-UThUbGXn/wN7zRJDVpbC0F3uvkhu6PAgpTbV1hdAaqLvYBewT794iEMAY5mMxT27zQ+ZOyqbICaBtKtq0BgDcA== - /source-map-resolve/0.5.3: - dependencies: - atob: 2.1.2 - decode-uri-component: 0.2.0 - resolve-url: 0.2.1 - source-map-url: 0.4.0 - urix: 0.1.0 - dev: false - resolution: - integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== - /source-map-url/0.4.0: - dev: false - resolution: - integrity: sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= /source-map/0.5.7: dev: true engines: @@ -2180,11 +2031,13 @@ packages: resolution: integrity: sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= /source-map/0.6.1: + dev: true engines: node: '>=0.10.0' resolution: integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== /source-map/0.7.3: + dev: true engines: node: '>= 8' resolution: @@ -2234,20 +2087,6 @@ packages: node: '>=6' resolution: integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - /stylus/0.54.8: - dependencies: - css-parse: 2.0.0 - debug: 3.1.0 - glob: 7.1.6 - mkdirp: 1.0.4 - safer-buffer: 2.1.2 - sax: 1.2.4 - semver: 6.3.0 - source-map: 0.7.3 - dev: false - hasBin: true - resolution: - integrity: sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg== /supports-color/5.5.0: dependencies: has-flag: 3.0.0 @@ -2286,14 +2125,11 @@ packages: /to-regex-range/5.0.1: dependencies: is-number: 7.0.0 + dev: true engines: node: '>=8.0' resolution: integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - /tslib/1.14.1: - dev: false - resolution: - integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== /tslib/2.0.3: dev: true resolution: @@ -2327,11 +2163,6 @@ packages: node: '>= 4.0.0' resolution: integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== - /urix/0.1.0: - deprecated: 'Please see https://github.com/lydell/urix#deprecated' - dev: false - resolution: - integrity: sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= /util-deprecate/1.0.2: dev: true resolution: @@ -2374,6 +2205,7 @@ packages: resolution: integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== /wrappy/1.0.2: + dev: true resolution: integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= /ws/7.4.1: @@ -2435,12 +2267,6 @@ packages: specifiers: '@angular/compiler': ^11.0.5 '@angular/compiler-cli': ^11.0.5 - '@types/less': ^3.0.2 - '@types/sass': ^1.16.0 - '@types/stylus': ^0.48.33 execa: ^5.0.0 - less: ^4.0.0 - sass: ^1.32.0 snowpack: ^2.18.5 - stylus: ^0.54.8 typescript: '4.0' diff --git a/src/compile.ts b/src/compile.ts index 8f91456..a7a26d1 100644 --- a/src/compile.ts +++ b/src/compile.ts @@ -1,6 +1,7 @@ import * as ng from '@angular/compiler-cli'; import ts from 'typescript'; import path from 'path'; +import { runTypeCheck } from './typeCheck'; export interface CompileArgs { rootNames: string[]; @@ -15,196 +16,37 @@ export interface CacheEntry { content?: string; } -export interface WatchCompilationResult extends ng.PerformCompilationResult { +export interface RecompileResult extends ng.PerformCompilationResult { recompiledFiles: string[]; } -export type RecompileFunction = ( - fileName: string, - src: string -) => WatchCompilationResult | null; - export type RecompileFunctionAsync = ( fileName: string, src: string -) => Promise; +) => Promise; -export const compile = ({ - rootNames, - compilerHost, - compilerOptions, -}: CompileArgs): ng.PerformCompilationResult => { - const compilationResult = ng.performCompilation({ +/** + * Based on `@angular/compiler-cli.performCompilation()` + */ +export const performCompilationAsync = async ( + { compilerHost, compilerOptions, rootNames, oldProgram }: CompileArgs, + typeCheck: boolean = true +): Promise => { + const diagnostics: (ts.Diagnostic | ng.Diagnostic)[] = []; + const program = ng.createProgram({ rootNames, host: compilerHost, options: compilerOptions, + oldProgram, }); - return compilationResult; -}; - -export const watchCompile = ({ - rootNames, - compilerHost, - compilerOptions, -}: CompileArgs) => { - const compiledFiles = new Set(); - const fileCache = new Map(); - const modifiedFile = new Set(); - let cachedProgram: ng.Program | undefined; - - const getCacheEntry = (fileName: string) => { - fileName = path.normalize(fileName); - let entry = fileCache.get(fileName); - if (!entry) { - entry = {}; - fileCache.set(fileName, entry); - } - return entry; - }; - - // Setup compilerHost to use cache - const oriWriteFile = compilerHost.writeFile; - compilerHost.writeFile = ( - fileName, - data, - writeByteOrderMark, - onError, - sourceFiles - ) => { - const srcRelativePath = path - .resolve(fileName) - .replace(path.resolve(compilerOptions.outDir!), ''); - compiledFiles.add(srcRelativePath); - return oriWriteFile( - fileName, - data, - writeByteOrderMark, - onError, - sourceFiles + await program.loadNgStructureAsync(); + if (typeCheck) + diagnostics.push( + ...runTypeCheck(rootNames, compilerOptions, program, compilerHost) ); - }; - const oriFileExists = compilerHost.fileExists; - compilerHost.fileExists = (fileName) => { - const cache = getCacheEntry(fileName); - if (cache.exists === null || cache.exists === undefined) - cache.exists = oriFileExists(fileName); - return cache.exists; - }; - const oriGetSourceFile = compilerHost.getSourceFile; - compilerHost.getSourceFile = (fileName, languageVersion) => { - const cache = getCacheEntry(fileName); - if (!cache.sf) cache.sf = oriGetSourceFile(fileName, languageVersion); - return cache.sf; - }; - const oriReadFile = compilerHost.readFile; - compilerHost.readFile = (fileName) => { - const cache = getCacheEntry(fileName); - if (!cache.content) cache.content = oriReadFile(fileName); - return cache.content; - }; - // Read resource is a optional function, - // it has priority over readFile when loading resources (html/css), - // async file processing will require a custom performCompilation to run `program.loadNgStuctureAsync()` - const oriReadResource = compilerHost.readResource; - if (oriReadResource) - compilerHost.readResource = (fileName) => { - const cache = getCacheEntry(fileName); - if (!cache.content) cache.content = oriReadResource(fileName) as string; - return cache.content; - }; - - compilerHost.getModifiedResourceFiles = () => { - return modifiedFile; - }; - - // Do first compile - const firstCompilation = compile({ - rootNames, - compilerHost, - compilerOptions, - }); - cachedProgram = firstCompilation.program; - - const recompile: RecompileFunction = (fileName: string, src: string) => { - // perhaps this function need debouncing like in perform_watch.ts - fileName = path.normalize(fileName); - fileCache.delete(fileName); - const compiledFilePath = path - .resolve(fileName) - .replace(path.resolve(path.join(process.cwd(), src)), ''); - if (!compiledFiles.has(compiledFilePath)) { - modifiedFile.add(fileName); - compiledFiles.clear(); - const oldProgram = cachedProgram; - cachedProgram = undefined; - const recompileResult = ng.performCompilation({ - rootNames, - host: compilerHost, - options: compilerOptions, - oldProgram, - }); - cachedProgram = recompileResult.program; - modifiedFile.clear(); - return { - program: recompileResult.program, - emitResult: recompileResult.emitResult, - recompiledFiles: [...compiledFiles], - diagnostics: recompileResult.diagnostics, - }; - } - return null; - }; - return { firstCompilation, recompile }; -}; - -/** - * Based on `@angular/compiler-cli.performCompilation()` - */ -export const performCompilationAsync = async ({ - compilerHost, - compilerOptions, - rootNames, - oldProgram, -}: CompileArgs): Promise => { - let program: ng.Program | undefined; - const diagnostics: (ng.Diagnostic | ts.Diagnostic)[] = []; - try { - program = ng.createProgram({ - rootNames, - host: compilerHost, - options: compilerOptions, - oldProgram, - }); - await program.loadNgStructureAsync(); - diagnostics.push(...ng.defaultGatherDiagnostics(program)); - // No errors - if (!diagnostics.some((d) => d.category === ts.DiagnosticCategory.Error)) { - const emitResult = program.emit(); - diagnostics.push(...emitResult.diagnostics); - return { diagnostics, program, emitResult }; - } - return { diagnostics, program }; - } catch (e) { - let errMsg: string; - let code: number; - if (e['ngSyntaxError']) { - // don't report the stack for syntax errors as they are well known errors. - errMsg = e.message; - code = ng.DEFAULT_ERROR_CODE; - } else { - errMsg = e.stack; - // It is not a syntax error we might have a program with unknown state, discard it. - program = undefined; - code = ng.UNKNOWN_ERROR_CODE; - } - diagnostics.push({ - category: ts.DiagnosticCategory.Error, - messageText: errMsg, - code, - source: ng.SOURCE, - }); - return { diagnostics, program }; - } + const emitResult = program.emit(); + diagnostics.push(...emitResult.diagnostics); + return { diagnostics, program, emitResult }; }; export const compileAsync = async ({ @@ -249,9 +91,10 @@ export const watchCompileAsync = async ({ onError, sourceFiles ) => { - const srcRelativePath = path - .resolve(fileName) - .replace(path.resolve(compilerOptions.outDir!), ''); + const srcRelativePath = path.relative( + path.resolve(compilerOptions.outDir!), + path.resolve(fileName) + ); compiledFiles.add(srcRelativePath); return oriWriteFile( fileName, @@ -311,30 +154,27 @@ export const watchCompileAsync = async ({ // perhaps this function need debouncing like in perform_watch.ts fileName = path.normalize(fileName); fileCache.delete(fileName); - const compiledFilePath = path - .resolve(fileName) - .replace(path.resolve(path.join(process.cwd(), src)), ''); - if (!compiledFiles.has(compiledFilePath)) { - modifiedFile.add(fileName); - compiledFiles.clear(); - const oldProgram = cachedProgram; - cachedProgram = undefined; - const recompileResult = await performCompilationAsync({ + modifiedFile.add(fileName); + compiledFiles.clear(); + const oldProgram = cachedProgram; + cachedProgram = undefined; + const recompileResult = await performCompilationAsync( + { rootNames, compilerHost, compilerOptions, oldProgram, - }); - cachedProgram = recompileResult.program; - modifiedFile.clear(); - return { - program: recompileResult.program, - emitResult: recompileResult.emitResult, - recompiledFiles: [...compiledFiles], - diagnostics: recompileResult.diagnostics, - }; - } - return null; + }, + false + ); + cachedProgram = recompileResult.program; + modifiedFile.clear(); + return { + program: recompileResult.program, + emitResult: recompileResult.emitResult, + recompiledFiles: [...compiledFiles], + diagnostics: recompileResult.diagnostics, + }; }; return { firstCompilation, recompile }; }; diff --git a/src/compilerService.ts b/src/compilerService.ts new file mode 100644 index 0000000..f3d36c0 --- /dev/null +++ b/src/compilerService.ts @@ -0,0 +1,287 @@ +import { AngularJsonWrapper, readAngularJson } from './configParser'; +import * as ng from '@angular/compiler-cli'; +import fs, { promises as fsp } from 'fs'; +import path from 'path'; +import execa from 'execa'; +import { + compileAsync, + RecompileFunctionAsync, + watchCompileAsync, +} from './compile'; +import { + TypeCheckWorker, + createTypeCheckWorker, + TypeCheckArgs, +} from './typeCheckWorker'; +import { + getTSDiagnosticErrorFile, + getTSDiagnosticErrorInFile, + tsFormatDiagnosticHost, +} from './typeCheck'; + +interface BuildStatus { + built: boolean; + building: boolean; + buildReadyCallback: VoidFunction[]; +} + +interface BuiltJSFile { + code: string; + map?: string; +} + +type TypeCheckErrorListener = (diagnostic: ng.Diagnostics) => void; + +export class AngularCompilerService { + private _angularConfig: AngularJsonWrapper; + private _ngCompilerHost: ng.CompilerHost; + private _ngConfiguration: ng.ParsedConfiguration; + private get _ngCompilerOptions(): ng.CompilerOptions { + return this._ngConfiguration.options; + } + private _builtFiles = new Map(); + private _buildStatus: BuildStatus = { + built: false, + building: false, + buildReadyCallback: [], + }; + private _recompileFunction?: RecompileFunctionAsync; + private _lastCompilationResult?: ng.PerformCompilationResult; + private _typeCheckErrorListeners = new Map(); + private _typeCheckErrorListenerId = 0; + private _typeCheckWorker?: TypeCheckWorker; + private _lastTypeCheckResult: ng.Diagnostics = []; + private _fileReplacements = new Map(); + + constructor( + angularJson: string, + private sourceDirectory: string, + private ngccTargets: string[], + private angularProject?: string + ) { + this._angularConfig = readAngularJson(angularJson); + const { tsConfig } = this._angularConfig.getResolvedFilePaths( + angularProject + ); + this._ngConfiguration = ng.readConfiguration(tsConfig); + this._ngCompilerHost = this.configureCompilerHost( + ng.createCompilerHost({ options: this._ngCompilerOptions }) + ); + } + private async runNgcc(targets: string[]) { + for (const target of targets) { + const ngcc = execa('ngcc', ['-t', target]); + const { stdout } = await ngcc; + stdout.split('\n').forEach((line) => { + if (line !== '') console.log(`[angular] ${line}`); + }); + } + console.log( + '[angular] Try clearing snowpack development cache with "snowpack --reload" if facing errors during dev mode' + ); + } + + private configureCompilerHost(host: ng.CompilerHost): ng.CompilerHost { + host.writeFile = (fileName, contents) => { + fileName = path.relative( + path.resolve(this._ngCompilerOptions.outDir || this.sourceDirectory), + path.resolve(fileName) + ); + // Prevent multiple sourceMappingUrl as snowpack will append it if sourceMaps is enabled + contents = contents.replace(/\/\/# sourceMappingURL.*/, ''); + this._builtFiles.set(fileName, contents); + }; + host.readResource = async (fileName) => { + const contents = await fsp.readFile(fileName, 'utf-8'); + return contents; + }; + const oriReadFile = host.readFile; + if (process.env.NODE_ENV === 'production') { + const replacementConfig = this.angularConfig.getProject( + this.angularProject + ).architect.build.configurations.production.fileReplacements; + replacementConfig.forEach((replacement) => { + this._fileReplacements.set( + path.resolve(replacement.replace), + path.resolve(replacement.with) + ); + }); + } + host.readFile = (fileName) => { + fileName = path.resolve(fileName); + if (this._fileReplacements.has(fileName)) { + const replaceWith = this._fileReplacements.get(fileName)!; + fileName = replaceWith; + } + return oriReadFile(fileName); + }; + return host; + } + + private registerTypeCheckWorker() { + this._typeCheckWorker = createTypeCheckWorker(); + this._typeCheckWorker.on('message', (msg) => { + this._lastTypeCheckResult = msg; + this._typeCheckErrorListeners.forEach((listener) => listener(msg)); + }); + } + + async buildSource(watch: boolean) { + if (this._buildStatus.built) return; + else if (!this._buildStatus.built && !this._buildStatus.building) { + this._buildStatus.building = true; + const compileArgs = { + rootNames: this._ngConfiguration.rootNames, + compilerHost: this._ngCompilerHost, + compilerOptions: this._ngCompilerOptions, + }; + await this.runNgcc(this.ngccTargets); + if (watch) { + this.registerTypeCheckWorker(); + const result = await watchCompileAsync(compileArgs); + this._recompileFunction = result.recompile; + this._lastCompilationResult = result.firstCompilation; + } else { + this._lastCompilationResult = await compileAsync(compileArgs); + } + this._buildStatus.built = true; + this._buildStatus.building = false; + this._buildStatus.buildReadyCallback.forEach((cb) => cb()); + this._buildStatus.buildReadyCallback = []; + } else { + return new Promise((resolve) => { + this._buildStatus.buildReadyCallback.push(resolve); + }); + } + } + + async recompile(modifiedFile: string) { + if (!this._recompileFunction) + throw new Error( + 'Cannot recompile as angular was not build with watch mode enabled' + ); + else { + const recompiledResult = await this._recompileFunction( + modifiedFile, + this.sourceDirectory + ); + this._lastCompilationResult = { + diagnostics: recompiledResult.diagnostics, + emitResult: recompiledResult.emitResult, + program: recompiledResult.program, + }; + this._lastTypeCheckResult = []; + const workerMessage: TypeCheckArgs = { + action: 'run_check', + data: { + options: this._ngCompilerOptions, + rootNames: this._ngConfiguration.rootNames, + }, + }; + this._typeCheckWorker!.postMessage(workerMessage); + const recompiledFiles = recompiledResult.recompiledFiles + .filter((file) => path.extname(file) === '.js') + .map((file) => + path + .resolve(path.join(this.sourceDirectory, file)) + .replace(path.extname(file), '.ts') + ); + return { + recompiledFiles, + recompiledResult, + }; + } + } + + getBuiltFile(filePath: string): BuiltJSFile | null { + filePath = path.relative( + path.resolve(this.sourceDirectory), + path.resolve(filePath) + ); + let result: BuiltJSFile | null = null; + const codeFile = filePath.replace(path.extname(filePath), '.js'); + const mapFile = filePath.replace(path.extname(filePath), '.js.map'); + if (this._builtFiles.has(codeFile)) { + result = { + code: this._builtFiles.get(codeFile)!, + }; + if (this._builtFiles.has(mapFile)) + result.map = this._builtFiles.get(mapFile); + } + return result; + } + + onTypeCheckError(listener: TypeCheckErrorListener) { + const id = this._typeCheckErrorListenerId++; + this._typeCheckErrorListeners.set(id, listener); + return id; + } + + removeTypeCheckErrorListener(id: number) { + this._typeCheckErrorListeners.delete(id); + } + + getIndexInjects() { + const { + index, + styles, + scripts, + main, + polyfills, + } = this._angularConfig.getResolvedFilePaths(this.angularProject); + const indexDir = path.dirname(index); + const injectStyles = styles.map((style) => { + const relativeUrl = path + .relative(indexDir, style) + .replace(path.extname(style), '.css'); + return ``; + }); + const injectScripts = scripts.map((script) => { + const relativeUrl = path + .relative(indexDir, script) + .replace(path.extname(script), '.js'); + return ``; + }); + const relativePolyfillsUrl = path + .relative(indexDir, polyfills) + .replace(path.extname(polyfills), '.js'); + const injectPolyfills = ``; + const relativeMainUrl = path + .relative(indexDir, main) + .replace(path.extname(main), '.js'); + const injectMain = ``; + return { + injectPolyfills, + injectMain, + injectStyles, + injectScripts, + }; + } + + getErrorInFile(filePath: string): ng.Diagnostics { + return getTSDiagnosticErrorInFile(filePath, [ + ...this._lastTypeCheckResult, + ...this._lastCompilationResult!.diagnostics, + ]); + } + + getErroredFiles(diagnostics: ng.Diagnostics): string[] { + return getTSDiagnosticErrorFile(diagnostics); + } + + formatDiagnostics(diagnostics: ng.Diagnostics) { + return ng.formatDiagnostics(diagnostics, tsFormatDiagnosticHost); + } + + get ngConfiguration() { + return this._ngConfiguration; + } + + get angularConfig() { + return this._angularConfig; + } + + getAngularCriticalFiles() { + return this._angularConfig.getResolvedFilePaths(this.angularProject); + } +} diff --git a/src/configParser.ts b/src/configParser.ts new file mode 100644 index 0000000..3ef424d --- /dev/null +++ b/src/configParser.ts @@ -0,0 +1,87 @@ +import fs from 'fs'; +import path from 'path'; + +export interface fileReplacement { + replace: string; + with: string; +} + +export interface AngularArchitectConfig { + fileReplacements: fileReplacement[]; + optimization: boolean; + sourceMap: boolean; +} + +export interface AngularArchitectSubset { + options: { + outputPath: string; + index: string; + main: string; + polyfills: string; + tsConfig: string; + aot: boolean; + assets: string[]; + styles: string[]; + scripts: string[]; + }; + configurations: { + [configurationName: string]: AngularArchitectConfig; + }; +} + +export interface AngularProjectSubset { + sourceRoot: string; + architect: { + [architectName: string]: AngularArchitectSubset; + }; +} + +export interface AngularJson { + projects: { + [projectName: string]: AngularProjectSubset; + }; + defaultProject: string; +} + +export interface AngularCriticalFiles { + index: string; + polyfills: string; + main: string; + styles: string[]; + scripts: string[]; + tsConfig: string; +} + +export interface AngularJsonWrapper { + angularJson: AngularJson; + getProject: (projectName?: string) => AngularProjectSubset; + getResolvedFilePaths: (projectName?: string) => AngularCriticalFiles; +} + +export const readAngularJson = (fileName: string): AngularJsonWrapper => { + const jsonContents = JSON.parse( + fs.readFileSync(fileName, 'utf-8') + ) as AngularJson; + return { + angularJson: jsonContents, + getProject(projectName?: string) { + if (!projectName) projectName = this.angularJson.defaultProject; + return this.angularJson.projects[projectName]; + }, + getResolvedFilePaths(projectName?: string): AngularCriticalFiles { + const project = this.getProject(projectName); + return { + index: path.resolve(project.architect.build.options.index), + polyfills: path.resolve(project.architect.build.options.polyfills), + main: path.resolve(project.architect.build.options.main), + styles: project.architect.build.options.styles.map((style) => + path.resolve(style) + ), + scripts: project.architect.build.options.scripts.map((script) => + path.resolve(script) + ), + tsConfig: project.architect.build.options.tsConfig, + }; + }, + }; +}; diff --git a/src/index.ts b/src/index.ts index 8d92b00..7202c35 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,229 +1,112 @@ -import type { - PluginLoadOptions, - SnowpackBuiltFile, - SnowpackConfig, - SnowpackPluginFactory, -} from 'snowpack'; -import * as ng from '@angular/compiler-cli'; -import ts from 'typescript'; +import { SnowpackPlugin, SnowpackPluginFactory } from 'snowpack'; +import { AngularCompilerService } from './compilerService'; import path from 'path'; -import { promises as fs } from 'fs'; -import { - compileAsync, - RecompileFunctionAsync, - watchCompileAsync, -} from './compile'; -import { createStyleHandler } from './stylehandler'; -import execa from 'execa'; -export interface AngularSnowpackPluginOptions { +export interface pluginOptions { /** @default 'src' */ src?: string; - /** @default 'normal' */ - logLevel?: 'normal' | 'debug'; - /** @default 'tsconfig.app.json' */ - tsConfig?: string; + /** @default 'angular.json' */ + angularJson?: string; + /** @default defaultProject in angular.json */ + angularProject?: string; /** @default [] */ ngccTargets?: string[]; + /** @default true */ + errorToBrowser?: boolean; } -/** - * Build Logic: Based on https://github.com/aelbore/rollup-plugin-ngc - * Watch Logic: Based on packages/compiler-cli/src/perform_watch.ts https://github.com/angular/angular - */ -const plugin: SnowpackPluginFactory = ( - options: SnowpackConfig, - pluginOptions?: AngularSnowpackPluginOptions +const pluginFactory: SnowpackPluginFactory = ( + snowpackConfig, + pluginOptions ) => { - const srcDir = pluginOptions?.src || 'src'; - const logLevel = pluginOptions?.logLevel || 'normal'; - const tsConfigPath = pluginOptions?.tsConfig || 'tsconfig.app.json'; + const angularJson = pluginOptions?.angularJson || 'angular.json'; + const angularProject = pluginOptions?.angularProject; + const sourceDirectory = pluginOptions?.src || 'src'; + const errorToBrowser = pluginOptions?.errorToBrowser ?? true; const ngccTargets = pluginOptions?.ngccTargets || []; - const buildSourceMap = options.buildOptions.sourceMaps; - let compilerHost: ng.CompilerHost; - let parsedTSConfig: ng.CompilerOptions; - const builtSourceFiles = new Map(); - const cwd = path.resolve(process.cwd()); - const styleHandler = createStyleHandler(); + ngccTargets.unshift( + '@angular/core', + '@angular/common', + '@angular/platform-browser-dynamic' + ); + const compiler = new AngularCompilerService( + angularJson, + sourceDirectory, + ngccTargets, + angularProject + ); + const skipRecompileFiles: string[] = []; + const index = compiler.getAngularCriticalFiles().index; - let rootNamesCompiled: boolean = false; - let rootNamesCompiling: boolean = false; - let rootNames: string[] = []; - let recompile: RecompileFunctionAsync; - let recompiledFiles: string[] = []; - let compilationResult: ng.PerformCompilationResult; - - let compilationReadyCb: (() => void)[] = []; - const isCompilationReady = () => { - return new Promise((resolve) => { - compilationReadyCb.push(resolve); - }); - }; - - const compileRootNames = async (isDev = false) => { - if (!rootNamesCompiled && !rootNamesCompiling) { - rootNamesCompiling = true; - pluginLog('Building source...'); - console.time('[angular] Source built in'); - if (isDev) { - const watchCompileResult = await watchCompileAsync({ - rootNames, - compilerHost, - compilerOptions: parsedTSConfig, - }); - recompile = watchCompileResult.recompile; - compilationResult = watchCompileResult.firstCompilation; - } else { - compilationResult = await compileAsync({ - rootNames, - compilerHost, - compilerOptions: parsedTSConfig, - }); - } - console.timeEnd('[angular] Source built in'); - for (const cb of compilationReadyCb) { - cb(); - } - compilationReadyCb = []; - rootNamesCompiling = false; - rootNamesCompiled = true; - } else if (rootNamesCompiling) { - return await isCompilationReady(); - } - }; - - const pluginLog = (contents: string): void => { - console.log(`[angular] ${contents}`); - }; - - const pluginDebug = (contents: string): void => { - if (logLevel === 'debug') pluginLog(contents); - }; - - const readAndParseTSConfig = (configFile: string): ng.ParsedConfiguration => { - configFile = path.resolve(configFile); - const parsedConfig = ng.readConfiguration(configFile); - return parsedConfig; - }; - - const tsFormatDiagnosticHost: ts.FormatDiagnosticsHost = { - getCanonicalFileName: (fileName) => fileName, - getCurrentDirectory: () => cwd, - getNewLine: () => '\n', - }; - - const runNgcc = async () => { - ngccTargets.unshift('@angular/platform-browser'); - for (const target of ngccTargets) { - const ngcc = execa('ngcc', ['-t', target]); - ngcc.stdout?.pipe(process.stdout); - await ngcc; - } - pluginLog('***************'); - pluginLog( - 'NGCC finished. Run "snowpack --reload" if strange errors regarding ivy appears during dev mode' - ); - pluginLog('***************'); - }; - - return { - name: 'angular', + const plugin: SnowpackPlugin = { + name: 'angular-snowpack-plugin', + knownEntrypoints: ['@angular/common'], resolve: { input: ['.ts'], - output: ['.js', '.ts'], - }, - async run() { - await runNgcc(); + output: ['.js'], }, config() { - const parsedConfig = readAndParseTSConfig(tsConfigPath); - parsedTSConfig = parsedConfig.options; - rootNames = parsedConfig.rootNames.map((file) => path.resolve(file)); - compilerHost = ng.createCompilerHost({ options: parsedTSConfig }); - compilerHost.writeFile = (fileName, contents) => { - fileName = path - .resolve(fileName) - .replace(path.resolve(parsedTSConfig.outDir!), ''); - pluginDebug(`File Compiled: ${fileName}`); - builtSourceFiles.set( - fileName, - contents.replace(/\/\/# sourceMappingURL.*/, '') // required, to prevent multiple sourceMappingUrl as snowpack will append it if sourceMaps is enabled - ); - }; - compilerHost.readResource = async (fileName) => { - pluginDebug(`Resource Read: ${fileName}`); - const contents = await fs.readFile(fileName, 'utf-8'); - if (styleHandler.needProcess(fileName)) { - pluginDebug(`Preprocessing Style: ${fileName}`); - const builtStyle = await styleHandler.process({ fileName, contents }); - return builtStyle.css; - } else { - return contents; + compiler.onTypeCheckError((diagnostic) => { + if (diagnostic.length > 0) { + if (!errorToBrowser) { + console.error( + `[angular] ${compiler.formatDiagnostics(diagnostic)}` + ); + } else { + const erroredFiles = compiler.getErroredFiles(diagnostic); + skipRecompileFiles.push(...erroredFiles); + erroredFiles.forEach((file) => this.markChanged!(file)); + } } - }; + }); }, - async load(options: PluginLoadOptions) { - const sourceMap = options.isDev || buildSourceMap; - pluginDebug(`Loading: ${options.filePath}`); - await compileRootNames(options.isDev); - const relativeFilePathFromSrc = path - .resolve(options.filePath) - .replace(path.resolve(srcDir), ''); - const fileBaseName = relativeFilePathFromSrc.replace('.ts', ''); - const result: SnowpackBuiltFile = {} as any; - const sourceFile: SnowpackBuiltFile = {} as any; - // Throw pretty diagnostics when error happened during compilation - if (compilationResult.diagnostics.length > 0) { - const formatted = ng.formatDiagnostics( - compilationResult.diagnostics, - tsFormatDiagnosticHost - ); - throw new Error(`[angular] ${formatted}`); - } - // Load the file from builtSourceFiles - if (builtSourceFiles.has(`${fileBaseName}.js`)) - result.code = builtSourceFiles.get(`${fileBaseName}.js`)!; - if (sourceMap && builtSourceFiles.has(`${fileBaseName}.js.map`)) - result.map = builtSourceFiles.get(`${fileBaseName}.js.map`); - // Not desirable, copy the original source file as well if sourceMaps is enabled. - sourceFile.code = sourceMap - ? await fs.readFile(options.filePath, 'utf-8') - : undefined!; + async load({ filePath, isDev }) { + const useSourceMaps = isDev || snowpackConfig.buildOptions.sourceMaps; + await compiler.buildSource(isDev); + const error = compiler.getErrorInFile(filePath); + if (error.length > 0) + throw new Error(`[angular] ${compiler.formatDiagnostics(error)}`); + const result = compiler.getBuiltFile(filePath); return { - '.js': result, - '.ts': sourceFile, + '.js': { + code: result?.code!, + map: useSourceMaps ? result?.map : undefined, + }, }; }, async onChange({ filePath }) { - // doRecompile is needed to avoid infinite loops where a component affects its module to be recompiled, or vice versa - // if false, nothing will be done, the recompiled file will be reloaded by snowpack - // changes to any resource files (.html/.ts/.css) will be sent to @angular/compiler-cli to recompile - const doRecompile = !recompiledFiles.includes(filePath); - if (!doRecompile) - recompiledFiles.splice(recompiledFiles.indexOf(filePath), 1); - else { - console.time('[angular] Incremental Build Finished, Took'); - const recompiledResult = await recompile(filePath, srcDir); - console.timeEnd('[angular] Incremental Build Finished, Took'); - if (recompiledResult) compilationResult = recompiledResult; - // map the compiled files path back to its source - const files = recompiledResult!.recompiledFiles - .map((file) => path.resolve(path.join(srcDir, file))) - .filter((file) => path.extname(file) === '.js') - .map((file) => file.replace(path.extname(file), '.ts')) - .filter((file) => file !== filePath); - if (files.length === 0) - // Not the best solution, but work for now, used when error happens during recompilation and no files were recompiled, forcing a reload to throw error to snowpack - // rootNames[0] is presumably src/main.ts (anything would work though) - files.push(rootNames[0]); - for (const file of files) { - recompiledFiles.push(file); - this.markChanged!(file); - } + filePath = path.resolve(filePath); + if (skipRecompileFiles.includes(filePath)) { + skipRecompileFiles.splice(skipRecompileFiles.indexOf(filePath), 1); + return; + } + const recompile = await compiler.recompile(filePath); + recompile.recompiledFiles = recompile.recompiledFiles.filter( + (file) => file !== filePath + ); + skipRecompileFiles.push(...recompile.recompiledFiles); + recompile.recompiledFiles.forEach((file) => this.markChanged!(file)); + }, + async transform({ id, contents }) { + id = path.resolve(id); + if (id === index) { + const { + injectMain, + injectPolyfills, + injectScripts, + injectStyles, + } = compiler.getIndexInjects(); + return contents + .toString('utf-8') + .replace('', `${injectStyles.join('')}\n`) + .replace( + '', + `${injectPolyfills}${injectMain}${injectScripts.join('')}\n` + ); } }, - async transform({ id }) {}, }; + return plugin; }; -export default plugin; +export default pluginFactory; diff --git a/src/stylehandler.ts b/src/stylehandler.ts deleted file mode 100644 index 68deec6..0000000 --- a/src/stylehandler.ts +++ /dev/null @@ -1,95 +0,0 @@ -import * as sass from 'sass'; -import * as path from 'path'; -import * as less from 'less'; -import * as stylus from 'stylus'; - -export interface StyleProcessArgs { - fileName: string; - contents: string; -} - -export interface BuiltStyle { - css: string; - map?: string; -} - -export const sassHandler = async ({ - contents, - fileName, -}: StyleProcessArgs): Promise => { - const sassResult = await new Promise((resolve, reject) => { - sass.render( - { - data: contents, - sourceMap: path.basename(fileName) + '.map', - }, - (err, res) => { - if (err) return reject(err); - resolve(res); - } - ); - }); - return { - css: sassResult.css.toString('utf-8'), - map: sassResult.map?.toString('utf-8'), - }; -}; - -export const lessHandler = async ({ - contents, - fileName, -}: StyleProcessArgs): Promise => { - const lessResult = await less.render(contents); - return { - css: lessResult.css, - map: lessResult.map, - }; -}; - -export const stylusHandler = async ({ - contents, - fileName, -}: StyleProcessArgs): Promise => { - const stylusResult = await new Promise((resolve, reject) => { - stylus.render(contents, {}, (err, css) => { - if (err) return reject(err); - resolve(css); - }); - }); - return { - css: stylusResult, - }; -}; - -const PROCESSABLE_FILEEXT = /\.(scss|sass|less|styl)$/; -export const createStyleHandler = () => { - return { - async process({ - fileName, - contents, - }: StyleProcessArgs): Promise { - let result: BuiltStyle = { - css: '', - }; - switch (path.extname(fileName)) { - case '.scss': - case '.sass': - result = await sassHandler({ fileName, contents }); - break; - case '.less': - result = await lessHandler({ fileName, contents }); - break; - case '.styl': - result = await stylusHandler({ fileName, contents }); - break; - default: - result.css = contents; - } - debugger; - return result; - }, - needProcess(fileName: string) { - return PROCESSABLE_FILEEXT.test(fileName); - }, - }; -}; diff --git a/src/typeCheck.ts b/src/typeCheck.ts new file mode 100644 index 0000000..4a2401d --- /dev/null +++ b/src/typeCheck.ts @@ -0,0 +1,81 @@ +import * as ng from '@angular/compiler-cli'; +import * as ts from 'typescript'; +import path from 'path'; +import { isTemplateDiagnostic } from '@angular/compiler-cli/src/ngtsc/typecheck/diagnostics/'; + +export const runTypeCheck = ( + rootNames: string[], + options: ng.CompilerOptions, + program?: ng.Program, + host?: ng.CompilerHost +): ng.Diagnostics => { + const diagnostics: (ng.Diagnostic | ts.Diagnostic)[] = []; + try { + if (!program) { + if (!host) host = ng.createCompilerHost({ options }); + program = ng.createProgram({ + rootNames, + host, + options, + }); + } + diagnostics.push(...ng.defaultGatherDiagnostics(program)); + // No errors + return diagnostics; + } catch (e) { + let errMsg: string; + let code: number; + if (e['ngSyntaxError']) { + // don't report the stack for syntax errors as they are well known errors. + errMsg = e.message; + code = ng.DEFAULT_ERROR_CODE; + } else { + errMsg = e.stack; + // It is not a syntax error we might have a program with unknown state, discard it. + code = ng.UNKNOWN_ERROR_CODE; + } + diagnostics.push({ + category: ts.DiagnosticCategory.Error, + messageText: errMsg, + code, + source: ng.SOURCE, + }); + return diagnostics; + } +}; + +export const getTSDiagnosticErrorFile = (diagnostics: ng.Diagnostics) => { + const tsDiagnostics = diagnostics.filter((diagnostic) => + ng.isTsDiagnostic(diagnostic) + ) as ts.Diagnostic[]; + return tsDiagnostics.map((diagnostic) => + path.resolve( + isTemplateDiagnostic(diagnostic) + ? diagnostic.componentFile.fileName + : diagnostic.file!.fileName + ) + ); +}; + +export const getTSDiagnosticErrorInFile = ( + filePath: string, + diagnostics: ng.Diagnostics +) => { + const tsErrors = diagnostics.filter((diagnostic) => + ng.isTsDiagnostic(diagnostic) + ) as ts.Diagnostic[]; + return tsErrors.filter( + (error) => + path.resolve( + isTemplateDiagnostic(error) + ? error.componentFile.fileName + : error.file!.fileName + ) === path.resolve(filePath) + ); +}; + +export const tsFormatDiagnosticHost: ts.FormatDiagnosticsHost = { + getCanonicalFileName: (fileName) => fileName, + getCurrentDirectory: () => path.resolve(process.cwd()), + getNewLine: () => '\n', +}; diff --git a/src/typeCheckWorker.ts b/src/typeCheckWorker.ts new file mode 100644 index 0000000..c5acecc --- /dev/null +++ b/src/typeCheckWorker.ts @@ -0,0 +1,33 @@ +import worker from 'worker_threads'; +import * as ng from '@angular/compiler-cli'; +import { runTypeCheck } from './typeCheck'; + +export type TypeCheckWorkerAction = 'run_check'; +export type TypeCheckWorker = worker.Worker; + +export interface TypeCheckArgs { + data: { + rootNames: string[]; + options: ng.CompilerOptions; + }; + action: TypeCheckWorkerAction; +} + +export const createTypeCheckWorker = () => { + const typeCheckWorker = new worker.Worker(__filename); + return typeCheckWorker; +}; + +const runWorker = () => { + worker.parentPort!.on('message', ({ action, data }: TypeCheckArgs) => { + switch (action) { + case 'run_check': + worker.parentPort!.postMessage( + runTypeCheck(data.rootNames, data.options) + ); + break; + } + }); +}; + +if (!worker.isMainThread) runWorker();